Spring 统一功能处理 - 拦截器与适配器

目录

拦截器简介

定义拦截器

注册配置拦截器

拦截器详解

拦截器的拦截路径配置

拦截器的执行流程

登录校验

定义拦截器

注册配置器

测试

[DispatcherServlet 源码](#DispatcherServlet 源码)

初始化

处理请求(核心)

适配器模式

适配器模式的实现


拦截器简介

前面图书管理系统案例的完善中,我们实现了强制登录的功能,后端程序 Session 来判断用户是否登录。但实现过程还是比较麻烦的,且有大量冗余重复的代码。

有一种方法,可以统一拦截所有的请求,并进行 Session 校验 --> 拦截器。

拦截器:是 Spring 框架提供的核心功能之一,主要用来拦截用户的请求。在指定方法的前后,根据业务的需要来执行我们预先设定好的代码。

定义拦截器

实现 HandlerInterceptor 接口,并重写所有方法

preHandle 方法:目标方法执行前执行,返回 true,继续执行后续操作。返回 false:中断后续操作。

postHandle 方法:目标方法执行后执行。

afterCompletion 方法:视图渲染完毕后执行,即最后执行。(前后端分离各自工程化,后端几乎不涉及视图,暂不了解~)

注册配置拦截器

实现 WebMvcConfigurer 接口,重写 addInterceptors 方法

当我们把 preHandle 方法中的返回值改为 false 的时候,再观察运行结果,发现拦截器拦截了请求,没有进行响应。

拦截器详解

拦截器的拦截路径配置

拦截路径,即我们定义的拦截器对那些请求生效。在注册配置拦截器的时候,通过 addPathPatterns() 方法,指定要拦截那些请求,也可以通过 excludePathPatterns() 指定不拦截那些请求。

在上面的 WebConfig 中,我们配置了 /**, 表示拦截所有的请求。

可以添加 excludePathPatterns("/user/login") 设置不拦截登录的请求

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

拦截器的执行流程

  1. 添加拦截器后,在执行控制器层的代码之前,请求会先被拦截器拦截住,执行 preHandle 方法。这个方法会返回一个布尔类型的值,如果返回 true,则放行本次操作,执行 controller 层代码。如果返回 false,则不会放行,请求在此处被拦截,controller 层的代码不会被执行。

  2. 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 方法

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

相关推荐
andongni20312 小时前
Spring Boot基础应用开发与部署
java·spring boot·后端
lupai12 小时前
增值税发票 OCR 识别 API 新手接入指南
java·前端·ocr
TELL52112 小时前
Sonar质量门禁
java
java修仙传12 小时前
从网页禅道到 AI 能调用的工具:我的禅道 MCP 实现思路分享
java·人工智能·python·ai应用·mcp开发
HAYDENR12 小时前
依赖数据迁移工具做增量同步有哪些易错点?调整数据迁移工具策略怎么保证断点续传可靠?
java·大数据·数据库
IT利刃出鞘12 小时前
SpringBoot--解决@Valid放在接口的List上时无效的问题
java·spring
mqiqe13 小时前
AgentScope Java Harness:2. 上下文压缩:让长期 Agent 永不“失忆“
java·开发语言
梦想的旅途213 小时前
企业微信API实战:Python自动化消息推送
java·前端·python·自动化·企业微信
YOU OU13 小时前
RabbitMQ运维
java·rabbitmq·java-rabbitmq
mldong13 小时前
流程定义设计:一份 LogicFlow JSON 全解
java·架构