Tomcat7源码分析(上)——启动过程和类加载器

上篇Java的文章中我将Java Servlet的笔记整理出来,其中简单介绍了Servlet的技术和相关概念。这篇文章我们来以Tomcat的源码为例,看看Servlet的容器怎么来实现。当然,这也是我本人较早时候的一个源码分析笔记,使用的Tomcat版本是7.0。

0. Tomcat简介

Tomcat,全名Apache Tomcat,最初是由Sun发起,后来捐赠给ASF,是Apache Jakarta下的一个子项目。Tomcat是对Servlet API定义的容器的一个完整实现,同时它也不仅仅是一个容器,也完全可以当做一个纯Java实现的HTTP服务器来使用。按照维基百科最早的记载,是在1999年发布了3.0.x版本(合并此前Sun维护的代码),可以说是一个比较早的Servlet容器实现。最初作为Sun对Servlet规范的一个参考实现,Tomcat完整性很好,最重要的,Tomcat是开源的,我们本文以Tomcat为例,进行分析。

当然,其它优秀的servlet容器还有很多,如Jetty等。

1. 启动过程

不管是操作系统,还是应用程序,都要从无到有,有一个循序渐进的启动过程。Tomcat也不例外,也是一切从头开始。嵌入其它应用,通过代码启动在新版本的Tomcat中也是支持的,但本文这里已最常见的命令行启动为例开始介绍。

我们打开Tomcat的压缩包,里面有startup.sh脚本和为了支持Windows环境的.bat文件,Tomcat启停的奥义就在这里。startup.sh里有一句:

exec "$PRGDIR"/"$EXECUTABLE" start "$@"

其中的$EXECUTABLE是在上面赋值的catalina.sh,也就是说会去执行catalina.sh,而且紧随其后的参数是start,我们继续看catalina.sh这个脚本,里面有这么一行开启的段落:

elif [ "$1" = "start" ] ; then

最终的执行语句是:

eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
      -Dcatalina.base="\"$CATALINA_BASE\"" \
      -Dcatalina.home="\"$CATALINA_HOME\"" \
      -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
      org.apache.catalina.startup.Bootstrap "$@" start \
      >> "$CATALINA_OUT" 2>&1 "&"

而$_RUNJAVA就是系统环境下的的java,忽略掉中间的参数不管,我们可以看到启动的Java类是org.apache.catalina.startup.Bootstrap。

_RUNJAVA="$JRE_HOME"/bin/java

而这个org.apache.catalina.startup.Bootstrap类里面有一个main方法,这和我们简单直接写的一个HelloWorld类编译执行的原理是一样的。 

这里,我们可以有一个结论,在读任何系统的源码时,都将有一个入口,把握住了入口就把握住了一切。而任何用java运行起来的应用最终也都会有一个public class和一个main,这是绝对的,虽然servlet开发不需关注,只要针对API编程即可,但即使这样最终还是通过servlet的container来满足这一点。

也就是这样,Tomcat启动了。

2. Bootstrap类

上文书说到org.apache.catalina.startup.Bootstrapmain方法得到执行。找到对应的tomcat源代码,我们看到其中可以分为2大块:静态对象daemon的创建和初始化、根据命令行参数对daemon调用对应的方法。

先看第一块,从daemon的声明我们可以看出其就是一个Bootstrap的对象,在其为null的时候需要新创建一个,并且执行init()方法,初始化后将其赋值给daemon。我们这里更进一步,看看Bootstrap的这个init()方法到底做了哪些事情。

    public void init()
        throws Exception
    {
        // Set Catalina path
        setCatalinaHome();
        setCatalinaBase();
        initClassLoaders();
        Thread.currentThread().setContextClassLoader(catalinaLoader);
        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        Class<?> startupClass =
            catalinaLoader.loadClass
            ("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.newInstance();

        // Set the shared extensions class loader
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;
    }

而这个方法,又可以分为两大部分:

  • 前5条语句,用来初始化catalina类加载器相关环境的变量,然后初始化各个类加载器对象,包括commonLoader、sharedLoader,其中最重要的就是catalinaLoader,将其设置为Bootstrap的一个属性值。
  • 从第六条语句开始,使用前面已经构件好的catalinaLoader加载tomcat最核心的对象,那就是org.apache.catalina.startup.Catalina类的对象catalinaDaemon,并以反射的方式调用其setParentClassLoader方法,把sharedLoader作为参数传入。

我们看到这个过程中,Bootstrap类持有的几个对象(静态的daemon、非静态的catalinaDaemon、commonLoader、sharedLoader、catalinaLoader)都得到了创建和必要的初始化。

那么接下来我们看main方法的后半部分。比如刚刚启动,我们得到的命令行参数是start,那么会执行如下这段代码:

            else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
            }

