Bootstrap

Spring-拦截器与过滤器

Spring-拦截器与过滤器

系列文章目录

  1. Spring-IOC
  2. Spring-SpringMVC-原理
  3. Spring-拦截器与过滤器
  4. Spring-SpringMVC-全局异常处理

0x01 摘要

本文主要是作者学习spring中的拦截器的一些简要记录。

0x02 拦截器介绍

2.1 拦截器简介

拦截器是spring中的一个重要概念。他被注册到spring,拦截指定规则的请求,基于回调机制执行。一般来说,拦截器只会拦截action请求,这一点与过滤器不同。
下面贴一张spring web请求的执行流程图:
spring-web请求流程

2.2 拦截器原理

2.2.1 拦截器定义

因为拦截器主要是对controller起作用,所以一般在webapp/WEB-INF下的类似spring-web-servlet.xml这样的配置文件中定义拦截器:

<mvc:interceptors>
   <mvc:interceptor>
       <mvc:mapping path="/main/*"/>
       <bean class="com.chengc.demos.web.demo1.interceptor.FirstInterceptor"/>
   </mvc:interceptor>
   <mvc:interceptor>
       <mvc:mapping path="/main/*"/> 
       <bean class="com.chengc.demos.web.demo1.interceptor.SecondInterceptor"></bean>
   </mvc:interceptor>
   <mvc:interceptor>
       <mvc:mapping path="/main/*"/> 
       <bean class="com.chengc.demos.web.demo1.interceptor.ThirdInterceptor"></bean>
   </mvc:interceptor>
</mvc:interceptors>

2.2.2 拦截器创建

Web容器在初始化DispatcherServlet(父类为FrameworkServlet)时,会创建其独有的XmlWebApplicationContext。在初始化该context,FrameworkServlet.configureAndRefreshWebApplicationContext中有一句wac.refresh()时,会在finishBeanFactoryInitialization(beanFactory)方法中调用AbstractHandlerMapping.initApplicationContext方法:

private final List<Object> interceptors = new ArrayList<Object>();
private final List<MappedInterceptor> mappedInterceptors = new ArrayList<MappedInterceptor>();

protected void initApplicationContext() throws BeansException {
    // 默认空实现
	extendInterceptors(this.interceptors);
	// 找到定义的interceptor
	detectMappedInterceptors(this.mappedInterceptors);
	// 对Interceptors进行初始化
	initInterceptors();
}
  • AbstractHandlerMapping.detectMappedInterceptors
protected void detectMappedInterceptors(List<MappedInterceptor> mappedInterceptors) {
	mappedInterceptors.addAll(
			BeanFactoryUtils.beansOfTypeIncludingAncestors(
					getApplicationContext(), MappedInterceptor.class, true, false).values());
}

该方法就会去初始化所有interceptor,调用他们的<clinit><init>

2.2.3 拦截器调用

先经过过滤器(机会多所有请求进行过滤),然后才会到拦截器。

  • DispatcherServlet会对请求进行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 {
	    // 传给ViewResolver进行视图解析的对象
		ModelAndView mv = null;
		Exception dispatchException = null;

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

			// 为当前请求准备好HandlerExecutionChain
			mappedHandler = getHandler(processedRequest, false);
			if (mappedHandler == null || mappedHandler.getHandler() == null) {
				noHandlerFound(processedRequest, response);
				return;
			}

			// 为当前请求准备好HandlerAdapter
			HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

			// Process last-modified header, if supported by the handler.
			String method = request.getMethod();
			boolean isGet = "GET".equals(method);
			if (isGet || "HEAD".equals(method)) {
				long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
				if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
					return;
				}
			}
            // 调用handler执行链上的interceptor的preHandle方法
			if (!mappedHandler.applyPreHandle(processedRequest, response)) {
				return;
			}

			try {
				// 真正调用handler(Controller)处理请求
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
			}
			finally {
				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}
			}

			applyDefaultViewName(request, mv);
			// 逆序调用handler执行链上的interceptor的postHandle方法
			mappedHandler.applyPostHandle(processedRequest, response, mv);
		}
		catch (Exception ex) {
			dispatchException = ex;
		}
		processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
	}
	catch (Exception ex) {
		triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
	}
	catch (Error err) {
		triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
	}
	finally {
		if (asyncManager.isConcurrentHandlingStarted()) {
			// Instead of postHandle and afterCompletion
			mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
			return;
		}
		// Clean up any resources used by a multipart request.
		if (multipartRequestParsed) {
			cleanupMultipart(processedRequest);
		}
	}
}
2.2.3.1 getHandler

