Tomcat源码阅读系列(六)类加载器

本文是Tomcat源码阅读系列的第六篇文章,本系列前五篇文章如下:
Tomcat源码阅读系列(一)使用IntelliJ IDEA运行Tomcat6源码
Tomcat源码阅读系列(二)Tomcat总体架构
Tomcat源码阅读系列(三)启动和关闭过程
Tomcat源码阅读系列(四)Connector连接器
Tomcat源码阅读系列(五)Catalina容器
本文主要介绍Tomcat的类加载器。
关于Java的类加载器的介绍,大家可以Google一下,网上关于其介绍比较多且比较详细,本文就不再重复介绍,本文主要介绍Tomcat的类加载器。关于Tomcat的类加载器可以分为两部分,第一个是Tomcat自身所使用的类加载器,这个是Tomcat服务器专用的,用于加载Tomcat服务器本身的class。另外一个就是WEB应用程序用的,每一个web应用程序都有自己专用的WebappClassLoader,用于加载属于自己应用程序的资源,例如/web-inf/lib下面的jar包,classes里面的class文件。下面分别介绍这Tomcat类加载器的创建和WebappClassLoader的特殊之处。

1 Tomcat类加载器的创建

说道Tomcat类加载的创建,还是从Bootstrap说起,Bootstrap#init()方法会调用initClassLoaders方法创建3个ClassLoader,它们分别是commonLoader,catalinaLoader,sharedLoader,其中catalinaLoader,sharedLoader的父亲加载器是commonLoader,initClassLoaders执行的过程中会执行createClassLoader,而createClassLoader是根据conf/catalina.properties文件中common.loader,server.loader,shared.loader的值来初始化,它的代码如下:

private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {
        String value = CatalinaProperties.getProperty(name + ".loader");
        //判断如果catalina.properties中没有配置对应的loader属性的话,直接返回父加载器,而默认情况下,
        //server.loader,shared.loader为空,那么此时的catalinaLoader,sharedLoader其实是同一个ClassLoader.
        if ((value == null) || (value.equals("")))
            return parent;

       //....................

       //根据环境变量的配置替换字符串中的值.默认情况下,common.loader的值为
       //common.loader=${catalina.base}/lib,${catalina.base}/lib/.jar,${catalina.home}/lib,${catalina.home}/lib/.jar

        //最终创建了org.apache.catalina.loader.StandardClassLoader实例
        ClassLoader classLoader = ClassLoaderFactory.createClassLoader
            (locations, types, parent);

        // ....................

    }

这里就可以看到创建了commonLoader,catalinaLoader与sharedLoader,而且可以很清楚的看到他们之间的双亲结构。上面分析了Tomcat在启动的时候,初始化的几个ClassLoader,接下来我们再来继续看看,commonLoader,catalinaLoader,sharedLoader具体都用在什么地方。
commonLoader
根据程序可知commonLoader除了在initClassLoaders处使用,在其他地方,并没有使用。
catalinaLoader和sharedLoader

 public void init() throws Exception{

        //..............

        //设置setContextClassLoader为catalinaLoader 设置当前线程classLoader  
        Thread.currentThread().setContextClassLoader(catalinaLoader);

        //设置securityClassLoad为catalinaLoader
        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        //catalinaLoader在这里使用
        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];

        //注意!!注意!! 这里将Catalina的ParentClassLoader设置为sharedLoader 
        //调用刚刚创建的org.apache.catalina.startup.Catalina对象的setParentClassLoader设置classLoader,shareloader  
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;//将这个启动的实例保存起来,这里引用的是catalina的类型的对象  
    }

通过上面的代码,我们可以清楚的看到调用了Catalina的setParentClassLoader放,那么到这里我们可能又要想知道,设置了parentClassLoader以后,sharedLoader又是在哪里使用的呢?
我们查找Catalina.parentClassLoader的引用,发现在createStartDigester()方法中引用了parentClassLoader,并有注释,

  // When the 'engine' is found, set the parentClassLoader.当发现engine时,将其设置为parentClassLoader
  digester.addRule("Server/Service/Engine",
                         new SetParentClassLoaderRule(parentClassLoader));

按图索骥,我们找到SetParentClassLoaderRule类,如下:

