详解拦截器(interceptor)

news/2024/7/2 23:16:52

目录

1.拦截器概述

2.拦截器的基本使用 

3.拦截路径配置

4.拦截器执行流程

5.实现登录校验

5.1定义拦截器

5.2注册配置拦截器

6.DispatcherServlet源码分析

6.1初始化

6.2处理请求(核心) 

7.适配器模式


1.拦截器概述

拦截器(Interceptor)是一种软件设计模式,用于在程序执行过程中拦截或截取特定的操作或事件。它可以在操作发生之前、之后或替代操作本身进行自定义的处理。

之前完成强制登录的功能是根据Session来判断用户是否登录, 但是实现方法是比较烦的

  需要修改每个接口的处理逻辑

  需要修改每个接口的返回结果

•  接口定义修改, 前端代码也需要跟着修改

有没有更简单的办法, 统⼀拦截所有的请求, 并进⾏Session校验呢, 学习⼀种新的解决办: 截器

 

2.拦截器的基本使用 

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

1.  定义拦截器

2.  注册配置拦截器

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

 @Slf4j
 @Component
 public class LoginInterceptor implements HandlerInterceptor { 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse res
         log.info("LoginInterceptor ⽬标⽅法执⾏前执⾏ ..");
         return true; 
     }

     @Override
     public void postHandle(HttpServletRequest request, HttpServletResponse respo
         log.info("LoginInterceptor ⽬标⽅法执⾏后执⾏");
     } 
     @Override
     public void afterCompletion(HttpServletRequest request, HttpServletResponse
         log.info("LoginInterceptor 视图渲染完毕后执⾏ ,最后执⾏");
     }
 }

preHandle()方法:标方法执行前执行. 返回true: 继续执行后续操作;   false:中断后续操作

postHandle()方法: 目标方法执行后执行

afterCompletion()方法:视图渲染完毕后执行 ,最后执行(暂不关注)

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

 @Configuration
 public class WebConfig implements WebMvcConfigurer {
     //⾃定义的拦截器对象 
     @Autowired
     private LoginInterceptor loginInterceptor;

     @Override
     public void addInterceptors(InterceptorRegistry registry) {
        //注册⾃定义拦截器对象
         registry.addInterceptor(loginInterceptor)
                 .addPathPatterns("/**");//设置拦截器拦截的请求路径
                                           // (/** 表⽰拦截所有方法)
     }
 }

启动服务, 试试访问任意请求, 观察后端日志

可以看到preHandle 方法执行之后就放行了, 始执行目标方法, 目标方法执行完成之后执行postHandleafterCompletion方法. 

我们把拦截器中preHandle方法的返回值改为false, 再观察运行结果

可以看到, 拦截器拦截了请求, 没有进行响应. 

3.拦截路径配置

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

我们在注册配置拦截器的时候, 通过 addPathPatterns()方法指定要拦截哪些请求

也可以通过excludePathPatterns()指定不拦截哪些请求

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

 @Configuration
 public class WebConfig implements WebMvcConfigurer {
     //⾃定义的拦截器对象 
     @Autowired
     private LoginInterceptor loginInterceptor;

     @Override
     public 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 等文件).

4.拦截器执行流程

正常的调用顺序:

有了拦截器之后 ,会在调用Controller 之前进行相应的业务处理 ,执行的流程如下图

添加拦截器后, 执行Controller的⽅法之前, 请求会先被拦截器拦截住. 执行preHandle()方法,这个方法需要返回⼀个布尔类型的值. 如果返回true, 就表示放行本次操作, 继续访问controller中的. 如果返回false ,则不会放行(controller中的方法也不会执行).

controller当中的方法执行完毕后 ,再回来执行postHandle()方法以及afterCompletion()方法

5.实现登录校验

学习拦截器的基本操作之后 ,接下来我们完成通过拦截器来完成登录校验功能

5.1定义拦截器

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

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("登录拦截器校验...");

        HttpSession session = request.getSession();
        UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
        if (userInfo!=null && userInfo.getId()>=0){
            return true;
        }
        response.setStatus(401);//401 表示未认证登录
        return false;
    }
}

5.2注册配置拦截器

 @Configuration
 public class WebConfig implements WebMvcConfigurer {
     //⾃定义的拦截器对象 
     @Autowired
     private LoginInterceptor loginInterceptor;

     @Override
     public 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 {
    @Autowired
    private LoginInterceptor loginInterceptor;

    private static List<String> excludePath = Arrays.asList(
            "/user/login",
            "/css/**",
            "/js/**",
            "/pic/**",
            "/**/*.html",
            "/test/**"
    );

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")//  /**表示给所有方法添加拦截器
                .excludePathPatterns(excludePath);
    }
}

