【统一功能处理】SpringBoot 统一功能专题:拦截器、数据封装、异常处理及 DispatcherServlet 源码初探
文章目录
- 2. 统一数据返回格式
- 回顾
- 后端统一返回结果
- 后端逻辑处理
- 2.1 快速入门
- 2.2 存在问题
- 2.3 优点
- 3. 统一异常处理
- 4. @ControllerAdvice 源码分析
- 1. initHandlerAdapters(context)
- 2. initHandlerExceptionResolvers(context)
- 总结
2. 统一数据返回格式
强制登录案例中,我们共做了两部分工作
- 通过Session来判断用户是否登录
- 对后端返回数据进行封装,告知前端处理的结果
回顾
后端统一返回结果
@Data
public class Result<T> {private int status;private String errorMessage;private T data;
}
后端逻辑处理
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest) {log.info("获取图书列表,pageRequest:{}", pageRequest);//用户登录,返回图书列表PageResult<BookInfo> pageResult = bookService.getBookListByPage(pageRequest);log.info("获取图书列表222,pageRequest:{}", pageResult);return Result.success(pageResult);
}
Result.success(pageResult) 就是对返回数据进行了封装
拦截器帮我们实现了第一个功能,接下来看SpringBoot对第二个功能如何支持,如何对后端返回数据进行封装,告知前端处理的结果
2.1 快速入门
统一的数据返回格式使用 @ControllerAdvice 和 ResponseBodyAdvice 的方式实现
@ControllerAdvice 表示控制器通知类
添加类 ResponseAdvice ,实现 ResponseBodyAdvice 接口,并在类上添加@ControllerAdvice 注解
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType,MediaType selectedContentType,Class selectedConverterType,ServerHttpRequest request,ServerHttpResponse response) {return Result.success(body);}
}

测试
测试接口:http://127.0.0.1:8080/book/queryBookById?bookId=1
添加统一数据返回格式之前:

添加统一数据返回格式之后:

2.2 存在问题
问题现象:
我们继续测试修改图书的接口:http://127.0.0.1:8080/book/updateBook

结果显示,发生内部错误
查看数据库,发现数据操作成功
查看日志,日志报错