getHandler主要是为当前request创建一个包含了若干路径匹配的interceptor和一个handler(即Controller)的HandlerExecutionChain(执行链)。

  • DispatcherServlet.getHandler
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    // 这里的handlerMappings就包含了最常用的RequestMappingHandlerMapping
	for (HandlerMapping hm : this.handlerMappings) {
		HandlerExecutionChain handler = hm.getHandler(request);
		if (handler != null) {
			return handler;
		}
	}
	return null;
}

这里看看hm.getHandler,在AbstractHandlerMapping.getHandler方法:

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    // 这一步就是根据request的路径找到Controller
	Object handler = getHandlerInternal(request);
	if (handler == null) {
		handler = getDefaultHandler();
	}
	if (handler == null) {
		return null;
	}
	// Bean name or resolved handler?
	// 正常的Controller应该是HandlerMethod类
	if (handler instanceof String) {
		String handlerName = (String) handler;
		handler = getApplicationContext().getBean(handlerName);
	}
	// 获取该Controller的执行链
	return getHandlerExecutionChain(handler, request);
}
  • AbstractHandlerMethodMapping.getHandlerInternal
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    // 请求的相对路径,比如/main/index
	String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
	HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
	// 这一步会真正创建含有目标handler和Method的HandlerMethod对象
	return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
  • AbstractHandlerMapping.getHandlerExecutionChain
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
	HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
			(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
	chain.addInterceptors(getAdaptedInterceptors());

	String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
	// 将我们前面提到的注册的拦截器读取遍历
	for (MappedInterceptor mappedInterceptor : this.mappedInterceptors) {
		if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
		// 如果拦截器的mapping和请求的路径匹配上了,就添加到该Handler的执行链中
			chain.addInterceptor(mappedInterceptor.getInterceptor());
		}
	}
    // 最后得到加上了拦截器的HandlerExecutionChain
	return chain;
}

这里,HandlerChain已经加入了interceptor:

HandlerChain
HandlerChain2

2.2.3.2 getHandlerAdapter

为当前请求创建合适的HandlerAdapter,准备开始执行handler:

一般Controller返回的就是RequestMappingHandlerAdapter

这就是典型的适配器模式,将某个Controller类适配为RequestMappingHandlerAdapter,后续会反射执行其方法。

2.2.3.3 preHandle

这个阶段会调用handler执行链上的interceptor的preHandle方法。

HandlerExecutionChain.applyPreHandle方法,这里就会调用所有符合该请求的路径映射的interceptor的preHandle方法:

  • HandlerExecutionChain.applyPreHandle
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
	if (getInterceptors() != null) {
	    // 遍历执行链上的Interceptor
		for (int i = 0; i < getInterceptors().length; i++) {
			HandlerInterceptor interceptor = getInterceptors()[i];
			// 这里就在调用每个interceptor的preHandle方法了
			if (!interceptor.preHandle(request, response, this.handler)) {
			// interceptor返回true代表给下一个handler,
			// 返回false代表调用链结束,直接调用handlers.afterCompletion方法.
				triggerAfterCompletion(request, response, null);
				return false;
			}
			this.interceptorIndex = i;
		}
	}
	return true;
}
2.2.3.4 HandlerAdapter.handle