运行程序, 通过Postman进行测试:

不登录直接进入图书列表页面

http://127.0.0.1:8080/book/getListByPage

观察返回结果: http状态码401 也可以通过Fiddler抓包观察

进行登录

http://127.0.0.1:8080/user/login?name=admin&password=admin

再次查看图书列表 

数据进行了返回

6.DispatcherServlet源码分析

观察我们的服务启动日志:

 Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.

所有请求都会先进到DispatcherServlet,执⾏doDispatch调度方法. 如果有拦截器, 会先执行拦截器的preHandle()方法的代码,如果preHandle()返回true,则继续访问Controller中的方法,Controller当中的方法执行完毕后,再回过来执行postHandle()和aferCompletion()返回给DispatcherServlet,最终给浏览器返回数据

 

6.1初始化

DispatcherServlet的初始化方法init()在其父类HttpServletBean中实现的,主要作用是加载

web.xml DispatcherServlet 的 配置, 并调用⼦类的初始化.

web.xmlweb项⽬的配置⽂件 ,⼀般的web⼯程都会⽤到web.xml来配置 ,主要⽤来配置

Listener Filter Servlet, Spring框架从3.1版本开始⽀持Servlet3.0, 并且从3.2版本开始通过配置 DispatcherServlet, 实现不再使用web.xml

 init() 具体代码如下:

	@Override
	public final void init() throws ServletException {

		// Set bean properties from init parameters.
		PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
		if (!pvs.isEmpty()) {
			try {
				BeanWrapper 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) {
				if (logger.isErrorEnabled()) {
					logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
				}
				throw ex;
			}
		}

		// Let subclasses do whatever initialization they like.
		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 {
			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的容器

 protected WebApplicationContext initWebApplicationContext() {
    //...
    if (!this.refreshEventReceived) {
      //初始化Spring MVC
       synchronized (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 ob
  */
 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中有配置默认的策略, 大致了解即可)

1.  初始化⽂件上传解析器MultipartResolver:从应用上下⽂中获取名称为multipartResolverBean ,如果没有名为multipartResolverBean ,则没有提供上传⽂件的解析器

2.  初始化区域解析器LocaleResolver:从应⽤上下⽂中获取名称为localeResolverBean ,如果没有这个Bean ,则默认使⽤AcceptHeaderLocaleResolver作为区域解析器

3.  初始化主题解析器ThemeResolver:从应⽤上下⽂中获取名称为themeResolverBean ,如果没有这个Bean ,则默认使⽤FixedThemeResolver作为主题解析器

4.  初始化处理器映射器HandlerMappings:处理器映射器作⽤ ,1 通过处理器映射器找到对应的  处理器适配器 ,将请求交给适配器处理;2)缓存每个请求地址URL对应的位置(Controller.xxx  ⽅法 如果在ApplicationContext发现有HandlerMappings ,则从ApplicationContext中获取 到所有的HandlerMappings ,并进⾏排序;如果在ApplicationContext中没有发现有处理器映射 ,则默认BeanNameUrlHandlerMapping作为处理器映射器

5.  初始化处理器适配器HandlerAdapter:作用是通过调用具体的⽅法来处理具体的请求;如果在 ApplicationContext发现有handlerAdapter ,则从ApplicationContext中获取到所有的

HandlerAdapter ,并进⾏排序;如果在ApplicationContext中没有发现处理器适配器 ,则不设置 异常处理器 ,则默认SimpleControllerHandlerAdapter作为处理器适配器

6.  初始化异常处理器解析器HandlerExceptionResolver:如果在ApplicationContext发现有

handlerExceptionResolver ,则从ApplicationContext中获取到所有的

HandlerExceptionResolver ,并进⾏排序;如果在ApplicationContext中没有发现异常处理器解析器 ,则不设置异常处理器

7.  初始化RequestToViewNameTranslator:其作⽤是从Request中获取viewName ,从 ApplicationContext发现有viewNameTranslatorBean ,如果没有 ,则默认使⽤

DefaultRequestToViewNameTranslator

8.  初始化视图解析器ViewResolvers:先从ApplicationContext中获取名为viewResolverBean 如果没有 ,则默认InternalResourceViewResolver作为视图解析器

9.  初始化FlashMapManager:其作⽤是⽤于检索和保存FlashMap(保存从⼀个URL重定向到另⼀URL时的参数信息),ApplicationContext发现有flashMapManagerBean ,如果没有 ,则默认使用DefaultFlashMapManager

6.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 {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				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;
				}

				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					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(); i++) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			if (!interceptor.preHandle(request, response, this.handler)) {
				triggerAfterCompletion(request, response, null);
				return false;
			}
			this.interceptorIndex = i;
		}
		return true;
	}

在applyPreHandle中会获取所有的拦截器HandlerInterceptor,并执行拦截器中的preHandle方法,这就会和上面我们定义的拦截器对应上了,如下图所示:

如果拦截器返回true, 整个方法就返回true, 继续执行后续逻辑处理 如果拦截器返回fasle, 则中断后续操作 

7.适配器模式

HandlerAdapter Spring MVC 中使⽤了适配器模式 适配器模式定义

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

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

⽐如下面两个接口 , 本⾝是不兼容的(参数类型不⼀样, 参数个数不⼀样等等)

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

适配器模式角色

•  Target: 目标接口 (可以是抽象类或接口), 客户希望直接⽤的接口

•  Adaptee: 适配者, 但是与Target不兼容

•  Adapter: 适配器类, 此模式的核心 . 通过继承或者引⽤适配者的对象, 把适配者转为目标接口

  client: 需要使用适配器的对象 适配器模式的实现

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

 //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;
     } 

     @Override
     public 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打印⽇志"); 
     }
 }

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

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

