乐趣区

Tomcat类加载机制浅析

前言
Tomcat 是后端服务最常见的 web 容器,关于 Tomcat 一个重要的话题就是它的类加载机制,本文就基于 9.0.16 版本浅析一下 Tomcat 的类加载机制


有几个类加载器?
在 Tomcat 的启动类 org.apache.catalina.startup.Bootstrap 里定义了三个 ClassLoader 类型的属性

ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;

在 Bootstrap 的 main 方法里会先 new 一个 Bootstrap 对象,然后调用 Bootstrap#init 方法,并在 init 方法里调用其 initClassLoaders 方法来初始化这三个属性。

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) {……}
}

private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;
    ……
    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

最后一句的 ClassLoaderFactory.createClassLoader 返回的是一个 URLClassLoader 对象。

createClassLoader 方法的关键在于第一个 if 语句。首先调用 CatalinaProperties.getProperty(name + “.loader”) 获取一个返回值,CatalinaProperties.getProperty 方法获取的是 conf/catalina.properties 文件里的配置值,如果这个值为空的话,就直接返回传入的 parent,如果不为空的话就走下面的逻辑来创建一个 URLClassLoader 对象。

初始化这三个属性时传入的参数分别是 “common” 和 null,”server” 和 commonClassLoader,”shared” 和 commonClassLoader,然而在 catalina.properties 里 common.loader,server.loader,shared.loader 分别为

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

只有 common.loader 是有值的。只有 createClassLoader(“common”, null) 调用会走到 if 之后的逻辑,createClassLoader(“server”, commonLoader) 和 createClassLoader(“shared”, commonLoader) 调用直接返回了 commonLoader 了。

到这里就可以知道,在默认情况下,commonLoader,catalinaLoader 和 sharedLoader 其实指向的是同一个 URLClassLoader 对象。

那么问题来了,既然都指向同一个 URLClassLoader 对象,那问什么要用三个属性呢?

其实,在早起的 Tomcat 版本里 catalina.properties 里的 common.loader,server.loader,shared.loader 都是有值的,比如在 5.0.28 版本里,这三个的值分别是

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

这个时候,catalinaLoader 和 sharedLoader 的 parent 就是 commonClassLoader 了,Tomcat 版本升级之后就把 server.loader,shared.loader 去掉了,至于为什么要去掉,这我就触及到我的知识盲区了。


应用程序里的类是怎么加载的
在 Bootstrap#init 方法里还执行了 Thread.currentThread().setContextClassLoader(catalinaLoader) 这一句把 catalinaLoader 设置为当前线程的 contextClassLoader。此外还用 catalinaLoader 加载了 org.apache.catalina.startup.Catalina 类并创建一个对象,而且通过反射调用了 Catalina 对象的 setParentClassLoader 方法并把 sharedLoader 作为参数传入,也就是把 sharedLoader 赋值给 Catalina 对象的 parentClassLoader 属性。

初始化那三个 ClassLoader 属性之后,Bootstrap 接下来就初始化并启动一系列的 Tomcat 组件了,其中包括 Catalina,Server,Service,Engine,Host,Context,Wrapper,Pipeline,Valve 等,其实就是解析 server.xml 里的配置文件,并创建这些组件,然后调用其相关方法(init 和 start 方法)。其中较为关键的是 Context 的 start 方法,应用程序的加载就是在这里。

另外 Engine,Host,Context,Wrapper 都是 Container 的子类,Container 里一般都有子 Container,Engine 的子 Container 是 Host,Host 的子 Container 是 Context,Context 的子 Container 是 Wrapper,Wrapper 没有子 Container。Pipeline 在这几个 Container 的构造方法中初始化的,每个 Container 都有一个 Pipeline 属性,而 Valve 是在一般是在 Pipeline 构造时或者构造之后设置给 Pipeline 的,一个 Pipeline 可以有多个 Valve。