这个阶段利用handler适配器真正调用handler执行代码逻辑。

我们直接看最关键的类InvocableHandlerMethod

private Object invoke(Object... args) throws Exception {
    // 将handler的目标方法设为可访问
	ReflectionUtils.makeAccessible(getBridgedMethod());
	try {
	    // 对目标handler实例调用目标方法
		return getBridgedMethod().invoke(getBean(), args);
	}
}	

可以看到,SpringMVC内是通过反射的方式访问目标handler的方法。

以上的getBridgedMethodgetBean两个方法都来自于该类的父类HandlerMethod,存储了目标handler的Bean和Method。

这里返回的信息可以是ModelAndView, 代表视图路径的字符串,纯信息字符串等。还需要进一步加工处理。经过加工后,都转为ModelAndView

2.2.3.5 postHandle

逆序调用handler执行链上的interceptor的postHandle方法:HandlerExecutionChain.applyPostHandle

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
	if (getInterceptors() == null) {
		return;
	}
	// 这里就能看到,为什么说是逆序调用postHandle了
	for (int i = getInterceptors().length - 1; i >= 0; i--) {
		HandlerInterceptor interceptor = getInterceptors()[i];
		interceptor.postHandle(request, response, this.handler, mv);
	}
}
2.2.3.6 processDispatchResult-视图解析

该步骤主要是处理ModelAndView,ViewResolver解析后得到View。然后将该View处理后,放入response,最终返回给客户端。

  • DispatcherServlet.render
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
	// 请求来源的区域
	Locale locale = this.localeResolver.resolveLocale(request);
	// 给response也用相同区域
	response.setLocale(locale);

	View view;
	if (mv.isReference()) {
	// mv指向视图的索引,需要ViewSolver解析
		view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
		if (view == null) {
			throw new ServletException("...")
		}
	}
	else {
	// ModelAndView本身就包含真实的视图对象
		view = mv.getView();
		if (view == null) {
			throw new ServletException("...");
		}
	}

	// Delegate to the View object for rendering.
	// 解析视图,放入
	try {
		view.render(mv.getModelInternal(), request, response);
	}
	catch (Exception ex) {
		if (logger.isDebugEnabled()) {
			logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" +
					getServletName() + "'", ex);
		}
		throw ex;
	}
}
2.2.3.7 triggerAfterCompletion

该步骤作为收尾步骤,主要是逆序执行interceptors的afterCompletion方法:HandlerExecutionChain.triggerAfterCompletion

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex)
			throws Exception {
	if (getInterceptors() == null) {
		return;
	}
	// 逆序遍历执行interceptor的afterCompletion方法
	for (int i = this.interceptorIndex; i >= 0; i--) {
		HandlerInterceptor interceptor = getInterceptors()[i];
		try {
			interceptor.afterCompletion(request, response, this.handler, ex);
		}
		catch (Throwable ex2) {
			logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
		}
	}
}

0x03 拦截器API

在Spring里,配置一个拦截器的常用方式是实现HandlerInterceptor接口。那我们可以通过看看该接口的代码以及注释对spring拦截器有更深了解。

3.1 HandlerInterceptor初印象

下面先贴一份出去了注释部分的纯源码:

package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.method.HandlerMethod;

