你的老朋友Tomcat😺(四)类加载机制

1,056 阅读7分钟

JVM类加载机制中的双亲委派模型大家应该都很熟悉,但在Tomcat中破坏了双亲委派模型,本篇介绍Tomcat为什么要破坏以及如何破坏的。

一、WebappClassLoader

在Tomcat中有一个自定义的类加载器WebappClassLoader,它继承了WebappClassLoaderBase,WebappClassLoaderBase类重写了loadClass()方法和findClass()方法,代码如下:

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

    synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
        if (log.isDebugEnabled())
            log.debug("loadClass(" + name + ", " + resolve + ")");
        Class<?> clazz = null;

        checkStateForClassLoading(name);

        // 在本地cache中查找该类是否被加载过
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        // 在系统类cache中查找是否加载过
        clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        String resourceName = binaryNameToPath(name, false);

        // 获取ExtClassLoader类加载器
        ClassLoader javaseLoader = getJavaseClassLoader();
        boolean tryLoadingFromJavaseLoader;
        try {
            URL url;
            if (securityManager != null) {
                PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
                url = AccessController.doPrivileged(dp);
            } else {
                url = javaseLoader.getResource(resourceName);
            }
            tryLoadingFromJavaseLoader = (url != null);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            tryLoadingFromJavaseLoader = true;
        }

        if (tryLoadingFromJavaseLoader) {
            try {
                // ExtClassLoader加载
                clazz = javaseLoader.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }

        ……

        boolean delegateLoad = delegate || filter(name, true);

        // (1) Delegate to our parent if requested
        if (delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader1 " + parent);
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }

        // 在本地目录搜索class并加载
        if (log.isDebugEnabled())
            log.debug("  Searching local repositories");
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from local repository");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 默认不使用委托模式,使用系统类加载器AppClassLoader加载
        if (!delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader at end: " + parent);
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }
    }

    throw new ClassNotFoundException(name);
}



@Override
public Class<?> findClass(String name) throws ClassNotFoundException {

    checkStateForClassLoading(name);
    ……
    Class<?> clazz = null;
    try {
        if (log.isTraceEnabled())
            log.trace("      findClassInternal(" + name + ")");
        try {
            if (securityManager != null) {
                PrivilegedAction<Class<?>> dp =
                    new PrivilegedFindClassByName(name);
                clazz = AccessController.doPrivileged(dp);
            } else {
                // 先在自己的Web应用目录下查找class
                clazz = findClassInternal(name);
            }
        } catch(AccessControlException ace) {
            log.warn(sm.getString("webappClassLoader.securityException", name,
                    ace.getMessage()), ace);
            throw new ClassNotFoundException(name, ace);
        } catch (RuntimeException e) {
            if (log.isTraceEnabled())
                log.trace("      -->RuntimeException Rethrown", e);
            throw e;
        }
        if ((clazz == null) && hasExternalRepositories) {
            try {
                // 使用父类的findClass方法查找
                clazz = super.findClass(name);
            } catch(AccessControlException ace) {
                log.warn(sm.getString("webappClassLoader.securityException", name,
                        ace.getMessage()), ace);
                throw new ClassNotFoundException(name, ace);
            } catch (RuntimeException e) {
                if (log.isTraceEnabled())
                    log.trace("      -->RuntimeException Rethrown", e);
                throw e;
            }
        }
        ……
    } catch (ClassNotFoundException e) {
        if (log.isTraceEnabled())
            log.trace("    --> Passing on ClassNotFoundException");
        throw e;
    }

    ……
    return clazz;

}
  1. 使用ExtClassLoader加载--> 防止Web应用自己的类覆盖JRE的核心类
  2. 使用WebappClassLoader在web应用目录下加载--> 在Web应用目录下加载,优先加载Web应用目录下的类
  3. 使用AppClassLoader(最先)加载--> 由系统类加载器在classpath下加载

二、Web应用间的隔离

