tomcat的类加载器初步认识


  • tags: tomcat
  • categories:笔记
  • date: 2017-03-09 21:02:08

前几天初步通过源代码看看并且学习了JDK的类加载器相关内容。其实在java的生态系统平台内,类加载器是至关重要的组件吧,因为设计到了OOP的对象抽象class的加载问题。若是连OOP的基础class都加载不了,那也不要说再干些其他的事情了。所以从另一方面来就说明了它的重要性,以至于不同的其他软件基金会例如apache中tomcat,我们经常使用的tomcat猫,就有自己的类加载机制。不过tomcat的类加载器与jdk自己的双亲委托模型的类加载可是有不同的,而且tomcat中对类加载的使用也是值得我们去深入了解了解的。这次,我想从基于我使用了挺久的tomcat猫看看,它的类加载器大致是如何工作的,核心的东西主要有那些呢?就来个对猫猫的初步探索吧,应该能给自己对这只猫的理解更细致吧。


cat.jpg

在开始呢,其实tomcat的源代码挺多的,内部细节挺多的,apache力量真是让人钦佩。造出了很多有意思,有用的轮子。给我们这些用轮子的人,能养家糊口,工作需要,学习需要等等。对于我这个java方向的码农,当然大apache啦。所以,就根据自己的理解和参考一些网上大神的理解自己也倒腾倒腾tomcat的类加载器方面的内容,也不枉我经常使用它。对他其实也是一知半解,都是在使用层次上理解它,它的内部的东西真是不甚了解,对我来说挺神秘的。所以,我想慢慢通过自己对它内部的重要组件的学习,剖析来揭开它的神秘面纱。↖(▔▽▔)↗ (这里使用的是tomcat7源代码)

在学习tomcat的类加载之前,必须要明确一个概念或者说的基本环境:tomcat服务器本身和部署在其之上的应用之间类关系,tomcat服务器自身启动,也是依赖与jdk的,也是需要通过自己的类加载加载它所需要的很多类;在tomcat启动成功之后,我们每次部署的应用也是需要加载自己所需要的类的,以及每个web应用当然都会加载自己所需要的类。所以,不同的类加载器,就将这些tomcat自己启动运行需要的类,单个应用需要的类,应用之间共享的基础类库资源进行了规划与管理。

tomcat的类加载器有哪些?

要想知道类加载器相关的内容,当然要知道除了在jdk基础上的三种类加载(Bootstrap,ext,appclassLoader),它有那些自定义的类加载呢?也只有在知道tomcat有那些自己定义的类加载的前提下,才能知道它们是如何联系的,具体是如何在tomcat运行中加载类的呢?与jdk的JVM加载类过程有什么不同么?

其实,想要知道tomcat的类加载很简单,就是调用getClassLoader和getParent等方法就够了。写个简单的servlet,然后对类加载器信息进行在浏览器页面输出即可。具体的例子可以参考下面:(多了写无关的格式输出,为了看起来更直观)

/*ClassLoaderServlet.java*/
public class ClassLoaderServlet extends HttpServlet{

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws Exception{

        response.setContentType("text/html");
        response.setCharacterEncoding("utf8");
        PrintWriter out = response.getWriter();
        ClassLoader loader = getClass().getClassLoader();
        String qname = getClass().getName();
        out.println("<!DOCTYPE \">");
        out.println("<HTML>");
        out.println("  <HEAD><TITLE>Tomtcat ClassLoader</TITLE></HEAD>");
        out.println("  <BODY>");
        out.println("当前类:"+qname.substring(qname.lastIndexOf(".")+1)+"的类加载器是:"+loader.getClass().getName()+"</br>");
        out.println(loader+"</br>");
        out.println("-----------------"+"</br>");
        while(loader != null){
            ClassLoader tmp = loader;
            loader = loader.getParent();
            if(loader != null){
                out.println(tmp.getClass().getName()+"的父类加载器是:"+ loader.getClass().getName()+"</br>");
            }else{
                out.println(tmp.getClass().getName()+"的父类加载器是:"+ loader+"</br>");
            }
            out.println("-----------------"+"</br>");
        }
        out.println("<hr>");
        out.println("  </BODY>");
        out.println("</HTML>");
        out.flush();
        out.close();

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception{
        // TODO Auto-generated method stub
        super.doGet(req, resp);
    }

}

在web.xml中进行简单的servlet配置,通过浏览器访问即可:

    <servlet>
        <servlet-name>ClassLoaderServlet</servlet-name>
        <servlet-class>servlet.classloader.ClassLoaderServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>ClassLoaderServlet</servlet-name>
        <url-pattern>/servlet/ClassLoaderServlet</url-pattern>
    </servlet-mapping>

最后可以得到tomcat的类加载信息:可以看到除了jdk基础的三层类加载器结构,其实下面还是有例如StandardClassLoader,WebappClassLoader,CommonClassLoader,ShareClassLoader,ServerClassLoader等自定义类加载器的。

当前类:ClassLoaderServlet的类加载器是:org.apache.catalina.loader.WebappClassLoader
WebappClassLoader context: /springmvcdemo delegate: false repositories: /WEB-INF/classes/ ----------> Parent Classloader: org.apache.catalina.loader.StandardClassLoader@38d1258b 
-----------------
org.apache.catalina.loader.WebappClassLoader的父类加载器是:org.apache.catalina.loader.StandardClassLoader
-----------------
org.apache.catalina.loader.StandardClassLoader的父类加载器是:sun.misc.Launcher$AppClassLoader
-----------------
sun.misc.Launcher$AppClassLoader的父类加载器是:sun.misc.Launcher$ExtClassLoader
-----------------
sun.misc.Launcher$ExtClassLoader的父类加载器是:null
-----------------

## 所以,根据上述的servlet程序,可以看到tomcat中的类加载器的层级结构如下:
## 但是要注意,这个类结构其实与tomcat官方文档的类加载结构有些区别。那具体有什么区别或者联系?
        BootStrapClassLoader
            |
        ExtClassLoader
            |   
        AppClassLoader
            |
        StandardClassLoader
            |
        WebappClassLoader

其实,在tomcat官方文档中,说明了tomcat7的类加载结构如下图:(每个层次的类加载器也有说明)
注意每层的类加载器与JVM的概念并不是完全的一样的。但是tomcat所有自定义的类加载都是继承AppClassLoader这个系统类加载器的。

