Tomcat源码深析之web.xml组件的处理
这篇文章主要是带着读者通过分析Tomcat的源码,深入了解Tomcat对web.xml配置的组件的的处理,文章内容主要包括Tomcat对上下文参数(contextParams),过滤器(Filters),应用监听器(listeners)以及Servlet的加载,初始化等等。
在Java Web开发中我们对web.xml这个配置文件并不陌生,也对web.xml中配置的常用组件很了解,我所指的即过滤器、监听器、Servlet三大组件。包括他们的加载顺序,初始化顺序,我相信这对于所有Java Web开发者来说是一定要掌握的基础知识。这也是开发Web中间件会经常用到的,随便列举一些例子:Spring、Struts、UrlRewrite、等等。
下面跟着博主一起来通过扒一扒Tomcat的源码来深入了解一下他们的相关知识吧,这比概念上去了解更深刻一些。
一、web.xml的解析
这个部分可以在类ContextConfig类中找到相关源码,Context即代表Servlet的上下文。里面有个protected的方法webConfig,这个方法里面主要做下面事情:
-
扫描应用打包的所有Jar来检索Jar包里面的web.xml配置并解析,放入内存。
-
对这些已经检索到的web配置进行排序。
-
基于SPI机制查找ServletContainerInitializer的实现,写web中间件的同学注意了,了解SPI以及 ServletContainerInitializer机制这对于你来说可能是一个很好的知识点。
-
处理/WEB-INF/classes下面的类的注解,某个版本Servlet支持注解方式的配置,可以猜测相关事宜就是在这里干 的。
-
处理Jar包中的注解类。
-
将web配置按照一定规则合并到一起。
-
应用全局默认配置,还记得Tomcat包下面的conf文件夹下面有个web.xml配置文件吧。
-
将JSP转换为Servlet,这让我想起了若干年前对JSP的理解。
-
将web配置应用到Servlet上下文,也即Servlet容器。
-
将配置信息保存起来以供其他组件访问,使得其他组件不需要再次重复上面的步骤去获取配置信息了。
-
检索Jar包中的静态资源。
-
将ServletContainerInitializer配置到上下文。
在上面这些步骤中,本片文章关系的入口在第9步,即Tomcat是如何将Web配置应用到上下文的。
二、根据web.xml配置装配Servlet上下文
我们跟着WebXml的configureContext进入方法的实现,这里我按顺序摘抄几个源码片段并说明:
for (Entry<String, String> entry : contextParams.entrySet()) { context.addParameter(entry.getKey(), entry.getValue()); }
for (FilterDef filter : filters.values()) { if (filter.getAsyncSupported() == null) { filter.setAsyncSupported("false"); } context.addFilterDef(filter); } for (FilterMap filterMap : filterMaps) { context.addFilterMap(filterMap); }
for (String listener : listeners) { context.addApplicationListener(listener); }
for (ServletDef servlet : servlets.values()) { Wrapper wrapper = context.createWrapper(); // Description is ignored // Display name is ignored // Icons are ignored // jsp-file gets passed to the JSP Servlet as an init-param if (servlet.getLoadOnStartup() != null) { wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); } if (servlet.getEnabled() != null) { wrapper.setEnabled(servlet.getEnabled().booleanValue()); } wrapper.setName(servlet.getServletName()); Map<String,String> params = servlet.getParameterMap(); for (Entry<String, String> entry : params.entrySet()) { wrapper.addInitParameter(entry.getKey(), entry.getValue()); } wrapper.setRunAs(servlet.getRunAs()); Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); for (SecurityRoleRef roleRef : roleRefs) { wrapper.addSecurityReference( roleRef.getName(), roleRef.getLink()); } wrapper.setServletClass(servlet.getServletClass()); MultipartDef multipartdef = servlet.getMultipartDef(); if (multipartdef != null) { if (multipartdef.getMaxFileSize() != null && multipartdef.getMaxRequestSize()!= null && multipartdef.getFileSizeThreshold() != null) { wrapper.setMultipartConfigElement(new MultipartConfigElement( multipartdef.getLocation(), Long.parseLong(multipartdef.getMaxFileSize()), Long.parseLong(multipartdef.getMaxRequestSize()), Integer.parseInt( multipartdef.getFileSizeThreshold()))); } else { wrapper.setMultipartConfigElement(new MultipartConfigElement( multipartdef.getLocation())); } } if (servlet.getAsyncSupported() != null) { wrapper.setAsyncSupported( servlet.getAsyncSupported().booleanValue()); } wrapper.setOverridable(servlet.isOverridable()); context.addChild(wrapper); } for (Entry<String, String> entry : servletMappings.entrySet()) { context.addServletMapping(entry.getKey(), entry.getValue()); }
从上面的代码我们至少可以总结下面值得注意的两点:
-
Servlet容器对上下文参数、监听器、过滤器、Servlet的装配顺序为:上下文参数->过滤器->监听器->Servlet。
-
Servlet支持容器启动时加载、是否异步配置以及配置覆盖。
三、组件的初始化
下面转入StandardContext这个类,StandardContext是Servlet上下文的标准实现,标准实现在Tomcat里面有一个系列,包括StandardServer、StandardService、StandardEngine、StandardHost等等,这些都是Tomcat不同级别的容器的标准实现。
我们可以直接定位到startInternal这个方法的实现,我们看下我们关系的部分步骤:
-
第一个是(Set up the context init params),这里我就不翻译了。
-
解析来的是Call ServletContainerInitializer,这里是值得web中间件开发者注意的,我们可以通过自定义ServletContainerInitializer服务来做一些组件初始化之前的事情,如在这个环节动态装配组件?获取容器上下文?
-
Configure and call application event listeners,包括下面的error信息(Error listenerStart)这里是很重要的一步,有经验的开发者肯定会对这个error信息有点熟悉,应用起不来?呵呵……,提一下熟悉Spring的同学都知道ContextLoaderListener这个东西,Spring就是将对Spring容器的初始化工作放在的这个监听器里面实现的,包括对Spring配置文件的解析,容器初始化……
-
Configure and call application filters,和error信息:Error filterStart。这里是对Filter进行了初始化。
-
Load and initialize all "load on startup" servlets。这里对配置了load on startup的Servlet进行初始化。
我想介绍的主要就是上面的五个步骤了,总结一下主要组件的初始化顺序为:上下文参数->监听器->过滤器->Servlet。
这里有一点不舒服的地方是Tomcat对着三个组件的装配和初始化顺序有点差别。无耻的贴一点代码一起欣赏下:
// Create context attributes that will be required if (ok) { getServletContext().setAttribute( JarScanner.class.getName(), getJarScanner()); } // Set up the context init params mergeParameters(); // Call ServletContainerInitializers for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializers.entrySet()) { try { entry.getKey().onStartup(entry.getValue(), getServletContext()); } catch (ServletException e) { log.error(sm.getString("standardContext.sciFail"), e); ok = false; break; } } // Configure and call application event listeners if (ok) { if (!listenerStart()) { log.error( "Error listenerStart"); ok = false; } } // Configure and call application filters if (ok) { if (!filterStart()) { log.error("Error filterStart"); ok = false; } } // Load and initialize all "load on startup" servlets if (ok) { if (!loadOnStartup(findChildren())){ log.error("Error loadOnStartup"); ok = false; } }
四、过滤器的执行顺序
过滤器装配的时候主要涉及到三个数据结构:filters、filterMaps、以及filterMappingNames。我们分析下,如果根据请求来执行过滤器链的话,那么我们肯定是需要映射规则的,因此我们锁定filterMaps这个数据,查找下findFilterMaps这个方法哪里调用就好了。果然我们在ApplicaitonFilterFactory里面找到了下面这个片段:
// Acquire the filter mappings for this Context StandardContext context = (StandardContext) wrapper.getParent(); FilterMap filterMaps[] = context.findFilterMaps();
这里其实是按照FilterMapping的配置来构造过滤器链的,那么我们深刻的了解到了一点,请求过滤链的顺序为FilterMapping的配置顺序。其中有行代码值得注意:
filterChain.setServlet(servlet);
在创建过滤器链的方法实现里面,Servlet也被放进去了。
五、过滤器链以及Servlet的最终执行
我们拿到过滤器链之后顺藤摸瓜,找到调用createFilterChain的地方,在StandardWrapperValve类里面(这里又是一个标准实现)。贴一个片段:
// Call the filter chain for this request // NOTE: This also calls the servlet's service() method try { if ((servlet != null) && (filterChain != null)) { // Swallow output if needed if (context.getSwallowOutput()) { try { SystemLogHandler.startCapture(); if (request.isAsyncDispatching()) { //TODO SERVLET3 - async ((AsyncContextImpl)request.getAsyncContext()).doInternalDispatch(); } else if (comet) { filterChain.doFilterEvent(request.getEvent()); request.setComet(true); } else { filterChain.doFilter(request.getRequest(), response.getResponse()); } } finally { String log = SystemLogHandler.stopCapture(); if (log != null && log.length() > 0) { context.getLogger().info(log); } }
上面的Comments可告诉我们过滤器链在这里执行,而且值得注意的是Servlet也是在这里面执行的。我从ApplicationFilterChain里面捞出了两句英文,大家慢慢体会:
-
Call the next filter if there is one.
-
We fell off the end of the chain -- call the servlet instance.
Tomcat的过滤器链是一种典型的责任链模式的实践,组织的也还算精巧,到此我们的分析已经结束了,相信通过对源码的分析,我们可以对web.xml有了更深刻的了解。
本片文章是基于apache-tomcat7.0.56版本源代码,由作者原创,如果有我没有讲到的地方欢迎大家在评论里面补充。
作者:陆晨
于2016年1月1日(元旦)