2.1 设计

  • Q:隔离不同Web应用之间的自定义类如Servlet

    • A:给每个Web应用创建一个WebappClassLoader类加载器实例。
  • Q:不同Web应用之间如何共享类库,不重复加载相同的类

    • A:Tomcat设计了一个SharedClassLoader作为WebappClassLoader的父加载器, 如果WebappClassLoader自己没有加载到某个类则会委托父加载器SharedClassLoader(遵守双亲委派,最先由AppClassLoader加载)在指定目录下加载共享类
  • Q:如何隔离Tomcat自身的类和Web应用的类

    • A:Tomcat设计了一个CatalinaClassLoader专门来加载Tomcat自身的类
  • Q:Tomcat和Web应用之间如何共享类库

    • A:Tomcat设计了一个CommonClassLoader作为CatalinaClassLoader和SharedClassLoader的父加载器,CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用

  • /common目录下:类库可以被Tomcat和Web应用程序共同使用;由CommonClassLoader类加载器加载目录下的类库
  • /server目录:类库只能被Tomcat可见;由CatalinaClassLoader类加载器加载目录下的类库
  • /shared目录:类库对所有Web应用程序可见,但对Tomcat不可见;由SharedClassLoader类加载器加载目录下的类库
  • /webapps/web应用/WEB-INF目录:仅仅对当前web应用程序可见;由WebappClassLoader类加载器加载目录下的类库

可以在conf目录下的catalina.properties文件里配置各种类加载器的加载路径


2.2 源码分析

  1. 上述提到的commonClassLoader、catalinaClassLoade、sharedClassLoader并非是类而是BootStrap中的实例。
  2. commonLoader的父类加载器最终由ClassLoader的构造方法赋值,是由getSystemClassLoader()得到的AppClassLoader。
  3. catalinaLoader、sharedLoader传入的父类加载器都是commonLoader,但是默认情况下catalina.porperties中的"server.loader"、"shared.loader"配置都为空,所以catalinaLoader、sharedLoader、commonLoader三者是同一个对象
private void initClassLoaders() {
    // debug-tomcat9-classLoader
    try {
        commonLoader = createClassLoader("common", null);
        if (commonLoader == null) {
            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);
    }
}

private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
    String value = CatalinaProperties.getProperty(name + ".loader");
    // 默认情况下server.loader、shared.loader为空返回parent即commonLoader
    if ((value == null) || (value.equals("")))
        return parent;
    ……
    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

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);
                }
            });
}

protected ClassLoader() {
    // getSystemClassLoader()获取系统类加载器AppClassLoader
    this(checkCreateClassLoader(), getSystemClassLoader());
}
  1. 在初始化完common、catalina、shared三个类加载器之后,反射调用Catalina.setParentClassLoader(), 将sharedClassLoader设置为Catalina的parentClassLoader
 public void init() throws Exception {
    // 初始化common、catalina、shared三个类加载器
    initClassLoaders();
    // 设置当前的线程上下文类加载器
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    SecurityClassLoad.securityClassLoad(catalinaLoader);
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    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);
    // 反射调用Catalina.setParentClassLoader()设置sharedLoader为父类加载器
    method.invoke(startupInstance, paramValues);
    catalinaDaemon = startupInstance;
}

  1. init()方法完成后通过load()方法反射调用Catalina.load()方法。Catalina.load()方法通过解析server.xml文件创建容器,并逐一设置parentClassLoader为sharedClassLoader
// SetParentClassLoaderRule类
@Override
public void begin(String namespace, String name, Attributes attributes) throws Exception {
    // 相当于解析server.xml文件出来的一个容器栈结构
    // 0 = {Catalina@1103} 
    // 1 = {StandardServer@1478} "StandardServer[8005]"
    // 2 = {StandardService@1479} "StandardService[Catalina]"
    // 3 = {StandardEngine@1480} "StandardEngine[Catalina]"
    Container top = (Container) digester.peek();
    top.setParentClassLoader(parentClassLoader);
}

