Tomcat过滤器链原理

责任链模式

Tomcat 采用“责任链模式”设计了过滤器链。该模式的含义是,一个请求进入后会经过一系列处理逻辑,这些处理逻辑可以分别写在不同的地方,每个逻辑处理完后将请求传给下一个处理逻辑。

这样做的好处是:处理逻辑之间解耦,每个逻辑只管做好自己的事,不需要知道自己的前后的处理逻辑;可以快速在这个逻辑链条中加入新的逻辑,不影响原有的逻辑和执行顺序。

过滤器链的简单实现

先定义一个过滤器接口:

1
2
3
4
5
6
/**
* 过滤器接口
*/
public interface Filter {
void doFilter(Request request, Response response, FilterChain filterChain);
}

其中的 Request、Response 是对HTTP请求、响应的模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 请求
*/
public class Request {
private String requestStr;

public String getRequestStr() {
return requestStr;
}

public void setRequestStr(String requestStr) {
this.requestStr = requestStr;
}
}

/**
* 响应
*/
public class Response {
private String responseStr;

public String getResponseStr() {
return responseStr;
}

public void setResponseStr(String responseStr) {
this.responseStr = responseStr;
}
}

最重要的 FilterChain 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FilterChain implements Filter {

private List<Filter> list = new ArrayList<>();

private int index = 0;

@Override
public void doFilter(Request request, Response response, FilterChain filterChain) {
if (index == list.size()) {
return;//这里是逆序处理响应的关键, 当index为容器大小时, 证明对request的处理已经完成, 下面进入对response的处理
}
Filter f = list.get(index);//过滤器链按index的顺序拿到filter
index++;
f.doFilter(request, response, filterChain);
}

public void addFilter(Filter filter) {
list.add(filter);
}
}

index初始化为0,此过滤器链的 doFilter 方法第一次被调用时,取出了过滤器链 list 中第一个过滤器,index 自增1,然后执行第一个过滤器的 doFilter 方法,过滤器链对象自身作为方法参数。此过滤器链的 doFilter 方法第二次被调用时,因为 index 已经自增,所以从 list 中取到的是第二个过滤器,也就顺理成章地执行第二个过滤器的 doFilter 方法。当所有过滤器执行完后,

其中一个过滤器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class FilterA implements Filter {
@Override
public void doFilter(Request request, Response response, FilterChain filterChain) {
String requestStr = request.getRequestStr();
System.out.println("==============================请求经过FilterA");
filterChain.doFilter(request, response, filterChain);//调用过滤器链的doFilter方法, 让它去执行下一个Filter的doFilter方法, 处理response的代码将被挂起
System.out.println("==============================响应经过FilterA");
if (response.getResponseStr() == null) {
response.setResponseStr(requestStr);
}
response.setResponseStr(response.getResponseStr() + "-FA");
}
}

下面结合过滤器 FilterA 的实现和过滤器链 FilterChain 的实现来理解过滤器链工作的原理。

因为要遍历所有过滤器,所以请求和响应对象首先进入filterChain.doFilter(request, response, filterChain)方法,参数包括 filterChain 对象自身。

因为index初始化为0,此 filterChain 对象的 doFilter 方法第一次被调用时,取出了过滤器链 list 中第一个过滤器,index 自增1,然后执行第一个过滤器的 doFilter 方法,filterChain 对象自身作为方法参数。

在过滤器 filterA.doFilter() 方法中,处理完请求后就调用 filterChain.doFilter() 方法,所以 filterChain.doFilter() 方法可以看作是一个递归方法。

在 filterChain.doFilter() 方法中,每次调用就取 index 下标对应的过滤器,调用过滤器的 doFilter 方法之前先将 index 自增1。下次进入这个方法时,取出的过滤器就是下一个过滤器。当过滤器全部执行完,即 index == list.size() 时就 return。

因为除了第一次调用,后续调用 filterChain.doFilter() 方法都是在每个过滤器的 doFilter 方法内,所以从 filterChain.doFilter() 方法 return 后就回到过滤器的 doFilter 方法,所以过滤器中的后续处理响应的逻辑会被执行,过滤器的 doFilter 执行完 return 又到了 filterChain.doFilter() 方法。以此类推,每个过滤器处理响应的逻辑都会被执行。

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
Request request = new Request();
request.setRequestStr("请求内容");
Response response = new Response();

FilterChain filterChain = new FilterChain();
filterChain.addFilter(new FilterA());
filterChain.addFilter(new FilterB());
filterChain.doFilter(request, response, filterChain);

System.out.println("最终响应:" + response.getResponseStr());
}
}

输出内容:

1
2
3
4
5
==============================请求经过FilterA
==============================请求经过FilterB
==============================响应经过FilterB
==============================响应经过FilterA
最终响应:请求内容-FB-FA

以上是对过滤器链的简单实现,与 Tomcat 的过滤器原理大概类似。Toomcat 是分别定义了一个 FilterChain 接口和 Filter 接口,FilterChain 接口 Tomcat自己实现了,我们自定义过滤器是实现 Filter 接口。

ApplicationFilterChain

ApplicationFilterChain是 Tomcat 实现的 FilterChain,而且 Tomcat 只有这一个实现类。

ApplicationFilterChain 中保存过滤器使用的是 ApplicationFilterConfig 数组。ApplicationFilterConfig类中的成员就是Filter,源码如下:

