Spring Boot 中自定义异常处理

September 8, 2019 · 532 words · 3 min

Spring Boot 中自定义异常处理

Spring Boot 中提供了默认的异常处理,但是对于应用来说,这些信息并不应该直接返回或者不够明确,需要结合自己的情况进行定制 自定义处理异常有两种方式:

  • org.springframework.web.servlet.HandlerExceptionResolver#resolveException方法
  • org.springframework.web.bind.annotation.RestControllerAdviceorg.springframework.web.bind.annotation.ControllerAdviceorg.springframework.web.bind.annotation.ExceptionHandler注解来实现

当两种方式都实现时,HandlerExceptionResolver要先于ControllerAdvice执行

使用 HandlerExceptionResolver 处理异常

  • CustomExceptionHandlerResolver.java
public class CustomExceptionHandlerResolver implements HandlerExceptionResolver {

    private static final ObjectMapper OBJECT_MAPPER;

    static {
        OBJECT_MAPPER = new ObjectMapper();
        OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        OBJECT_MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }

    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {

        String message = "服务器错误";

        if (o instanceof HandlerMethod) {
            if (e instanceof IllegalArgumentException) {
                message = "参数错误";
            } else if (e instanceof SecurityException) {
                message = "不允许访问";
            } else if (e instanceof NullPointerException) {
                message = "空指针异常";
            }
        } else if (e instanceof NoHandlerFoundException) {
            message = "未找到相应资源";
        } else if (e instanceof HttpMediaTypeNotSupportedException) {
            message = "请求类型不支持";
        }

        httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
        httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);

        try {
            httpServletResponse.getWriter()
                               .write(
                                       OBJECT_MAPPER.writeValueAsString(
                                               CustomResponseContent.builder()
                                                                    .code(500)
                                                                    .status("fail")
                                                                    .message(message)
                                                                    .build()
                                       )
                               );
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        return new ModelAndView();
    }
}

还需要将该配置添加到应用中

  • CustomWebMvcConfigurer.java
@Configuration
public class CustomWebMvcConfigurer implements WebMvcConfigurer {

    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new CustomExceptionHandlerResolver());
    }
}

测试

  • 添加接口
    @GetMapping("/null")
    @CustomResponse
    public Object getProduct() {
        throw new NullPointerException();
    }

    @GetMapping("/arg")
    public Object getProducts() {
        throw new IllegalArgumentException();
    }
  • 调用接口
curl localhost:8080/null
{"code":500,"status":"fail","data":null,"message":"空指针异常"}%

curl localhost:8080/arg
{"code":500,"status":"fail","data":null,"message":"参数错误"}%

使用 RestControllerAdvice/ControllerAdvice 和 ExceptionHandler 处理异常

  • CustomControllerExceptionResolver.java
@RestControllerAdvice
@ControllerAdvice
public class CustomControllerExceptionResolver {

