首页 > 编程笔记

Spring Boot异常处理详解

人非圣贤,孰能无过。再精明强干的程序员编写的程序也会出现错误。在 Java 中,程序出现错误会抛出“不正常信息”(Throwable)。Throwable 又被分为两种:
  1. “错误”(Error)。
  2. “异常”(Exception)。

有别于人为失误造成的“故障”(Bug),异常在程序中代表的是出现了当前代码无法处理的状况。

例如:在一个对象不存在(值为 Null)的情况下,调用该对象的某个方法引发了空指针;用户输入了一段 URL,但并没有找到对应的资源;一段计算过程中,0被当作除数……完善的错误处理,使程序不会意外崩溃甚至能友好地提示用户进行正确操作,这是让程序变得愈发健壮的重要处理步骤。

图 1 所示为 Spring Boot 的 Whitelabel Error Page。
图1 Whitelabel Error Page
图1 Whitelabel Error Page

在 Java 开发中,异常特别是检查型异常(Checked Exception),通常需要进行 try/catch 处理。而在基于 Spring Boot 的开发过程中,异常处理有了更多处理方式。

使用@ExceptionHandler处理异常

首先要介绍的解决方案是使用注解 @ExceptionHandler。该注解主要用于在 Controller 层面进行相同类型的异常处理。在对应 Controller 类中定义异常处理方法,并为其使用 @ExcptionHandler 注解。Spring 将检测到该注解,把该方法注册为对应异常类及其子类的异常处理程序。异常处理的示例代码如下:
@ExceptionHandler({MyException.class})
public void handleException(MyException e){
    //这里可以任意编写异常处理逻辑
    log.info("got an exception"+e.toString());
}
使用该注解的方法可以拥有非常灵活的签名,包括以下类型:

1)异常类型(Throwable)

可以选择一个大概的异常类型。例如,示例里的签名可以改为“Throwable e”或者“Exception e”,或者一个具体的异常类型。

2)请求与响应对象(Request/Response)

比如 ServletRequest/HttpServletRequest。

3)InputStream/Reader

用于访问请求的内容。

4)OutputStream/Writer

用户访问响应的内容。

5)Model

作为从该方法返回 Model 的替代方案。

在返回类型方面也有灵活的选择:

使用HandlerExceptionResolver处理异常

@ExceptionHandler 功能足够强大,但在不进行特殊处理的前提之下只能处理单个 Controller 的异常。面对多个 Controller 抛出的异常,还需要依赖HandlerExceptionResolver 这一手段进行处理。使用 HandlerExceptionResolver 可以解决应用程序内的任何异常,并且依赖它可以实现 RESTful 服务的统一异常处理机制。

HandlerExceptionResolver 是一个公共接口。常见的使用方式是实现一个自定义的处理类。在自定义处理类之前,可以了解一下现有的部分实现:
示例代码如下:
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyException extends Exception {

    public MyException() {
    }

    public MyException(String message) {
        super(message);
    }
}
之所以需要自定义处理类,原因在于以上实现无法控制响应体的内容。而大多数情况下,REST 服务的响应都需要有 JSON 或者 XML 格式的响应内容。

自定义处理类的示例代码如下:
@Component
@Slf4j
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

    @Override
    protected ModelAndView doResolveException(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                return handleIllegalArgument((IllegalArgumentException) ex, response, request);
            }
            //异常处理逻辑
        } catch (Exception handlerException) {
            log.warn("Handling of [" + ex.getClass().getName() + "]resulted in Exception", handlerException);
        }
        return null;
    }

    private ModelAndView
    handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response, HttpServletRequest request)
            throws IOException {
        response.sendError(HttpServletResponse.SC_CONFLICT);
        String accept = request.getHeader(HttpHeaders.ACCEPT);
        //处理响应内容
        return new ModelAndView();
    }
}

使用@ControllerAdvice处理异常

在 Spring 3.2 版本引入了 @ControllerAdvice 这一注解,为全局的 @ExceptionHandler 提供了支持。将这个注解批注在一个处理类上,即可让该类下由@ExceptionHandler 批注的方法在全局层面对异常进行处理。

请求中包含不符合要求的内容将抛出异常 MethodArgumentNotValidException。默认的响应内容如下:

{
    "timestamp":"2020-08-27T14:24:38.927+00:00",
    "status":400,
    "error":"Bad Request",
    "trace":
    "org.springframework.web.bind.MethodArgumentNotValidException: ......",
    "message":"Validation failed for object='submitArticleQuery'. Error count: 1",
    "errors":[
        {
            "codes":[
                "NotNull.submitArticleQuery.content",
                "NotNull.content",
                "NotNull.java.lang.String",
                "NotNull"
        ],
            "arguments":[
                {
                    "codes":[
                        "submitArticleQuery.content",
                        "content”
                    ],
                    "arguments":null,
                    "defaultMessage":"content",
                    "code":"content"
                }
            ],
            "defaultMessage":"Content must not be null.",
            "objectName":"submitArticleQuery",
            "field":"content",
            "rejectedValue":null,
            "bindingFailure":false,
            "code":"NotNull"
        }
    ],
    "path":"/article"
}


其中 trace 属性将会输出大段落的堆栈信息,在此处做了省略处理。对于调用方而言,返回的信息或许存在冗余。可以根据需求使用该方案对其进行调整。

在路径 src/main/java/com/example/myblog/controller 下创建 MyBlogControllerAdvice.java:
@ControllerAdvice
@Slf4j
public class MyBlogControllerAdvice {
    @ResponseBody
    @ExceptionHandler(value =MethodArgumentNotValidException.class)
    public Result<String> errorHandler(MethodArgumentNotValidException e) {
        String errorMsg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        log.error("未处理异常" + errorMsg);
        return new Result<String>().setMessage("参数错误:"+ errorMsg);
    }
}
响应结果如下:

{
"code": 0,
"data": null,
"message": "参数错误:Content must not be null."
}

抛出ResponseStatusException异常

上面介绍的方法大多适用于解决一个切面上的问题。如果仅需要针对少数接口进行异常处理,控制其返回给客户端的 HTTP 状态码、错误指引以及报错原因,那么 ResponseStatusException 将会是一个不错的选择。示例代码如下:
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id")Long id, HttpServletResponse response) {
    try{
        Foo resourceById=RestPreconditions.checkFound(service.findOne(id));
        eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
        return resourceById;
    }
    catch(MyResourceNotFoundException exc) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND,"Foo Not Found", exc);
    }
}

优秀文章