Context#start 方法里做的事情非常多,本文在这里只挑与主题相关的。

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

public ClassLoader getParentClassLoader() {if (parentClassLoader != null)
        return parentClassLoader;
    if (getPrivileged()) {return this.getClass().getClassLoader();} else if (parent != null) {return parent.getParentClassLoader();
    }
    return ClassLoader.getSystemClassLoader();}

首先在 start 方法其实是 Context 的实现类 StandardContext 里的 startInternal 方法里,首先创建一个 WebappLoader 对象,并赋值给 Loader 类型的属性 loader,构造方法里传入的一个 ClassLoader 对象并赋值给 WebappLoader 的 parentClassLoader 属性。

StandardContext 自己的 parentClassLoader 是为 null 的,getPrivileged() 方法返回的也是 false,因此会返回 parent.getParentClassLoader(),而 Context 的 parent 是 Host,也就是说会返回 Host 的 parentClassLoader 属性所指向的对象。Host 对象是在 Tomcat 解析 sever.xml 时创建的,它的 parentClassLoader 属性也是在创建的时候设置的,设置的值是从 Host 的 parent,也就是 Engine 里的属性 parentClassLoader 复制来的,而 Engine 的 parentClassLoader 属性的赋值也是在解析 server.xml 的时候赋值的,赋的值就是 Catalina 的属性 parentClassLoader,也就是 sharedClassLoader 指向的 URLClassLoader。

这里有点饶,其实最终要说明的是 WebappLoader 里的 parentClassLoader 的值就是 sharedClassLoader 的值,是指向同一个 URLClassLoader。

创建完 WebappLoader 之后,就调用了它的 start 方法。

Loader loader = getLoader();
if (loader instanceof Lifecycle) {((Lifecycle) loader).start();}

调用 loader.start 方法其实就是调用 WebappLoader 的 start 方法,最终会执行到 WebappLoader#startInternal 方法,在这个方法里有几个重要的操作

private WebappClassLoaderBase classLoader = null;
{
    ……
    classLoader = createClassLoader();
    classLoader.setResources(context.getResources());
    ……
    classLoader.start();
    ……
}
private WebappClassLoaderBase classLoader = null;
private String loaderClass = ParallelWebappClassLoader.class.getName();
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;
}

先调用 createClassLoader 创建一个 WebappClassLoaderBase 的子类 ParallelWebappClassLoader 类的实例,并设置了起 parentClassLoader 为 WebappLoader  的 parentClassLoader 属性的值,也就是 sharedClassLoader 指向的 URLClassLoader 对象。