public interface HandlerInterceptor {
	boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
	    throws Exception;
	void postHandle(
			HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
			throws Exception;
	void afterCompletion(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception;
}

可以看到这个接口相当简洁,就定以了3个方法。import的前两个类
HttpServletRequestHttpServletResponse我们也很熟悉,属于javax.servlet包。下面开始细讲。

3.2 HandlerInterceptor接口注释

接口注释很长,我这里把doc文档翻译整理下:

3.2.1 拦截器基本定义

这是一个工作流接口,它允许自定义的handler执行链。应用可以为指定的handler组注册任意数量的自带的或是自定义的拦截器,这样能不修改handler实现代码就可以为之添加通用的提前处理功能。

3.2.2 拦截器调用时机

适合的HandlerAdapter在触发handler(即SpringMVC中的Controller)执行前会先调用HandlerInterceptor。这个机制可以被用于大范围的预处理前,例如认证授权检查或是常见的handler行为如区域设置和主题更改。总的来说,拦截器的主要作用就是提出共用的处理代码,减少代码冗余。

3.2.3 异步拦截器

在异步处理的场景中,handler可在一个单独线程中执行,与此同时主线程在没有调用postHandlerafterCompletion回调的情况下就退出了。当并发的handler执行完成后,此次请求会被回派,这样做的目的是呈现model而且该合同的所有方法又会被调用一次。关于异步场景的更多信息请查看org.springframework.web.servlet.AsyncHandlerInterceptor

3.2.4 拦截器链

拦截器调用顺序
在这里插入图片描述
一般来说,每个HandlerMapping bean定义一个拦截器链。为了将某个拦截器链应用于一组handlers,需要通过一个HandlerMapping bean来映射到期望的handlers。拦截器本身是在application context内被定义的bean,mapping bean通过 interceptors属性引用。
拦截器链

3.3 preHandle

3.3.1 方法定义

拦截handler执行。拦截器利用preHandle方法对handler进行预处理。

在preHandle方法中,每个拦截器都能决定是(return false)否(return true)要终止执行链,在决定要终止时通常是发送HTTP错误或是返回一个自定义响应。

以下是该方法定义:

	/**
	 * @param request 当前 HTTP 请求
	 * @param response 当前 HTTP 响应
	 * @param handler 被选择执行的handler,用于类型 和/或 实例评估
	 * @return true 时代表执行链应该继续调用下一个拦截器或是handler本身;
	 * 如果 return false,DispatcherServlet会假定此拦截器自己已经处理了响应。
	 * @throws Exception 当发生错误时
	 */
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
	    throws Exception;

3.3.2 调用时机

preHandle方法是进行处理器拦截用的,顾名思义,该方法将在HandlerMapping选定一个适合的handler(Controller)对象之后,HandlerAdapter调用handler(Controller)之前调用。

3.3.3 链式调用的顺序

Spring中DispatcherServlet链式处理多个拦截器,一个调用链中可以同时存在多个拦截器,且将该handler本身作为执行链的末尾元素。Spring会根据过滤器定义的前后顺序正序地执行拦截器的preHandle方法。

Spring的这种Interceptor链式结构也是可以进行中断的,这种中断方式是令preHandle的返回值为false,也就是说当preHandle方法的返回值为false的时候整个请求就结束了。

3.4 postHandle

3.4.1 方法定义

拦截handler执行。拦截器利用postHandle方法对handler进行"后处理"。

以下是该方法定义:

	/**
	 * @param request 当前 HTTP 请求
	 * @param response 当前 HTTP 响应
	 * @param handler handler 或 HandlerMethod 开启异步执行,用于类型 和/或 实例检查
	 * @param modelAndView 由handler返回的ModelAndView,可能为空
	 * @throws Exception in case of errors
	 */
void postHandle(
			HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
			throws Exception;

3.4.2 调用时机

注意:postHandle方法只会在当前这个拦截器的preHandle方法成功完成且返回值为true的时候才会执行。

在HandlerAdapter实际调用handler(controller)之后,DispatcherServlet渲染视图之前调用postHandle方法。可以通过给定的ModelAndView暴露额外的model对象给视图。

3.4.3 链式调用的顺序

注意,与preHandle方法不同,DispatcherServlet以执行链的相反顺序调用postHandle方法,也就是说先声明的拦截器拥有的postHandle方法反而会被后调用。

3.5 afterCompletion

3.5.1 方法定义

在handler任意输出的情况下都会调用afterCompletion方法,因此可被用来做适当的资源清理。

以下是该方法定义:

	/**
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param handler handler (or {@link HandlerMethod}) that started async
	 * execution, for type and/or instance examination
	 * @param ex exception thrown on handler execution, if any
	 * @throws Exception in case of errors
	 */
	void afterCompletion(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception;

3.5.2 调用时机

注意:afterCompletion方法只会在当前这个拦截器的preHandle方法成功完成且返回值为true的时候才会执行。

在对请求的处理完成后进行回调,也就是在渲染视图之后。

3.5.3 链式调用的顺序

postHandle方法相同,DispatcherServlet以执行链的相反顺序调用afterCompletion方法,也就是说先声明的拦截器拥有的afterCompletion方法反而会被后调用。

0x04 过滤器

4.1 基本概念

public class MyCharsetFilter implements Filter

所有servlet过滤器都必须实现javax.servlet.Filter接口。也就是说,他跟servlet相关,而跟Spring无直接关联。

根据JDK Doc定义,Filter过滤器被定义为:对到资源的请求或来自资源的响应进行过滤任务的对象。

4.2 重要方法

4.2.1 init

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
  • Servlet过滤器的初始化方法,Web容器创建、初始化过滤器实例后,将唯一一次调用init方法
  • init方法可以读取web.xml中的Servlet过滤器的配置初始化参数,所以一般是在init 方法内执行一些初始化内容。
  • init 方法必须执行成功,否则不会起作用,出现以下两种情况时,web 容器中 Filter 无效:
    • 抛出 ServletException
    • init方法执行时间超过 web 容器定义的时间阈值

4.2.2 doFilter

public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain ) 
throws IOException, ServletException;
  • Web 容器每接收到一次请求都会调用doFilter方法。
  • 过滤器就是用这个方法进行过滤任务
  • 该方法的参数包含Servlet请求和响应以及用来调用下一个 Filter的FilterChain(Filter执行链)。
  • FilterChain被用来将request和response传递到过滤器链中的下一个过滤器
  • doFilter方法内经典实现步骤如下:
    1. 检查request合法性
    2. (可选的)使用自定义实现包装request对象,以过滤输入过滤的content或header
    3. (可选的)使用自定义实现包装response对象,以过滤输出过滤的content或header
    4. 通过chain.doFilter交给执行链中下一个对象继续处理,或是不这么干以阻塞请求处理。
    5. 在调用过滤器链中的下一个过滤器后,直接在response上设置headers。

doFilter的最终目的只有一个,调用internalDoFilter,中间可能会增加一些安全策略,估计Globals.IS_SECURITY_ENABLE与是否开启https服务有关,具体没仔细研究过。

doFilter方法后的代码,将在Filter执行链和Servlet执行完后就行执行,顺序如下:
在这里插入图片描述

4.2.3 internalDoFilter

internalDoFilter的最终目的也只有一个,就是调当前pos指向的过滤器链中的某一个filter的doFilter(request, response, this)方法,中间可能会增加一些安全策略,以及当所有过滤器调用完了,进行的一些收尾清理工作,包括调用servlet.service(request, response)方法,来处理真正的请求,以及清除threadLocal中保存的当前的request和response,为下一次请求做准备。

4.2.4 destroy

public void destroy();
  • Web容器在销毁过滤器实例前(该过滤器的doFilter方法内的所有线程已经退出或是时间阈值已经超过)调用destroy方法(仅一次)
  • destroy方法中可以释放Servlet过滤器占用的资源
  • 当Web容器调用destrofy方法后,不再会调用该过滤器实例的doFilter方法了
  • 可以利用destroy方法来清理资源,如内存、文件句柄、线程等

4.3 小结

  1. 一个request请求进来了,先把自己交给filterChain;
  2. filterChain启动过滤器链,从头开始,把request交给第一个filter,并把自己传给filter;
  3. filter在doFilter里做完自己的过滤逻辑,再调用filterChain的doFilter,以启动下一个过滤器;
  4. filterChain游标移动,启动下一个过滤器,如此循环下去…
  5. 过滤器游标走到链的末尾,filterChain执行收尾工作;

最后给一个简单的过滤器流程图:
过滤器

4.4 过滤器例子

4.4.1 web.xml配置

<filter>
    <filter-name>filter</filter-name>
    <filter-class>com.chengc.demos.web.demo1.filters.MyCharsetFilter</filter-class>
    <init-param>
        <param-name>charset</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>contentType</param-name>
        <param-value>text/html;charset=UTF-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

4.4.2 java代码

public class MyCharsetFilter implements Filter {
    private static final Logger logger = LoggerFactory
            .getLogger(MyCharsetFilter.class);
    static {
        System.out.println("MyCharsetFilter static");
    }
    public MyCharsetFilter(){
        System.out.println("MyCharsetFilter initiated");
    }
    private FilterConfig config = null;

    /**
     * Servlet过滤器的初始化方法,Servlet容器创建Servlet过滤器实例后,将调用这个方法。
     * 这个方法可以读取web.xml中的Servlet过滤器的配置初始化参数
     * @param filterConfig
     * @throws ServletException
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.config = filterConfig;
        logger.info("MyCharsetFilter初始化...");
        String filterName = filterConfig.getFilterName();
        // 得到在web.xml文件中配置的初始化参数
        String initParam1 = filterConfig.getInitParameter("charset");
        String initParam2 = filterConfig.getInitParameter("contentType");
        // 返回过滤器的所有初始化参数的名字的枚举集合。
        Enumeration<String> initParameterNames = filterConfig
                .getInitParameterNames();
        logger.info("filterName=" + filterName + ", initParam1=" + initParam1 + ", initParam2=" + initParam2);
        while (initParameterNames.hasMoreElements()) {
            String paramName = (String) initParameterNames.nextElement();
            logger.info(paramName);
        }
    }

    /**
     * 这个方法完成实际的过滤操作,当客户请求访问于过滤器关联的URL时,Servlet容器将先调用过滤器的doFilter方法。
     * FilterChain参数用于访问后续过滤器
     * @param servletRequest
     * @param servletResponse
     * @param filterChain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        logger.info("before doFilter...");
        servletRequest.setCharacterEncoding("UTF-8");
        servletResponse.setCharacterEncoding("UTF-8");
        // 强制类型转换
//        HttpServletRequest request = (HttpServletRequest)servletRequest;
//        HttpServletResponse response = (HttpServletResponse)servletResponse;
        // 获取web.xm设置的编码集,设置到Request、Response中
//        response.setContentType(config.getInitParameter("contentType"));
//        response.setCharacterEncoding(config.getInitParameter("charset"));
        // 将请求转发到目的地
        filterChain.doFilter(servletRequest, servletResponse);
        ApplicationContext ctx2 = ContextLoader.getCurrentWebApplicationContext();
        TestService ts = (TestService)ctx2.getBean("testServiceImpl");
        ts.sayHello();
        logger.info("after  doFilter...");
    }

    /**
     * Web容器在销毁过滤器实例前调用该方法,这个方法中可以释放Servlet过滤器占用的资源
     */
    @Override
    public void destroy() {
        logger.info("MyCharsetFilter准备销毁...");
    }
}

4.5 过滤器应用

  • 身份认证,资源访问权限管理
    通过控制对chain.doFilter的方法的调用,来决定是否需要访问目标资源。

    比如,可以在用户权限验证等等。判断用户是否有访问某些资源的权限,有权限放行,没权限不执行chain.doFilter方法。

  • 登录和审计

  • 图像转换

  • 数据压缩

  • 加密

  • trigger触发器

  • 乱码解决
    通过在调用chain.doFilter方法之前,做些处理来达到某些目的。比如,解决中文乱码的问题等等。可以在doFilter方法前,执行设置请求编码与响应的编码。甚至可以对request接口进行封装装饰来处理get请求方式的中文乱码问题(重写相应的request.getParameter方法)。

4.6 OncePerRequestFilter

4.6.1 原理

org.springframework.web.filter.OncePerRequestFilter
保障每个请求只执行一次过滤逻辑,有的请求不止被过滤器执行一次过滤逻辑。比如一个例子:在Spring Security里面,一般使用Filter来实现身份认证和鉴权,此时每个请求会在发到servlet前穿过filter chain两次.

源码如下:
在这里插入图片描述
可以看到是通过一个该Filter为请求打赏的AlreadyFiltered Attribute来判定是否已经处理过。

4.6.2 应用

日志记录
可参考How to Record Request and Response Bodies in Sping Boot Applications

当想打印所有request和response的事后由于数据流只能被读取一次,所以如果在拦截器或者过滤器中提前读取,那在controller里面就读不到了。比如Filter这样读取请求:

public class AccessLogFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream());

        log.info("request body = {}", new String(requestBody, StandardCharsets.UTF_8));

        filterChain.doFilter(request, response);
    }
}

