当前位置: 首页 > news >正文

【统一功能处理】从入门到源码:拦截器学习指南(含适配器模式深度解读)

文章目录

  • 1. 拦截器
    • 1.1 拦截器快速入门
    • 1.2 拦截器详解
      • 1.2.1 拦截路径
      • 1.2.2 拦截器执行流程
    • 1.3 登录校验
      • 1.3.1 定义拦截器
      • 1.3.2 注册配置拦截器(两种写法)
    • 1.4 DispatcherServlet 源码分析
      • 1.4.1 初始化(了解)
      • 1.4.2 处理请求(核心)
      • 1.4.3 适配器模式
        • 适配器模式定义
        • 适配器模式角色
        • 适配器模式的实现
        • 适配器模式应用场景

1. 拦截器

图书管理系统中我们完成了强制登录的功能,后端程序根据Session来判断用户是否登录,但是实现方法是比较麻烦的

  • 需要修改每个接口的处理逻辑
  • 需要修改每个接口的返回结果
  • 接口定义修改,前端代码也需要跟着修改

有没有更简单的办法,统一拦截所有的请求,并进行Session校验呢,这里我们讨论一种新的解决办法:拦截器

1.1 拦截器快速入门

什么是拦截器?

拦截器是Spring框架提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码.
也就是说,允许开发人员提前预定义一些逻辑,在用户的请求响应前后执行.也可以在用户请求前阻止其执行.

在拦截器当中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息.如果有就可以放行,如果没有就进行拦截.

在这里插入图片描述

比如我们去银行办理业务,在办理业务前后,就可以加一些拦截操作
办理业务之前,先取号,如果带身份证了就取号成功
业务办理结束,给业务办理人员的服务进行评价.
这些就是"拦截器"做的工作.

  • 拦截器的基本使用.

拦截器的使用步骤分为两步:

  1. 定义拦截器
  2. 注册配置拦截器

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

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("目标方法执行前执行...");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("post...目标方法执行后执行");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("after...视图渲染完毕后执行,最后执行");}
}
  • preHandle()方法:目标方法执行前执行. 返回true: 继续执行后续操作; 返回false: 中断后续操作.
  • postHandle()方法:目标方法执行后执行
  • afterCompletion()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图,暂不了解)

注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法

@Configuration
public class WebConfig implements WebMvcConfigurer {//自定义拦截对象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径//(/** 表示拦截所有请求)}
}

1.2 拦截器详解

拦截器的入门程序完成之后,接下来我们来介绍拦截器的使用细节。拦截器的使用细节我们主要介绍两个部分:

1. 拦截器的拦截路径配置
2. 拦截器实现原理

1.2.1 拦截路径

拦截路径是指我们定义的这个拦截器,对哪些请求生效.

我们在注册配置拦截器的时候,通过 addPathPatterns() 方法指定要拦截
哪些请求.也可以通过 excludePathPatterns() 指定不拦截哪些请求.
上述代码中,我们配置的是 /** ,表示拦截所有的请求.

比如用户登录校验,我们希望可以对除了登录之外所有的路径生效.

@Configuration
public class WebConfig implements WebMvcConfigurer {//自定义拦截对象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象//设置拦截器拦截的请求路径//(/** 表示拦截所有请求)registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login");//设置拦截器拦截的请求路径}
}

在拦截器中除了可以设置 /** 拦截所有资源外,还有一些常见拦截路径设置:

拦截路径含义举例
/*一级路径能匹配/user/book/login,不能匹配 /user/login
/**任意级路径能匹配/user/user/login/user/reg
/book/*/book下的一级路径能匹配/book/addBook,不能匹配/book/addBook/1/book
/book/**/book下的任意级路径能匹配/book/book/addBook/book/addBook/2,不能匹配/user/login

以上拦截规则可以拦截此项目中的使用 URL,包括静态文件(图片文件, JS 和 CSS 等文件).

1.2.2 拦截器执行流程

正常的调用顺序:

在这里插入图片描述

有了拦截器之后,会在调用Controller 之前进行相应的业务处理