多测试几种不同的返回结果,发现只有返回结果为String类型时才有这种错误发生
@RequestMapping("/test")
@RestControllerpublic class TestController {@RequestMapping("/t1")public String t1(){return "t1";}@RequestMapping("/t2")public boolean t2(){return true;}@RequestMapping("/t3")public Integer t3(){return 200;}
}
- 解决方案
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {if (body instanceof String){return objectMapper.writeValueAsString(Result.success(body));}if (body instanceof Result) {return body;}return Result.success(body);}
}
- 原因分析:
SpringMVC默认会注册一些自带的HttpMessageConverter
(从先后顺序排列分别为
ByteArrayHttpMessageConverter,
StringHttpMessageConverte,SourceHttpMessageConverter,
SourceHttpMessageConverter,AllEncompassingFormHttpMessageConverter )
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapterimplements BeanFactoryAware, InitializingBean {//...public RequestMappingHandlerAdapter() {this.messageConverters = new ArrayList<>(4);this.messageConverters.add(new ByteArrayHttpMessageConverter());this.messageConverters.add(new StringHttpMessageConverter());if (!shouldIgnoreXml) {try {this.messageConverters.add(new SourceHttpMessageConverter<>());}catch (Error err) {// Ignore when no TransformerFactory implementation is available}}this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());}//...
}
其中AllEncompassingFormHttpMessageConverter 会根据项目依赖情况 添加对应的
HttpMessageConverter
Spring会根据返回的数据类型, 从 messageConverters 链选择合适的
HttpMessageConverter .
当返回的数据是非字符串时,使用的 MappingJackson2HttpMessageConverter 写入返回对象.
当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为
StringHttpMessageConverter 可以使用.
在
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage)的处理中,调用父类的write方法
由于StringHttpMessageConverter重写了addDefaultHeaders方法,所以会执行子类的方法
然而子类
StringHttpMessageConverter的addDefaultHeaders方法定义接收参数为String,此时为Result类型,所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常
2.3 优点
- 方便前端程序员更好的接收和解析后端数据接口返回的数据
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的.
- 有利于项目统一数据的维护和修改.
- 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容.
3. 统一异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,
@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件
具体代码如下:
```java
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {@ExceptionHandlerpublic Object handler(Exception e) {return Result.fail(e.getMessage());}
}
类名,方法名和返回值可以自定义,重要的是注解
接口返回为数据时,需要加 @ResponseBody 注解
以上代码表示,如果代码出现Exception异常(包括Exception的子类),就返回一个 Result的对象,Result对象的设置参考 Result.fail(e.getMessage())
我们可以针对不同的异常,返回不同的结果
@Slf4j
@ControllerAdvice
@ResponseBody
//@RestControllerAdvice
public class ExceptionAdvice {// @ExceptionHandler
// public Result handler(Exception e){
// log.error("发生异常, e:", e);
// return Result.fail("内部错误, 请联系管理员");
// }
//
// @ExceptionHandler
// public Result handler(NullPointerException e){
// log.error("发生异常, e:", e);
// return Result.fail("发生空指针异常, 请联系管理员");
// }
//
// @ExceptionHandler
// public Result handler(IndexOutOfBoundsException e){
// log.error("发生异常, e:", e);
// return Result.fail("数组越界异常, 请联系管理员");
// }@ExceptionHandler(Exception.class)public Result handler(Exception e){log.error("发生异常, e:", e);return Result.fail("内部错误, 请联系管理员");}@ExceptionHandler(NullPointerException.class)public Result handler2(Exception e){log.error("发生异常, e:", e);return Result.fail("发生空指针异常, 请联系管理员");}@ExceptionHandler(IndexOutOfBoundsException.class)public Result handler3(Exception e){log.error("发生异常, e:", e);return Result.fail("数组越界异常, 请联系管理员");}}
4. @ControllerAdvice 源码分析
统一数据返回和统一异常都是基于 @ControllerAdvice 注解来实现的,通过分析@ControllerAdvice 的源码,可以知道他们的执行流程.
点击 @ControllerAdvice 实现源码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {@AliasFor("basePackages")String[] value() default {};@AliasFor("value")String[] basePackages() default {};Class<?>[] basePackageClasses() default {};Class<?>[] assignableTypes() default {};Class<? extends Annotation>[] annotations() default {};
}
从上述源码可以看出 @ControllerAdvice 派生于 @Component 组件,这也就是为什么没有五大注解, ControllerAdvice 就生效的原因.
下面我们看看Spring是怎么实现的,还是从 DispatcherServlet 的代码开始分析.
DispatcherServlet 对象在创建时会初始化一系列的对象:
对于 @ControllerAdvice 注解,我们重点关注 initHandlerAdapters(context) 和 initHandlerExceptionResolvers(context) 这两个方法.
1. initHandlerAdapters(context)
initHandlerAdapters(context) 方法会取得所有实现了 HandlerAdapter 接口的bean并保存起来,其中有一个类型为 RequestMappingHandlerAdapter 的bean,这个bean就是 @RequestMapping 注解能起作用的关键,这个bean在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的bean对象,并做进一步处理
这个方法在执行时会查找使用所有的 @ControllerAdvice 类,把 ResponseBodyAdvice 类放在容器中,当发生某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装
2. initHandlerExceptionResolvers(context)
接下来看 DispatcherServlet 的 initHandlerExceptionResolvers(context) 方法,这个方法会取得所有实现了 HandlerExceptionResolver 接口的bean并保存起来,其中就有一个类型为 ExceptionHandlerExceptionResolver 的bean,这个bean在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的bean对象做进一步处理
当Controller抛出异常时,DispatcherServlet 通过ExceptionHandlerExceptionResolver 来解析异常,而ExceptionHandlerExceptionResolver 又通过 ExceptionHandlerMethodResolver来解析异常,ExceptionHandlerMethodResolver 最终解析异常找到适用的@ExceptionHandler标注的方法
public class ExceptionHandlerMethodResolver {//...private Method getMappedMethod(Class<? extends Throwable> exceptionType) {List<Class<? extends Throwable>> matches = new ArrayList();//根据异常类型,查找匹配的异常处理方法//比如NullPointerException会匹配两个异常处理方法://handler(Exception e) 和 handler(NullPointerException e)for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {if (mappedException.isAssignableFrom(exceptionType)) {matches.add(mappedException);}}//如果找到多个匹配,就进行排序,找到最使用的方法。排序的规则依据抛出异常相对于声明异常的深度//比如抛出的是NullPointerException(继承于RuntimeException,RuntimeException又继承于Exception)//相对于handler(NullPointerException e) 声明的NullPointerException深度为0,//相对于handler(Exception e) 声明的Exception 深度 为2//所以 handler(NullPointerException e)标注的方法会排在前面if (!matches.isEmpty()) {if (matches.size() > 1) {matches.sort(new ExceptionDepthComparator(exceptionType));}return this.mappedMethods.get(matches.get(0));} else {return NO_MATCHING_EXCEPTION_HANDLER_METHOD;}}//...
}
总结
主要介绍了SpringBoot对一些统一功能的处理支持.
- 拦截器的实现主要分两部分: 1. 定义拦截器(实现HandlerInterceptor接口) 2. 配置拦截器
- 统一数据返回格式通过
@ControllerAdvice + ResponseBodyAdvice来实现 - 统一异常处理使用
@ControllerAdvice + @ExceptionHandler来实现,并且可以分异常来处理 - 了解了DispatcherServlet的一些源码.