会分别调用Bootstrap类的daemon对象的setAwait()、load()和start()三个方法。这三个方法我们略微深入一些,发现是从Bootstrap的方法到Catalina方法的一个调用,而且中间都是用了反射方式。至于这3个方法最终具体做了什么,我们这里先不细说,后面会分块做详细整理,不过可以简单了解下,其中:

  • setAwait()是设置了Catalina对象的一个属性值,其作用是告诉服务器启动后保持运行状态,并开启特定端口监听后续发来的指令,直到收到SHUTDOWN指令,做关闭服务器处理。
  • load()则是加载和初始化。对整个Tomcat服务器相关的配置文件进行加载和解析处理,并对Tomcat的各个组件进行初始化配置操作。
  • start(),这个其实不用多说,就是正式启动Catalina,或者说启动了Tomcat服务器的核心工作。

以此为例,对于Bootstrap的其它命令,比如stop等,本文就不想详细叙述了,感兴趣的可以按照这个路子去查看源代码。

3. Tomcat中的类加载器

了解Java和JVM的各位朋友一定知道,现代的JVM中通常有三层默认的类加载器,分别是bootstrap类加载器、扩展类加载器和系统类加载器。这三者每两者间都是父子关系,即前者是后者的父亲或者双亲类加载器,并由此构建了一个“双亲委派关系”,或叫“代理”关系。关于JVM和Java类加载器,我本人争取在后面有机会单独给大家介绍,这里先简单提一下,感兴趣的可以先参考这篇文章:http://www.ibm.com/developerworks/cn/java/j-lo-classloader

除了Java环境自身的三层类加载器,前面提到的,在Tomcat中主要有commonLoader、catalinaLoader、sharedLoader这几个类加载器。细看源码,实际上这几个loader都是Tomcat中的org.apache.catalina.loader.StandardClassLoader类。StandardClassLoader直接继承于java.net中的URLClassLoader类,最终继承于java.lang.ClassLoader类。

而除了这三个loader外,Tomcat中还有个关键的ClassLoader,我也在这里一起介绍,那就是org.apache.catalina.loader.WebappClassLoader类。在实际的Tomcat实例中,会由多个WebappClassLoader类对象,就像其名字说描述的,一个Web app,就有一个这样的loader。

此节上面的第一段中提到,JVM中已经给我们提供了3层类加载器,并通过java.lang.ClassLoader的loadClass()方法实现逻辑,我们可以知道这3层类加载器中使用了“双亲委派”的方式来加载和定义类,从IBM那篇文章中我们也可以看到这一点。这样做的方式,一来大大提高了系统的灵活性和可扩展性,拓宽了Java类定义的空间,二来按照“双亲委派”模式,java.lang包中的系统类只能由系统自身加载,提高了系统的安全性。

对于一般的Java应用开发来讲,我们其实并不需要太过关注JVM所提供的运行环境,不必关注ClassLoader。但在Tomcat中,为了提高系统的灵活性,引入了commonLoader、sharedLoader、catalinaLoader;为了支持和分隔多个web应用,使用了WebappClassLoader。

  • Tomcat中的系统类加载器。Tomcat也是一个Java应用,他也是在最初系统提供的几层类加载器环境下运行起来的。那么Tomcat的一些最基本的类,也和其它简单Java应用一样,是通过系统的类加载器来加载的,比如默认配置下的tomcat/bin目录下的bootstrap.jar、tomcat-juli.jar、commons-daemon.jar这几个jar包中的类。
  • Tomcat的Common Loader。Common Loader是Tomcat在系统类加载器之上建立起来的,其父loader是系统类加载器。Common Loader负责上面几个jar包外的Tomcat的大部分java类,通常情况下是tomcat/lib下的所有jar包。
  • Webapp Class Loader。这个类加载器可以说是Tomcat种最重要的Class Loader,它创造了各个Web app空间。在实现上,它打破了系统默认规则,或者说是打破了java.lang.ClassLoader逻辑中的“双亲委派”模式,提供了一套自定义的类加载流程。默认情况下,对于一个未加载过的类,WebappClassLoader会先让系统加载java.lang.Object等Java本身的基础类,如果不是基础类则优先在当前Web app范围内查找并加载,如果没加载到,再交给common loader走标准的双亲委派模式加载。

对于上面这三点做一些说明:

  • 在tomcat/conf目录的catalina.properties中有common.loader、server.loader、shared.loader的配置,这分别对应着commonLoader、catalinaLoader和sharedLoader。我们可以看到,默认情况下,serverl.loader和shared.loader的配置是空的,这意味着此两者在运行时和commonLoader相同。实际上,在Tomcat5.5之后的版本中,做了简化,只有commonLoader具备实际意义。而在5.5及之前的版本中,三者各不相同,各有分工。可参看官方文档作对比:Tomcat-5.5-ClassLoaderTomcat-7.0-ClassLoader
  • WebappClassLoader的代理关系或者说类查找加载顺序,其实是可以通过delegate来进行配置的。如果配置不同,代码在查找类时会走不同的路劲。

以上,Tomcat7.0的类加载器结构图总体来看大致如下:

      Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 ...

有关Tomcat的启动过程和类加载器,本文先总结至此,后面的文章将继续对Tomcat的主体部分基于源代码作分析。

此条目发表在 Java, Java的应用, 开发, 计算机技术 分类目录,贴了 , , , 标签。将固定链接加入收藏夹。

Tomcat7源码分析(上)——启动过程和类加载器》有 4 条评论

  1. 秋瘦 说:

    后文在哪里??

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>