而且设置了 ParallelWebappClassLoader 的 WebResourceRoot 类型的属性 resources 的值(classLoader.setResources 方法),传入的实参是 context.getResources()  的返回值,也就是 StandardContext 的 resources 属性的值,而 StandardContext 的 resources 属性是在 StandardRoot 类型对象(在 StandardContext#startInternal 方法里赋值的)。

然后调用 ParallelWebappClassLoader 对象的 start 方法。

这个 ParallelWebappClassLoader 对象就是 Context 用来加载应用程序的类的。ParallelWebappClassLoader 继承自 WebappClassLoaderBase,而 WebappClassLoaderBase 继承自 URLClassLoader,所以 ParallelWebappClassLoader 其实也是一个 URLClassLoader。

创建完之后就触发 Lifecycle.CONFIGURE_START_EVENT,执行 Context 里的 LifecycleListener 的 lifecycleEvent 方法。

{fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
}
protected void fireLifecycleEvent(String type, Object data) {LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {listener.lifecycleEvent(event);
    }
}

Context 的 LifecycleListener 里有一个 ContextConfig 类对象,这个对象是在 Context 的父类 Host 的一个 LifecycleListener 实现类 HostConfig 对象的 lifecycleEvent 方法中创建并设置给 Context 的(具体是在 HostConfig#deployWar 或者 HostConfig#deployDirectory 方法里),而 HostConfig 是在解析 server.xml 的时候创建并设置给 Host 的。

在 ContextConfig#lifecycleEvent 方法逻辑比较繁琐,主要是解析 web.xml,并把解析出来的 listener,filter,分别设置到 Context 的 applicationListeners 和 filterDefs 里。然后把解析出来的 servlet 封装成 Wrapper,并通过 Context#addChild 将 Wrapper 设置为 Context 的子容器。

从 web.xml 解析出 listener、filter、servlet 之后,StandardContext#startInternal 方法接下来就要构造并初始化这些类对象了,startInternal 方法里分别调用了 listenerStart()、filterStart(),和 loadOnStartup(findChildren()) 来初始化。

在调用上面说的三个方法前,StandardContext#startInternal 方法还执行了这一句

setInstanceManager(new DefaultInstanceManager(context,
        injectionMap, this, this.getClass().getClassLoader()));

这一句是创建一个 DefaultInstanceManager 对象,并把它赋值给 StandardContext 的 instanceManager 属性,这个 DefaultInstanceManager 是用来辅助加载应用程序类的。

protected final ClassLoader classLoader;
protected final ClassLoader containerClassLoader;
public DefaultInstanceManager(Context context,
        Map<String, Map<String, String>> injectionMap,
        org.apache.catalina.Context catalinaContext,
        ClassLoader containerClassLoader) {classLoader = catalinaContext.getLoader().getClassLoader();
    privileged = catalinaContext.getPrivileged();
    this.containerClassLoader = containerClassLoader;
    ……
}

在 DefaultInstanceManager 的构造方法里分别给其 classLoader 和 containerClassLoader 赋了值。containerClassLoader 赋的值就是系统类加载器 Laucher$AppClassLoader,而 classLoader 赋的值是 Context 里的 Loader 实现类实例里的 classLoader 属性所指向的对象,也就是上面提到的 ParallelWebappClassLoader 对象。

构造这三种实例基本上是一样的,这里以 listener 为例。在 listenerStart 方法里会执行下面的语句来创建实例

String listener = listeners[i];
results[i] = getInstanceManager().newInstance(listener);

这里就用到了 InstanceManager 的 newInstance(String className) 方法

public Object newInstance(String className) throws IllegalAccessException,
        InvocationTargetException, NamingException, InstantiationException,
        ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
    return newInstance(clazz.getConstructor().newInstance(), clazz);
}

private Object newInstance(Object instance, Class<?> clazz)
        throws IllegalAccessException, InvocationTargetException, NamingException {if (!ignoreAnnotations) {Map<String, String> injections = assembleInjectionsFromClassHierarchy(clazz);
        populateAnnotationsCache(clazz, injections);
        processAnnotations(instance, injections);
        postConstruct(instance, clazz);
    }
    return instance;
}

这个方法先获取 Class 对象,然后反射创建实例,newIntance 的重载方法里只是处理一下注解之类的操作。关键是在 loadClassMaybePrivileged 方法,loadClassMaybePrivileged 方法会调用 loadClass(String className, ClassLoader classLoader) 方法,直接看这个方法。

protected Class<?> loadClass(String className, ClassLoader classLoader)
        throws ClassNotFoundException {if (className.startsWith("org.apache.catalina")) {return containerClassLoader.loadClass(className);
    }
    try {Class<?> clazz = containerClassLoader.loadClass(className);
        if (ContainerServlet.class.isAssignableFrom(clazz)) {return clazz;}
    } catch (Throwable t) {ExceptionUtils.handleThrowable(t);
    }
    return classLoader.loadClass(className);
}

传入的形参 classLoader 就是 DefaultInstanceManager 的属性 classLoader。loadClass 方法会先尝试用 containerClassLoader 也就是 Laucher$AppClassLoader 去加载,如果加载不到的话,就用 classLoader 也就是 ParallelWebappClassLoader 去加载。

那 ParallelWebappClassLoader 是怎么加载的呢。通常自己实现的类加载器,都要实现 ClassLoader 的 findClass(String name) 方法,在自己实现的 findClass(String name) 方法里,先根据 name 找到对应的 class 文件,然后将 class 文件加载到内存,用字节数组表示,然后通过调用 ClassLoader 类的 defineClass 方法将字节数组转换成 Class 对象。ParallelWebappClassLoader 也不例外。所以问题的关键在于 ParallelWebappClassLoader 是怎么在 findClass 方法里根据类名找对应的 class 文件了。

ParallelWebappClassLoader#findClass 方法主要是调用 ParallelWebappClassLoader#findClasInternal 方法

protected Class<?> findClassInternal(String name) {
    ……
    String path = binaryNameToPath(name, true);
    ResourceEntry entry = resourceEntries.get(path);
    WebResource resource = null;
    if (entry == null) {resource = resources.getClassLoaderResource(path);
        ……
        synchronized (resourceEntries) {
            ……
            resourceEntries.put(path, entry);
        }
    }
    Class<?> clazz = entry.loadedClass;
    if (clazz != null)
        return clazz;
    synchronized (getClassLoadingLock(name)) {
        ……
        byte[] binaryContent = resource.getContent();
        ……
        clazz = defineClass(name, binaryContent, ……);
        ……
        entry.loadedClass = clazz;
    }
    return clazz;
}

在 findClassInternal 里,先从缓存 resourceEntries 里取 ResourceEntry,如果有就返回 ResourceEntry#loadedClass。resourceEntries 是一个 map,ResourceEntry 只是简单封装了 Class 对象。

protected final Map<String, ResourceEntry> resourceEntries =
        new ConcurrentHashMap<>();

public class ResourceEntry {
    public long lastModified = -1;
    public volatile Class<?> loadedClass = null;
}

如果取不到就先调用 resources.getClassLoaderResource(path) 获取一个 WebResource 对象,然后调用 WebResource#getContent 获取字节数组,最后调用 defineClass 将字节数组转换为 Class 对象,并把 Class 对象封装成 ResourceEntry 缓存在 resourceEntries 这个 Map 里。

WebResource 是对 Tomcat 对应用程序资源的一个封装,可以是指一个目录,一个文件(一个 jar 包,或者 jar 包里的 class 文件,或者其他的文件,比如 META-INF/resources/ 目录下的资源文件等)。

resources.getClassLoaderResource(path) 是根据类名找到 class 文件的关键了。

protected WebResourceRoot resources = null;

resources 是 WebResourceRoot 类型的属性,它指向的是 StandardRoot 类型的对象。

public WebResource getClassLoaderResource(String path) {return getResource("/WEB-INF/classes" + path, true, true);
}
private WebResource getResource(String path, boolean validate,
        boolean useClassLoaderResources) {
    ……
    return getResourceInternal(path, useClassLoaderResources);
}
protected final WebResource getResourceInternal(String path,
        boolean useClassLoaderResources) {
    WebResource result = null;
    WebResource virtual = null;
    WebResource mainEmpty = null;
    for (List<WebResourceSet> list : allResources) {for (WebResourceSet webResourceSet : list) {if (!useClassLoaderResources &&  !webResourceSet.getClassLoaderOnly() ||
                    useClassLoaderResources && !webResourceSet.getStaticOnly()) {result = webResourceSet.getResource(path);
                if (result.exists()) {return result;}
                if (virtual == null) {if (result.isVirtual()) {virtual = result;} else if (main.equals(webResourceSet)) {mainEmpty = result;}
                }
            }
        }
    }
    if (virtual != null) {return virtual;}
    return mainEmpty;
}

getClassLoaderResource 会遍历 allResources 里所有的 WebResourceSet。这些 WebResourceSet 是在 StandardRoot#startInternal 方法里加入的。

private final List<WebResourceSet> preResources = new ArrayList<>();
private WebResourceSet main;
private final List<WebResourceSet> classResources = new ArrayList<>();
private final List<WebResourceSet> jarResources = new ArrayList<>();
private final List<WebResourceSet> postResources = new ArrayList<>();
private final List<WebResourceSet> mainResources = new ArrayList<>();
private final List<List<WebResourceSet>> allResources =
        new ArrayList<>();
{allResources.add(preResources);
    allResources.add(mainResources);
    allResources.add(classResources);
    allResources.add(jarResources);
    allResources.add(postResources);
}
……
protected void startInternal() throws LifecycleException {
    ……
    main = createMainResourceSet();
    mainResources.add(main);
    for (List<WebResourceSet> list : allResources) {if (list != classResources) {for (WebResourceSet webResourceSet : list) {webResourceSet.start();
            }
        }
    }
    processWebInfLib();
    for (WebResourceSet classResource : classResources) {classResource.start();
    }
    ……
}

createMainResourceSet 方法创建的是一个 DirResourceSet 对象,用的目录就是 Context 的 docBase 属性(就是 server.xml 里 Context 标签下的 docBase 属性),这个 DirResourceSet 对象是非常有用的,后续将 WEB-INF/lib 里的 jar 包加入到 classResources 列表里,以及查找  WEB-INF/classes 文件目录下的 class 文件都是通过这个 DirResourceSet 做的。WebResourceSet 可以理解成 WebResource 的集合。

processWebInfLib 方法的逻辑是将 docBase 目录下的 WEB-INF/lib 目录下的 jar 文件都封装成 JarResourceSet,并把这些 JarResourceSet 加入到 classResources 这个列表里。

protected void processWebInfLib() throws LifecycleException {WebResource[] possibleJars = listResources("/WEB-INF/lib", false);
    for (WebResource possibleJar : possibleJars) {if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) {
            createWebResourceSet(ResourceSetType.CLASSES_JAR,
                    "/WEB-INF/classes", possibleJar.getURL(), "/");
        }
    }
}

