Tomcat源码深析之web.xml组件的处理

jopen 8年前

    这篇文章主要是带着读者通过分析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,这个方法里面主要做下面事情:

  1. 扫描应用打包的所有Jar来检索Jar包里面的web.xml配置并解析,放入内存。

  2. 对这些已经检索到的web配置进行排序。

  3. 基于SPI机制查找ServletContainerInitializer的实现,写web中间件的同学注意了,了解SPI以及                           ServletContainerInitializer机制这对于你来说可能是一个很好的知识点。

  4. 处理/WEB-INF/classes下面的类的注解,某个版本Servlet支持注解方式的配置,可以猜测相关事宜就是在这里干          的。

  5. 处理Jar包中的注解类。

  6. 将web配置按照一定规则合并到一起。

  7. 应用全局默认配置,还记得Tomcat包下面的conf文件夹下面有个web.xml配置文件吧。

  8. 将JSP转换为Servlet,这让我想起了若干年前对JSP的理解。

  9. 将web配置应用到Servlet上下文,也即Servlet容器。

  10. 将配置信息保存起来以供其他组件访问,使得其他组件不需要再次重复上面的步骤去获取配置信息了。

  11. 检索Jar包中的静态资源。

  12. 将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());  }

从上面的代码我们至少可以总结下面值得注意的两点:

  1. Servlet容器对上下文参数、监听器、过滤器、Servlet的装配顺序为:上下文参数->过滤器->监听器->Servlet。

  2. Servlet支持容器启动时加载、是否异步配置以及配置覆盖。

三、组件的初始化

下面转入StandardContext这个类,StandardContext是Servlet上下文的标准实现,标准实现在Tomcat里面有一个系列,包括StandardServer、StandardService、StandardEngine、StandardHost等等,这些都是Tomcat不同级别的容器的标准实现。

我们可以直接定位到startInternal这个方法的实现,我们看下我们关系的部分步骤:

  1. 第一个是(Set up the context init params),这里我就不翻译了。

  2. 解析来的是Call ServletContainerInitializer,这里是值得web中间件开发者注意的,我们可以通过自定义ServletContainerInitializer服务来做一些组件初始化之前的事情,如在这个环节动态装配组件?获取容器上下文?

  3. Configure and call application event listeners,包括下面的error信息(Error listenerStart)这里是很重要的一步,有经验的开发者肯定会对这个error信息有点熟悉,应用起不来?呵呵……,提一下熟悉Spring的同学都知道ContextLoaderListener这个东西,Spring就是将对Spring容器的初始化工作放在的这个监听器里面实现的,包括对Spring配置文件的解析,容器初始化……

  4. Configure and call application filters,和error信息:Error filterStart。这里是对Filter进行了初始化。

  5. 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里面捞出了两句英文,大家慢慢体会:

  1. Call the next filter if there is one.

  2. We fell off the end of the chain -- call the servlet instance.

Tomcat的过滤器链是一种典型的责任链模式的实践,组织的也还算精巧,到此我们的分析已经结束了,相信通过对源码的分析,我们可以对web.xml有了更深刻的了解。

本片文章是基于apache-tomcat7.0.56版本源代码,由作者原创,如果有我没有讲到的地方欢迎大家在评论里面补充。


作者:陆晨

于2016年1月1日(元旦)

来自: http://my.oschina.net/andylucc/blog/596046