Why does Tomcat break parental delegation

Before we begin, we have a question to ask: is it possible for Tomcat to use the default parent delegate class loading mechanism?

First, let’s think about what problems Tomcat needs to solve as a Web container.

  1. If you have several applications deployed on Tomcat, those applications may depend on different versions of the same third-party libraries, so Tomcat must support the isolation of each application’s libraries from each other
  2. Different applications deployed on the same Tomcat, the same version of the same class library should be shared, otherwise a large number of the same classes will be loaded into the virtual machine
  3. Tomcat also has its own dependent libraries, which can be confused with application-dependent libraries and should be separated for security reasons
  4. To support Jsp file modifications, the generated classes can be loaded into the JVM in a timely manner without a restart

So, can the default parent delegate class loading mechanism solve these problems?

  • Problems 1 and 3: If Tomcat uses the default parent delegate loading mechanism, it cannot load different versions of the same class library, because the default parent delegate loading mechanism checks the uniqueness of the class by the fully qualified name
  • Problem 2, the default parent delegate class loading mechanism can be implemented because it is inherently unique
  • Problem 4: We know that when the Jsp file is updated, the class file is actually updated. In this case, the fully qualified name of the class is not changed. After modifying the Jsp file, the classloader will fetch the existing Jsp file directly from the method area, which will cause the modified Jsp file to not be reloaded. Wouldn’t it solve the problem if you simply unmounted the Jsp file’s classloader and re-created it to load the modified Jsp file? Then you can guess that each Jsp file should have a unique classloader

At this point, we can conclude that Tomcat using only the default parent delegate class loading mechanism is not feasible!

Custom class loaders in Tomcat

Several major custom class loaders for Tomcat

  • CommonClassLoader: a CommonClassLoader that loads classes that can be accessed by the Tomcat container itself as well as various webapps
  • CatalinaClassLoader: Private class loader that loads classes that are not visible to Webapp
  • ShareClassLoader: A class loader shared by various WebApps that loads classes visible to all WebApps but not to the Tomcat container itself
  • WebappClassLoader: A private class loader for each Webapp that loads classes visible only to the current Webapp

From the picture above, it is not difficult to see:

  • CommonClassLoaderAny class that can be loaded can beCatalinaClassLoaderandShareClassLoaderUse, so as to achieve the public class library public, andCatalinaClassLoaderandShareClassLoaderThe classes each loads are isolated from the other
  • WebappClassLoaderYou can useShareClassLoaderLoad classes, but eachWebappClassLoaderSeparate from each other
  • JasperLoaderThe load scope of the.class file is only the one that the JSP file was compiled from, and the one-to-one design is to discard it at any time, and Tomcat will replace the current one when it detects that the JSP file has been modifiedJasperLoaderAnd by creating a new one againJasperLoaderExample to achieve JSP file hot loading function

Each WebappClassLoader loads a.class file in its own directory and does not pass it to the parent loader. This breaks the parent delegate mechanism, which is designed to achieve isolation.

The following code emulates the implementation of Tomcat’s WebappClassLoader to load its own WAR package and the coexistence and isolation of different versions of classes within the application

package org.laugen.jvm;
import sun.misc.PerfCounter;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class TestCustomizeClassLoader {
    static class CustomizeClassLoader extends ClassLoader {
        private String classPath;

        public CustomizeClassLoader(String classPath) {
            this.classPath = classPath;
        }

        // Read the class bytecode file
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\ \."."/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        @Override
        protectedClass<? > findClass(String name)throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw newClassNotFoundException(); }}@Override
        protectedClass<? > loadClass(String name,boolean resolve) throws ClassNotFoundException {
            synchronized(getClassLoadingLock(name)) { Class<? > c = findLoadedClass(name);if (c == null) {
                    long t1 = System.nanoTime();
                    if (name.startsWith("org.laugen.jvm.Note")) {
                        c = findClass(name);
                    } else {
                        c = this.getParent().loadClass(name);
                    }
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
                if (resolve) {
                    resolveClass(c);
                }
                returnc; }}}public static void main(String args[]) throws Exception {
        CustomizeClassLoader classLoader1 = new CustomizeClassLoader("D:/MyClasses-v1");
        System.out.println("Parent loader of custom class loader:" + classLoader1.getParent().getClass().getName());
        Class clazz1 = classLoader1.loadClass("org.laugen.jvm.Note");
        Object obj1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("print".null);
        method1.invoke(obj1, null);
        System.out.println("The classloader for the Note class is:" + clazz1.getClassLoader().getClass().getName());
        
        System.out.println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
        
        CustomizeClassLoader classLoader2 = new CustomizeClassLoader("D:/MyClasses-v2");
        System.out.println("Parent loader of custom class loader:" + classLoader2.getParent().getClass().getName());
        Class clazz2 = classLoader2.loadClass("org.laugen.jvm.Note");
        Object obj2 = clazz2.newInstance();
        Method method2 = clazz2.getDeclaredMethod("print".null);
        method2.invoke(obj2, null);
        System.out.println("The classloader for the Note class is:"+ clazz2.getClassLoader().getClass().getName()); }}Copy the code

The running results are as follows:

The parent loader of a custom class loader: Sun.misc.Launcher$AppClassLoader loads the org.laugen.jvm.Note class (V1) and creates an instance of org.laugen.jvm.Note class (V1). Note class loaders are: org.laugen.jvm.TestCustomizeClassLoader$CustomizeClassLoader = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = custom class loader loader's father: Sun.misc.Launcher$AppClassLoader loads the org.laugen.jvm.Note class (version V2) and creates an instance of org.laugen.jvm.Note class (version V2). Note the class loader is: org. Laugen. JVM. $CustomizeClassLoader TestCustomizeClassLoaderCopy the code

As a result, two class objects with the same fully qualified name can coexist in the same JVM because their classloaders can be different

So, do you know how Tomcat JasperLoader hot loading is implemented?