public void createWebResourceSet(ResourceSetType type, String webAppMount,
        URL url, String internalPath) {……}

其中 processWebInfLib 方法里的 listResources 最终也是调用 getResourceInternal 方法获取到 WEB-INF/lib 下所有的 jar 包的名字,而这个操作就是通过 DirResourceSet 完成的。

如果需要加载的类不在 WEB-INF/classes 目录下,而在 WEB-INF/lib 目录下的某个 jar 包里,getResourceInternal 的遍历就会遍历到 classResources 这个 List 的 JarResourceSet,然后通过 JarResourceSet 找到 jar 里里面的 class 文件。

小结
Tomcat 的类加载机制一直是 Tomcat 知识体系里比较重要的一块,本文先分析了 Tomcat 里的较为熟悉的 commonClassLoader,catalinaClassLoader 和 sharedClassLoader,了解了在较高版本的 Tomcat 里默认情况下,这三个指向的是同一个 URLClassLoader 对象。接着本分分析了 Tomcat 是怎么加载应用程序的类的,其中的关键步骤就是 Context 的 start 方法了,在 Context 的 start 方法里,创建了一个 WebappLoader 对象,在 WebappLoader 的 start 方法里创建了一个 ParallelWebappClassLoader 对象,这个对象就是加载应用程序的类的关键类加载器、而且设置了这个 ParallelWebappClassLoader 对象的 parentClassLoader 为 sharedClassLoader。然后,Context 的 start 方法里解析了 web.xml 文件,之后就使用 ParallelWebappClassLoader 来加载 web.xml 里定义的应用程序的 filter,listener,servlet 等对象。

Tomcat 的类加载的逻辑比较复杂,由于本人能力有限,只能写出这些了,如果有错误的地方,欢迎指出!!!

退出移动版