Controller读取请求会报错

@RestController
@RequestMapping("/api/demo")
@Slf4j
public class DemoController {

    @PostMapping(consumes = MediaType.TEXT_PLAIN_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public Map<String, String> handle(@RequestBody String body) {
        log.info(body);
        return Map.of("requestBody", body, "timestamp", System.currentTimeMillis() + "", "success", "true");
    }
}

报错内容如下:

2022-03-14 15:19:05.189  WARN 2936 --- [  XNIO-1 task-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public java.util.Map<java.lang.String, java.lang.String> io.springcloud.demo.controller.DemoController.handle(java.lang.String)]

要想即记录请求日志又能在Controller重复读取,可使用
ContentCachingRequestWrapper、ContentCachingResponseWrapper,例子如下:


import java.io.IOException;
import java.nio.charset.StandardCharsets;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import lombok.extern.slf4j.Slf4j;

@WebFilter(urlPatterns = "/*")
@Slf4j
public class AccessLogFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
		
		// 1. 过滤转发前封装ContentCachingWrapper
        ContentCachingRequestWrapper req = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper resp = new ContentCachingResponseWrapper(response);

        // 2. 请求转发
        filterChain.doFilter(req, resp);
        
        // 3. 从cache中读取
        byte[] requestBody = req.getContentAsByteArray();
        byte[] responseBody = resp.getContentAsByteArray();
        
