Tomcat-类加载器


​ 以问题切入,深入剖析了commonLoader与WebappClassLoader各自的定位与设计初衷,重点分析了WebappClassLoader如何在保证JVM核心类安全的前提下,部分打破双亲委派机制,实现Web应用之间的真正隔离。

​ 以当前8.5.x版本为例,在具体分析之前,先想一个问题,带着这个问题再去思考为什么这么设计:

​ 当我们把一个 Web 项目部署到 Tomcat 里后,项目里用到的各种 class 文件,Tomcat 到底是从哪些地方去加载它们的?以及为什么要用这些不同的类加载器去加载?

整体上可以简单归类为三部分来源:

  1. Tomcat 自己的依赖(由commonLoader加载)安装目录里lib目录下所有jar,这些都是 Tomcat 启动和运行时所需要的核心库。
  2. Web 应用本身的依赖(由WebappClassLoader加载),以context为user举例。
    1. webapps/user/WEB-INF/classes/:存放应用自身编译后的 class 文件
    2. webapps/user/WEB-INF/lib/ :存放应用所依赖的第三方 jar 包
  3. JDK依赖(由 BootstrapClassLoader 加载):比如 rt.jar(JDK 8 及以下)或 java.base(JDK 9 及以后),这些属于 JVM 层面的标准库,

​ Web 应用在运行时所依赖的 class,基本都来自于上述几个目录。而 Tomcat 为了支持不同context之间的隔离、避免类冲突、支持热部署等需求,专门设计了一套层级化的类加载器体系,来负责加载这些不同来源的 class。

commonLoader

conf/catalina.properties相关配置

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

初始化代码

调用路径为:Bootstrap#main -> Bootstrap#start -> Bootstrap#init -> Bootstrap#initClassLoaders

默认的conf/catalina.properties配置里server.loader和shared.loader都为空,所以catalinaLoader和sharedLoader都被赋值为了commonLoader


/**
 * 初始化三层类加载器:common、server(catalina)、shared
 */
private void initClassLoaders() {
    try {
        // commonLoader:公共类加载器,供 Tomcat 内部和所有 Web 应用共享
        // 默认搜索目录:
        //   ${catalina.base}/lib/
        //   ${catalina.base}/lib/*.jar
        //   ${catalina.home}/lib/
        //   ${catalina.home}/lib/*.jar
        //
        // 说明:
        //   - ${catalina.base}:运行时指定的工作目录,可以实现多实例共享一个 Tomcat 安装
        //   - ${catalina.home}:Tomcat 的安装目录
        //
        // 如果未在 catalina.properties 里配置 common.loader,则默认使用当前类的 ClassLoader
        commonLoader = createClassLoader("common", null);
        if (commonLoader == null) {
            commonLoader = this.getClass().getClassLoader();
        }
        // 默认为上面的commonLoader
        catalinaLoader = createClassLoader("server", commonLoader);
        // 默认为上面的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 {
    // 从 catalina.properties 读取对应的 loader 配置
    String value = CatalinaProperties.getProperty(name + ".loader");
    // value为空,则使用parent
    if ((value == null) || (value.equals(""))) {
        return parent;
    }

    // value解析和占位符替换
    value = replace(value);

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

    String[] repositoryPaths = getPaths(value);

    for (String repository : repositoryPaths) {
        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));
        }
    }
    // 根据class所在的位置,创建一个URLClassLoader
    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

classLoader引用设置

  1. 在Bootstrap#init()方法里,会将catalinaLoader绑定到当前线程的contextClassLoader里,同时,通过反射将sharedLoader设置到Catalina.parentClassLoader字段里
public void init() throws Exception {

    initClassLoaders();

    // 用catalinaLoader作为当前线程上下文的contextClassLoader,用于加载tomcat class
    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

  
    // 使用反射将sharedLoader设置到Catalina.parentClassLoader字段
    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);
}
  1. 在 Catalina 中创建用于解析 server.xml 的 Digester 时,涉及到类加载器的一些关键配置。这里仅整理与类加载器相关的部分逻辑

    • 设置Digester.useContextClassLoader=true

      ​ 这一配置让 Digester 在解析 server.xml 并实例化各个 Tomcat 组件时,优先使用当前线程的 contextClassLoader(即 catalinaLoader,本质上就是 commonLoader)来加载类。这样可以确保 Tomcat 核心组件统一通过 common 层加载

    • 设置 Engine 的 parentClassLoader

      ​ 在实例化 Engine 时,会将其内部的 ContainerBase.parentClassLoader 设置为 sharedLoader(其实也是 commonLoader)。这一步非常关键,因为后续每个 WebappClassLoader 的父加载器就是从这里继承而来