// CopyParentClassLoaderRule类
@Override
public void begin(String namespace, String name, Attributes attributes) throws Exception {
    // 同上
    // 0 = {Catalina@1103} 
    // 1 = {StandardServer@1478} "StandardServer[8005]"
    // 2 = {StandardService@1479} "StandardService[Catalina]"
    // 3 = {StandardEngine@1480} "StandardEngine[Catalina]"
    // 4 = {StandardEngine@1481} "StandardHost[Catalina]"
    Container child = (Container) digester.peek(0);
    Object parent = digester.peek(1);
    Method method = parent.getClass().getMethod("getParentClassLoader", new Class[0]);
    ClassLoader classLoader = (ClassLoader) method.invoke(parent, new Object[0]);
    child.setParentClassLoader(classLoader);
}

  1. 在load()方法结束之后执行start()方法,经过层层调用最终来到StandardContext.startInternal()方法,new了一个WebappLoader(成员变量ParallelWebappClassLoader,设置非双亲委派模式),然后调用它的start()方法来到它的startInternal()方法。
// StandardContext类
@Override
protected synchronized void startInternal() throws LifecycleException {

    ……
    // debug-tomcat9-classLoader
    if (getLoader() == null) {
        WebappLoader webappLoader = new WebappLoader();
        webappLoader.setDelegate(getDelegate());
        setLoader(webappLoader);
    }

    ……

    try {
        if (ok) {
            // debug-tomcat9-classLoader
            Loader loader = getLoader();
            if (loader instanceof Lifecycle) {
                ((Lifecycle) loader).start();
            }

            ……
}
  1. 在这个方法中通过调用createClassLoader()方法对它的类加载器进行了初始化(创建了一个类加载器设置它的父类加载器为sharedClassLoader)。
// WebappLoader类
@Override
protected void startInternal() throws LifecycleException {

    ……

    try {
        // debug-tomcat9-classLoader
        // 创建一个classLoader
        classLoader = createClassLoader();
        classLoader.setResources(context.getResources());
        // 设置非委托模式
        classLoader.setDelegate(this.delegate);

        setClassPath();
        setPermissions();
        classLoader.start();

        String contextName = context.getName();
        if (!contextName.startsWith("/")) {
            contextName = "/" + contextName;
        }
        ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
                classLoader.getClass().getSimpleName() + ",host=" +
                context.getParent().getName() + ",context=" + contextName);
        Registry.getRegistry(null, null)
            .registerComponent(classLoader, cloname, null);

    } catch (Throwable t) {
        ……
    }
    setState(LifecycleState.STARTING);
}

// WebappLoader类
private WebappClassLoaderBase createClassLoader() throws Exception {

    Class<?> clazz = Class.forName(loaderClass);
    WebappClassLoaderBase classLoader = null;

    // 设置父类加载器为sharedClassLoader
    if (parentClassLoader == null) {
        // context为StandardContext[/mywebapp]容器
        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;
}

  1. 调用classLoader.start()方法开始从"/WEB-INF/classes"、"/WEB-INF/lib"读取类,在访问时进行加载
@Override
public void start() throws LifecycleException {

    state = LifecycleState.STARTING_PREP;

    WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
    for (WebResource classes : classesResources) {
        if (classes.isDirectory() && classes.canRead()) {
            localRepositories.add(classes.getURL());
        }
    }
    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;
}

三、线程上下文加载器

  • 在Spring中的例子

    Spring管理创建业务类的时候通过Class.forName()来加载业务类,使用了Spring的加载器加载。 Spring由SharedClassLoader来加载的,但是Web应用目录不在SharedClassLoader的加载路径下。此时通过线程上下文加载器解决这个问题。线程上下文加载器是一种类加载器传递机制,将类加载器保存在线程私有数据里,只要是同一个线程,设置了线程上下文加载器后在线程后续执行过程中就能把这个类加载器取出来用。Tomcat为每个Web应用创建一个WebAppClassLoader类加载器,并在启动Web应用的线程里设置线程上下文加载器,这样Spring在启动时就将线程上下文加载器取出来用来加载Bean

总结

加载Web应用中类的过程:

  1. 在本地cache中查找该类是否被加载过
  2. 在系统类加载器cache中查找是否加载过
  3. 获取ExtClassLoader类加载器,使用它加载
  4. 在本地Web应用目录搜索class并加载
  5. 使用父类加载器(sharedClassLoader)进行加载,shraedClassLoader是使用双亲委派模式的, 所以加载顺序为BootstrapClassLoader -> ExtClassLoader -> AppClassLoader -> commonClassLoader -> sharedClassLoader