        log.info("request body = {}", new String(requestBody, StandardCharsets.UTF_8));
        
        log.info("response body = {}", new String(responseBody, StandardCharsets.UTF_8));
        
        // 4. response读取后,还需要将cache中的数据写入到response返回给客户端
        resp.copyBodyToResponse();
    }
}

原理:

  1. 在filter先把request、response进行ContentCaching Wrapper包装

  2. Spring内部读取ContentCachingRequestWrapper.InputStream的时候,会先正常读取流到目标buffer,然后写入本对象中的ByteArrayOutputStream cachedContent
    在这里插入图片描述
    在这里插入图片描述

  3. Spring内部读取ContentCachingResponseWrapper.OutputStream的时候,初始化ResponseServletOutputStream
    内部有个FastByteArrayOutputStream缓存
    在这里插入图片描述
    在这里插入图片描述
    可以看到,内容是写入这个cache content输出流
    在这里插入图片描述

  4. 随后就可以通过getContentAsByteArray进行数据多次重复读取。所以代码中在filterChain处理完后,使用此方法来读取req、resp进行日志记录:
    ContentCachingRequestWrapper:
    在这里插入图片描述
    可以看到,每次从ContentCachingRequestWrapper读取的是一个拷贝
    在这里插入图片描述
    而ContentCachingResponseWrapper#getContentAsByteArray也类似
    在这里插入图片描述

  5. resp.copyBodyToResponse将cache中的response数据拷贝回去,以返回给客户端

