Tomcat源码五部曲(二)、Web项目加载过程

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

在上一章说过,当StandardContext被添加到StandardHost下,会触发StandardContext的start方法进行启动,首先自然是调用StandardContext的initInternal(),接着是startInternal(),initInternal()也没做多少关键的事,而startInternal()又过于多,300的方法实在很难分析,所以,还是只找几个关键的地方。

一、交给ContextConfig处理

在上一章也说过,StandardContext还要依赖ContextConfig来进行工作,而StandardContext要想调用ContextConfig,需要发出一个事件,遍历所有监听者,把事件传递给他,也就是下面这段。事件名称为CONFIGURE_START_EVENT。

 //告诉ContextConfig开始配置启动了
  fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

接着是ContextConfig的lifecycleEvent方法,首先判断事件类型,如果是CONFIGURE_START_EVENT,则直接调用configureStart开始加载,configureStart具体实现也在webConfig方法中。

 if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
     /**
      * 当StandardContext发出CONFIGURE_START_EVENT事件时,开始配置web项目
      */

     configureStart();
 } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
     beforeStart();
 } 
 protected synchronized void configureStart() {
     /**
      * 具体配置实现
      */

     webConfig();
  }
 ............

webConfig方法也就是本文的重中之重,这里面涉及很多复杂的问题。

大概分四步骤,读取解析配置文件,也就是web.xml、找出带有注解的Class、合并、向StandardContext中设置对应属性,包括创建和添加Wrapper。

这里找出注解的意思是找到带有WebServlet、WebFilter、WebListener注解的类,也就是如为什么我们不用在web.xml中配置servlet,而使用@WebServlet即可的原因。

一个项目中有多个Servlet,每个Servlet要被Wrapper所管理,也就是一个Servlet对应一个Wrapper,这些Wrapper还要被StandardContext管理,故要将创建的Wrapper作为子容器添加到StandardContext。

二、读取配置文件

当然读取web.xml也是通过Digester来完成,此时的Digester通过DigesterFactory来创建,web.xml的读取规则在WebRuleSet中,也是非常多。

protected void webConfig() {
     /**
      * 处理web.xml的配置
      */

     if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
         ok = false;
     }
}

读取后存放在webXml对象中,如内部变量servlets存放着所有配置的Servlet信息,servlets是一个HaspMap,key为<servlet-name>配置的值,value为ServletDef,ServletDef内部的servletClass变量保存着这个Servlet的完整类名。

还有内部变量servletMappings保存着映射关系,也是一个HashMap,key为url地址,value为servlets变量的key。

三、处理带有注解的Class

 if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
     /**
      * 处理带有注解的Servlet
      */

     processClasses(webXml, orderedFragments);
 }

但是你会发现,这一步是有条件的,第一个条件也就是web.xml配置的属性meta,如果配置为true,最终取反为false,那就是说必须第二个条件满足才处理注解。Tomcat源码五部曲(二)、Web项目加载过程metadata-complete属性意思也就是告诉Tomcat是否寻找注解。

而第二个条件虽然判断的只是size>0,但是往这个集合中put的数据的过程还是复杂的,在前一步的processServletContainerInitializers方法中处理,大概就是扫描当前应用每一个jar包里面META-INF/services/javax.servlet.ServletContainerInitializer所指定的实现类,并且这个实现类带有注解HandlesTypes才添加,默认貌似是0个,所以,处不处理注解由metadata-complete说了算。

要完本步任务,还不止一个方法,首先看processClasses,在这个方法中首先列举出/WEB-INF/classes下的所有资源,然后遍历,如果不是META-INF目录,则再交给processAnnotationsWebResource处理。

 protected void processClasses(WebXml webXml, Set<WebXml> orderedFragments) {

     Map<String, JavaClassCacheEntry> javaClassCache = new HashMap<>();
     if (ok) {
         /**
          * 获取项目/WEB-INF/classes下所有资源
          */

         WebResource[] webResources =
                 context.getResources().listResources("/WEB-INF/classes");
         for (WebResource webResource : webResources) {
             /**
              * 如果为META-INF目录则跳过
              */

             if ("META-INF".equals(webResource.getName())) {
                 continue;
             }
             processAnnotationsWebResource(webResource, webXml,
                     webXml.isMetadataComplete(), javaClassCache);
         }
     }
     if (ok) {
         processAnnotations(orderedFragments, webXml.isMetadataComplete(), javaClassCache);
     }
     javaClassCache.clear();
 }

