【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader

java中的类加载器

在我看来,java的类加载器,其实就是将.class文件,变成java中的java.lang.Class对象的工具,其中包含查找文件,加载字节码,转换字节码的过程。
在java中,有三种自带的类加载器:

  • 启动类加载器(Bootstrap ClassLoader)
    主要加载java的一些核心库(路径为:<JAVA_HOME>/jre/lib),这个类加载器与其他的类加载器有些不同,它由C/C++实现,所以不是java.lang.ClassLoader的子类,我们在java代码中是用不了这个加载器的。

  • 扩展加载器(Extention ClassLoader)
    加载<JAVA_HOME>/jre/lib/ext里的类,它的父类是启动类加载器(Bootstrap ClassLoader),所以它由启动类加载器加载,但是由于启动类加载器是由C/C++实现的,如果你尝试用getParent获取它的父类加载器,会得到null值。

  • 应用程序类加载器/系统类加载器(Application ClassLoader/System ClassLoader)
    此类加载器由启动类加载,它的父加载器为扩展加载器,负责加载(CLASSPATH)指定的类,可以通过ClassLoader.getSystemClassLoader()获取。

除了自带的类加载器,用户还可以自定义类加载器
用户通过继承java.lang.ClassLoader,重写findClass和loadClass方法即可。

各个类加载器的关系图:


类加载器图

双亲委派模型

在这里先举一个例子,假如我们在一个java项目中新建一个java.lang的包,然后再在这个包下新建一个String的类,如下图,从代码上看,完全没问题。


但是仔细一想,如果我某个类需要使用String类,new出来,究竟使用哪个类呢?虚拟机该怎么加载呢?

为了解决这一问题,java实现了双亲委派这一模型,这种模型规定:

除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器,假如有一个类要加载进来,一个类加载器不会马上尝试自己将其加载,而是委派给父类加载器,父类加载器收到后又尝试委派给其父类加载器,以此类推,直到委派给启动类加载器,这样一层一层往上委派。只有父类加载器反馈自己没法完成这个加载时,子加载器才会尝试自己加载。——汪建《Tomcat内核设计剖析》

再回到我们刚刚的例子,现在有两个String类,一个是在<JAVA_HOME>/jre/lib下的rt.jar包下,由启动类加载器加载,一个是我们自己写的,由应用程序类加载器加载。
如果我们请求载入String类,我们自己编辑的java程序,默认由Application加载器加载,如果有双亲委派机制,Application加载器不会先尝试自己加载这个类,先请求父类加载器,一直到启动类加载器,这是发现启动类加载器下面的jar包中已经有String这个类了,直接加载成功返回,这样既解决了加载选择的问题,也防止了jdk自带的程序被用户恶意破坏。

虽然jdk默认使用双亲委派机制,但有时候我们为了自己的需求,也可以破坏这一机制,那就是使用自定义类加载器,除了重写findClass方法之外,还需重写loadClass方法,在loadClass方法中先自己加载,不找super.loadClass方法。

tomcat中的类加载器解析

在前面Tomcat启动过程简述中,提到了三个类加载器,commonLoader、catalinaLoader和sharedLoader,除了这几个类加载器以外,其实还有WebappClassLoader。
那么Tomcat为什么要定义这么多类加载器呢,其实是为了解决一下几个问题:

  • 有一个统一的目录,让每个apps目录下的web项目共享类库
  • 每个web项目之间如果有相同的jar依赖,不能冲突,保持各项目私有jar库互相隔离
  • 支持热部署的功能
    有了需求以后,再来探究tomcat是怎么处理这一机制的。

我们再看这三个类加载器的初始化代码:

private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader=this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", 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 {

        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;

        value = replace(value);

        List<Repository> repositories = new ArrayList<>();

        String[] repositoryPaths = getPaths(value);

        for (String repository : repositoryPaths) {
            // Check for a JAR URL repository
            try {
                @SuppressWarnings("unused")
                URL url = new URL(repository);
                repositories.add(
                        new Repository(repository, RepositoryType.URL));
                continue;
            } catch (MalformedURLException e) {
                // Ignore
            }

            // Local repository
            if (repository.endsWith("*.jar")) {
                repository = repository.substring
                    (0, repository.length() - "*.jar".length());
                repositories.add(
                        new Repository(repository, RepositoryType.GLOB));
            } else if (repository.endsWith(".jar")) {
                repositories.add(
                        new Repository(repository, RepositoryType.JAR));
            } else {
                repositories.add(
                        new Repository(repository, RepositoryType.DIR));
            }
        }
       return ClassLoaderFactory.createClassLoader(repositories, parent);
    }