在这里插入图片描述

  1. 添加拦截器后,执行Controller的方法之前,请求会先被拦截器拦截住.执行 preHandle() 方法,这个方法需要返回一个布尔类型的值.如果返回true,就表示放行本次操作,继续访问controller中的方法.如果返回false,则不会放行(controller中的方法也不会执行).
  2. controller当中的方法执行完毕后,再回过来执行 postHandle() 这个方法以及 afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据.

1.3 登录校验

学习拦截器的基本操作之后,接下来我们需要完成最后一步操作:通过拦截器来完成图书管理系统中的登录校验功能

1.3.1 定义拦截器

从session中获取用户信息,如果session中不存在,则返回false,并设置http状态码为401,否则返回true.

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession(false);if(session != null && session.getAttribute(Constants.SESSION_USER_KEY )!= null){return true;}response.setStatus(401);return false;}
}

http状态码401: Unauthorized

Indicates that authentication is required and was either not provided or has failed. If the request already included authorization credentials, then the 401 status code indicates that those credentials were not accepted.

中文解释:未经过认证.指示身份验证是必需的,没有提供身份验证或身份验证失败.如果请求已经包含授权凭据,那么401状态码表示不接受这些凭据。

1.3.2 注册配置拦截器(两种写法)

@Configuration
public class WebConfig implements WebMvcConfigurer {//自定义拦截对象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login")//设置拦截器拦截的请求路径.excludePathPatterns("/**/*.js")//排除前端静态资源.excludePathPatterns("/**/*.css").excludePathPatterns("/**/*.png").excludePathPatterns("/**/*.html");}
}

或者可以写成