protected Digester createStartDigester() {
    long t1=System.currentTimeMillis();
    // Initialize the digester
    Digester digester = new Digester();
    // 使用当前Thead的contextClassLoader加载指定的class
    digester.setUseContextClassLoader(true);
    // 将sharedLoader设置到Engine里的parentClassLoader字段
    digester.addRule("Server/Service/Engine",
                     new SetParentClassLoaderRule(parentClassLoader));
    return digester;

}

关键要点

通过上诉的配置解析和设置,可总结出如下关键要点,对后续的WebappClassLoader分析有很大作用:

  1. sharedLoader 和 catalinaLoader 基本都被废弃了,最终它们都被统一设置为了 commonLoader,即默认只使用 common 层的类加载器。
  2. Tomcat 主线程的 contextClassLoader 被设置为了 catalinaLoader(也就是 commonLoader),主要用于加载 Tomcat 自身运行所需的类(即 lib 目录下的 jar 或 class 文件)。
  3. StandardEngineparentClassLoader 被设置为了 sharedLoader(实际上还是 commonLoader),用于作为后续每个 WebappClassLoader(即 web 应用类加载器)的父加载器

WebappClassLoader

通过之前分析的文章,StandardContext#start会触发WebappLoader#start,需要注意的是,WebappLoader 本身并不是一个类加载器,而是通过实现 LoaderLifecycle 接口,负责在各个生命周期阶段对真正的 WebappClassLoader 进行管理和控制。其创建真正的WebappClassLoader在WebappLoader#start阶段,核心如下

protected void startInternal() throws LifecycleException {
    try {

        // 创建真正的 Web 应用类加载器 WebappClassLoader
        classLoader = createClassLoader();
        classLoader.setResources(context.getResources());
        // 设置类加载委托模型,决定优先使用父加载器还是自身加载器(默认为false,即不优先使用父加载器加载class)
        classLoader.setDelegate(this.delegate);

        setClassPath();

        setPermissions();

        // 启动 WebappClassLoader(缓存/WEB-INF/classes和/WEB-INF/lib下的资源)
        ((Lifecycle) classLoader).start();

    } catch (Throwable t) {
        // ...省略
    }

    setState(LifecycleState.STARTING);
}

private WebappClassLoaderBase createClassLoader()
        throws Exception {

    // loader为org.apache.catalina.loader.ParallelWebappClassLoader
    Class<?> clazz = Class.forName(loaderClass);
    WebappClassLoaderBase classLoader = null;

    if (parentClassLoader == null) {
        // 内部会递归向上查找,也就是最终会使用Engine内部的parentClassLoader,即sharedLoader
        parentClassLoader = context.getParentClassLoader();
    } else {
        context.setParentClassLoader(parentClassLoader);
    }
    // 使用反射将sharedLoader作为构造参数,实例化出ParallelWebappClassLoader加载器
    Class<?>[] argTypes = { ClassLoader.class };
    Object[] args = { parentClassLoader };
    Constructor<?> constr = clazz.getConstructor(argTypes);
    classLoader = (WebappClassLoaderBase) constr.newInstance(args);

    return classLoader;
}

ParallelWebappClassLoader

start

相关字段和方法如下,主要是将当前项目里的/WEB-INF/classes和/WEB-INF/lib目录下的class资源缓存到localRepositories(可以理解为classpath),以供后续来实际加载class


// 当前 Web 应用的本地 classpath 资源(/WEB-INF/classes 以及 /WEB-INF/lib 下的所有 jar)
private List<URL> localRepositories = new ArrayList<>();

/**
 * 生命周期的start方法,缓存class目录资源
 */
public void start() throws LifecycleException {

    state = LifecycleState.STARTING_PREP;

    // 1. 扫描 /WEB-INF/classes 目录,将其加入本地 classpath
    WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
    for (WebResource classes : classesResources) {
        if (classes.isDirectory() && classes.canRead()) {
            localRepositories.add(classes.getURL());
        }
    }
    // 2. 扫描 /WEB-INF/lib 目录下所有可读的 jar 包,将其加入本地 classpath,同时记录最后修改时间
    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;
}

loadClass

