SpringBoot拦截器实战与原理剖析
SpringBoot 统一功能处理
拦截器
拦截器是 Spring 框架提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码
拦截器的使用步骤分为两步:
1.定义拦截器
2.注册配置拦截器
自定义拦截器

preHandle 方法:目标方法执行前执行,返回 true:继续执行后续操作,返回 false:中断后续操作
postHandle 方法: 目标方法执行后执行
afterCompletion 方法:视图渲染完毕之后执行,最后执行
注册配置拦截器

拦截器使用细节
1.拦截器的拦截路径配置
2.拦截器实现原理
拦截路径
拦截路径 是指我们定义的这个拦截器,对那些请求生效

addPathPatterns( ) 指定要拦截哪些请求
excludePathPatterns( ) 指定不拦截哪些请求
常见的拦截路径设置:

拦截器执行流程
添加拦截器之后,执行 Controller 方法之前,请求会先被拦截器拦截住,执行 preHandle()方法,如果这个方法返回 true 则会放行,反之不放行。
在 controller 方法执行完毕之后,再回来执行 postHandle( ) 这个方法以及 afterCompletion( ) 方法,执行完毕之后,最终给浏览器响应数据
其实这跟我们前面说的这些方法的定义是一样的
登录校验
定义拦截器


注册配置拦截器
我们排除了 登录 和 前端静态资源



我们在未登录的情况下访问其他接口不出意外的被阻拦了,并且返回了401,这在我们的意料之中


我们发现这次没有拦截成功,正是因为我们登录了,session 中有我们的信息
DispatcherServlet 源码分析
当我们观察服务器启动日志之后:

在 Tomcat 启动之后,有一个核心类 DispatcherServlet它来控制程序的执行顺序,所有的请求都会先进到 DispatcherServlet,执行 doDispatch 调度方法,如果有拦截器,会先执行拦截器 preHandle( ) 方法的代码,如果这个方法返回 true 则会放行,反之不放行。
在 controller 方法执行完毕之后,再回来执行 postHandle( ) 这个方法以及 afterCompletion( ) 方法,执行完毕之后,最终给浏览器响应数据。
这跟我们前面说的拦截器执行顺序是一样的
当然我们还是来看源码
当我们来到源码后,发现很多的东西,这时候不必想着这些是干啥的,我们要清楚我们来的目的就是来探寻 DispatcherServlet 收到请求之后是咋做的。
初始化


在进行一系列的初始化之后,就开始工作了
处理请求
DispatcherServlet 接收到请求后,执行 doDispatch 调度方法,再将请求转给 Controller,我们来看doDispatch()方法的具体实现

我们看到这么多陌生的东西,其实只需要看try里面的就好,先找找有没有自己认识的,找来找去终于找到了认识的了,字面意思不就是应用我们拦截器里的 prehandle( ) 方法嘛
在开始执行 Controller 之前,会先调用预处理方法 applyPreHandle( ) 方法
进入 applyPreHandle( ) 方法

在 applyPreHandle 中会获取所有拦截器 HandleIntercetor,并执行拦截器中的preHandle 方法

如果返回 true,继续执行后续逻辑处理,返回false中断后续操作,这可以在 applyPreHandle( ) 看到
适配器模式
适配器模式也叫包装器模式,将一个类的接口,转换成客户期望的另一个接口,适配器让原本不兼容的类可以合作无间,比如日常用到的转接头
适配器模式角色
Target: 目标接口
Adaptee: 适配者
Adapter: 适配器类,此模式的核心,通过继承或者引用适配者对象,把适配者转为目标接口
Client: 需要使用适配器的对象
适配器模式的实现
之前用的 slf4j 就是使用了适配器模式,slf4j 作为门面对象提供了一系列打印日志的 api,真正底层调用的是 log4j 或者 logback 来打印日志,我们作为调用者只需调用 slf4j 的 api就行了
/*** slf4j接口*/
interface Slf4jApi{void log(String message);
}/*** log4j 接口*/
class Log4j{void log4jLog(String message){System.out.println("Log4j打印:"+message);}
}/*** slf4j和log4j适配器*/
class Slf4jLog4JAdapter implements Slf4jApi{private Log4j log4j;public Slf4jLog4JAdapter(Log4j log4j) {this.log4j = log4j;}@Overridepublic void log(String message) {log4j.log4jLog(message);}
}/*** 客户端调用*/
public class Slf4jDemo {public static void main(String[] args) {Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j());slf4jApi.log("使用slf4j打印日志");}
}适配器模式的应用场景
适配器模式可以看做一种 “补偿模式”,用来补救设计上的缺陷,所以它更多的应用场景是对正在运行的代码进行改造,并且希望可以复用原有代码实现新的功能
统一数据返回格式
在前面的图书管理系统下,我们做了两部分的工作:
1.通过 Session 来判断用户是否登录
2.对后端返回数据进行封装,告知前端处理的结果
统一返回结果


