【问题标题】:How do you retain Spring's Built-In REST Response JSON body with a @ControllerAdvise (and @RestControllerAdvise) Class?如何使用 @ControllerAdvise(和 @RestControllerAdvise)类保留 Spring 的内置 REST 响应 JSON 主体?
【发布时间】:2017-08-26 11:22:34
【问题描述】:

在 Spring 4.x 中,如果您使用扩展 ResponseEntityExceptionHandler@RestControllerAdvise(或 @ControllerAdvice),则默认情况下不再为标记为的参数返回带有漂亮且信息丰富的 JSON 响应主体的默认异常处理@Valid.

在使用基于ResponseEntityExceptionHandler@RestControllerAdvice 时,如何获取要返回的默认 JSON 正文?

以下是描述此问题的简单但完整的示例。使用这些类:

@RestController
class CarsController {

  @PostMapping("/cars")
  public void createCar(@RequestBody @Valid Car car) {
    System.out.println("Creating " + car);
    throw new WhateverException();
  }

  @ExceptionHandler(WhateverException.class)
  @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
  public void handleWhateverException(){
    System.out.println("Handling a WhateverException.");
  }

}

class Car {

  @NotNull
  private String make;

  @NotNull
  private String model;

  ...getter/setters omitted for brevity...
}

class WhateverException extends RuntimeException {}

如果您使用

/cars 提交POST
{
    "make": "BMW"
}

它以 400 和以下正文进行响应:

{
  "timestamp": 1491020374642,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.bind.MethodArgumentNotValidException",
  "errors": [
    {
      "codes": [
        "NotNull.car.model",
        "NotNull.model",
        "NotNull.java.lang.String",
        "NotNull"
      ],
      "arguments": [
        {
          "codes": [
            "car.model",
            "model"
          ],
          "arguments": null,
          "defaultMessage": "model",
          "code": "model"
        }
      ],
      "defaultMessage": "may not be null",
      "objectName": "car",
      "field": "model",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotNull"
    }
  ],
  "message": "Validation failed for object='car'. Error count: 1",
  "path": "/cars"
}

但是,如果您将异常处理方法移到它自己的标记为 @RestControllerAdvice 的类中,该类从 ResponseEntityExceptionHandler 扩展而来,如下所示:

@RestControllerAdvice
class RestExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(WhateverException.class)
  @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
  public void handleWhateverException(WhateverException e, HttpServletRequest httpServletRequest) {
    System.out.println("Handling a WhateverException.");
  }

}

你会得到一个带有空主体400,这是由于ResponseEntityExceptionHandler提供了一个方法(handleMethodArgumentNotValid(..)),该方法在主体为null的地方构建了一个响应.

您将如何更改此 @RestControllerAdvice 类以触发发生的原始处理,该处理提供一个 JSON 正文来描述为什么提交的请求无效?

【问题讨论】:

  • 这就是你的RestExceptionHandler在现实中的样子吗?您是否出于演示目的截断了真实版本?
  • 我当然不能发布我的公司代码,所以这只是一个人为的例子来说明问题。
  • 遇到了同样的问题。如果您已经解决了它或找到了不同的解决方案,它会对我有所帮助:)
  • 不,我还没有解决这个问题。由于这种情况,我不得不放弃 Spring 的内置处理。如果有人确实解决了这个问题,那就太好了,因为我很想回去使用它。

标签: java spring spring-mvc spring-boot


【解决方案1】:

这个怎么样?

   @RestControllerAdvice
    class RestExceptionHandler {

      @ExceptionHandler(org.springframework.validation.BindException.class)
      public ResponseEntity<String> handleBindException(org.springframework.validation.BindException e) {
         return ResponseEntity.badRequest().body(e.getMessage());
      }
   }

@Valid 失败时,它会抛出 BindException。你可以这样处理它。或者你可以只做throw e,它会给你与之前抛出的完全相同的响应

【讨论】:

    【解决方案2】:

    对我有用的一种方法是不扩展 ResponseEntityExceptionHandler

    所以结果类看起来像:

    @RestControllerAdvice
    class RestExceptionHandler {
    
      @ExceptionHandler(WhateverException.class)
      @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
      public ResponseEntity<String> handleWhateverException(WhateverException e, HttpServletRequest httpServletRequest) {
        System.out.println("Handling a WhateverException.");
        return new ResponseEntity(...);
      }    
    }
    

    但另请参阅:Ambiguous @ExceptionHandler method mapped for [class org.springframework.web.bind.MethodArgumentNotValidException]

    【讨论】:

      【解决方案3】:

      您可以在@ControllerAdvice 扩展ResponseEntityExceptionHandler模拟 spring 的行为:

      @Override
      protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body,
                                   HttpHeaders headers, HttpStatus status, WebRequest request) {
          if (body == null) {
              body = ImmutableMap.builder()
                         .put("timestamp", LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond())
                         .put("status", status.value())
                         .put("error", status.getReasonPhrase())
                         .put("message", ex.getMessage())
                         .put("exception", ex.getClass().getSimpleName())  // can show FQCN like spring with getName()
                         .put("path", ((ServletWebRequest)request).getRequest().getRequestURI())
                         .build();
          }
          return super.handleExceptionInternal(ex, body, headers, status, request);
      }
      

      * ImmutableMap 需要番石榴。

      **LocalDateTime 需要 java8。

      【讨论】:

      • 迄今为止我见过的最好的主意。谢谢!
      【解决方案4】:

      我也遇到过同样的问题,在花了很多时间调试问题后,我发现 jackson 没有正确反序列化 ErrorResponse 对象。

      这是因为我没有为ErrorResponse 对象中定义的字段添加getter 和setter。我正在使用构造函数初始化字段,并且没有为这些字段定义 getter 和 setter。

      解决方案(针对空体):

      所以当我从

      更新我的ErrorResponse 对象时
      import com.fasterxml.jackson.annotation.JsonRootName;
      import java.util.List;
      
      @JsonRootName("error")
      public class ErrorResponse {
      
        private String message;
        private List<String> details;
      
        public ErrorResponse(String message, List<String> details) {
          this.message = message;
          this.details = details;
        }
      }
      

      下面的getter和setter方法

      import com.fasterxml.jackson.annotation.JsonRootName;
      import java.util.List;
      
      @JsonRootName("error")
      public class ErrorResponse {
      
        private String message;
        private List<String> details;
      
        public ErrorResponse(String message, List<String> details) {
          this.message = message;
          this.details = details;
        }
      
        public String getMessage() {
          return message;
        }
      
        public void setMessage(String message) {
          this.message = message;
        }
      
        public List<String> getDetails() {
          return details;
        }
      
        public void setDetails(List<String> details) {
          this.details = details;
        }
      }
      

      Jackson 现在正在正确反序列化 ErrorResponse,我在响应中得到序列化的正文。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-11-13
        • 1970-01-01
        • 2017-02-22
        • 1970-01-01
        • 2019-09-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多