如何在返回字符串的Spring MVC @ResponseBody方法中使用HTTP 400错误响应

452

我正在使用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);

...但是我无法在这里使用它,因为我的方法的返回类型是String,而不是ResponseEntity。

13个回答

711

将返回类型更改为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);

9
如果你返回的不是一个字符串,而是一个POJO或其他对象怎么办? - mrshickadance
13
这将是“ResponseEntity <YourClass>”。 - Bassem Reda Zohdy
5
使用这种方法,您不再需要@ ResponseBody注释。 - Ilya Serbis
@SandeepanNath 这个回答是5年前的,你可以看到它被足够多的人接受,证明它对他们有效,也许你没有实现好或者你使用的框架版本有问题,请在github上分享你的代码,让我帮你检查一下。 - Bassem Reda Zohdy
还可以使用return ResponseEntity.badRequest(),同时带有body的话可以这样写:return ResponseEntity.badRequest().body(json); - mao95
显示剩余6条评论

118

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

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

5
谢谢!这个方法有效而且非常简单。(在这种情况下,可以进一步简化,删除未使用的“body”和“request”参数。) - Jonik
我不清楚的是如何设置HTTP状态以表示成功返回,而不是错误。 - MiguelMunoz

57

我认为这并不是最紧凑的方法,但非常简洁:

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和@ExceptionHandler一起使用会出现问题 [SPR-6902] #11567


2
抱歉,似乎这个不起作用。它在日志中产生了HTTP 500“服务器错误”和长堆栈跟踪: ERROR org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - Failed to invoke @ExceptionHandler method: public controller.TestController$MyError controller.TestController.handleException(controller.TestController$BadThingException) org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation 答案中是否有遗漏的内容? - Jonik
1
另外,我并没有完全理解定义另一个自定义类型(MyError)的意义。这是必要的吗?我正在使用最新的Spring(3.2.2)。 - Jonik
1
它对我有效。我使用javax.validation.ValidationException代替。(Spring 3.1.4) - Jerry Chen
1
这应该是被接受的答案,因为它将异常处理代码移出了正常流程并隐藏了HttpServlet*。 - lilalinux
@Jonik,顺便说一下,我遇到了“找不到可接受的表示”错误,因为Jackson无法序列化对象(这可能有很多原因)。无论如何,也不需要自定义类型。请查看https://dev59.com/nGQo5IYBdhLWcg3wOdFZ#70283232,我认为这是最干净的方法,不需要更改您的返回类型,即控制器会抛出异常。 - xlm
显示剩余3条评论

51

我会稍微修改实现:

首先,我会创建一个UnknownMatchException异常:

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UnknownMatchException extends RuntimeException {
    public UnknownMatchException(String matchId) {
        super("Unknown match: " + matchId);
    }
}
请注意使用了@ResponseStatus注解,该注解将被Spring的ResponseStatusExceptionResolver识别。如果抛出异常,它将创建一个带有相应响应状态的响应。(我还改变了状态代码为404-未找到,因为我认为这更适合此用例,但如果您喜欢,可以使用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);
}

我有500,同时记录如下:2015年5月28日下午5:23:31,org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage 严重错误:在处理错误时发生错误,放弃! org.apache.cxf.interceptor.Fault - razor
完美的解决方案,我只想补充一点,希望DTO是Match和其他对象的组合。 - Marco Sulla
这种方法的问题在于@ResponseStatus注解的JavaDocs告诉我们不要在RESTful服务器中使用它。使用此注解会导致服务器发送HTML响应,这对于Web应用程序来说是可以接受的,但对于Restful服务来说则不然。然而,已经定义了一个名为ResponseStatusException的新RuntimeException,它可能具有相同的目的,尽管我还没有太多的使用经验,但它没有像@ResponseStatus那样的警告。 - MiguelMunoz

40

这里有一种不同的方法。创建一个自定义的Exception,并用@ResponseStatus进行注释,就像以下的例子。

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

1
这种方法允许您在堆栈跟踪中的任何位置终止执行,而无需返回应指定要返回的HTTP状态代码的“特殊值”。 - Muhammad Gelbana
1
链接(实际上)已经失效了。它上面没有关于异常的任何内容。 - Peter Mortensen
嗨@PeterMortensen,我找不到损坏链接的替代品,所以我将其删除了。 - danidemi
这种方法的问题在于@ResponseStatus注解的JavaDocs告诉我们不要在RESTful服务器中使用它。使用此注解会导致服务器发送HTML响应,这对于Web应用程序来说是可以接受的,但对于Restful服务来说则不然。然而,已经定义了一个名为ResponseStatusException的新RuntimeException,它可能具有相同的目的,尽管我还没有太多的使用经验,但它没有像@ResponseStatus那样的警告。 - MiguelMunoz

36

最简单的方法是抛出一个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;
}

10
最佳答案:无需更改返回类型,也无需创建自己的异常。ResponseStatusException允许在需要时添加原因消息。 - Migs
6
需要注意的是,ResponseStatusException 仅在 Spring 版本 5 及以上版本中可用。 - Ethan Conner
请把下面的程序相关内容从英语翻译成中文。只需提供翻译后的文本: - Joaquín L. Robles
响应不能是 JSON 格式的主体。 - Emy Blacksmith
这应该被标记为答案,在Web控制器上很有用,不需要更改返回类型。 - Cheung
我不清楚的是:当没有错误时,什么决定状态码?我该如何设置它?它默认是什么? - MiguelMunoz

25

如某些答案中所述,有能力为要返回的每个HTTP状态创建一个异常类。我不喜欢为每个项目创建每个状态的类的想法。因此,我提出了以下解决方案。

  • 创建一个接受HTTP状态的通用异常
  • 创建一个控制器增强异常处理程序

让我们看一下代码

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/


2
非常好的方法。我更喜欢返回一个带有errorCode和message字段的JSON,而不是一个简单的字符串。 - Ismail Yavuz
2
这应该是正确的答案,一个通用和全局的异常处理程序,具有自定义状态代码和消息:D - Pedro Silva
浏览器无法链接该链接:“警告:潜在的安全风险... javaninja.net 的证书已于11/18/2021过期。” - Peter Mortensen

13

我正在我的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);
}

需要解释一下。例如,什么是想法/要点?来自帮助中心:“...始终解释为什么您提出的解决方案是合适的以及它是如何工作的”。请通过编辑(更改)您的答案进行回复,而不是在此处进行评论(不包括“Edit:”,“Update:”或类似内容-答案应该看起来像今天写的)。 - Peter Mortensen

3

使用Spring Boot,我不完全确定为什么需要这样做(即使在@ExceptionHandler上定义了@ResponseBody,我也得到了/error回退),但以下内容本身并未起作用:

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

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

// 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."}

2
另一种方法是使用 @ControllerAdvice 中的 @ExceptionHandler 来将所有处理程序集中在同一个类中。否则,您必须将处理程序方法放在每个要管理异常的控制器中。
您的处理程序类:
@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;
  }
}

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接