《深入剖析Tomcat》第十章介绍了Tomcat的安全机制,主要就是对servlet的访问做安全验证,如果Tomcat中设置了某些servlet需要指定角色的用户才能访问,则需要客户端进行登录验证,如果用户名密码正确并且该用户拥有该角色的话,Tomcat就会放行,否则就会返回403(无权访问)。用户名密码及角色配置是需要配置在Tomcat内部的,通常是放在一个叫做“tomcat-users.xml”的文件中。
鉴于当前java项目开发基本都在使用springboot框架,也不需要关注Tomcat本身的安全性问题(项目的权限验证由项目自身来实现),大家也没必要去给Tomcat去配用户名密码角色。所以这块内容就不再深入了解了,直接步入下一章:详解StandardWrapper。
StandardWrapper的基本流程
StandardWrapper是Wrapper容器的标准实现,Tomcat中使用的就是这个类,Wrapper容器是对一个servlet的包装,主要功能就是要去调用某个特定servlet的service方法。
所以不难想到,StandardWrapper首先要知道它持有的servlet是哪个,实现方式是它持有一个【String servletClass】属性,用来存放servlet的全限定名,通过 setServletClass(String servletClass) 方法可以设置这个属性值。
有了servlet的全限定名,StandardWrapper要想获取到一个servlet的实例,就必须要先对这个servlet进行类加载,要进行类加载就必须要拿到一个类加载器,而在Tomcat中类加载器是被包含一个叫做"加载器"的组件中的,加载器通常是 WebappLoader(实现了Loader接口)这个类的一个实例,StandardWrapper拿加载器的逻辑是这样的:先看自己有没有,没有的话就去拿父容器(Context容器)的,所以通常来说Tomcat会给Context容器分配一个加载器,Context底下的Wrapper小弟们都用它的就行。
public Loader getLoader() {
if (loader != null) return loader;
if (parent != null) return parent.getLoader();
return null;
}
拿到 WebappLoader 实例后就拿到了 ClassLoader 实例(类加载器),下一步很显然就是对servlet进行类加载操作,这个很简单,直接 ClassLoader.loadClass(String name) 方法就能把类加载好了,然后就是反射操作 newInstance 创建一个servlet的实例,接着servlet.service() 方法一调用,StandardWrapper的使命就基本完成了。
了解了StandardWrapper的基本工作流程后,再来看看它里面比较重要的两个设计 SingleThreadModel 与 过滤器。
SingleThreadModel
javax.servlet.SingleThreadModel 是一个接口,没有任何方法定义,只是一个标识,用修饰servlet的具体实现类,如果一个servlet没有实现SingleThreadModel这个接口,那么在Tomcat的整个生命周期内,针对该servlet对象的StandardWrapper实例就只创建一个servlet实例,每个线程都公用这一个实例,允许并发调用。也就是说该servlet是单例的。
如果一个servlet实现了SingleThreadModel接口,那么多个线程要使用该servlet时,StandardWrapper就会为每个线程分配一个servlet实例,一个servlet实例在同一时刻只会被一个线程调用。Tomcat为了避免重复创建servlet对象耗费资源,所以在每个StandardWrapper实例中都维护了一个servlet对象池,一个线程要使用servlet对象时,就从对象池里拿一个给它用,如果池子里没有了就创建一个新的;线程使用完servlet对象后,StandardWrapper会再将其回收到对象池里。也就是说该servlet是多例的。
如果一个servlet实现了SingleThreadModel那么该servlet就是线程安全的吗?
这是一个认知误区?何为线程安全,那就是多线程并发调用的情况下,公共资源的完整性和逻辑正确性不会遭到破坏,公共资源就是多个线程都会用到的资源。而SingleThreadModel的实现逻辑只是保证了每个servlet内部持有的属性不会被其他线程破坏,但是如果该servlet用到了其他类里面的属性,那么仍然是存在线程安全问题的。
注意!SingleThreadModel这个接口在Servlet API 2.4版本(对应Tomcat 5.5.x版本)后已经被打上了 @Deprecated 标记了,不再推荐使用,但是代码逻辑仍然保留着。
上面提到的StandardWrapper的类加载、创建sevlet实例及SingleThreadModel的逻辑基本都包含在 StandardWrapper#allocate() 这个方法中,allocate() 方法的目的就是返回一个可用的 servlet 实例。
过滤器
过滤器的逻辑其实并不在StandardWrapper中,而是在它的基础阀 StandardWrapperValve 中。
先说一下StandardWrapperValve的作用。
- 将Catalina接收到的request与response对象转换成servlet可用的HttpServletRequest与HttpServletResponse。
- 调用StandardWrapper的 allocate() 方法获取到一个servlet实例。
- 构建过滤器链 FilterChain。
- 执行所有过滤器的doFilter方法,最后调用 servlet 的 service() 方法。
过滤器链 FilterChain 又是一个“责任链”机制的应用,Tomcat中用到责任链的地方还挺多,每个容器中的 Pipeline 也是应用的“责任链”调用机制。不了解 Pipeline的同学可以看看第五章的文章【深入剖析Tomcat(五) 剖析Servlet容器并实现一个简易Context与Wrapper容器】
FilterChain的调用过程大致如下图
如何自定义过滤器并加入到过滤器链中呢?
首先需要有一个自定义过滤器的类,这个类需要实现 javax.servlet.Filter 接口,比如我自定义的过滤器叫MyFilter1
public class MyFilter1 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("MyFilter1 start");
chain.doFilter(request,response);
System.out.println("MyFilter1 end");
}
}
MyFilter1的前置逻辑和后置逻辑都比较简单,就是简单输出了一个字符串。
然后怎么使这个类生效呢?也就是说怎么做才能将这个类加到Wrapper容器的过滤器链中呢?
两种方式(基于Spring框架):
1.声明一个FilterRegistrationBean(推荐使用)
这种方式需要为自定义过滤器类创建一个bean,Tomcat的Wrapper容器在构建过滤器链时会找到这个bean,并将其加入到过滤器链中,同时声明这个Bean时也可以为它设置排序值order,过滤器链中的过滤器都是按其order值从小到大排序的。
@Configuration
public class ServletConfiguration {
@Bean
public FilterRegistrationBean<MyFilter1> myFilter() {
FilterRegistrationBean<MyFilter1> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new MyFilter1());
registrationBean.addUrlPatterns("/servlet/*");
registrationBean.setOrder(2);
return registrationBean;
}
}
2.使用@WebFilter注解
在自定义Filter类上加上 @WebFilter 注解,然后Spring启动类上加上 @ServletComponentScan 注解, Wrapper容器也能找到它并将其纳入过滤器链中,不过缺点是不能自定义排序值,order值永远是2147483647(Integer.MAX_VALUE)。有的人说配合spring的 @Order 注解就能排序了,其实不然,说这话的人肯定也没有实践过。
@WebFilter(urlPatterns = "/servlet/*")
@Order(100) // 这个注解在这里不生效,没什么作用
public class MyFilter1 implements Filter {
………
}
综合上面两种方式,我还是推荐第一种:FilterRegistrationBean 的方式,毕竟给过滤器排序在很多场景下都是需要的。
源码分析
在Tomcat中,众多过滤器其实是挂在Context容器下的,StandardContext类下有个 filterMaps 属性,该属性存放了与该Context容器相关联的多个过滤器,SpringBoot在启动时会搜寻所有过滤器并将其加入到 filterMaps 属性中。当servlet请求打进来后,Wrapper容器在组装过滤器链时会去Context容器中取 filterMaps 属性来拼装过滤器链。
接下来基于SpringBoot框架来看看过滤器生效的过程
首先启动SpringBoot,ServletWebServerApplicationContext#selfInitialize 方法中会调用 getServletContextInitializerBeans() 方法来获取所有过滤器类(我们自定义的过滤器生不生效在这里就可以看出来了),这时候就能看到我们自定义的过滤器已经被检索到了
接下来for循环中执行方法“beans.onStartup(servletContext)” ,该方法逻辑中会调用到StandardContext的 addFilterMapBefore() 或者addFilterMap() 方法,这两个方法都是给Context容器添加过滤器,逻辑大致相似,区别是将过滤器往过滤器链的头上加还是尾上加,自定义过滤器会通过 addFilterMapBefore 这个方法被添加进来。注意看这里就是往 StandardContext 的 filterMaps 属性中放值的过程。
SpringBoot启动完后,StandardContext中的filterMaps 也就放好值了。
然后这时候来了一个servlet请求,通过Context的Pipeline和Wrapper Pipeline的流转,请求来到了Wrapper的基础阀:StandardWrapperValve,StandardWrapperValve的invoke方法中会去构建一个过滤器链的对象
ApplicationFilterFactory.createFilterChain方法中会取StandardContext中的 filterMaps 属性来构建过滤器链,构建过程就不展示代码了,感兴趣的可以翻翻这块的代码。
filterChain 对象构建好后,还是在StandardWrapperValve的invoke方法中,执行 filterChain 的 doFilter 方法,该方法会按照顺序来依次执行各个过滤器的 doFilter 方法,在过滤器链的末尾调用一下servlet的service方法,拿到返回结果,到此为止,在这一次servlet请求中该Wrapper容器的任务就完成了。
过滤器链(ApplicationFilterChain)的doFilter方法会调用internalDoFilter方法,过滤器链的执行逻辑就在internalDoFilter方法中,为了表达主要意思,我将删减后的代码展示给你,执行后的效果就和上面画的【Servlet过滤器链调用示意图】一样。
private void internalDoFilter(ServletRequest request,ServletResponse response) throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
filter.doFilter(request, response, this);
return;
}
// We fell off the end of the chain -- call the servlet instance
servlet.service(request, response);
}
OK,StandardWrapper的功能介绍就到此为止,Wrapper容器是对某个具体servlet的一个封装,并提供了 SingleThreadModel和过滤器的机制来丰富 servlet 的功能,通过这篇文章你应该也对过滤器有了一个全面的认识,这下不会再对Tomcat的过滤器和spring的拦截器傻傻分不清楚了吧,如果你真的还不清楚的话,就等我出spring mvc的文章吧,哈哈!
下篇文章来分析下Context容器的标准实现:StandardContext,敬请期待!