设计模式的使⽤⾮常灵活, ⼀个项⽬中通常会含有多种设计模式.

适配器模式应用场景

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

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

 


http://lihuaxi.xjx100.cn/news/2215486.html

相关文章

Android集成高德天气API 天气预报

1.新建工程项目WeatherForecast。 2.在AndroidManifest文件中添加网络访问相关权限。 <uses-permission android:name"android.permission.INTERNET"/> 3.设计页面布局activity_main.xml&#xff0c;界面效果如图所示。 4.注册高德开放平台&#xff0c;查阅…

vue实现图片预览

在 Vue 中实现图片预览功能&#xff0c;通常涉及监听文件输入的变化&#xff0c;并在用户选择文件后&#xff0c;使用 FileReader API 来读取文件内容&#xff0c;然后显示这个内容作为图片的预览。以下是一个简单的 Vue 组件示例&#xff0c;它实现了图片预览功能&#xff1a;…

C# OpenCvSharp 逻辑运算-bitwise_and、bitwise_or、bitwise_not、bitwise_xor

bitwise_and 函数 🤝 作用或原理: 将两幅图像进行与运算,通过逻辑与运算可以单独提取图像中的某些感兴趣区域。如果有掩码参数,则只计算掩码覆盖的图像区域。 示例: 在实际应用中,可以用 bitwise_and 来提取图像中的某些部分。例如,我们可以从图像中提取出一个特定的颜…

IDEA快速入门03-代码头统一配置

三、代码规范配置 3.1 文件头和作者信息 配置入口&#xff1a;依次打开 File -> Settings -> Editor -> File and Code Templates。 Class /*** Copyright (C) 2020-${YEAR}, Glodon Digital Supplier & Purchaser BU.* * All Rights Reserved.*/ #if (${PACKA…

Ubuntu server 24 (Linux) 新增磁盘 lvm 动态扩容磁盘空间

1 新增一块硬盘 #查看 sudo fdisk -l #重新分区&#xff0c;转换成lvm类型 sudo fdisk /dev/sdb 2 查看磁盘 df -h3 lvm 配置 #查看lvm逻辑卷 sudo lvdisplay #创建物理卷 sudo pvcreate /dev/sdb1 #扩展卷组 sudo vgextend ubuntu-vg /dev/sdb1 #扩展逻辑卷 sudo lvexte…

ndk-build

目录 一、运行ndk二、Application.mk三、Android.mk3.0、模块名定义3.1、源码3.2、头文件搜索3.3、头文件导出3.4、编译、链接flags配置3.5、产物类型 四、模块依赖处理1、源码模块依赖2、预编译库依赖 一、运行ndk NDK_APPLICATION_MK&#xff1a;指定Application.mk文件所在…

QUIC 和 TCP: 深入解析为什么 QUIC 更胜一筹

引言 在过去的三十年里&#xff0c;HTTP&#xff08;超文本传输协议&#xff09;一直是互联网的支柱。我们可以通过 HTTP 浏览网页、下载文件、流式传输电影等。这一协议随着时间的推移已经得到了重大改进。 HTTP 协议是一个应用层协议&#xff0c;它基于 TCP&#xff08;传输…

我主编的电子技术实验手册(07)——串联电路

本专栏是笔者主编教材&#xff08;图0所示&#xff09;的电子版&#xff0c;依托简易的元器件和仪表安排了30多个实验&#xff0c;主要面向经费不太充足的中高职院校。每个实验都安排了必不可少的【预习知识】&#xff0c;精心设计的【实验步骤】&#xff0c;全面丰富的【思考习…