如何响应一个HTTP 400错误在一个Spring MVC @ResponseBody方法返回字符串

我使用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。

493753 次浏览

这不是最紧凑的方式,但在我看来很干净:

if(json == null) {
throw new BadThingException();
}
...


@ExceptionHandler(BadThingException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public @ResponseBody MyError handleException(BadThingException e) {
return new MyError("That doesn’t work");
}

如果使用Spring 3.1+,可以在异常处理方法中使用@ResponseBody,否则使用ModelAndView或其他方法。

@ResponseBody does not work with @ExceptionHandler [sp -6902] #11567

这样的方法应该是可行的,但我不确定是否有更简单的方法:

@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;
}

将你的返回类型更改为ResponseEntity<>,然后你可以使用下面的400:

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

对于正确的请求:

return new ResponseEntity<>(json,HttpStatus.OK);

Spring 4.1之后,ResponseEntity中有了帮助方法,可以用作:

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);

而且

return ResponseEntity.ok(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 />添加到配置中,它将默认添加。参见参考文档):

@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);
}

我在Spring Boot应用程序中使用这个:

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public ResponseEntity<?> match(@PathVariable String matchId, @RequestBody String body,
HttpServletRequest request, HttpServletResponse response) {


Product p;
try {
p = service.getProduct(request.getProductId());
} catch(Exception ex) {
return new ResponseEntity<String>(HttpStatus.BAD_REQUEST);
}


return new ResponseEntity(p, HttpStatus.OK);
}

这里有一个不同的方法。创建一个带有@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;
}

正如在一些回答中提到的,可以为您想要返回的每个HTTP状态创建一个异常类。我不喜欢为每个项目的每个状态创建一个类的想法。这是我想出的替代方案。

  • 创建接受HTTP状态的通用异常
  • 创建一个Controller Advice异常处理程序

让我们来看看代码

package com.javaninja.cam.exception;


import org.springframework.http.HttpStatus;




/**
* The exception used to return a status and a message to the calling system.
* @author norrisshelton
*/
@SuppressWarnings("ClassWithoutNoArgConstructor")
public class ResourceException extends RuntimeException {


private HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;


/**
* Gets the HTTP status code to be returned to the calling system.
* @return http status code.  Defaults to HttpStatus.INTERNAL_SERVER_ERROR (500).
* @see HttpStatus
*/
public HttpStatus getHttpStatus() {
return httpStatus;
}


/**
* Constructs a new runtime exception with the specified HttpStatus code and detail message.
* The cause is not initialized, and may subsequently be initialized by a call to {@link #initCause}.
* @param httpStatus the http status.  The detail message is saved for later retrieval by the {@link
*                   #getHttpStatus()} method.
* @param message    the detail message. The detail message is saved for later retrieval by the {@link
*                   #getMessage()} method.
* @see HttpStatus
*/
public ResourceException(HttpStatus httpStatus, String message) {
super(message);
this.httpStatus = httpStatus;
}
}

然后我创建一个控制器通知类

package com.javaninja.cam.spring;




import com.javaninja.cam.exception.ResourceException;


import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;




/**
* Exception handler advice class for all SpringMVC controllers.
* @author norrisshelton
* @see org.springframework.web.bind.annotation.ControllerAdvice
*/
@org.springframework.web.bind.annotation.ControllerAdvice
public class ControllerAdvice {


/**
* Handles ResourceExceptions for the SpringMVC controllers.
* @param e SpringMVC controller exception.
* @return http response entity
* @see ExceptionHandler
*/
@ExceptionHandler(ResourceException.class)
public ResponseEntity handleException(ResourceException e) {
return ResponseEntity.status(e.getHttpStatus()).body(e.getMessage());
}
}

使用它

throw new ResourceException(HttpStatus.BAD_REQUEST, "My message");

http://javaninja.net/2016/06/throwing-exceptions-messages-spring-mvc-controller/

使用Spring Boot,我不完全确定为什么这是必要的(我得到了/error回退,即使@ResponseBody是在@ExceptionHandler上定义的),但以下方法本身不起作用:

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorMessage handleIllegalArguments(HttpServletRequest httpServletRequest, IllegalArgumentException e) {
log.error("Illegal arguments received.", e);
ErrorMessage errorMessage = new ErrorMessage();
errorMessage.code = 400;
errorMessage.message = e.getMessage();
return errorMessage;
}

它仍然抛出一个异常,显然是因为没有可生产的媒体类型被定义为request属性:

// AbstractMessageConverterMethodProcessor
@SuppressWarnings("unchecked")
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {


Class<?> valueType = getReturnValueType(value, returnType);
Type declaredType = getGenericType(returnType);
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
if (value != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + valueType);   // <-- throws
}


// ....


@SuppressWarnings("unchecked")
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<MediaType>(mediaTypes);

所以我把它们加了进去。

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorMessage handleIllegalArguments(HttpServletRequest httpServletRequest, IllegalArgumentException e) {
Set<MediaType> mediaTypes = new HashSet<>();
mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
httpServletRequest.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
log.error("Illegal arguments received.", e);
ErrorMessage errorMessage = new ErrorMessage();
errorMessage.code = 400;
errorMessage.message = e.getMessage();
return errorMessage;
}

这让我有了一个“受支持的兼容媒体类型”,但它仍然不起作用,因为我的ErrorMessage有故障:

public class ErrorMessage {
int code;


String message;
}

JacksonMapper没有把它作为“可转换的”来处理,所以我必须添加getter /setter,并且我还添加了@JsonProperty注释

public class ErrorMessage {
@JsonProperty("code")
private int code;


@JsonProperty("message")
private String message;


public int getCode() {
return code;
}


public void setCode(int code) {
this.code = code;
}


public String getMessage() {
return message;
}


public void setMessage(String message) {
this.message = message;
}
}

然后我如愿地收到了我的信息

{"code":400,"message":"An \"url\" parameter must be defined."}

你也可以通过throw new HttpMessageNotReadableException("error description")从Spring的默认错误处理中获益。

但是,与那些默认错误的情况一样,不会设置响应体。

我发现这些在拒绝合理地只能手工制作的请求(可能表明恶意意图)时很有用,因为它们掩盖了基于更深层次的自定义验证及其标准拒绝请求的事实。

最简单的方法是抛出ResponseStatusException:

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId, @RequestBody String body) {
String json = matchService.getMatchJson(matchId);
if (json == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
return json;
}

另一种方法是使用@ExceptionHandler@ControllerAdvice来将所有处理程序集中在同一个类中。如果不是,则必须将处理程序方法放入希望为其管理异常的每个控制器中。

你的处理程序类:

@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {


@ExceptionHandler(MyBadRequestException.class)
public ResponseEntity<MyError> handleException(MyBadRequestException e) {
return ResponseEntity
.badRequest()
.body(new MyError(HttpStatus.BAD_REQUEST, e.getDescription()));
}
}

您的自定义异常:

public class MyBadRequestException extends RuntimeException {


private String description;


public MyBadRequestException(String description) {
this.description = description;
}


public String getDescription() {
return this.description;
}
}

现在,您可以从任何控制器抛出异常,并且可以在通知类中定义其他处理程序。

使用带有状态代码的自定义响应。

是这样的:

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)

在控制器中处理异常而无需显式返回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