protected void copyBodyToResponse(boolean complete) throws IOException {
	if (this.content.size() > 0) {
		// 获取包装的真实响应
		HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
		if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) {
			if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null) {
				rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);
			}
			this.contentLength = null;
		}
		// 将缓存的FastByteArrayOutputStream content写入真实相应
		this.content.writeTo(rawResponse.getOutputStream());
		// 重置缓存的输出流
		this.content.reset();
		if (complete) {
			// 强制清空缓存,返回给客户端
			super.flushBuffer();
		}
	}
}

0x05 拦截器与过滤器

5.1 重要对比

拦截器Interceptor过滤器Filter
原理属于Spring范畴。会和路径匹配的Interceptor组成HandlerChain,基于java的反射机制调用handler方法,在其前后调用拦截器的方法属于Servlet规范定义。基于函数回调
创建在context.xml中配置。由Spring容器初始化。在web.xml中配置filter基本属性。由web容器创建
servlet容器拦截器不直接依赖于servlet容器过滤器依赖于servlet容器
作用对象拦截器只能对action请求起作用过滤器则可以对几乎所有的请求起作用
访问范围拦截器可以访问action上下文、值栈里的对象,可以获取IOC容器中的各个bean,这点很重要,在拦截器里注入一个service,可以调用业务逻辑;过滤器也可以使用ContextLoader.getCurrentWebApplicationContext()获取到根Context即XmlWebApplicationContext,注意该context只能访问除了Controller以外的bean,因Controller在另一个xml中配置
可用场景即可用于Web,也可以用于其他非Web Application基于Servlet规范,只能用于Web
使用选择可以深入到方法执行前后,使用场景更广只能在Servlet前后起作用
适用场景记录日志、监控统计、权限控制、方法执行时间统计等登录和审计如shiro权限过滤、字符编码、加密