在进一层,在此方法中,逻辑也比较简单,就是依次拿出所有class文件,在交给processAnnotationsStream处理。

protected void processAnnotationsWebResource(WebResource webResource,
                                             WebXml fragment, boolean handlesTypesOnly,
                                             Map<String, JavaClassCacheEntry> javaClassCache)
 
{
    /**
     * 如果还是文件夹,递归遍历
     */

    if (webResource.isDirectory()) {
        WebResource[] webResources =
                webResource.getWebResourceRoot().listResources(
                        webResource.getWebappPath());
        if (webResources.length > 0) {
            if (log.isDebugEnabled()) {
                log.debug(sm.getString(
                        "contextConfig.processAnnotationsWebDir.debug",
                        webResource.getURL()));
            }
            for (WebResource r : webResources) {
             //递归
                processAnnotationsWebResource(r, fragment, handlesTypesOnly, javaClassCache);
            }
        }
        /**
         * 如果是一个文件,并且文件名以.class结尾
         */

    } else if (webResource.isFile() &&
            webResource.getName().endsWith(".class")) {
        /**
         * 拿到他的输入流
         */

        try (InputStream is = webResource.getInputStream()) {
            /**
             * 处理这个Class
             */

            processAnnotationsStream(is, fragment, handlesTypesOnly, javaClassCache);
        } catch (IOException e) {
            log.error(sm.getString("contextConfig.inputStreamWebResource",
                    webResource.getWebappPath()), e);
        } catch (ClassFormatException e) {
            log.error(sm.getString("contextConfig.inputStreamWebResource",
                    webResource.getWebappPath()), e);
        }
    }
}

processAnnotationsStream中的难点其实是JavaClass对象。也就是如何解析parser.parse(),这个过程没有深入了解。

最终可以根据JavaClass获得其class的父类、接口集合、类名、注解等信息。

但是要判断handlesTypesOnly变量,为false才继续处理,其实在此处有一个有意思的事,在后面会说。

 protected void processAnnotationsStream(InputStream is, WebXml fragment,
                                         boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache)

         throws ClassFormatException, IOException 
{
     ClassParser parser = new ClassParser(is);
     /**
      * 解析成JavaClass对象
      */

     JavaClass clazz = parser.parse();
     checkHandlesTypes(clazz, javaClassCache);
     if (handlesTypesOnly) {
         return;
     }
     processClass(fragment, clazz);
 }

这是倒数第二步,根据不同的注解处理不同的逻辑,其实都是向WebXml中添加对象,也就是添加和映射Servlet、Filter、Listener。

protected void processClass(WebXml fragment, JavaClass clazz) {
    /**
     * 获取类中所有注解
     */

    AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
    if (annotationsEntries != null) {
        String className = clazz.getClassName();
        /**
         * 遍历类中的注解
         */

        for (AnnotationEntry ae : annotationsEntries) {
            String type = ae.getAnnotationType();
            /**
             * 如果是@WebServlet注解
             */

            if ("Ljavax/servlet/annotation/WebServlet;".equals(type)) {
                processAnnotationWebServlet(className, ae, fragment);
                /**
                 * 如果是@WebFilter注解
                 */

            } else if ("Ljavax/servlet/annotation/WebFilter;".equals(type)) {
                processAnnotationWebFilter(className, ae, fragment);
                /**
                 * 如果是@WebListener注解
                 */

            } else if ("Ljavax/servlet/annotation/WebListener;".equals(type)) {
                fragment.addListener(className);
            } else {
                
            }
        }
    }
}

当上面这些结束后,当前的webXml对象中存放着项目所有Servlet、Listener等信息,也就是把web.xml中的配置信息都映射到了WebXml对象中。

四、合并

接下来还要为项目添加一个默认的处理,默认的处理通过getDefaultWebXmlFragment获取,通过webXml.merge(defaults);合并。但这一步还是要metadata-complete=false的,

你会发现默认有个/地址,如果我们的项目也配置了个/该怎么处理?

其实Tomcat还是会优先我们的。Tomcat源码五部曲(二)、Web项目加载过程

五、configureContext

这算是最后一步,这部分主要将WebXml对象中的信息转移到StandardContext中。所以,我们会在这个方法中大量看到context.setXXX(webXml.getxxx());

在上面也提到过会把Servlet类放入Wrapper中,让Wrapper管理我们的Servlet,所以,就有了下面一段代码,最终构造好Wrapper将他添加到StandardContext中。

