我使用Spring MVC为一个简单的JSON API,使用@ResponseBody的方法,如下所示。(我已经有了一个直接生成JSON的服务层。)
@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId) {
String json = matchService.getMatchJson(matchId);
if (json == null) {
// TODO: how to respond with e.g. 400 "bad request"?
}
return json;
}
在给定的场景中,响应HTTP 400错误的最简单、最干净的方法是什么?
我确实遇到过这样的方法:
return new ResponseEntity(HttpStatus.BAD_REQUEST);
...但我不能在这里使用它,因为我的方法的返回类型是字符串,而不是ResponseEntity。
这里有一个不同的方法。创建一个带有@ResponseStatus注解的自定义Exception,如下所示。
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Not Found")
public class NotFoundException extends Exception {
public NotFoundException() {
}
}
需要的时候扔出去。
@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId) {
String json = matchService.getMatchJson(matchId);
if (json == null) {
throw new NotFoundException();
}
return json;
}
在控制器中处理异常而无需显式返回ResponseEntity的最简单、最干净的方法是添加@ExceptionHandler方法。
使用Spring Boot 2.0.3.RELEASE的示例片段:
// Prefer static import of HttpStatus constants as it's cleaner IMHO
// Handle with no content returned
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(BAD_REQUEST)
void onIllegalArgumentException() {}
// Return 404 when JdbcTemplate does not return a single row
@ExceptionHandler(IncorrectResultSizeDataAccessException.class)
@ResponseStatus(NOT_FOUND)
void onIncorrectResultSizeDataAccessException() {}
// Catch all handler with the exception as content
@ExceptionHandler(Exception.class)
@ResponseStatus(I_AM_A_TEAPOT)
@ResponseBody Exception onException(Exception e) {
return e;
}
题外话:
如果在所有上下文/用法中,matchService.getMatchJson(matchId) == null是无效的,那么我的建议是让getMatchJson抛出一个异常,例如,IllegalArgumentException,而不是返回null,并让它冒泡泡到控制器的@ExceptionHandler。
如果用null来测试其他条件,那么我将有一个特定的方法,例如matchService.hasMatchJson(matchId)。一般来说,我尽可能避免null,以避免意外的NullPointerException。
这样的方法应该是可行的,但我不确定是否有更简单的方法:
@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId, @RequestBody String body,
HttpServletRequest request, HttpServletResponse response) {
String json = matchService.getMatchJson(matchId);
if (json == null) {
response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
}
return json;
}
我将稍微改变一下实现:
首先,我创建了一个UnknownMatchException:
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UnknownMatchException extends RuntimeException {
public UnknownMatchException(String matchId) {
super("Unknown match: " + matchId);
}
}
注意@ResponseStatus的使用,它将被Spring的ResponseStatusExceptionResolver识别。如果抛出异常,它将创建具有相应响应状态的响应。(我还擅自将状态代码更改为404 - Not Found,我认为这更适合这个用例,但您可以坚持使用HttpStatus。BAD_REQUEST如果你喜欢。)
接下来,我将更改MatchService具有以下签名:
interface MatchService {
public Match findMatch(String matchId);
}
最后,我将更新控制器并委托给Spring的MappingJackson2HttpMessageConverter来自动处理JSON序列化(如果您将Jackson添加到类路径中,并将@EnableWebMvc或<mvc:annotation-driven />添加到配置中,则默认添加JSON序列化。请参阅参考文档):
@RequestMapping(value = "/matches/{matchId}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Match match(@PathVariable String matchId) {
// Throws an UnknownMatchException if the matchId is not known
return matchService.findMatch(matchId);
}
注意,将域对象与视图对象或DTO对象分离是非常常见的。这可以通过添加一个小型DTO工厂来轻松实现,该工厂返回可序列化的JSON对象:
@RequestMapping(value = "/matches/{matchId}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public MatchDTO match(@PathVariable String matchId) {
Match match = matchService.findMatch(matchId);
return MatchDtoFactory.createDTO(match);
}
这里有一个不同的方法。创建一个带有@ResponseStatus注解的自定义Exception,如下所示。
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Not Found")
public class NotFoundException extends Exception {
public NotFoundException() {
}
}
需要的时候扔出去。
@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId) {
String json = matchService.getMatchJson(matchId);
if (json == null) {
throw new NotFoundException();
}
return json;
}
使用带有状态代码的自定义响应。
是这样的:
class Response<T>(
val timestamp: String = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
.withZone(ZoneOffset.UTC)
.format(Instant.now()),
val code: Int = ResultCode.SUCCESS.code,
val message: String? = ResultCode.SUCCESS.message,
val status: HttpStatus = HttpStatus.OK,
val error: String? = "",
val token: String? = null,
val data: T? = null
) : : ResponseEntity<Response.CustomResponseBody>(status) {
data class CustomResponseBody(
val timestamp: String = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
.withZone(ZoneOffset.UTC)
.format(Instant.now()),
val code: Int = ResultCode.SUCCESS.code,
val message: String? = ResultCode.SUCCESS.message,
val error: String? = "",
val token: String? = null,
val data: Any? = null
)
override fun getBody(): CustomResponseBody? = CustomResponseBody(timestamp, code, message, error, token, data)