核心字段和方法如下(已删除不重要的部分),通过如下源码可总结出loadClass的步骤:

  1. 缓存检查
    • 先检查当前 WebappClassLoader 是否已加载过指定的类,如果已加载,直接返回已加载的类,避免重复加载
  2. 检查是否应尝试使用 JVM 内部类加载器(类似传统的类加载器加载方式)
    • JVM内部类加载器能找到指定class资源,则由其加载指定的class
  3. 根据委派规则(delegateLoad)决定加载顺序,delegateLoad一般都为false(若为true则颠倒顺序)
    • 先本地加载(当前 Web 应用内),尝试从/WEB-INF/classes、/WEB-INF/lib文件夹下加载指定class
    • 否则交由父加载器(sharedLoader)兜底,即从tomcat的lib目录里的jar加载指定class
  4. 以上都没找到,则抛出ClassNotFoundException
public abstract class WebappClassLoaderBase extends URLClassLoader
        implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {

    // 绑定的 Web 资源接口,实际提供 /WEB-INF/classes、/WEB-INF/lib 等资源访问能力
    protected WebResourceRoot resources = null;

    // 当前加载过的class的缓存(/WEB-INF/classes目录下)
    protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();

    // 是否强制委派给父类加载器优先加载。默认为false,表示优先当前类加载器先加载类
    protected boolean delegate = false;

    // /WEB-INF/lib 目录下jar包的修改时间(热加载使用)
    private final HashMap<String, Long> jarModificationTimes = new HashMap<>();

    // tomcat的sharedLoader(URLClassLoader)
    protected final ClassLoader parent;

    // 标识是否有外部 repository 被注册(即 localRepositories 是否有值)
    private boolean hasExternalRepositories = false;

    // 当前 Web 应用的本地 classpath 资源(/WEB-INF/classes 以及 /WEB-INF/lib 下的所有 jar)
    private List<URL> localRepositories = new ArrayList<>();

    /**
     * 把 webapp 的所有 classpath 资源(包括 /WEB-INF/classes 和 /WEB-INF/lib/*.jar)整理成 URL
     * 数组,
     * 供自己这个URLClassLoader实现去加载class。hasExternalRepositories也会被设置为true
     */
    @Override
    public URL[] getURLs() {
        ArrayList<URL> result = new ArrayList<>();
        result.addAll(localRepositories);
        result.addAll(Arrays.asList(super.getURLs()));
        return result.toArray(new URL[0]);
    }

    /**
     * 父类URLClassLoader的方法,配合getURLs()方法,设置hasExternalRepositories为true
     */
    @Override
    protected void addURL(URL url) {
        super.addURL(url);
        hasExternalRepositories = true;
    }

    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {
            Class<?> clazz = null;

            // 先检查 Web 应用是否已停止,防止在已停止的 webapp 中加载类
            checkStateForClassLoading(name);

            // 1、从缓存中resourceEntries查找当前类加载器是否已经加载了这个class
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (resolve) {
                    resolveClass(clazz);
                }
                return clazz;
            }

            // 2、从当前类加载器的native中查找是否已经缓存过(范围比findLoadedClass0更大)
            clazz = findLoadedClass(name);
            if (clazz != null) {

                if (resolve) {
                    resolveClass(clazz);
                }
                return clazz;
            }

            // class名转换为路径。比如:site.shanzhao.Demo => /site/shanzhao/Demo.class
            String resourceName = binaryNameToPath(name, false);

            // 检查jvm的类加载器是否加载过当前class
            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {

                URL url = javaseLoader.getResource(resourceName);
                tryLoadingFromJavaseLoader = (url != null);
            } catch (Throwable t) {
                tryLoadingFromJavaseLoader = true;
            }
            // 3. jvm类加载器加载过,直接从jvm类加载器获取这个Class
            if (tryLoadingFromJavaseLoader) {
                try {
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                        if (resolve) {
                            resolveClass(clazz);
                        }
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // 强制委派父加载器加载 or 一些特定核心类 :都先走父类加载器进行加载
            // 一般情况都应为false
            boolean delegateLoad = delegate || filter(name, true);

            // 4. 设置了delegate为true,则尝试使用父类加载器(默认即tomcat的common类加载器加载)
            if (delegateLoad) {

                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {

                        if (resolve) {
                            resolveClass(clazz);
                        }
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // ================== 走到这,才开始尝试使用本类加载加载指定的class
            try {
                // 5. 开始使用当前类加载器加载(即 /WEB-INF/classes 与 /WEB-INF/lib)
                clazz = findClass(name);
                if (clazz != null) {
                    if (resolve) {
                        resolveClass(clazz);
                    }
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            if (!delegateLoad) {
                try {
                    // 6. delegateLoad为false,再使用父类加载器(为sharedLoader,默认也是commonLoader)尝试加载指定的class
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (resolve) {
                            resolveClass(clazz);
                        }
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
        }

        throw new ClassNotFoundException(name);
    }

    /**
     * 从当前项目的/WEB-INF/classes 与 /WEB-INF/lib中加载指定class
     */
    public Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> clazz = null;
        try {
            try {
                // 1. 从/WEB-INF/classes文件夹下加载class(如果找到,还会缓存到resourceEntries字段里)
                clazz = findClassInternal(name);
            } catch (Exception e) {
                // ... 异常处理
            }
            if ((clazz == null) && hasExternalRepositories) {
                try {
                    // 2. 利用父类URLClassLoader的功能,加载/WEB-INF/lib文件夹下jar里的class
                    clazz = super.findClass(name);
                } catch (Exception e) {
                    // ... 异常处理
                }
            }
            // =========== 到这说明/WEB-INF/classes 和/WEB-INF/lib 里都没找到指定的class,直接抛异常 =========
            if (clazz == null) {
                throw new ClassNotFoundException(name);
            }
        } catch (ClassNotFoundException e) {
            throw e;
        }
        return clazz;

    }
}

总结和思考

  1. commonLoader存在的意义?

    ​ Tomcat 里其实只有一个 JVM,但可以部署多个 web 应用(多个 context)。这些应用共用 Tomcat 核心组件,比如:Server、Connector、Valve、Pipeline 等。这些 Tomcat 核心组件的 class 没必要每个应用都各自加载一份,只需 JVM 里统一加载一次就行

    commonLoader 就起这个作用,它其实就是一个 URLClassLoader,负责加载 $CATALINA_HOME/lib 目录下的 jar,把 Tomcat 自己的核心代码加载进来,供所有 Web 应用共享

  2. WebappClassLoader的意义?

    ​ 这里就是隔离问题的核心:全限定类名是类加载器判定是否已加载的唯一依据

    ​ 假设有多个 web 应用,它们各自依赖了同一个类 site.shanzhao.Demo,但版本不同。如果 JVM 里只有一份类加载器,那只能加载一份,版本冲突就没法解决了。

    ​ 所以 Tomcat 设计了 WebappClassLoader:每个 context 都有自己的类加载器,各自加载自己的 /WEB-INF/classes/WEB-INF/lib 目录,互不影响。这样同一个类名的 class 文件可以在不同应用里加载出不同版本,实现真正的隔离

    ​ 另外,有独立的 WebappClassLoader 也方便支持应用的热部署和卸载:当某个 context 重新加载或被移除时,直接销毁这个 WebappClassLoader,连带其加载的所有 class、资源、线程都能一起回收,减少内存泄露风险。

  3. 为什么不使用jdk的AppClassLoader替换commonLoader?从表面看,AppClassLoader 和 Tomcat 自定义的 commonLoader 都继承自 URLClassLoader,两者在能力上类似,都可以加载来自指定 classpath 的类或资源。但 Tomcat 并没有使用 AppClassLoader 来加载自身核心组件,为什么呢?

    1. AppClassLoader 的 classpath 是在 JVM 启动阶段静态确定的(通过系统属性:java.class.path),而Tomcat 的类加载器的classpath支持占位符(如${catalina.base}和${catalina.home}),即允许用户扩展 classpath,更加灵活
    2. Tomcat 在早期设计中区分了commonLoader,catalinaLoader和sharedLoader这三个类加载器用以实现模块隔离,AppClassLoader无法实现
    3. WebappClassLoader 更不可能使用 AppClassLoader。单是启动时无法确定需要部署哪些Context以及各自classpath这条理由,就不可能用AppClassLoader,更别提Tomcat还支持了Web环境隔离和热加载这种高级特性了
  4. 哪个类加载器打破了双亲委派机制?

    其实只有 WebappClassLoader 部分打破了双亲委派

    commonLoader、sharedLoader 本身都是标准的 URLClassLoader,没有动过 loadClass 逻辑,完全遵守双亲委派。

    WebappClassLoader 才重写了 loadClass,实现了一个 可控双亲委派

    • 没完全打破JVM 自带的类(JDK 标准库等)依然优先交给 JVM 自己的类加载器加载,保证不会被项目里的同名 class 覆盖,比如你项目里搞个自定义 java.lang.String,Tomcat 也不会用你这份
    • 部分打破对于非 JVM 内部类,WebappClassLoader 可以根据 delegate 参数决定是否优先本地加载,而不强行一律交给父加载器sharedLoader