(Wrapper的实现类是StandardWrapper)

 
 for (ServletDef servlet : webxml.getServlets().values()) {
     Wrapper wrapper = context.createWrapper();
     //wrapper.setXXX .....
     //给Wrapper设置Servlet名。
     wrapper.setServletClass(servlet.getServletClass());
 //添加到Context中
 context.addChild(wrapper);
}

六、结尾1

回到StandardContext的startInternal方法,下面还有个关键点,也就是下面这段,findChildren此时返回所有Wrapper。

  /**
   * 加载并启动项目
   */

  if (!loadOnStartup(findChildren())){
      log.error(sm.getString("standardContext.servletFail"));
      ok = false;
  }

loadOnStartup方法找出loadOnStartup大于0的Wrapper,并按照loadOnStartup的大小依次实例化并调用Servlet的init()方法。

这里的loadOnStartup也就是我们配置的<load-on-startup>

 public boolean loadOnStartup(Container children[]) {
     
     TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
     /**
      * 遍历Wrapper
      */

     for (int i = 0; i < children.length; i++) {
         Wrapper wrapper = (Wrapper) children[i];
         int loadOnStartup = wrapper.getLoadOnStartup();
         /**
          * 如果loadOnStartup小于0,跳过开始下一个
          */

         if (loadOnStartup < 0)
             continue;
         Integer key = Integer.valueOf(loadOnStartup);
         ArrayList<Wrapper> list = map.get(key);
         if (list == null) {
             list = new ArrayList<>();
             map.put(key, list);
         }
         /**
          * 记录下这个loadOnStartup大于0的wrapper
          */

         list.add(wrapper);
     }
     /**
      * 遍历所有loadOnStartup值大于0的Wrapper
      */

     for (ArrayList<Wrapper> list : map.values()) {
         for (Wrapper wrapper : list) {
             try {
                 /**
                  * 启动
                  */

                 wrapper.load();
             } catch (ServletException e) {
                 getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
                       getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
                 if(getComputedFailCtxIfServletStartFails()) {
                     return false;
                 }
             }
         }
     }
     return true;
 }

Warpper.load先根据Servlet类名实例化,后在initServlet(),initServlet就是调用到我们的init方法。

 private synchronized void initServlet(Servlet servlet)
         throws ServletException 
{
  //  xxxxxxxxxxxxxxxxxxxxxx
      servlet.init(facade); //init
  //  xxxxxxxxxxxxxxxxxxxx

七、结尾2

在上面说过,这段代码还要判断handlesTypesOnly为false才继续解析注解,processAnnotationsStream被两个地方所调用,其中一个地方就是上述在解析项目目录下所有class文件的时候,还有个地方是在解析jar包的时候。

protected void processAnnotationsStream(InputStream is, WebXml fragment,
                                        boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache)

        throws ClassFormatException, IOException 
{
    ClassParser parser = new ClassParser(is);
    /**
     * 解析成JavaClass对象
     */

    JavaClass clazz = parser.parse();
    checkHandlesTypes(clazz, javaClassCache);
    if (handlesTypesOnly) {
        return;
    }
    processClass(fragment, clazz);
}

这又得看 processJarsForWebFragments方法,返回/WEB-INF/lib下的所有jar。

也就是说,我们只要写一个类,继承HttpServlet,并且加入注解@WebServlet,打包成jar,放入WEB-INF/lib下,也是可以正常工作的。

其实加不加载这个jar中Servlet还是由这段控制,如果htOnly运算结果为false,则解析,但是processJarsForWebFragments的返回值还不止是/WEB-INF/lib下的jar,还有TOMCAT_HOME/lib目录下的jar(这里指非Tomcat本身的jar),这是不是说明我们打包后放入这个下面也能解析?

其实不是的,fragment.getWebappJar()的返回值代表是不是项目的一部分,如果不是则返回false,最终取反为true,那么到了processAnnotationsStream中是不会继续解析的。

(fragment.getWebappJar()通过类加载器来判断)

protected void processAnnotations(Set<WebXml> fragments,
                                  boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache)
 
{
    for (WebXml fragment : fragments) {

        boolean htOnly = handlesTypesOnly || !fragment.getWebappJar() ||
                fragment.isMetadataComplete();
  ...............
        processAnnotationsUrl(url, annotations, htOnly, javaClassCache);
 ...............
    }
}


原文始发于微信公众号(十四个字节):Tomcat源码五部曲(二)、Web项目加载过程