前言
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 的类加载的逻辑比较复杂,由于本人能力有限,只能写出这些了,如果有错误的地方,欢迎指出!!!