目录
[DispatcherServlet 源码](#DispatcherServlet 源码)
拦截器简介
前面图书管理系统案例的完善中,我们实现了强制登录的功能,后端程序 Session 来判断用户是否登录。但实现过程还是比较麻烦的,且有大量冗余重复的代码。
有一种方法,可以统一拦截所有的请求,并进行 Session 校验 --> 拦截器。
拦截器:是 Spring 框架提供的核心功能之一,主要用来拦截用户的请求。在指定方法的前后,根据业务的需要来执行我们预先设定好的代码。

定义拦截器
实现 HandlerInterceptor 接口,并重写所有方法

preHandle 方法:目标方法执行前执行,返回 true,继续执行后续操作。返回 false:中断后续操作。
postHandle 方法:目标方法执行后执行。
afterCompletion 方法:视图渲染完毕后执行,即最后执行。(前后端分离各自工程化,后端几乎不涉及视图,暂不了解~)
注册配置拦截器
实现 WebMvcConfigurer 接口,重写 addInterceptors 方法

当我们把 preHandle 方法中的返回值改为 false 的时候,再观察运行结果,发现拦截器拦截了请求,没有进行响应。
拦截器详解
拦截器的拦截路径配置
拦截路径,即我们定义的拦截器对那些请求生效。在注册配置拦截器的时候,通过 addPathPatterns() 方法,指定要拦截那些请求,也可以通过 excludePathPatterns() 指定不拦截那些请求。
在上面的 WebConfig 中,我们配置了 /**, 表示拦截所有的请求。
可以添加 excludePathPatterns("/user/login") 设置不拦截登录的请求

还有一些其他的常见配置:

拦截器的执行流程

-
添加拦截器后,在执行控制器层的代码之前,请求会先被拦截器拦截住,执行 preHandle 方法。这个方法会返回一个布尔类型的值,如果返回 true,则放行本次操作,执行 controller 层代码。如果返回 false,则不会放行,请求在此处被拦截,controller 层的代码不会被执行。
-
controller 当中的方法执行完毕后,再回过来执行 postHandle 以及 afterCompletion 方法。执行完毕之后,最终给浏览器响应数据。
登录校验
通过拦截器,我们补充完善图书管理系统中的登录校验功能。
定义拦截器
从 Session 中获取用户信息,如果 session 中不存在,则返回 false,并设置状态码为 401。

注册配置器

还有另一种形式的写法:

此时就可以将前面我们的登录校验代码删除掉了:

测试
此时打开 postman,在未登录情况下,输入 :http://127.0.0.1:8080/book/getListByPage

http 状态码为 401
当我们进行登录操作后 http://127.0.0.1:8080/user/login?name=admin&password=admin
再访问 http://127.0.0.1:8080/book/getListByPage
数据进行了返回~
DispatcherServlet 源码
我们的服务器启动日志中: 有如下日志

Tomcat 启动后,有一个核心的类 DispatcherServlet,用来控制程序的执行程序。
所有的请求,都会先进入到 DispathcherServlet 中,执行 doDispatch 调度方法。如果有拦截器,会先执行拦截器 preHandle() 方法的代码,如果 preHandle() 返回 true,继续访问 controller 中的方法。controller 中的方法执行完毕后,再执行 postHandle 和 atterCompletion,返回给 DispatcherServlet,最终给浏览器响应数据。

初始化
前面我们研究过 Servlet 的生命周期,知道起生命周期为 init service destory。
DispatcherServlet 根据其名字也可知道,是一个 Servlet,根据代码我们可以知道,还是 extend 的 Sevlet
DispatcherServlet 的初始化方法 init 是在其父类 HttpServletBean 中实现的。
主要作用是加载 web.xml 中的 DispathcerServlet 的配置,并调用子类初始化

父类 HttpServletBean 中的 init 方法源码如下:
为了方便阅读源码,我大致将其分为几个区域。

区域 1:源码的实现人员对该方法做的总的注释。
我们可以通过翻译软件了解:
"将配置参数映射到此 Servlet 的 bean 属性,并且调用子类进行初始化"
"ServletException:如果 bean 属性无效(或者缺少必要的属性),或者子类初始化失败,抛出该异常"
区域 2:对下面的代码操作进行一个大致的解释:
从初始化参数中设置 bean 属性。(也就是总的注释中的第一句:将配置参数映射到 Servlet 的 bean 属性)
在源码中,我们可以看到记载了 ServletConfigPropertyValues 静态内部类,获得了一些参数,使用 BeanWrapper PropertyAccessorFactory 来构造了一个 DispatcherServlet,并对 DispatcherServlet 进行了一些属性的赋值。
区域 4:catch 中捕获到的一些异常,非主线流程,暂且可不看~
区域 5:译为让子类自行完成他们可能的初始化操作
区域 6:另一个初始化的方法
跳转到另一个初始化方法后:

方法上面的注释:子类可以重写这个方法以实现自定义初始化。在调用这个方法之前,此 Servlet 的所有 bean 属性都被设置。
然后这个方法直接看是空方法,我们点击左边的图标, 既可以找到实现类。

总注释翻译为:覆盖了 HttpServletBean 中的方法,在设置任何 bean 属性之后调用。创建此 Servlet 的 WebApplicationContext。
即 HttpServletBean 的 inti 中调用了 initServletBean,initServletBean 在 FrameworkServlet 类中实现,主要作用是建立了 WebApplicationContext 容器(上下文),并加载 SpringMVC 配置文件中定义的 Bean 到该容器,最后将该容器添加到 ServletContext 中(红色框中的代码)
紫色框中的打印日志,正是我们控制台打印出来的日志


源码追踪技巧:
阅读框架源码时,一定抓住关键点,找到核心主线流程。
切忌从头到尾一行一行看。应该先从宏观上对整个流程或者整个原理有一个了解。
我们再研究上面红色框中的代码:initWebApplicationContext 方法

分开来看:

这段代码。
先判断"注入的 web 应用上下文(this.webApplicationContext)"是否存在,存在就使用,赋值给 wac
接着检查这个上下文是否属于可配置的 Web 应用上下文类型(ConfigurableWebApplicationContext),Configurable 为可配置的,且还未激活,!isActive,即还没有执行刷新流程(刷新流程我们下面会将)
如果这个上下文没有显示的上下文,就将根目录(rootContext) 上下文设置为它的父
最后调用 configureAndRefreshWebApplicationContext ,最这个 web 应用上下文做配置并触发刷新。

如果注入的 web 应用上下文不存在,执行 find 和 create 操作

if 中的 !refreshEventReceived 条件,判断是否收到刷新事件,如果没有,就执行 if 中的代码。
此时这里两种情况:要么上下文是不支持刷新的 ConfigurableApplicationContext,要么构造注入的时候,已经被刷新过了。
此时就要手动触发 onRefresh 方法,来完成初始化操作。

如果需要发布上下文,就把 Spring 的应用上下文(wac)放到 Servlet 中作为一个属性。这样 Web 应用中的其他组件就能方便的获取到这个上下文了。
再回过头看 onRefresh 方法

这个方法属于模板方法,即支持子类重写以自定义逻辑。
点击左侧的小按钮找到实现类

实现类中调用了 initStrategies 方法,去初始化各种功能策略。

在 initStrategies 中进行了 9 大组件的初始化。
initMultipartResolver 处理文件上传的工具
initLocalResolver 处理语言 / 地区的工具
initHandlerMappings 处理请求映射到控制器的工具
initHandlerDapters 适配控制器逻辑的工具
initHandlerExceptionResolver 处理异常的工具
initrequestToViewNameTranslator 请求转换为视图名的工具
initViewResolvers 将逻辑视图转换为实际页面的工具
initFlashMapManager 管理重定向时数据传递的工具
如果没有配置相应的组件,则在 DispatcherServlet.properties 中有配置默认的策略


处理请求(核心)
DispatcherServlet 在接收到请求之后,执行 doDispatch 调度方法,再将请求转给 Controller
以下为 doDispatch 方法的具体实现

HandlerAdapter 在 Spring MVC 中使用了适配器模式,适配器模式能将两个不兼容的接口通过一定的方式使之兼容。
HandlerAdatper 用于支持不同类型的处理器(Controller,HttpRequestHandler 或者 Servlet 等)让他们能够适配统一的请求处理流程。这样,Spring MVC 可以根据一个统一的接口来处理来自各种处理器的请求。
常见源码术语:

适配器模式
HandlerAdapter 在 Spring MVC 中使用了适配器模式
将一个类的接口,转换成客户期望可以使用的另一个接口,让原本接口不兼容的类可以合作无间,
即:若目标类不能直接使用,通过一个新的类,重新包装以下,适配调用方使用。使两个不兼容的接口通过一定的方式使之兼容。
如下:接口 A 和 接口 B 因参数类型,个数等原因不匹配

可以使用适配器的方式,使之兼容

角色:
Target:目标接口(可以是抽象类或接口),客户希望直接用的接口
Adaptee:适配者,与 Target 不兼容
Adapter:适配器类,此模式的核心,通过继承或者引用适配者对象,将其转变为目标接口
client:需要使用适配器的对象
适配器模式的实现
我们前面打印日式使用的 slf4j 就使用了适配器模式。 slf4j 提供了一系列打印日志的 api,但底层调用的是 log4j 或者 logback 来打日志,我们作为 client,只需要调用 slf4j 的 api 即可~
如下:现在有 Log4j

slg4j 接口:

当 client 想通过 Slf4jApi 直接调用 log4jLog 接口,因为方法名不同,无法直接调用
中间可以加一层适配器

在适配器中,实现了 Slaf4jApi,同时又持有了 Log4j 的实例
在使用中:

客户端想要使用 Slf4jApi 的方式打印日志。
先创建 Log4j 实例,再将 Log4j 实例传给 Log4jSlf4jAdapter 适配器,让适配器持有被适配者。
客户端再通过 Slf4jApi 接口的 log 方法发起调用(客户端期望的接口形式)
在适配器内部中,把 Slf4jApi 的 log 调用,转发给了持有的 Log4j 实例的 log4jLog 方法

适配器模式是一种补偿模式,用来补救设计上的缺陷,协调接口不兼容问题。