在initClassLoaders方法中,可以看出,commonLoader父加载器为应用加载器,catalinaLoader 和sharedLoader 的类加载器的父加载器为commonLoader。createClassLoader方法中,第一句,意思是去conf/catalina.properties中找对应的配置:

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

server.loader=

shared.loader=

在Tomcat中,它希望这三个加载器对应的加载目录中的类库可以有以下约束和功能:

  • 放置在common.loader配置的目录:类库可被Tomcat和所有的Web应用程序共同使用。
  • 放置在server.loader配置的目录:类库可被Tomcat使用,对所有的Web应用程序都不可见。
  • 放置在shared.loader配置的目录:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。

事实上,tomcat团队为了简化部署过程,除了common对应的配置中有值,其他的都配置为空的,所以都指向commonLoader这一个类加载器。

创建完这三个类加载器以后,马上执行

Thread.currentThread().setContextClassLoader(catalinaLoader);

这句话是什么意思呢?

使用线程上下文类加载器,可以打破双亲委派机制,实现让一个加载器请求其他非父类加载器去加载它需要的类。比如:
我们通过前面的内容已经知道,commonLoader的父类加载器为应用加载器,假设我们再common目录下导入Spring依赖的jar包,去管理webapps目录下的项目,显然Spring相关的类是commonLoader加载的,但是webapps项目下的WEB-INF/lib下的包不在commonLoader的范围内,是由WebAppClassLoader(后文会讲解)加载的,如果按照双亲委派模型去加载,是加载不成功的。

为了解决这一问题,首先要知道,如果没有显式得声明是由哪个类加载器加载的类,比如我们代码中的new 一个类,默认由当前线程的加载器加载,如果没有用Thread.currentThread().setContextClassLoader进行设置,默认由ContextClassLoader加载,这个加载器属于系统类加载器。因此对于上面的问题,我们只需要执行Thread.currentThread().setContextClassLoader(WebAppClassLoader),Spring访问webapps下的类时,用WebAppClassLoader加载就可以了。

设置了线程类加载器以后,执行如下代码:

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);

这一段通过反射,调用了Catalina类的setParentClassLoader方法,且看这个方法:


public void setParentClassLoader(ClassLoader parentClassLoader) {
        this.parentClassLoader = parentClassLoader;
}

通过这两段代码可以看到,将sharedLoader赋值给了Catalina的parentClassLoader 属性。然后我们查看Catalina的getParentClassLoader方法的调用栈:





通过查看调用栈,我们看到,在StandardContext类中调用了这个方法,StandardContext组件在tomcat中,相当于部署在tomcat上的一个web项目,我们继续跟如何设置和启动WebApp类加载器的:
由tomcat启动流程可以知道,组件中的startInternal方法,是由LifecycleBase的子类调用start方法时,调用了自身实现的startInternal钩子方法。
在StandardContext的startInternal方法中,有这么一段:

if (getLoader() == null) {
    WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
    webappLoader.setDelegate(getDelegate());
    setLoader(webappLoader);
}

这里将sharedLoader设置为了此StandardContext对象的类加载器了,继续跟入setLoader方法:

    @Override
    public void setLoader(Loader loader) {

        Lock writeLock = loaderLock.writeLock();
        writeLock.lock();
        Loader oldLoader = null;
        try {
            // Change components if necessary
            oldLoader = this.loader;
            if (oldLoader == loader)
                return;
            this.loader = loader;

            // Stop the old component if necessary
            if (getState().isAvailable() && (oldLoader != null) &&
                (oldLoader instanceof Lifecycle)) {
                try {
                    ((Lifecycle) oldLoader).stop();
                } catch (LifecycleException e) {
                    log.error("StandardContext.setLoader: stop: ", e);
                }
            }

            // Start the new component if necessary
            if (loader != null)
                loader.setContext(this);
            if (getState().isAvailable() && (loader != null) &&
                (loader instanceof Lifecycle)) {
                try {
                    ((Lifecycle) loader).start();
                } catch (LifecycleException e) {
                    log.error("StandardContext.setLoader: start: ", e);
                }
            }
        } finally {
            writeLock.unlock();
        }

        // Report this property change to interested listeners
        support.firePropertyChange("loader", oldLoader, loader);
    }

再看getLoader方法:

    @Override
    public Loader getLoader() {
        Lock readLock = loaderLock.readLock();
        readLock.lock();
        try {
            return loader;
        } finally {
            readLock.unlock();
        }
    }

可见这里巧妙地用了读写锁loaderLock设置和获取类加载器对象。
在设置过程中,如果有旧的不同的类加载器,先停掉,再开始当前的类加载器,进入 ((Lifecycle) loader).start();方法,进行生命周期管理的状态设置等,在start方法中,会调用其startInternal方法,如下:

/**
     * Start associated {@link ClassLoader} and implement the requirements
     * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected void startInternal() throws LifecycleException {

        if (log.isDebugEnabled())
            log.debug(sm.getString("webappLoader.starting"));

        if (context.getResources() == null) {
            log.info("No resources for " + context);
            setState(LifecycleState.STARTING);
            return;
        }

        // Construct a class loader based on our current repositories list
        try {

            classLoader = createClassLoader();
            classLoader.setResources(context.getResources());
            classLoader.setDelegate(this.delegate);

            // Configure our repositories
            setClassPath();

            setPermissions();

            ((Lifecycle) classLoader).start();

            String contextName = context.getName();
            if (!contextName.startsWith("/")) {
                contextName = "/" + contextName;
            }
            ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
                    classLoader.getClass().getSimpleName() + ",host=" +
                    context.getParent().getName() + ",context=" + contextName);
            Registry.getRegistry(null, null)
                .registerComponent(classLoader, cloname, null);

        } catch (Throwable t) {
            t = ExceptionUtils.unwrapInvocationTargetException(t);
            ExceptionUtils.handleThrowable(t);
            log.error( "LifecycleException ", t );
            throw new LifecycleException("start: ", t);
        }

        setState(LifecycleState.STARTING);
    }

截取这一段:

            classLoader = createClassLoader();
            classLoader.setResources(context.getResources());
            classLoader.setDelegate(this.delegate);
 /**
     * Create associated classLoader.
     */
    private WebappClassLoaderBase createClassLoader()
        throws Exception {

        Class<?> clazz = Class.forName(loaderClass);
        WebappClassLoaderBase classLoader = null;

        if (parentClassLoader == null) {
            parentClassLoader = context.getParentClassLoader();
        }
        Class<?>[] argTypes = { ClassLoader.class };
        Object[] args = { parentClassLoader };
        Constructor<?> constr = clazz.getConstructor(argTypes);
        classLoader = (WebappClassLoaderBase) constr.newInstance(args);

        return classLoader;
    }

这里其实就是通过反射,生成一个WebappClassLoader加载器对象,并将之前传入的sharedLoader作为父类加载器。

接下来我们查看WebappClassLoader的start方法,由于它是WebappClassLoaderBase的子类,直接会调用WebappClassLoaderBase的start方法,如下:

    /**
     * Start the class loader.
     *
     * @exception LifecycleException if a lifecycle error occurs
     */
    @Override
    public void start() throws LifecycleException {

        state = LifecycleState.STARTING_PREP;

        WebResource classes = resources.getResource("/WEB-INF/classes");
        if (classes.isDirectory() && classes.canRead()) {
            localRepositories.add(classes.getURL());
        }
        WebResource[] jars = resources.listResources("/WEB-INF/lib");
        for (WebResource jar : jars) {
            if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
                localRepositories.add(jar.getURL());
                jarModificationTimes.put(
                        jar.getName(), Long.valueOf(jar.getLastModified()));
            }
        }

        state = LifecycleState.STARTED;
    }

可以清楚地看到,WebappClassLoader加载了项目下/WEB-INF/classes和/WEB-INF/lib目录。
由于Jsp加载过程涉及到语法分析,生成java文件的过程,这里不再说明,以后章节专门来看。

至此,我们可以总结出tomcat中的类加载器为:

tomcat类加载器关系图

ClassLoaderFactory——tomcat中的类加载器工厂

Tomcat使用java提供的URLClassLoader将创建类加载器的细节封装在了ClassLoaderFactory中,使其能够很方便地创建自己的自定义类加载器。
我们先看创建类加载器的定义:



通过传入内部类Repository的结合和父加载器,便可以返回一个自定义类加载器。
且看两个内部类的定义:

    public static enum RepositoryType {
        DIR,
        GLOB,
        JAR,
        URL
    }

    public static class Repository {
        private final String location;
        private final RepositoryType type;

        public Repository(String location, RepositoryType type) {
            this.location = location;
            this.type = type;
        }

        public String getLocation() {
            return location;
        }

        public RepositoryType getType() {
            return type;
        }
    }

RepositoryType 枚举类用来表示资源类型,单词意思很显然,包括了
DIR目录下的class和jar,GLOB目录下的jar资源,JAR单个jar包,URL从URL获取的资源。
类加载器工厂在创建类加载器的过程中,主要是将所有传入的Repository 对象转换成URL对象,放入到一个HashSet中,然后通过AccessController绕过权限检查,调用URLClassLoader的构造方法进行实例化类加载器。以后我们生成类加载器,也可以直接搬过来用了,哈哈。

结束语

Tomcat中的类加载器结构清晰,设计巧妙,由于篇幅原因,很多细节未讲到,通过这一篇的引导,希望可以在看Tomcat类加载器的时候,有一定的方向,以便以后更加深入的了解这一体系。

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