    @ExceptionHandler({NullPointerException.class})
    @ResponseBody
    public ResponseEntity<?> handlerNullPointerException(HttpServletRequest request, Throwable throwable) {
        CustomResponseContent responseContent = CustomResponseContent.builder()
                                                                     .code(500)
                                                                     .status("fail")
                                                                     .message("空指针异常")
                                                                     .build();
        return new ResponseEntity<>(responseContent, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler({IllegalArgumentException.class})
    @ResponseBody
    public ResponseEntity<?> handlerIllegalArgumentException(HttpServletRequest request, Throwable throwable) {
        CustomResponseContent responseContent = CustomResponseContent.builder()
                                                                     .code(400)
                                                                     .status("fail")
                                                                     .message("参数错误")
                                                                     .build();
        return new ResponseEntity<>(responseContent, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseBody
    public ResponseEntity<?> handlerMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
        String errorMessage = e.getBindingResult()
                               .getAllErrors()
                               .stream()
                               .map(f -> ((FieldError) f).getField() + ":" + f.getDefaultMessage())
                               .collect(Collectors.joining(";"));

        CustomResponseContent responseContent = CustomResponseContent.builder()
                                                                     .code(400)
                                                                     .status("fail")
                                                                     .message(errorMessage)
                                                                     .build();
        return new ResponseEntity<>(responseContent, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler({Exception.class})
    @ResponseBody
    public ResponseEntity<?> handlerException(HttpServletRequest request, Throwable throwable) {
        CustomResponseContent responseContent = CustomResponseContent.builder()
                                                                     .code(500)
                                                                     .status("fail")
                                                                     .message(throwable.getMessage())
                                                                     .build();
        return new ResponseEntity<>(responseContent, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

测试

  • 添加接口
    @GetMapping("/null")
    @CustomResponse
    public Object getProduct() {
        throw new NullPointerException();
    }

    @GetMapping("/arg")
    public Object getProducts() {
        throw new IllegalArgumentException();
    }

    @PostMapping("/product")
    @CustomResponse
    public Product product(@RequestBody @Validated Product product) {
        return product;
    }
  • 添加接口参数校验
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

    private static final Long serialVersionUID = 1435515995276255188L;


    @NotNull(message = "id 不能为空")
    @Min(value = 1, message = "id 必须大于0")
    @Max(value = 10, message = "id 不能小于 10")
    private Long id;

    @NotNull(message = "名称不能为空")
    @Pattern(regexp = "product[\\w]+", message = "名称必须以 product 开头")
    private String name;

    @PositiveOrZero(message = "价格必须是正数")
    private Long price;

}
  • 测试
curl localhost:8080/null
{"code":500,"status":"fail","data":null,"message":"空指针异常"}%

curl localhost:8080/arg
{"code":400,"status":"fail","data":null,"message":"参数错误"}%


curl -X POST \
  http://localhost:8080/product \
  -H 'Content-Type: application/json' \
  -d '{
    "id": 1
}'
{"code":400,"status":"fail","data":null,"message":"name:名称不能为空"}%


curl -X POST \
  http://localhost:8080/product \
  -H 'Content-Type: application/json' \
  -d '{
    "id": 0,
    "name": 1,
    "price": -100
}'
{"code":400,"status":"fail","data":null,"message":"id:id 必须大于0;price:价格必须是正数;name:名称必须以 product 开头"}%

403/404… 等错误

添加了上述的异常处理后,还有一部分异常无法处理,如404 错误,这是因为这些错误的异常处理并不经过异常处理器,而是被转发到 /error的路径下,默认由org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController处理,所以,继承org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController并添加相应实现即可自定义处理该异常

返回 JSON 数据

  • CustomNoHandlerExceptionResolver.java
@Controller
@RequestMapping("/error")
public class CustomNoHandlerExceptionResolver extends AbstractErrorController {

    public CustomNoHandlerExceptionResolver(ErrorAttributes errorAttributes) {
        super(errorAttributes);
    }

    @Override
    public String getErrorPath() {
        return null;
    }

    @RequestMapping
    @ResponseBody
    public ResponseEntity<?> error(HttpServletRequest request, HttpServletResponse response) {
        Optional<String> originRequestUri = Optional.ofNullable((String) request.getAttribute("javax.servlet.error.request_uri"));
        String reasonPhrase = HttpStatus.valueOf(response.getStatus()).getReasonPhrase();

        CustomResponseContent responseContent = CustomResponseContent.builder()
                                                                     .code(response.getStatus())
                                                                     .status("fail")
                                                                     .message(originRequestUri.orElse("/error") + ":" + reasonPhrase)
                                                                     .build();

        return new ResponseEntity<>(responseContent, HttpStatus.valueOf(response.getStatus()));
    }
}
  • 测试
curl localhost:8080/404
{"code":404,"status":"fail","data":null,"message":"/404:Not Found"}%

返回错误页面

当通过页面访问时,我们希望返回一个页面,可以通过指定 produces="text/html"处理来自网页的请求

resources/public/error目录下添加相应的文件,如 404.html等,根据不同的错误返回相应的页面

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView pageError(HttpServletRequest request, HttpServletResponse response) {
        return new ModelAndView(String.format("error/%d.html", response.getStatus()));
    }