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