Spring-拦截器与过滤器
系列文章目录
0x01 摘要
本文主要是作者学习spring中的拦截器的一些简要记录。
0x02 拦截器介绍
2.1 拦截器简介
拦截器是spring中的一个重要概念。他被注册到spring,拦截指定规则的请求,基于回调机制执行。一般来说,拦截器只会拦截action请求,这一点与过滤器不同。
下面贴一张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:
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的方法。
以上的getBridgedMethod
和getBean
两个方法都来自于该类的父类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
的前两个类
HttpServletRequest
和HttpServletResponse
我们也很熟悉,属于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可在一个单独线程中执行,与此同时主线程在没有调用postHandler
和afterCompletion
回调的情况下就退出了。当并发的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方法内经典实现步骤如下:
- 检查request合法性
- (可选的)使用自定义实现包装request对象,以过滤输入过滤的content或header
- (可选的)使用自定义实现包装response对象,以过滤输出过滤的content或header
- 通过
chain.doFilter
交给执行链中下一个对象继续处理,或是不这么干以阻塞请求处理。 - 在调用过滤器链中的下一个过滤器后,直接在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 小结
- 一个request请求进来了,先把自己交给filterChain;
- filterChain启动过滤器链,从头开始,把request交给第一个filter,并把自己传给filter;
- filter在doFilter里做完自己的过滤逻辑,再调用filterChain的doFilter,以启动下一个过滤器;
- filterChain游标移动,启动下一个过滤器,如此循环下去…
- 过滤器游标走到链的末尾,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();
}
}
原理:
-
在filter先把request、response进行ContentCaching Wrapper包装
-
Spring内部读取ContentCachingRequestWrapper.InputStream的时候,会先正常读取流到目标buffer,然后写入本对象中的
ByteArrayOutputStream cachedContent
:
-
Spring内部读取ContentCachingResponseWrapper.OutputStream的时候,初始化ResponseServletOutputStream
内部有个FastByteArrayOutputStream缓存
可以看到,内容是写入这个cache content输出流
-
随后就可以通过
getContentAsByteArray
进行数据多次重复读取。所以代码中在filterChain处理完后,使用此方法来读取req、resp进行日志记录:
ContentCachingRequestWrapper:
可以看到,每次从ContentCachingRequestWrapper读取的是一个拷贝
而ContentCachingResponseWrapper#getContentAsByteArray也类似
-
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代码,或是权限验证等场景
- 过滤器被用来在资源请求或相应中进行过滤任务,如身份验证、日志和审计、图片请求过滤、数据压缩过滤、加密、令牌