@Configuration
public class WebConfig implements WebMvcConfigurer {//自定义拦截对象@Autowiredprivate LoginInterceptor loginInterceptor;private List<String> excludePaths = Arrays.asList("/user/login","/**/*.js","/**/*.css","/**/*.png","/**/*.html");@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册registry.addInterceptor(loginInterceptor).addPathPatterns("/**") //拦截所有请求.addPathPatterns(excludePaths);//设置拦截器排除拦截的路径}

运行程序,通过postman进行测试

在这里插入图片描述
观察结果:http状态码 401

或者 fiddler 抓包

在这里插入图片描述

登录之后再次查看图书列表

在这里插入图片描述

1.4 DispatcherServlet 源码分析

在这里插入图片描述

观察我们的服务启动日志:
在这里插入图片描述

当Tomcat启动之后, 有一个核心的类DispatcherServlet,它来控制程序的执行顺序.

所有请求都会先进到DispatcherServlet,执行doDispatch 调度方法. 如果有拦截器, 会先执行拦截器 preHandle() 方法的代码

如果 preHandle() 返回true, 继续访问controller中的方法. controller当中的方法执行完毕后,再回过来执行 postHandle()afterCompletion() ,返回给DispatcherServlet, 最终给浏览器响应数据.

在这里插入图片描述

1.4.1 初始化(了解)

DispatcherServlet的初始化方法 init() 在其父类 HttpServletBean 中实现的.

主要作用是加载 web.xml 中 DispatcherServlet 的配置,并调用子类的初始化.
web.xml是web项目的配置文件,一般的web工程都会用到web.xml来配置,主要用来配置Listener,Filter,Servlet等, Spring框架从3.1版本开始支持Servlet3.0,并且从3.2版本开始通过配置DispatcherServlet,实现不再使用web.xml

init() 具体代码如下:

@Override
public final void init() throws ServletException {try {// ServletConfigPropertyValues 是静态内部类,使用 ServletConfig 获取 web.xml 中配置的参数PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);// 使用 BeanWrapper 来构造 DispatcherServletBeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));initBeanWrapper(bw);bw.setPropertyValues(pvs, true);} catch (BeansException ex) {}// 让子类实现的方法,这种在父类定义在子类实现的方式叫做模版方法模式initServletBean();
}

在这里插入图片描述

在 HttpServletBean 的 init() 中调用了 initServletBean() , 它是在 FrameworkServlet 类中实现的,主要作用是建立 WebApplicationContext 容器 (有时也称上下文),并且加载 SpringMVC 配置文件中定义的 Bean到该容器中,最后将该容器添加到 ServletContext 中.下面是 initServletBean() 的具体代码:

@Override
protected final void initServletBean() throws ServletException {getServletContext().log("Initializing Spring " + getClass().getSimpleName()+ " '" + getServletName() + "'");if (logger.isInfoEnabled()) {logger.info("Initializing Servlet '" + getServletName() + "'");}long startTime = System.currentTimeMillis();try {//创建ApplicationContext容器this.webApplicationContext = initWebApplicationContext();initFrameworkServlet();}catch (ServletException | RuntimeException ex) {logger.error("Context initialization failed", ex);throw ex;}if (logger.isDebugEnabled()) {String value = this.enableLoggingRequestDetails ?"shown which may lead to unsafe logging of potentially sensitive data" :"masked to prevent unsafe logging of potentially sensitive data";logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +"': request parameters and headers will be " + value);}if (logger.isInfoEnabled()) {logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");}
}

该源码打印的日志,也正是控制台打印的日志

源码跟踪技巧:
在阅读框架源码的时候,一定要抓住关键点,找到核心流程.
切忌从头到尾一行一行代码去看,一个方法的去研究,一定要找到关键流程,抓住关键点,先在宏观上对整个流程或者整个原理有一个认识,有精力再去研究其中的细节.

初始化web容器的过程中,会通过onRefresh 来初始化SpringMVC的容器

```java
protected WebApplicationContext initWebApplicationContext() {//...if (!this.refreshEventReceived) {//初始化Spring MVCsynchronized (this.onRefreshMonitor) {onRefresh(wac);}}return wac;
}@Override
protected void onRefresh(ApplicationContext context) {initStrategies(context);
}/*** Initialize the strategy objects that this servlet uses.* <p>May be overridden in subclasses in order to initialize further strategy* objects.*/
protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);
}

在initStrategies()中进行9大组件的初始化,如果没有配置相应的组件,就使用默认定义的组件(在DispatcherServlet.properties中有配置默认的策略,大致了解即可)

方法initMultipartResolver、initLocaleResolver、initThemeResolver、initRequestToViewNameTranslator、initFlashMapManager的处理方式几乎都一样(1.2.3.7.8,9),从应用文中取出指定的Bean,如果没有,就使用默认的.
方法initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers的处理方式几乎都一样(4,5,6)

1.4.2 处理请求(核心)

DispatcherServlet 接收到请求后,执行doDispatch 调度方法,再将请求转给Controller.

我们来看doDispatch 方法的具体实现

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {try {ModelAndView mv = null;Object dispatchException = null;try {processedRequest = this.checkMultipart(request);multipartRequestParsed = processedRequest != request;mappedHandler = this.getHandler(processedRequest);if (mappedHandler == null) {this.noHandlerFound(processedRequest, response);return;}HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());String method = request.getMethod();boolean isGet = HttpMethod.GET.matches(method);if (isGet || HttpMethod.HEAD.matches(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {return;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}this.applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);} catch (Exception var20) {dispatchException = var20;} catch (Throwable var21) {dispatchException = new ServletException("Handler dispatch failed: " + String.valueOf(var21), var21);}this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);} catch (Exception var22) {triggerAfterCompletion(processedRequest, response, mappedHandler, var22);} catch (Throwable var23) {triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + String.valueOf(var23), var23));}} finally {if (asyncManager.isConcurrentHandlingStarted()) {if (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}asyncManager.setMultipartRequestParsed(multipartRequestParsed);} else if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {this.cleanupMultipart(processedRequest);}}}

在这里插入图片描述
在这里插入图片描述
HandlerAdapter 在 Spring MVC 中使用了适配器模式,下面详细再介绍

适配器模式,也叫包装器模式.简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方使用.把两个不兼容的接口通过一定的方式使之兼容.

HandlerAdapter 主要用于支持不同类型的处理器(如 Controller、HttpRequestHandler 或者 Servlet等),让它们能够适配统一的请求处理流程。这样,Spring MVC 可以通过一个统一的接口来处理来自各种处理器的请求.

从上述源码可以看出在开始执行 Controller 之前,会先调用预处理方法 applyPreHandle,而 applyPreHandle 方法的实现源码如下:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {// 获取项目中使用的拦截器 HandlerInterceptorHandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);if (!interceptor.preHandle(request, response, this.handler)) {this.triggerAfterCompletion(request, response, (Exception)null);return false;}}return true;
}

在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor ,并执行拦截器中的 preHandle 方法,这样就会咱们前面定义的拦截器对应上了

在这里插入图片描述
如果拦截器返回true,整个流程就返回true,继续执行后续逻辑处理
如果拦截器返回false,则中断后续操作

1.4.3 适配器模式

HandlerAdapter在 Spring MVC 中使用了适配器模式

适配器模式定义

适配器模式,也叫包装器模式.将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间.

简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方使用.把两个不兼容的接口通过一定的方式使之兼容.
比如下面两个接口,本身是不兼容的(参数类型不一样,参数个数不一样等等)

在这里插入图片描述

适配器模式角色
  • Target: 目标接口 (可以是抽象类或接口), 客户希望直接用的接口
  • Adaptee: 适配者, 但是与Target不兼容
  • Adapter: 适配器类, 此模式的核心. 通过继承或者引用适配者的对象, 把适配者转为目标接口
  • client: 需要使用适配器的对象
适配器模式的实现

场景: 前面学习的slf4j就使用了适配器模式, slf4j提供了一系列打印日志的api, 底层调用的是log4j 或者 logback来打日志, 我们作为调用者, 只需要调用slf4j的api就行了.

```java
/*** 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打印日志");}
}

可以看出,我们不需要改变log4j的api,只需要通过适配器转换下,就可以更换日志框架,保障系统的平稳运行.

适配器模式的实现并不在slf4j-core中(只定义了Logger),具体实现是在针对log4j的桥接器项目slf4j-log4j12中

设计模式的使用非常灵活,一个项目中通常会含有多种设计模式.

适配器模式应用场景

一般来说,适配器模式可以看作一种"补偿模式",用来补救设计上的缺陷.应用这种模式算是"无奈之举",如果在设计初期,我们就能协调规避接口不兼容的问题,就不需要使用适配器模式了

所以适配器模式更多的应用场景主要是对正在运行的代码进行改造,并且希望可以复用原有代码实现新的功能.比如版本升级等.

http://www.dtcms.com/a/601024.html

相关文章:

  • linux 解析并生成一个platform_device设备具体过程
  • 编译器使用的开发语言 | 解析编译器的实现原理及其开发语言的选择
  • 佛山企业网站建设流程织梦营销型网站模板
  • 洛谷 P11965:[GESP202503 七级] 等价消除 ← 位运算(异或) + STL map
  • 智慧团建网登录入口移动网站如何优化排名
  • linux drm子系统专栏介绍
  • termux编译opencv给python用
  • 4.子任务四:Hive 安装配置
  • Lua学习记录(3) --- Lua中的复杂数据类型_table
  • 郑州做定制网站的公司南宁有名的seo费用
  • 华为SRv6技术:引领IP网络进入新时代的智能导航系统
  • 视频汇聚平台EasyCVR:构建通信基站“可视、可管、可控”的智慧安防体系
  • 在云手机中云计算的作用都有哪些?
  • 绿盟防火墙机制
  • 查询数据库上所有表用到图片和视频的数据,并记录到excel表
  • MUVERA:让RAG系统中的多向量检索像单向量一样高效
  • 数据分析笔记02:数值方法
  • 没有网站可以做cpa广告么自己怎么做网站优化
  • Spring Boot实现多数据源连接和切换
  • 【架构设计方法论】概念架构:系统设计的指路明灯
  • 将标签格式为xml的数据集按照8:2的比例划分为训练集和验证集
  • 实战派 JMeter 指南:核心功能、并发压测实操与常见问题解决方案
  • 宁晋网站建设地址信息采集平台
  • 17网站一起做网店如何下单下载站用什么网站系统
  • VMware替代 | ZStack ZSphere虚拟化平台金融级高可用能力解析
  • Go语言编译器源码 | 深入解析Go编译器的设计与实现原理
  • Bootstrap4 文字排版
  • 第13节 93年高能所被入侵,开启中国网络安全发展进程
  • [学习笔记] An Introduction to Flow Matching and Diffusion Models
  • 从零开始的Qt开发指南:(三)信号与槽的概念与使用