final class SetParentClassLoaderRule extends Rule {
    public SetParentClassLoaderRule(ClassLoader parentClassLoader) {
        this.parentClassLoader = parentClassLoader;
    }
    ClassLoader parentClassLoader = null;
    public void begin(String namespace, String name, Attributes attributes)
        throws Exception {
        if (digester.getLogger().isDebugEnabled())
            digester.getLogger().debug("Setting parent class loader");
        Container top = (Container) digester.peek();

        //由Server/Service/Engine的配置可以看出,出现StandardEngine时,将其parentClassLoader设置为sharedLoader
        top.setParentClassLoader(parentClassLoader);
    }
}

可以知道,StandardEngine的parentClassLoader被设置为sharedLoader。到此,我们已经知道了commonLoader,catalinaLoader,sharedLoader在不同地方的使用。如果要深挖,那么就需要知道StandardEngine的parentClassLoader在哪里使用了。
StandardEngine的parentClassLoader
实际上调用top.setParentClassLoader(parentClassLoader);时,实际调用的是ContainerBase的setParentClassLoader方法,

 public void setParentClassLoader(ClassLoader parent) {
        ClassLoader oldParentClassLoader = this.parentClassLoader;
        this.parentClassLoader = parent;//设置parentClassLoader为sharedLoader
        support.firePropertyChange("parentClassLoader", oldParentClassLoader,this.parentClassLoader);                            
    }

如果想知道ContainerBase.parentClassLoader的使用,我们仅仅需要知道哪里调用了ContainerBase.getParentClassLoader()即可。分析getParentClassLoader的调用栈,我们可以看到StandardContext.start()方法调用了这个方法。如下

//................
 if (getLoader() == null) {
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
}
//................
try {
    if (ok) {
        // Start our subordinate components, if any
        if ((loader != null) && (loader instanceof Lifecycle))
            ((Lifecycle) loader).start();
        //other code    
    }catch(Exception e){
    }

通过查看上面的代码,我们看到在StandardContext启动的时候,会创建webapploader,创建webapploader的时候会将getParentClassLoader方法返回的结果(这里返回的其实就是sharedLoader)赋值给自己的parentClassLoader变量,接着又会调用到Webapploader的start方法,接下来就来看看WebappLoader的start方法,我们摘取一部分与本篇相关的代码片段如下:

  classLoader = createClassLoader();
  classLoader.setResources(container.getResources());
  classLoader.setDelegate(this.delegate);
  classLoader.setSearchExternalFirst(searchExternalFirst);

从上的代码可以看到调用了WebappLoader#createClassLoader方法创建一个classLoader,那么我们再看来看看WebappLoader#createClassLoader的代码:

  private WebappClassLoader createClassLoader()
        throws Exception {

        Class clazz = Class.forName(loaderClass);
        WebappClassLoader classLoader = null;
        //parentClassLoader为sharedLoader不为空
        if (parentClassLoader == null) {
            parentClassLoader = container.getParentClassLoader();
        }
        Class[] argTypes = { ClassLoader.class };
        Object[] args = { parentClassLoader };
        Constructor constr = clazz.getConstructor(argTypes);
        classLoader = (WebappClassLoader) constr.newInstance(args);

        return classLoader;

    }

在上面的代码里面,loaderClass是WebappLoader的实例变量,其值为org.apache.catalina.loader.WebappClassLoader,那么上面的代码其实就是通过反射调用了WebappClassLoader的构造函数,然后传递了sharedLoader作为其父亲加载器。
代码阅读到这里,我们已经基本清楚了Tomcat中ClassLoader的总体结构,总结如下: 在Tomcat存在common,cataina,shared三个公共的classloader,默认情况下,这三个classloader其实是同一个,都是common classloader,针对每个webapp,也就是Standardcontext都有自己的WebappClassLoader来加载每个应用自己的类(对应代码中的StandardContext类,在StandardContext.start()方法调用了创建WebappLoader和loader.start方法!)。上面的描述,我们可以通过下图形象化的描述:
描述

2 WebappClassLoader

我们知道Java的ClassLoader机制有parent-first的机制,而这种机制是在loadClass方法保证的,一般情况下,我们只需要重写findClass方法就好了,而对于WebAppClassLoader,通过查看源代码,我们发现loadClass和findClass方法都进行了重写,那么我们首先就来看看它的loadClass方法,它的代码如下:

public synchronized Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException {

        if (log.isDebugEnabled())
            log.debug("loadClass(" + name + ", " + resolve + ")");
        Class clazz = null;

        // Log access to stopped classloader
        if (!started) {
            try {
                throw new IllegalStateException();
            } catch (IllegalStateException e) {
                log.info(sm.getString("webappClassLoader.stopped", name), e);
            }
        }

        // (0) Check our previously loaded local class cache 
        //从当前ClassLoader的本地缓存中加载类,如果找到则返回
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }

        // (0.1) Check our previously loaded class cache
        // 本地缓存没有的情况下,调用ClassLoader的findLoadedClass方法查看jvm是否已经加载过此类,如果已经加载则直接返回。
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            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
        //通过系统的来加载器加载此类,这里防止应用写的类覆盖了J2SE的类,这句代码非常关键,如果不写的话,
        //就会造成你自己写的类有可能会把J2SE的类给替换调,另外假如你写了一个javax.servlet.Servlet类,放在当前应用的WEB-INF/class中,
        //如果没有此句代码的保证,那么你自己写的类就会替换到Tomcat容器Lib中包含的类。
        try {
            clazz = system.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // (0.5) Permission to access this class when using a SecurityManager
        //如果启用了SecurityManager,则检查此类是否允许被载入,如果不允许,则抛出异常
        if (securityManager != null) {
            int i = name.lastIndexOf('.');
            if (i >= 0) {
                try {
                    securityManager.checkPackageAccess(name.substring(0,i));
                } catch (SecurityException se) {
                    String error = "Security Violation, attempt to use " +
                        "Restricted Class: " + name;
                    log.info(error, se);
                    throw new ClassNotFoundException(error, se);
                }
            }
        }

        //判断是否需要委托给父类加载器进行加载,delegate属性默认为false,那么delegatedLoad的值就取决于filter的返回值了,
        //filter方法中根据包名来判断是否需要进行委托加载,默认情况下会返回false.因此delegatedLoad为false
        boolean delegateLoad = delegate || filter(name);

        // (1) Delegate to our parent if requested
        //因为delegatedLoad为false,那么此时不会委托父加载器去加载,这里其实是没有遵循parent-first的加载机制。
        if (delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader1 " + parent);
            ClassLoader loader = parent;
            if (loader == null)
                loader = system;
            try {
                clazz = loader.loadClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                ;
            }
        }

        // (2) Search local repositories
        if (log.isDebugEnabled())
            log.debug("  Searching local repositories");
        //调用findClass方法在webapp级别进行加载
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from local repository");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            ;
        }

        // (3) Delegate to parent unconditionally
        //如果还是没有加载到类,并且不采用委托机制的话,则通过父类加载器去加载。
        if (!delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader at end: " + parent);
            ClassLoader loader = parent;
            if (loader == null)
                loader = system;
            try {
                clazz = loader.loadClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                ;
            }
        }

        throw new ClassNotFoundException(name);

    }