5.2 拦截器通过context获取bean示例

public boolean preHandle(HttpServletRequest request,
           HttpServletResponse response, Object handler) throws Exception {
       logger.info("FirstInterceptor preHandle 你妹");
       ServletContext servletContext = request.getSession().getServletContext();
       // 这个方法是通过servletContext的
       // getAttribute(ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)拿到context
       ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
       TestService ts0 = (TestService)ctx.getBean("testServiceImpl");
       ts0.sayHello();
       // 也可以通过这种方式获取ApplicationContext
       ApplicationContext ctx2 = ContextLoader.getCurrentWebApplicationContext();
       TestService ts = (TestService)ctx2.getBean("testServiceImpl");
       ts.sayHello();
       // TODO Auto-generated method stub
       return true;
   }

5.3 重要的点

  • 调用次数
    网上的说法,存疑:[在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次]
  • 拦截器同servlet2.3版本的过滤器基本相似,但是与过滤器不同,HandlerInterceptor只允许自定义预处理(使用禁止handler自身执行选项)以及自定义后处理。
  • 过滤器更强大,例如过滤器可允许交换传递链中的请求和响应对象。请注意,过滤器的在web.xml中配置,web.xml是应用程序上下文中的HandlerInterceptor。

5.4 拦截器与过滤器的执行时机

在这里插入图片描述
在这里插入图片描述

5.5 过滤器、拦截器、监听器

  • 过滤器(Filter)
    当你有一堆东西的时候,你只希望选择符合你要求的某一些东西。定义这些要求的工具,就是过滤器。
  • 拦截器(Interceptor)
    在一个流程正在进行的时候,你希望干预它的进展,甚至终止它进行,这是拦截器做的事情。
  • 监听器(Listener)
    当一个事件发生的时候,你希望获得这个事件发生的详细信息,而并不想干预这个事件本身的进程,这就要用到监听器。

在这里插入图片描述

0xFE 总结

  • 拦截器一般用来提出常用的Controller代码,或是权限验证等场景
  • 过滤器被用来在资源请求或相应中进行过滤任务,如身份验证、日志和审计、图片请求过滤、数据压缩过滤、加密、令牌

0xFF 参考文档

;