Spring Mvc全局异常处理及统一结果返回

2,517 阅读4分钟

java的异常体系

erDiagram
Throwable ||--o{ Error : impl
Throwable || --o{ Exception : impl

Throwable作为最顶层的类,下面分为Exception(异常)和Error(错误)

  • Error

    程序中无法处理的错误,表示运行应用程序中出现了严重的错误,一半由jvm引起,常见的有NoClassDefFoundErrorOutOfMemoryError

  • Exception

    程序运行过程中产生的异常,又分为可查异常(checked exception)不可查异常(unchecked exception)

    • 可查异常(checked exception)

    编译器要求必须处理的异常,需要try-catch捕获或者throws语句抛出否则编译不通过,常见的有ClassNotFoundExceptionNoSuchMethodException等。

    • 不可查异常(unchecked exception)

    编译器不会进行检查并且不要求必须处理的异常,包括RuntimeException以及其子类,常见的有NullPointerExceptionIllegalArgumentException等。

全局异常处理

Spring Mvc使用@ExceptionHandler注解来处理由控制层抛出的异常

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
	Class<? extends Throwable>[] value() default {};
}

@ExceptionHandler只有一个方法value()可以填写的值为继承自ThrowableClass数组,也就是说一个ExceptionHandler可以处理多个异常或错误

首先我们新建一个自定义异常方便后面的演示,接收一个message参数

public class CustomException extends RuntimeException{

    public CustomException(String message) {
        super(message);
    }
}
  • @RestController@Controller

    在控制层使用如下

    @RestController
    @Slf4j
    public class DomainController {
    
        @ExceptionHandler(CustomException.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public ResponseEntity<Object> domainExceptionHandler(CustomException e){
            log.error("exception from  DomainController", e);
            return ResponseEntity.status(500).body(e.getMessage());
        }
    
        @GetMapping("/domain")
        public void test(int code){
            if(code == 1){
                throw new CustomException("自定义异常");
            }
        }
    }
    

    请求接口传入参数code = 1,可以看到控制台打印了

    exception from  DomainController
    

    说明异常被ExceptionHandler处理了,该方式只能处理单个控制器的异常

  • @ControllerAdvice@RestControllerAdvice

    @ControllerAdvice@RestControllerAdvice可以同时处理多个的控制器,通过下列的方式可以调整处理范围,因为在某些情况下我们并不想让异常控制器处理有些第三方框架的异常

    // 处理所有@RestController注解
    @ControllerAdvice(annotations = RestController.class)
    public class ExampleAdvice1 {}
    
    // 处理包路径下的所有控制器
    @ControllerAdvice(basePackages = "org.example.controllers")
    public class ExampleAdvice2 {}
    
    // 指定特定的类处理
    @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
    public class ExampleAdvice3 {}
    

    注意:annotations和basePackages生效的前提是被处理的控制器已经被扫描成Spring Bean 在大部分情况下我们只需要配置全局的异常处理,也就是通过@RestControllerAdvice处理所有的控制器,在单个控制器中配置的@ExceptionHandler优先级会高于全局的,我们可以利用这点来处理有些特殊的异常或者某些定制化需求(当然最好少一些定制化需求,会导致项目后期维护困难)。

关于Error

我们现在定义一个@ExceptionHandler处理所有的异常,如下

    @ExceptionHandler({Exception.class})
    public ResponseEntity<Object> handler(Exception e){
        log.error("error happened ", e);
        return ResponseEntity.status(500).body(e.getMessage());
    }

修改测试代码code = 2时抛出Error

    @GetMapping("/domain")
    public void test(int code){
        if(code == 1){
            throw new CustomException("自定义异常");
        }
        if(code == 2){
            throw new AssertionError("code is 2");
        }
    }

前面我们已经介绍过了ErrorException的区别,在这抛出Error,我们的异常处理应该不会处理,因为我们只处理了所有的Exception,并没有处理Error,启动项目调用接口,控制台得到如下信息

error happened Caused by: java.lang.AssertionError: code is 2

可以看到AssertionError被异常处理器处理了,这是因为在Spring 4.3以后,DispatcherServletdoDispatch方法会处理从处理程序抛出的错误,使它们可用于@ExceptionHandler方法和其他场景。

...
catch (Exception ex) {
    dispatchException = ex;
}
catch (Throwable err) {
    // As of 4.3, we're processing Errors thrown from handler methods as well,
    // making them available for @ExceptionHandler methods and other scenarios.
    dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
...

统一结果返回

在实际项目中我们通常会定义统一的返回结构,常见如下

@Getter
@Builder
public class ResponseData<T> {

    private long timestamp;
    private int status;
    private String message;
    private T data;
}

我们不想在每个Controller上写重复的包装代码,可以定义一个统一的返回处理,实现ResponseBodyAdvice接口

@RestControllerAdvice
@Slf4j
public class ResponseHandler  implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return false;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return null;
    }
}

其中提供的两个方法

  • supports

    此方法有两个参数

    • returnType:控制器返回值类型
    • converterType:http消息转换器类型, 只有该方法返回true时,beforeBodyWrite才会执行,我们可以利用这个方法做一些配置,比如通过自定义注解@IgnoreBodyAdvice让某些接口不使用统一返回结构。
    @Override
    public boolean supports(final @NotNull MethodParameter methodParameter,
                            final Class<? extends HttpMessageConverter<?>> aClass) {
        Class<?> clz = methodParameter.getDeclaringClass();
        if (method == null) {
            return false;
        } 
        // 检查注解是否存在
        if (clz.isAnnotationPresent(IgnoreBodyAdvice.class)) {
            return false;
        } else
            return !method.isAnnotationPresent(IgnoreBodyAdvice.class);
    }
  • beforeBodyWrite

    此方法有6个参数,会在HttpMessageConverterwrite方法之前调用

    • body:返回的消息
    • returnType: controller的返回值类型
    • selectedContentType:选定的消息类型,比如application/json
    • selectedConverterType:http消息转换器类型,比如StringHttpMessageConverter
    • request:当前请求
    • response:当前响应

    常见的用法如下

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    if(body == null){
        return ResponseData.builder().message("ok").build();
    } else if(body instanceof ResponseData){
        return body;
    } else {
        return ResponseData.builder().data(body).build();
    }
}

这只是一个最简单的例子,在实际项目可能还会有很多判断条件,可以根据项目情况自行添加。

至此本文就结束了,如果有疑问或发现错误可以评论留言或者邮件yp.yang7@foxmail.com,谢谢