【问题标题】:How to respond with HTTP 400 error in a Spring MVC @ResponseBody method returning String?如何在返回字符串的 Spring MVC @ResponseBody 方法中响应 HTTP 400 错误?
【发布时间】:2013-04-20 09:57:36
【问题描述】:

我将 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。

【问题讨论】:

    标签: java spring spring-mvc http-error


    【解决方案1】:

    无需显式返回ResponseEntity 即可在控制器中处理异常的最简单、最简洁的方法是添加@ExceptionHandler 方法。

    使用 Spring Boot 2.0.3.RELEASE 的示例 sn-p:

    // 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

    【讨论】:

      【解决方案2】:

      带有状态码的自定义响应

      像这样:

      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)
      

      【讨论】:

        【解决方案3】:

        另一种方法是使用@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;
          }
        }
        

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

        【讨论】:

          【解决方案4】:

          最简单的方法是发送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;
              }
          

          【讨论】:

          • 最佳答案:无需更改返回类型,也无需创建自己的异常。此外,ResponseStatusException 允许在需要时添加原因消息。
          • 需要注意的是 ResponseStatusException 仅在 Spring 5+ 版本中可用
          • 这个答案应该在顶部
          • 您不能将 JSON 正文作为响应。
          【解决方案5】:

          您也可以通过 throw new HttpMessageNotReadableException("error description") 受益于 Spring 的 default error handling

          但是,就像那些默认错误一样,不会设置响应正文。

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

          第, dtk

          【讨论】:

          • HttpMessageNotReadableException("error description") 已弃用。
          【解决方案6】:

          将您的返回类型更改为ResponseEntity&lt;&gt;,然后您可以在下面使用 400

          return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
          

          为了正确的请求

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

          更新 1

          spring 4.1 之后,ResponseEntity 中的辅助方法可以用作

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

          return ResponseEntity.ok(json);
          

          【讨论】:

          • 啊,所以你也可以像这样使用ResponseEntity。这很好用,只是对原始代码的一个简单更改——谢谢!
          • 欢迎您随时添加自定义标头检查 ResponseEntity 的所有构造函数
          • 如果你传回字符串以外的东西怎么办?就像在 POJO 或其他对象中一样?
          • 它将是 'ResponseEntity'
          • 使用这种方法你不再需要@ResponseBody 注释
          【解决方案7】:

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

          【讨论】:

            【解决方案8】:

            对于 Spring Boot,我不完全确定为什么这是必要的(尽管 @ResponseBody 是在 @ExceptionHandler 上定义的,但我得到了 /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."}
            

            【讨论】:

              【解决方案9】:

              正如一些答案中提到的,可以为您想要返回的每个 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/

              【讨论】:

              • 非常好的方法.. 而不是一个简单的字符串,我更喜欢返回一个带有 errorCode 和消息字段的 jSON..
              • 这应该是正确的答案,具有自定义状态代码和消息的通用和全局异常处理程序:D
              【解决方案10】:

              这是一种不同的方法。创建一个自定义的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;
              }
              

              在此处查看 Spring 文档:http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions

              【讨论】:

              • 此方法允许您在堆栈跟踪中的任何位置终止执行,而无需返回应指定您希望返回的 HTTP 状态代码的“特殊值”。
              【解决方案11】:

              我认为这个线程实际上有最简单、最干净的解决方案,它不会牺牲 Spring 提供的 JSON 军事工具:

              https://stackoverflow.com/a/16986372/1278921

              【讨论】:

                【解决方案12】:

                我会稍微改变一下实现:

                首先,我创建一个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&lt;mvc:annotation-driven /&gt; 添加到您的配置中,则默认添加它,见reference docs):

                @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 对象分开是很常见的。这可以通过添加一个返回可序列化 JSON 对象的小型 DTO 工厂轻松实现:

                @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 年 2 月 28 日下午 5:23:31 org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage 严重:错误处理期间发生错误,放弃! org.apache.cxf.interceptor.Fault
                • 完美解决方案,我只想补充一点,我希望DTO是Match和其他一些对象的组合。
                【解决方案13】:

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

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

                【讨论】:

                • 谢谢!这很有效,也很简单。 (在这种情况下,可以通过删除未使用的bodyrequest 参数来进一步简化。)
                【解决方案14】:

                不一定是最紧凑的方式,但 IMO 非常干净

                if(json == null) {
                    throw new BadThingException();
                }
                ...
                
                @ExceptionHandler(BadThingException.class)
                @ResponseStatus(value = HttpStatus.BAD_REQUEST)
                public @ResponseBody MyError handleException(BadThingException e) {
                    return new MyError("That doesnt work");
                }
                

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

                https://jira.springsource.org/browse/SPR-6902

                【讨论】:

                • 对不起,这似乎不起作用。它会在日志中产生带有长堆栈跟踪的 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 答案中是否缺少某些内容?
                • 另外,我还没有完全理解定义另一个自定义类型 (MyError) 的意义。那有必要吗?我正在使用最新的 Spring (3.2.2)。
                • 它对我有用。我改用javax.validation.ValidationException。 (春季 3.1.4)
                • 这在你的服务和客户端之间有一个中间层的情况下非常有用,中间层有自己的错误处理能力。感谢您提供此示例@Zutty
                • 这应该是公认的答案,因为它将异常处理代码移出正常流程并隐藏了 HttpServlet*
                猜你喜欢
                • 2013-03-08
                • 2014-10-14
                • 2015-12-02
                • 2013-01-20
                • 2014-06-22
                • 2023-03-29
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多