Spring Boot自定义ErrorAttributes http状态未设置为响应

时间:2017-03-01 20:33:54

标签: java spring rest spring-mvc spring-boot

在Spring Boot documentation之后我定义了自己的ErrorAttributes bean(见下文),我能够通过使用自定义异常使json响应显示我想要的信息,包括我自己的错误代码和消息包装该信息并从中生成错误响应。唯一的问题是响应的http状态与我在status属性中定义的状态不匹配,它没有被覆盖。

@Bean
public ErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes() {
        @Override
        public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {

            Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);

            Throwable error = getError(requestAttributes);

            if (error instanceof MyException) {
                MyException myException = (MyException) error;

                errorAttributes.put("errorCode", myException.getErrorCode());
                errorAttributes.put("message", myException.getMessage());
                errorAttributes.put("status", myException.getStatus());

                HttpStatus correspondentStatus = HttpStatus.valueOf(myException.getStatus());
                errorAttributes.put("error", correspondentStatus.getReasonPhrase());
            }

            return errorAttributes;
        }
    };
}

响应的http状态与json中的状态不匹配,例如:

HTTP/1.1 500 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 01 Mar 2017 18:48:22 GMT
{
   "timestamp": "2017-03-01T18:48:21.894+0000",
   "status": 403,
   "error": "Forbidden",
   "exception": "com.myapp.MyException",
   "message": "You are not authorized. This user doesn't exist in the db",
   "path": "/account",
   "errorCode": "00013"
}

5 个答案:

答案 0 :(得分:4)

您正在做的就是构建错误响应的正文,正如您从示例中看到的那样。 Spring是处理状态代码的人。

如果您希望完全控制响应的所有部分,那么您应该使用ControllerAdvice方法,如文档中所示:

@ControllerAdvice(basePackageClasses = FooController.class)
public class FooControllerAdvice extends ResponseEntityExceptionHandler {

   @ExceptionHandler(MyException.class)
   public ResponseEntity<Message> handleRequestErrorMyException(HttpServletRequest request, MyException myException) {
        HttpStatus status = HttpStatus.valueOf(myException.getStatus();
        return new ResponseEntity<>(new CustomErrorType(status.value(), ex.getMessage()), status);
    }

}

使用此bean,将由handleRequestErrorMyException捕获并处理FooController包下的任何控制器抛出的所有MyException,对原始请求的响应将是此方法返回的响应。只需确保在您的Configuration类中扫描此包。

答案 1 :(得分:3)

我找到了一种在创建自定义ErrorAttributes bean的逻辑中设置http状态的方法,这样我就可以重新使用开箱即用的Spring Boot错误响应创建并使用我的自定义信息更新它需要异常处理程序和控制器建议。

通过添加下一行,您可以设置http状态,该状态将覆盖requestAttributes中的当前状态。

requestAttributes.setAttribute("javax.servlet.error.status_code", httpStatus, 0);

httpStatus是您要设置的状态。

这是带有添加行的完整bean定义:

@Bean
public ErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes() {
        @Override
        public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {

            Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);

            Throwable error = getError(requestAttributes);

            if (error instanceof MyException) {
                MyException myException = (MyException) error;
                int httpStatus = myException.getStatus();

                errorAttributes.put("errorCode", myException.getErrorCode());
                errorAttributes.put("message", myException.getMessage());
                errorAttributes.put("status", httpStatus);

                HttpStatus correspondentStatus = HttpStatus.valueOf(httpStatus);
                errorAttributes.put("error", correspondentStatus.getReasonPhrase());
                requestAttributes.setAttribute("javax.servlet.error.status_code", httpStatus, 0);
            }

            return errorAttributes;
        }
    };
}

我是怎么找到的?通过查看DefaultErrorAttributes类,我发现有一个方法addStatus是私有的,但是它显示了代码用来生成响应的http-status的属性的名称,这是我正在寻找的线索:< / p>

private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
    Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");
...

仔细查看代码,我发现在那里调用的getAttribute方法实际上是从RequestAttributes接口调用该方法:

private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
    return requestAttributes.getAttribute(name, 0);
}

在我发现的界面内部检查还有一个setAttribute方法。它奏效了。

HTTP/1.1 403 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 01 Mar 2017 20:59:33 GMT
{
   "timestamp": "2017-03-01T20:59:32.774+0000",
   "status": 403,
   "error": "Forbidden",
   "exception": "com.myapp.MyException",
   "message": "You are not authorized. This user doesn't exist in the db",
   "path": "/account",
   "errorCode": "00013"
}

答案 2 :(得分:1)

在Spring Boot 2.3.0应用程序中,可接受的答案对我不起作用,我必须继承ErrorController来覆盖原始状态。

这是BasicErrorController的代码,默认应用:

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }

如您所见,原始状态保留在调用getErrorAttributes()之前获得的变量上。因此,在自定义requestAttributes.setAttribute("javax.servlet.error.status_code", httpStatus, 0);中添加getErrorAttibutes()并没有任何作用。

BasicErrorController的自定义扩展名(请记住将其添加为bean)中,您可以覆盖error()并确保status获得所需的值:

public class CustomBasicErrorController extends BasicErrorController {

    public CustomBasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
        super(errorAttributes, errorProperties);
    }

    public CustomBasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, errorProperties, errorViewResolvers);
    }

    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        Integer status = (Integer) body.get("status");
        if (status == HttpStatus.NO_CONTENT.value()) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        }
        return new ResponseEntity<>(body, HttpStatus.valueOf(status));
    }
}

答案 3 :(得分:1)

或者您可以使用

MyException extends ResponseStatusException {
 public MyException (String msg) {
 super(HttpStatus.FORBIDDEN, msg);
} 

答案 4 :(得分:0)

@Getter
public class AppException extends ResponseStatusException {
  private final ErrorAttributeOptions options;

  public AppException(HttpStatus status, String message, ErrorAttributeOptions options){
      super(status, message);
      this.options = options;
  }
}
  1. sendError用于ExceptionHandler中的状态:

    @ExceptionHandler(AppException .class)
    public void appException(HttpServletResponse response) throws IOException {
         response.sendError(ex.getStatus().value());
    }
    

请参见Spring REST Error Handling Example

  1. 您可以像这样将ResponseEntityExceptionHandlerDefaultErrorAttributes进行杂交(Spring Boot 2.3附带了其他ErrorAttributeOptions):
@RestControllerAdvice
@AllArgsConstructor
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    private final ErrorAttributes errorAttributes;

    @ExceptionHandler(AppException.class)
    public ResponseEntity<Map<String, Object>> appException(AppException ex, WebRequest request) throws IOException {
        Map<String, Object> body = errorAttributes.getErrorAttributes(request, ex.getOptions());
        HttpStatus status = ex.getStatus();
        body.put("status", status.value());
        body.put("error", status.getReasonPhrase());
        return ResponseEntity.status(status).body(body);
    }
}

我已经检查了它的MESSAGEEXCEPTIONSTACK_TRACE选项。
另请参见Using ErrorAttributes in our custom ErrorController