上图是Tomcat文档中所展示的Tomcat类加载结构。在这个结构中Bootstartap和System的类加载器由java虚拟机实现。common类加载器由Tomcat容器实现,它对 Tomcat 内部类和所有 Web 应用程序都是可见的。此类加载器搜索的位置$CATALINA_BASE/conf/catalina.properties 中的common.loader属性定义。在catalina.properties文件也定义了server.loader和shared.loader属性,这两个属性分别由Server和Shared两个类加载器加载。
接下来让我们看一下Tomcat如何实现common类加载器。首先我们需要找到Bootstrap类的main方法。在main方中有一段代码如下,这段代码的大意是先判断Bootstrap是否为null,不为null,直接将Catalina ClassLoader设置到当前线程,用于加载服务器相关类,为null则进入bootstrap的init方法。
...... synchronized (daemonLock) { if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } daemon = bootstrap; } else { // When running as a service the call to stop will be on a new // thread so make sure the correct class loader is used to // prevent a range of class not found exceptions. Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } } ......
init方法会调用initClassLoaders()方法,在该方法中会调用createClassLoader方法创建commonLoader、catalinaLoader、sharedLoader三种类加载器。与上文中所介绍的三种类加载器一一对应。
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); } }
createClassLoader方法终会调用如下代码,通过这段代码可以知到commonLoader是一个URLClassLoader。
public static ClassLoader createClassLoader(List<Repository> repositories, final ClassLoader parent) throws Exception { ...... return AccessController.doPrivileged( new PrivilegedAction<URLClassLoader>() { @Override public URLClassLoader run() { if (parent == null) return new URLClassLoader(array); else return new URLClassLoader(array, parent); } }); }
在Bootstartap中的init方法中调用完initClassLoaders方法后,就开始了对类加载器的使用。Tomcat用catalinaLoader来加载Catalina类,这个类就是我们经常说的容器。加载完Catalina后会将sharedLoader作为参数传递给Catalina类,以便于后续给Webappx设置父加载器。
public void init() throws Exception { initClassLoaders(); ...... // 加载Catalina类 Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.getConstructor().newInstance(); // 设置sharedLoad加载器 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); catalinaDaemon = startupInstance; }
上文介绍了common类加载器的创建和使用,那么Webappx类加载器又是如何被创建和使用的呢?
在看Tomcat源码是如何创建webappx类加载器之前,让我们来做一个实验。假设我们已经编译好了一个全限定名为com.example.WebAppClassLoader.class文件,那么应该如何来加载这个类文件呢?一种方法是自定义一个类加载器。
public class MyClassLoader extends URLClassLoader { public MyClassLoader() { super(new URL[0]); } @Override protected Class<?> findClass(String name) { String myPath = "file:///D:/www/tomact-test-war/tomcat-test-case008/src/main/webapp/WEB-INF/" + name.replace(".","/") + ".class"; byte[] cLassBytes = null; Path path = null; try { path = Paths.get(new URI(myPath)); cLassBytes = Files.readAllBytes(path); } catch (IOException | URISyntaxException e) { e.printStackTrace(); } Class clazz = defineClass(name, cLassBytes, 0, cLassBytes.length); return clazz; } }
创建一个名为MyClassLoader的类并继承URLClassLoader类,重写findClass方法,一个简单的类加载器就创建完成了。
public static void main(String[] args) throws Exception { MyClassLoader cl = new MyClassLoader(); Class<?> wacl = cl.findClass("com.example.WebAppClassLoader"); try { Object obj = wacl.newInstance(); Method method = wacl.getMethod("test_1"); method.invoke(obj); } catch (Exception e) { e.printStackTrace(); } }
package com.example; public class WebAppClassLoader { public WebAppClassLoader() { } public void test_1() { System.out.println("自定类加载器"); } }
在main方法中创建MyClassLoader对象,并调用findClass方法,至此就将一个class文件加载到了虚拟机中。
好了,实验做完后,让我们回过头来看看Tomcat是如何创建WebappX类加载器的。
在StandardContext的startInternal方法中有这样一段代码
if (getLoader() == null) { WebappLoader webappLoader = new WebappLoader(); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); }
它会创建WebappLoader对象,并通过setLoader(webappLoader)赋值到一个实例变量中,然后会调用WebappLoader的start方法:
...... classLoader = createClassLoader(); classLoader.setResources(context.getResources()); classLoader.setDelegate(this.delegate); ......
进入createClassLoader方法:
private WebappClassLoaderBase createClassLoader() throws Exception { Class<?> clazz = Class.forName(loaderClass); WebappClassLoaderBase classLoader = null; if (parentClassLoader == null) { parentClassLoader = context.getParentClassLoader(); } else { context.setParentClassLoader(parentClassLoader); } Class<?>[] argTypes = { ClassLoader.class }; Object[] args = { parentClassLoader }; Constructor<?> constr = clazz.getConstructor(argTypes); classLoader = (WebappClassLoaderBase) constr.newInstance(args); return classLoader; }
该方法会实例化一个ParallelWebappClassLoader实例,并且传递了sharedLoader作为其父亲加载器。
ParallelWebappClassLoader继承了WebappClassLoaderBase抽象类,WebappClassLoaderBase继承了URLClassLoader。在WebappClassLoaderBase类中重写了findClass方法。至此WebappX类加载器 就创建完成了。
那Webappx类加载器又是被如何使用的呢?
还记得在Tomcat动态部署一章介绍的那个webConfig方法吗?这个方法非常复杂。在这个方法的第四步中会调用populateJavaClassCache方法
private void populateJavaClassCache(String className, Map<String,JavaClassCacheEntry> javaClassCache) { ...... try (InputStream is = context.getLoader().getClassLoader().getResourceAsStream(name)) { if (is == null) { return; } ClassParser parser = new ClassParser(is); JavaClass clazz = parser.parse(); populateJavaClassCache(clazz.getClassName(), clazz, javaClassCache); } catch (ClassFormatException | IOException e) { log.debug(sm.getString("contextConfig.invalidSciHandlesTypes", className), e); } ...... }
现在总结如下: 在Tomcat存在common、cataina、shared三个公共的classloader,默认情况下,这三个classloader其实是同一个,都是common classloader,而针对每个webapp,也就是context(对应代码中的StandardContext类),都有自己的WebappClassLoader实例来加载每个应用自己的类,该类加载的父类加载器就是是Shared ClassLoader。这样前面关于tomcat的类加载层次应该就清楚起来了。
在context.xml文件中可以配置delegate属性,以用来控制Webappx类加载器的类加载机制。delegate属性默认是false。
当delegate为true时webappx的类加载顺序如下:
当delegate为false时webappx的类加载顺序如下:
让我再来简单回忆一下JVM的双亲委派模型,在JVM中一个类的加载首先使用其父类加载器去加载,如果加载不到在使用自身的加载器去加载。
我们以Tomcat的类加载器结构为例,当delegate属性是true时,加载一个自定义servlet是从根加载器,然后是系统类加载器一步步找下来的,这一过程与JVM的双亲委派模型是一致的。但是当delegate为false时却不然,当delegate为fasle时首先依旧从根加载器加载类文件,但是第二步是从webappx类加载器中加载类文件,然后是系统类加载器,最后才是通用类加载器,这与标准的JVM模型并不一致,我们也可以说此时Tomcat打破了双亲委派模型。
Tomcat默认打破了双亲委派,这样的好处之一是当我们在Tomcat中部署多个应用时,即使这些应用程序依赖同一个第三方类库,虽然其版本不同但并不会相互影响。