我们对返回的数据进行了封装
我们来看SpringBoot 是如何帮我们实现统一的数据返回的

它的实现需要 @ControllerAdvice 和 ResponseBodyAdvice 的方式实现
@ControllerAdvice 表示控制器通知类
添加类 ResponseAdvice,实现 ResponseBodyAdvice 接口,并在类上添加 @ControllerAdvice 注解
supports 方法:判断是否要执行 beforeBodyWrite 方法,true 为执行,false 不执行,通过该方法可以选择哪些类或哪些方法的 response 要进行处理,现在我们统一为都处理
beforeBodyWrite 方法,对 response 方法进行具体操作处理
测试:
添加统一数据返回格式之前:

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

我们继续测试其他接口,发现一个问题
在测试修改的时候发生问题了,它报错信息显示的是,接口声明返回的是 String 但是却返回的是
Result 这肯定是因为统一接口返回格式出现了问题



虽然是报错了,但是数据库确实修改成功了
这种错误只有在 返回结果 String 类型时才有这种错误发生
我们来看看解决方案

我们在遇到属于 String 类型的时候进行单独处理
为啥要这样做呢?
SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter (分别为 ByteArrayHttpMessageConverter , StringHttpMessageConverter , 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
public AllEncompassingFormHttpMessageConverter() {if (!shouldIgnoreXml) {try {addPartConverter(new SourceHttpMessageConverter<>());}catch (Error err) {// Ignore when no TransformerFactory implementation is available}if (jaxb2Present && !jackson2XmlPresent) {addPartConverter(new Jaxb2RootElementHttpMessageConverter());}}if (kotlinSerializationJsonPresent) {addPartConverter(new KotlinSerializationJsonHttpMessageConverter());}if (jackson2Present) {addPartConverter(new MappingJackson2HttpMessageConverter());}else if (gsonPresent) {addPartConverter(new GsonHttpMessageConverter());}else if (jsonbPresent) {addPartConverter(new JsonbHttpMessageConverter());}if (jackson2XmlPresent && !shouldIgnoreXml) {addPartConverter(new MappingJackson2XmlHttpMessageConverter());}if (jackson2SmilePresent) {addPartConverter(new MappingJackson2SmileHttpMessageConverter());}
}在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到messageConverters 链的末尾.
Spring会根据返回的数据类型,从 messageConverters 链选择合适的HttpMessageConverter .
当返回的数据是⾮字符串时,使⽤的 MappingJackson2HttpMessageConverter 写⼊返回对象.
当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为StringHttpMessageConverte 可以使用
在 ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) 的处理中,调⽤⽗类的write⽅法
由于 StringHttpMessageConverter 重写了addDefaultHeaders⽅法,所以会执⾏⼦类的⽅法


然⽽⼦类 StringHttpMessageConverter 的addDefaultHeaders⽅法定义接收参数为String,此 时t为Result类型,所以出现类型不匹配"Resultcannotbecasttojava.lang.String"的异常
所以我们的返回结果如果为 String 类型,使用SpringBoot 内置提供的 Jackson 来实现信息的序列化

优点
1.方便前端程序员更好的接收和解析后端数据接口返回的数据
2.降低前端和后端的沟通成本,按照某个格式实现就可以了,所有的接口都是这样返回的
3.有利于项目统一数据的维护和修改
统一异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的
@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当前出现异常的时候执行某个通知
比如:


根据异常类型返回不同的信息