1
2
3
4
5
6
7
public final class ApplicationFilterChain implements FilterChain {
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
}

public final class ApplicationFilterConfig implements FilterConfig, Serializable {
private transient Filter filter = null;
}

ApplicationFilterChain 的 doFilter 方法调用到自己的 internalDoFilter 方法,在这个方法内,可以看到我们上一节实现的逻辑:把过滤器下标+1,取过滤器,调用过滤器doFilter方法,入参为请求+响应+过滤器链自身。并且,在这一系列操作前先判断过滤器下标,即是否执行完所有过滤器了,如果执行完了,调用 servlet.service() 方法。servlet 方法返回后,整个过程就结束了。

ApplicationFilterChain 的 internalDoFilter 方法部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++]; // 取过滤器,下标+1
try {
Filter filter = filterConfig.getFilter();
// ...
filter.doFilter(request, response, this);
// ...

} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
return; // 每个过滤器方法执行完后 return
}

// 当执行完所有过滤器 pos == n 时
servlet.service(request, response);
// ...

}

配置 Tomcat 过滤器用的是 web.xml 配置文件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- 编码过滤器 -->  
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 登录过滤器 -->
<filter>
<filter-name>LoginFilter</filter-name>
<filter-class>com.imooc.filter.LoginFilter</filter-class>
<init-param>
<param-name>noLoginPaths</param-name>
<param-value>login.jsp;fail.jsp;Login</param-value>不会被过滤掉的页面
</init-param>
<init-param>
<param-name>charset</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>LoginFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

这些过滤器的执行顺序遵守以下规则:

  1. 以url-pattern方式配置的filter运行时肯定先于以servlet-name方式配置的filter
  2. 以url-partern方式配置的filter中,如果有多个与当前请求匹配,则按web.xml中filter-mapping出现的顺序来运行

SpringBoot对Tomcat过滤器的自动配置

如果项目是 Servlet+JSP 或 SpringMVC,就需要手动在 web.xml 中配置过滤器,但在 SpringBoot 实现起来就简单了,我们只需要实现将过滤器类像下面这样注册为 Bean,SpringBoot 就会帮我们自动配置。

这里的 Tomcat 版本是9.0.35

自定义过滤器示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@WebFilter(urlPatterns = {"/*"}) // 拦截URI
public class CustomFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("==========CustomFilter===============init");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) request;
System.out.println("==========CustomFilter===============doFilter");
chain.doFilter(request, response);
}

@Override
public void destroy() {
// TODO Auto-generated method stub
}
}

项目启动后,调试一下 FilterChain 对象的中的过滤器,会看到:

1
2
3
4
5
0 = {ApplicationFilterConfig@5065} "ApplicationFilterConfig[name=characterEncodingFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter]"
1 = {ApplicationFilterConfig@5066} "ApplicationFilterConfig[name=formContentFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedFormContentFilter]"
2 = {ApplicationFilterConfig@5067} "ApplicationFilterConfig[name=requestContextFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter]"
3 = {ApplicationFilterConfig@5068} "ApplicationFilterConfig[name=customFilter, filterClass=com.example.demo.config.CustomFilter]"
4 = {ApplicationFilterConfig@5069} "ApplicationFilterConfig[name=Tomcat WebSocket (JSR356) Filter, filterClass=org.apache.tomcat.websocket.server.WsFilter]"

这里的原理简单说是,容器启动时,会从 beanFactory=DefaultListableBeanFactory 中取出所有实现了javax.servlet.Filter接口的 Bean(除了我们自定义的过滤器,其他是 SpringBoot 提供的),进行一个升序排序,序号来自于它们使用的@Order注解或PriorityOrdered接口。如果没有使用,序号就是 Integer.MAX。上面的前3个过滤器的序号都是负数,所以位置靠前。

这部分逻辑在下面这个方法(在ServletContextInitializerBeans类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private <T> List<Entry<String, T>> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class<T> type,
Set<?> excludes) {
String[] names = beanFactory.getBeanNamesForType(type, true, false); // 取Bean,type=javax.servlet.Filter
Map<String, T> map = new LinkedHashMap<>();
for (String name : names) {
if (!excludes.contains(name) && !ScopedProxyUtils.isScopedTarget(name)) {
T bean = beanFactory.getBean(name, type); // 生成Bean
if (!excludes.contains(bean)) {
map.put(name, bean);
}
}
}
List<Entry<String, T>> beans = new ArrayList<>(map.entrySet());
beans.sort((o1, o2) -> AnnotationAwareOrderComparator.INSTANCE.compare(o1.getValue(), o2.getValue())); // 排序
return beans;
}

过滤器链 ApplicationFilterChain 是在每次请求来时被创建的,负责创建工作的是 ApplicationFilterFactory.createFilterChain() 方法。在这个方法中,遍历StandardContext的成员Map<String, ApplicationFilterConfig> filterConfigs(其实是遍历 StandardContext 的成员 filterMap,这个 Map 中保存了所有 filterName,再根据每个 filterName 从 filterConfigs 取 ApplicationFilterConfig 对象),把这个成员内的所有 ApplicationFilterConfig 对象添加到 ApplicationFilterChain 中。而 StandardContext 的成员 filterConfigs 的元素也是来自于上面 SpringBoot 容器中的过滤器 Bean。

创建好 ApplicationFilterChain 之后,就是调用它的 doFilter 方法,这之后就是我们上面说的过滤器链的执行逻辑。