    Bootstrap  --也不完全是JVM的bootstrap
       |
     System    --也不完全是JVM的appClassLoader的概念
       |
     common
    /   \
  Webapp1   Webapp2....

对于每个层次的类加载是这么说明的:(英文翻译有点菜,肯定有差异啦,建议英语好的可以自己看英文去啦,可以点击官网查看官方tomcat7类加载器说明](http://tomcat.apache.org/tomcat-7.0-doc/class-loader-howto.html))

那CATALINA_HOME和CATALINA_BASE有什么区别呢?总的来说,区别就是在设置tomcat多实例的时候。在单实例时候,也就是我们经常使用的状态,这两个变量指向的都是tomcat的安装目录(包含完整的bin,conf,webapps,logs,temp,work等等文件夹); 多实例也就是在一个主tomcat下(可被公有使用),配置多个私有tomcat实例,每个实例都包含完整的私有目录,如conf,webapps,work,logs目录。总结来说,CATALINA_HOME是安装目录,CATALINA_BASE是每个tomcat实例的工作根目录
tomcat单实例与多实例实践
CATALINA_HOME与CATALINA_BASE

  • Bootstrap:
    这个类加载用于加载JVM运行提供的基础类以及拓展路径($JAVA_HOME/jre/lib/ext)类库中的类。(自己口水话:其实也就是为tomcat自身服务器的启动运行以及应用中用到的一些JDK基础类库中的类进行加载)

  • System:
    这个类加载器通常是用来初始化那些在CLASSPATH环境变量路径上的变量内容的,在这类库路径上的所有类对于tomcat服务器本身以及服务器上部署的web应用程序来说都是可见的。然而,对于标准的tomcat容器启动时的脚本($CATALINA_HOME/bin/catalina.sh 或者%CATALINA_HOME%\bin\catalina.bat)来说,这些启动脚本总是忽略CLASSPATH路径上的内容变量,转而读取其他路径上的类库来构建system类加载。

    eg:我们通常如果在tomcat安装目录中启动tomcat服务器,都是运行startup.bat或者startup.sh,其实这些脚本内部主要工作也就是调用catalina.bat来启动服务器。看看startup.bat中部分windows脚本,也就是读取tomcat主目录,查找catalina.bat脚本,若是存在,则执行该脚本啦:

if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
echo The CATALINA_HOME environment variable is not defined correctly
echo This environment variable is needed to run this program
goto end
:okHome

set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat"   --设置执行脚本文件路径
rem Check that target executable exists
if exist "%EXECUTABLE%" goto okExec                 --脚本存在,则调到执行子函数

然后我们可以看看catalina.bat中对于CLASSPATH环境变量的设置:(截取部分相关内容)

rem Ensure that any user defined CLASSPATH variables are not used on startup,
rem but allow them to be specified in setenv.bat, in rare case when it is needed.
set CLASSPATH=

rem Add on extra jar file to CLASSPATH
rem Note that there are no quotes as we do not want to introduce random
rem quotes into the CLASSPATH
if "%CLASSPATH%" == "" goto emptyClasspath
set "CLASSPATH=%CLASSPATH%;"
:emptyClasspath
set "CLASSPATH=%CLASSPATH%%CATALINA_HOME%\bin\bootstrap.jar"

set "CLASSPATH=%CLASSPATH%;%CATALINA_BASE%\bin\tomcat-juli.jar"
goto juliClasspathDone
:juliClasspathHome
set "CLASSPATH=%CLASSPATH%;%CATALINA_HOME%\bin\tomcat-juli.jar"
:juliClasspathDone

从脚本代码中,确实可以看到,CLASSPATH变量在这个类加载阶段是要读取bootstrap.jar和tomcat-juli.jar两个类库文件的。

system类加载器的读取路径包括以下几个:

  • $CATALINA_HOME/bin/bootstrap.jar : 这个类库包含了在启动tomcat服务器时,用于初始化服务器配置的main()方法中所依赖的所有类的实现。(main方法内,调用了bootstrap.init()方法,也就是bootstrap.jar时tomcat服务器启动阶段的核心类库,不能加载这个类库的话,tomcat服务器启动失败)

看了看bootstrap.jar中目录结构如下:

bootstrap.jar
├─loader
│      StandardClassLoader.class
│      StandardClassLoaderMBean.class
│
├─security
│      SecurityClassLoad.class
│
└─startup
        Bootstrap.class  --主要有个init()方法,用于初始化CatalinaHome,CatalinaBase,配置线程类加载器,读取了catalina.properties文件加载commonLader,catalinaLoader,sharedLoader等类加载器。该类包含了tomcat服务器生命周期服务的所有方法。
        catalina.properties
        CatalinaProperties.class
        ClassLoaderFactory.class
        Tool.class
  • $CATALINA_HOME/bin/tomcat-juli.jar或者$CATALINA_BASE/bin/tomcat-juli.jar: 哎呀,这个java类库包,就是用来强化tomcat日志相关方面的。不要小看日志,日志可是很重要的东西哦。从上面catalina.bat脚本也看到了CLASSPATH配置了tomcat-juli.jar包。

  • $CATALINA_HOME/bin/commons-daemon.jar: 这个类库不在catalina.bat|.sh脚本构建的CLASSPATH变量内,而是与bootstrap.jar类库引用相关。在tomcat服务启动时相关守护线程的管理。

public final class Bootstrap
{

  private static Bootstrap daemon = null;
  private Object catalinaDaemon = null;
  ...
   public static void main(String[] args)
  {
    if (daemon == null)
    {
      Bootstrap bootstrap = new Bootstrap();
      try {
        bootstrap.init();
      } catch (Throwable t) {
        handleThrowable(t);
        t.printStackTrace();
        return;
      }
      daemon = bootstrap;

   } .... 
}
  • Common
    该类加载器加载那些可被tomcat服务器自身和部署的所有web应用均可见(可以调用)的类。通常一般不会修改或者用其他类库来替换Common加载的原有类库。该类加载器加载类库的搜索路径时被定义在catalina.properties($CATALINA_BASE/conf/catalina.properties)文件中的common.loader属性值中。common.loader默认的类库搜索路径会按照以下排列顺序进行加载:
    A。加载$CATALINA_BASE/lib中未被打包的类和资源。
    B。加载$CATALINA_BASE/lib中所有jar类库。
    C。加载$CATALINA_HOME/lib中未被打包的类文件与资源。
    D。加载$CATALINA_HOME/lib中所有jar类库包
    至于那些lib下的每个jar包的资源有什么作用可以查看官方文档,有详细说明。
    我们可以查看catalina.properties文件中common.loader属性,看看有那些默认的配置:
#$CATALINA_BASE/conf/catalina.properties
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
  • WebappX
    这个类加载器被创建是用来加载tomcat服务器单实例上每个应用自己所需要的类。(注意每个实例之间的类默认是互不可见的),该加载器加载的类库不仅包括每个web应用下/WEB-INF/classes目录下的所有.class类和资源文件,还包括web应用下/WEB-INF/lib目录下所有的jar类库。这两个路径下的类字节码和jar类库对于每个单应用程序都是可见的,但是对于同个实例上其他web应用是不可见的。
    通常web应用目录大概如下:(每个web应用编译后的java文件到classes目录和lib目录在WEB-INF同一级子目录,不过classes文件加载类在lib之前)
├─common
│  ├─ace
│  ├─css
│  ├─image
│  ├─js
│  └─jsp
├─demo
├─jsp
├─META-INF
└─WEB-INF
    ├─classes
    │  └─demo
    │      └─classloader
    └─lib

在知道了tomcat有那些类加载之后,我们下一步,就可以探索它们之间的,自己内部的加载器有什么联系和它们是如何协同来加载class的,与jdk的双亲委托加载有什么不同和相同点?

内部类加载关系与加载类过程?

根据官网上所说的,将tomcat类加载器级别笼统的分为Bootstrap,System,Common,WebappX级别。也是按照父子级别从左往右形成层级关系。那么tomcat服务器上的每个web应用程序是如何加载一个类呢?还是双亲委托模型么?tomcat程序的加载类的过程与JVM加载类的双亲委托机制是有些差异的,我们就要关注这个差异点

类加载路径顺序?

当tomcat应用中,在处理加载某个类的请求时候,默认情况下,首先处理该请求的是WebappX级别层次的类加载器。在WebappX类加载器加载类的时候,首先会从自己的应用的类库路径中查找,而不是将该请求委托给自己的父级类加载器来处理[这里的父子级别类加载器只是限于tomcat自定义的类加载]。
但是,在WebappX加载类这个阶段,也是有例外的情况。也就是属于JRE类库中的基类是不能被覆盖重写的(eg:在web应用中定义了java.lang.Object类,由于是WebappX类加载器先加载,那当然不能将该自定义同名的class类将JRE的java.lang.Object类给替换覆盖了)。
而其他某些类,例如J2SE1.4后的解析XML组件类库或者其他非JRE基础类可以被重写(关于XML Parsers与java JDK之间的实现封装关系可以自己去查看)。
特别的,在web应用中任何包含servlet api类的jar文件都会被该类加载器显式的忽略掉,这就意味着不必要将这些类型的jars文件放置在web应用类库中。在web应用加载某个类的时候,除了WebappX类加载器可以首先尝试加载该类,其余的类加载器都会按照JVM中的双亲委托模型来加载该类。

因此,根据web应用程序的目录结构,在web应用中加载某个类或者资源的时候,类加载器查找类将会按照下面的路径顺序来查找:(tomcat服务器正常启动后)

  • JVM内bootstrap 基础类库(对应JVM中的BootstrapClassLoader,ExtClassLoader)。
  • 每个web应用项目下/WEB-INF/classes目录下的所有类 (相当于WebappX级别加载器)
  • 每个web应用项目下/WEB-INF/lib/*.jar所有jar库包 (相当于WebappX级别加载器)
  • System 类加载器加载的类库(上面所说的)
  • Common 类加载加载的类库(上面所说的)

但是,这个路径顺序也是可以通过配置修改的,我们可以看到上面ClassLoaderServlet在输出类加载的时候,注意到输出WebappClassLoader属性:

WebappClassLoader context: /springmvcdemo delegate: false repositories: /WEB-INF/classes/ 
----------> Parent Classloader: org.apache.catalina.loader.StandardClassLoader@38d1258b 

注意到,有个delegate:false字段,就是是否代理的意思。可以看到,在没有使用代理情况下,类加载器是按照上述路径查找的。反过来想想若是强制显式的使用代理配置,类查找路径将会有影响的。在web应用中,可以通过设置Loader组件配置<Loader delegate="true"/>来委托查找类,配置详情
顺序将会修改成下面顺序:

  • JVM内bootstrap 基础类库。
  • System 类加载器加载的类(上面所说的)
  • Common 类加载加载的类(上面所说的)
  • 每个web应用项目下/WEB-INF/lib/*.jar所有jar库包 (相当于WebappX级别加载器)
  • 每个web应用项目下/WEB-INF/classes目录下的所有类 (相当于WebappX级别加载器)

可以看到完全是与JVM的双亲委托模型加载机制一样了,最后才是自定义的WebappX来加载类。

tomcat的web应用class加载过程?

这一点,其实通过tomcatd的WebappClassLoader.java源代码可以看出:

    /**
     * Should this class loader delegate to the parent class loader
     * <strong>before</strong> searching its own repositories (i.e. the
     * usual Java2 delegation model)?  If set to <code>false</code>,
     * this class loader will search its own repositories first, and
     * delegate to the parent only if the class or resource is not
     * found locally. Note that the default, <code>false</code>, is
     * the behavior called for by the servlet specification.
     */
    protected boolean delegate = false;

想要具体看看tomcat的web应用上class是如何加载,那么可以通过查看WebappClassLoader.java源代码来分析分析了, 加载类的loadClass方法如下:

    /**
     * Load the class with the specified name, searching using the following
     * algorithm until it finds and returns the class.  If the class cannot
     * be found, returns <code>ClassNotFoundException</code>.
     * <ul>
     * <li>Call <code>findLoadedClass(String)</code> to check if the
     *     class has already been loaded.  If it has, the same
     *     <code>Class</code> object is returned.</li>
     * <li>If the <code>delegate</code> property is set to <code>true</code>,
     *     call the <code>loadClass()</code> method of the parent class
     *     loader, if any.</li>
     * <li>Call <code>findClass()</code> to find this class in our locally
     *     defined repositories.</li>
     * <li>Call the <code>loadClass()</code> method of our parent
     *     class loader, if any.</li>
     * </ul>
     * If the class was found using the above steps, and the
     * <code>resolve</code> flag is <code>true</code>, this method will then
     * call <code>resolveClass(Class)</code> on the resulting Class object.
     *
     * @param name Name of the class to be loaded
     * @param resolve If <code>true</code> then resolve the class
     *
     * @exception ClassNotFoundException if the class was not found
     */
    @SuppressWarnings("sync-override")
    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLockInternal(name)) {

            Class<?> clazz = null;
            //注意,这里是查找的本地tomcat应用的查找class缓存
        //调用的是本类中自定义的方法
            // (0) Check our previously loaded local class cache
            clazz = findLoadedClass0(name);     
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
            //调用的是java.lang.ClassLoader的JVM的查找class缓存
            // (0.1) Check our previously loaded class cache
            clazz = findLoadedClass(name);      
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }

            // (0.2) Try loading the class with the system class loader, to prevent
            //       the webapp from overriding J2SE classes
            try {
                //可以看到这里是因为webappClassLoader首先加载web应用class类的缘故,
                //从而会判断class是不是JVMSE中的基础类库中类。防止覆盖基础类实现。
                clazz = j2seClassLoader.loadClass(name);  
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            boolean delegateLoad = delegate || filter(name);    
           /*先判断是否设置了delegate属性,设置为true,那么就会完全按照JVM的"双亲委托"机制流程加载类。
            *若是默认的话,是先使用WebappClassLoader自己处理加载类的。
            *当然了,若是委托了,使用双亲委托亦没有加载到class实例,那还是最后使用WebappClassLoader加载。
            */
            // (1) Delegate to our parent if requested
            if (delegateLoad) {                         
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // (2) Search local repositories
            try {
    //若是没有委托,则默认会首次使用WebappClassLoader来加载类。通过自定义findClass定义处理类加载规则。
                clazz = findClass(name);    
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

//若是WebappClassLoader在/WEB-INF/classes,/WEB-INF/lib下还是查找不到class,
//那么无条件强制委托给System,Common类加载器去查找该类。
            // (3) Delegate to parent unconditionally
            if (!delegateLoad) {        
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
        }

        throw new ClassNotFoundException(name);
    }


###这里注意到上面的j2seClassLoader.loadClass其实就是调用JVM中双亲类加载机制来确保J2SE基础类不被破环,
###与上述的类查找步骤第一步bootstrap基础类库是想吻合的。
    public WebappClassLoaderBase(ClassLoader parent) { 
        ...
        ClassLoader j = String.class.getClassLoader();  
        if (j == null) {
            j = getSystemClassLoader();   //调用JVM的java.lang.ClassLoader的方法得到appClassLoader
            while (j.getParent() != null) {
                j = j.getParent();      //得到ExtClassLoader
            }
        }
        this.j2seClassLoader = j;
        ...
    }


哎呀,这个过程与JVM的类加载器过程差不多,就不用画流程图了。这里也要知道,在查找某个class缓存时候,先是找的tomcat应用自己在启动时候加载的类资源缓存,若是查找不到了,才找JVM的class缓存。也要注意某些重要的方法有override重写关键字,对于我们理解它们关系还是有帮助的。类加载器之间的关系和各自职责也差不多了解了,最后,在看看webappClassLoader是如何加载某个class的吧。具体的细节,就是在上述代码最后的findClass方法中。

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {

        // Ask our superclass to locate this class, if possible
        // (throws ClassNotFoundException if it is not found)
        Class<?> clazz = null;
        try {
            if (hasExternalRepositories && searchExternalFirst) {
                try {
                /*又试图先调用父类URLClassLoader的findClass方法,URLClassLoader确实有该方法,
                *该方法是用来通过URLClassPath查找name对应的符合URL规范的class类路径,读取该类字节码,
                *在调用defineClass方法定义对应class实例。(注意super与getParent可是有很大的不同)
                */
                    clazz = super.findClass(name);  
                } catch(ClassNotFoundException cnfe) {
                    // Ignore - will search internal repositories next
                } catch(AccessControlException ace) {
                    log.warn("WebappClassLoaderBase.findClassInternal(" + name
                            + ") security exception: " + ace.getMessage(), ace);
                    throw new ClassNotFoundException(name, ace);
                } catch (RuntimeException e) {
                    if (log.isTraceEnabled())
                        log.trace("      -->RuntimeException Rethrown", e);
                    throw e;
                }
            }
            if ((clazz == null)) {      //父类无法加载该类
                try {
                /*加载当前web应用所有资源路径下的类字节文件,就是处理ResourceEnty,path,class等等东西
                *最后也都是在找到class二进制字节流后,使用defineClass方法来定义class实例。
                */
                    clazz = findClassInternal(name);    
                } catch(ClassNotFoundException cnfe) {
                    if (!hasExternalRepositories || searchExternalFirst) {
                        throw cnfe;
                    }
                } catch(AccessControlException ace) {
                    log.warn("WebappClassLoaderBase.findClassInternal(" + name
                            + ") security exception: " + ace.getMessage(), ace);
                    throw new ClassNotFoundException(name, ace);
                } catch (RuntimeException e) {
                    if (log.isTraceEnabled())
                        log.trace("      -->RuntimeException Rethrown", e);
                    throw e;
                }
            }
            if ((clazz == null) && hasExternalRepositories && !searchExternalFirst) {
                try {
                    clazz = super.findClass(name);
                } catch(AccessControlException ace) {
                    log.warn("WebappClassLoaderBase.findClassInternal(" + name
                            + ") security exception: " + ace.getMessage(), ace);
                    throw new ClassNotFoundException(name, ace);
                } catch (RuntimeException e) {
                    if (log.isTraceEnabled())
                        log.trace("      -->RuntimeException Rethrown", e);
                    throw e;
                }
            }
            if (clazz == null) {
                if (log.isDebugEnabled())
                    log.debug("    --> Returning ClassNotFoundException");
                throw new ClassNotFoundException(name);
            }
        } catch (ClassNotFoundException e) {
            if (log.isTraceEnabled())
                log.trace("    --> Passing on ClassNotFoundException");
            throw e;
        }
        return (clazz);

    }

其实,总的来说呢,tomcat在启动时候,就会大致创建以下几种类加载器:

  • Bootstrap类加载器
     加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)
  • System tomcat系统类加载器
    加载tomcat启动的类,比如bootstrap.jar,tomcat-guli.jar。通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。
  • Common tomcat服务通用类加载器
    加载tomcat自身使用以及部署在其上面的应用程序通用的一些类,加载路径可以在catalina.properties中common.loader属性配置。
  • WebappClassLoader tomcat应用类加载器
     每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。

在应用程序中,需要使用某个类的时候,tomcat的类加载器加载类的过程如下:(在默认未设置委托情况下)

Bootstrap(包含JVM的bootstrapClassLoader,extClassLoader加载的所有基础的,依赖的类) 
--->/WEB-INF/Classes/*.class
--->/WEB-INF/lib/*.jar 
---> System tomcat(与JVM中的System类加载器appClassLoader不同)加载的相关类集合 (%CATALINA_HOME%\bin\catalina.bat)
---> Common加载的相关类集合(%CATALINA_HOME%\conf\catalina.properties)

不同web应用间的类共享与隔离?

在讲完了tomcat的类加载器相关知识,就来说说如何利用类加载器的加载机制以及查找类路径特点,如何将不同的类在不同的应用程序之间共享或者隔离?
其实隔离的话,不同的tomcat上应用的类在编译部署之后都会在/WEB-INF/classes/目录下,和/WEB-INF/lib/目录下的jar库一样,两者在天生设计上就是属于每个单个应用自己可见,其他应用不可见的。

除了这两个目录下处于隔离的类库,我们主要还是要看看如何在同一个tomcat实例上,不同的应用如何共享一些公共的类库?
如何让tomcat多实例之间使用共享的类库?。主要就是看看catalina.properties文件,该文件位于$CATALINA_HOME|BASE/conf。catalina.properties配置文件里面对于类库的定义细节。

#
#
# List of comma-separated paths defining the contents of the "common"
# classloader. Prefixes should be used to define what is the repository type.
# Path may be relative to the CATALINA_HOME or CATALINA_BASE path or absolute.
# If left as blank,the JVM system loader will be used as Catalina's "common"
# loader.
# Examples:
#     "foo": Add this folder as a class repository
#     "foo/*.jar": Add all the JARs of the specified folder as class
#                  repositories
#     "foo/bar.jar": Add bar.jar as a class repository
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar

#
# List of comma-separated paths defining the contents of the "server"
# classloader. Prefixes should be used to define what is the repository type.
# Path may be relative to the CATALINA_HOME or CATALINA_BASE path or absolute.
# If left as blank, the "common" loader will be used as Catalina's "server"
# loader.
server.loader=

#
# List of comma-separated paths defining the contents of the "shared"
# classloader. Prefixes should be used to define what is the repository type.
# Path may be relative to the CATALINA_BASE path or absolute. If left as blank,
# the "common" loader will be used as Catalina's "shared" loader.
# Please note that for single jars, e.g. bar.jar, you need the URL form
# starting with file:.
shared.loader=

可以看见,有三种类加载器可以设置,默认三者配置都是相同的,即都与common.loader的key值相同。关于三者具体代表什么,可以参考此篇文章Tomcat ClassLoader机制介绍
common.loader加载器是server.loader,shared.loader两个类加载器的父类加载器,可以通过$CATALINA_HOME/bin/bootstrap.jar中可以看出:

public final class Bootstrap
{
  ...
  protected ClassLoader commonLoader = null;
  protected ClassLoader catalinaLoader = null;
  protected ClassLoader sharedLoader = null;

  private void initClassLoaders()
  {
    try
    {
      this.commonLoader = createClassLoader("common", null);
      if (this.commonLoader == null)
      {
        this.commonLoader = getClass().getClassLoader();
      }
      this.catalinaLoader = createClassLoader("server", this.commonLoader);
      this.sharedLoader = createClassLoader("shared", this.commonLoader);
    } catch (Throwable t) {
      handleThrowable(t);
      log.error("Class loader creation threw exception", t);
      System.exit(1);
    }
    ...

  }

    private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception
  {
   ...

   }

 ...
}

那这三个类加载器对我们应用之间的类库之间共享有什么作用?又该如何使用呢?

通常来说,common.loaderserver.loader都是tomcat服务器自身级别上面的类库设置,我们在日常开发中使用不是太多。若是我们任意修改这两个文件,对于不同实例或者同实例不同应用程序之间都会有或多或少的影响,如版本冲突,加载不到类等。其实关于多实例的这三个类加载器的使用,自己也还没有实践过,也不能妄下定论。日后经过实践之后,再来总结和修改。但是,单实例tomcat上多项目共享类库倒是可以通过shared.loader来配置,也有不少好处。

若是我们在单个tomcat上(catalina_home与catalina_base相同),部署着多个web项目,我们想设置一些对于所有web项目都可见,可访问,即共享。我们可以catalina.properties的shared.loader配置:

shared.loader=shared.loader=${catalina.base}/shared/lib,${catalina.base}/shared/lib/A.jar,b.jar,*.jar...

这就需要我们在在$catalina_home|base目录下创建shared目录或者是shared/lib目录,并将所有想要共享的jar库包,添加到该目录下。那么每个应用都能加载获取到该位置的class。在类加载器:common class loader ---> server class loader ---> shared class loader过程,最后可以获取到指定的共享类。这个路径既可以是相对路径,也可以是绝对路径,只要路径中URL指向的jar库能获取到class字节码即可。(其实也可以在common.loader上配置,但是在common上配置,感觉侵入式的力度大了些。<( ̄︶ ̄)> )

共享类库的好处:

  • 可以避免各个web项目之间重复加载相同的jar包,对JVM中存放class信息的内存区压力增大。这些没必要的开销可以减少。
  • 也可以提高tomcat的启动速度,因为减少每个web项目重复加载jar的时间,tomcat的reload速度得到提升。
    ....

好了,自己对tomcat的类加载器的理解写下来,但是自己对tomcat的理解还不够深,理解会有偏差。该片文章主要用于自己记录笔记并且供日后修改。仅供参考。

参考: Class Loader HOW-TO - tomcat 7 Tomcat ClassLoader机制介绍

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容