通过上面的描述,我们可以知道Tomcat在加载webapp级别的类的时候,默认是不遵守parent-first的,这样做的好处是更好的实现了应用的隔离,但是坏处就是加大了内存浪费,同样的类库要在不同的app中都要加载一份。接下来,我们再看一下findClass方法调用的findClassInternal方法

  protected Class findClassInternal(String name)
        throws ClassNotFoundException {

        if (!validate(name))
            throw new ClassNotFoundException(name);

        String tempPath = name.replace('.', '/');
        String classPath = tempPath + ".class";

        ResourceEntry entry = null;

        if (securityManager != null) {
            PrivilegedAction<ResourceEntry> dp =
                new PrivilegedFindResourceByName(name, classPath);
            entry = AccessController.doPrivileged(dp);
        } else {
            //通过名称去当前webappClassLoader的仓库中查找对应的类文件
            entry = findResourceInternal(name, classPath);
        }

       //....................

            try {
            //通过defineClass转变为Jvm可以识别的Class对象返回
                clazz = defineClass(name, entry.binaryContent, 0,
                        entry.binaryContent.length, 
                        new CodeSource(entry.codeBase, entry.certificates));
            } catch (UnsupportedClassVersionError ucve) {
                throw new UnsupportedClassVersionError(
                        ucve.getLocalizedMessage() + " " +
                        sm.getString("webappClassLoader.wrongVersion",
                                name));
            }
            entry.loadedClass = clazz;
            entry.binaryContent = null;
            entry.source = null;
            entry.codeBase = null;
            entry.manifest = null;
            entry.certificates = null;
        }

        return clazz;

    }

重写了loadClass方法,对J2SE的类,使用系统类加载器加载,对Web应用自己的类使用自己的类加载器加载,不遵守parent-first的,这样做的好处是更好的实现了应用的隔离,这便是WebappClassLoader的精妙之处,但是坏处就是加大了内存浪费,同样的类库要在不同的app中都要加载一份。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值