Tomcat Source Code Analysis--Tomcat Class Loader

Keywords: Java Tomcat JSP Apache

Before looking at tomcat class loading, let's review or consolidate java's default class loader. The landlord used to be ignorant about class loading, so he took this opportunity to review it well.

The landlord opened the second edition of the magic book "Deep Understanding of Java Virtual Machine", p227, about the class loader. See:

What is the class loading mechanism?

Java virtual machine loads data describing classes from Class files into memory, verifies data, transforms, parses and initializes data, and finally forms Java types that can be directly used by virtual machines. This is the class loading mechanism of virtual machines.

The virtual machine design team put the action "Get a binary byte stream describing this class by a fully qualified name of the class" in the class loading stage outside the Java virtual machine, so that the application can decide how to get the required class. The code module that implements this action becomes a class loader.

The relationship between classes and class loaders

Although Class loaders are only used to implement Class loading actions, their role in Java programs is far from limited to the Class loading stage. For any Class, it is necessary to establish its uniqueness in the Java virtual machine by loading its Class loader and the Class itself. Each Class loader has a separate Class namespace. Comparing two classes is "equal" only if they are loaded by the same Class loader. Otherwise, even if the two classes come from the same Class file and are loaded by the same virtual machine, as long as their Class loaders are different, the two classes will be meaningful. Each Class must be unequal.

What is the Parent Appointment Model?

  1. From the point of view of Java virtual machine, there are only two kinds of class loaders: Bootstrap Class Loader, which is implemented in C++ language (HotSpot only), and all other class loaders, which are implemented by Java language. Now, it is independent of the virtual machine and inherits from the abstract class java.lang.ClassLoader.

  2. From a Java developer's point of view, class loading can also be partitioned more carefully. Most Java programmers use classloaders provided by the following three systems:

    • Bootstrap Class Loader: This class loader is complex and will be stored in the JAVA_HOME/lib directory, or the path specified by the - Xbootclasspath parameter, and is recognized by the virtual machine (only by the file name, such as rt.jar, the class library whose name does not match will not even be placed in the Lib directory). Heavy load).
    • Extension Class Loader: This class loader is implemented by Sun. misc. Launcher $ExtClass Loader, which is responsible for all class libraries with JAVA_HOME/lib/ext directories or paths specified by java.ext.dirs system variables. Developers can use the Extended Class Loader directly.
    • Application Class Loader: This class loader is implemented by sun.misc.Launcher$AppClassLoader. Since this class loader is the return value of the getSystemClassLoader method of the ClassLoader species, it also becomes the system class loader. It is responsible for loading the class libraries specified on the ClassPath. Developers can use this class loader directly. If no class loader has been defined in the application, it is usually the default class loader in the program.

The relationships between these class loaders are generally shown in the following figure:

The relationship between the classloaders in the diagram becomes the Parents Dlegation Mode of the class loader. The Parent Delegation Model requires that all class loaders except the top-level boot class loaders should be loaded by their own parent class loaders. In this case, the parent-child relationship between class loaders is not usually implemented by inheritance, but by using combination relations to reuse the code of the parent loader.

The parent-to-parent delegation model of class loader was introduced during JDK 1.2 and widely used in all subsequent Java programs, but it is not a mandatory constraint model, but a class loader implementation recommended by Java designers to developers.

The working process of the Parent Delegation Model is that if a class loader receives a class loading request, he will not try to load the class himself first, but delegate the request to the parent class loader to complete. This is true for every level of class loader, so all load requests should eventually be sent to the top-level boot class loader. Only when the parent loader feeds back that it is unable to complete the request (the required class is not found in his search scope), will the sub-loader try to load it by itself.

Why?

If the user writes a class called java.lang.Object and puts it in the ClassPath of the program, the system will have many different Object classes, and the most basic behavior in the Java type system cannot be guaranteed. Applications will also become a mess.

How does the parental appointment model work?

Very simple: All the code is in the loadClass method in java.lang.ClassLoader, and the code is as follows:

First check whether it has been loaded, if not, call the loadClass method of the parent loader. If the parent loader is empty, the startup class loader is used as the parent loader by default. If the parent class fails to load, after throwing the ClassNotFoundException exception, it calls its own findClass method to load.

How to break the parental appointment model?

The parental appointment model is not a mandatory constraint model, but a proposed class loader implementation. Most class loaders in the Java world follow the model, but there are exceptions. So far, the Parent Delegation Model has been "broken" three times on a large scale.
First time: before the Parent Delegation Model came into being - before JDK 1.2 was released.
Second: It is the defect of the model itself. We say that the Parent Delegation Model is a good solution to the unification of the basic classes of each class loader (the more basic classes are loaded by the upper loader). The reason why the basic classes are called "base" is that they are always used as API s called by user code, but not absolutely, if the basic class calls the user. What about the code?

This is not impossible. A typical example is the JNDI service. JNDI is now the standard Java service. Its code is loaded by the boot class loader (rt.jar put in at JDK 1.3), but it needs to call the JNDI interface provider (SPI, Service Provider Interface) implemented by an independent vendor and deployed under the ClassPath of the application. ) Code, but it's impossible to "know" the code by starting the class loader. Because these classes are not in RT. jar, starting the class loader requires loading. What shall I do?

To solve this problem, the Java design team had to introduce a less elegant design: Thread Context Class Loader. This class loader can be set through the setContextClassLoader method of the java.lang.Thread class. If the thread is not set when it is created, it will inherit one from the parent thread. If there are not too many settings in the global scope of the application, the class loader defaults even to the application class loader.

Hey hey, with a thread context loader, JNDI services use this thread context loader to load the required SPI code, that is, the parent loader requests the subclass loader to complete the action of class loading. This behavior actually opens up the hierarchy of the parent delegation model to reverse the use of the class loader. In fact, it has violated the general principle of parental assignment model. But there's no way out. All the SPI-related loading actions in Java basically win this way. For example, JNDI, JDBC, JCE, JAXB, JBI, etc.

Third: In order to achieve hot plug, hot deployment, modularization, which means adding a function or subtracting a function without restarting, only need to replace this module with the class loader to achieve the hot replacement of code.

How is Tomcat's class loader designed?

First of all, let's ask a question:

Can Tomcat use the default class loading mechanism?

Let's think about it: Tomcat is a web container, so what's it going to solve?

  1. A web container may need to deploy two applications. Different applications may depend on different versions of the same third-party class library. The same class library cannot be required to have only one copy on the same server. Therefore, it is necessary to ensure that each application's class library is independent and isolated from each other.
  2. The same version of the same class library deployed in the same web container can be shared. Otherwise, if the server has 10 applications, it would be ridiculous to have 10 identical libraries loaded into the virtual machine.
  3. web containers also have their own class libraries, which can not be confused with application libraries. For security reasons, container libraries should be isolated from program libraries.
  4. Web containers need to support jsp modifications. We know that jsp files are ultimately compiled into class files to run in the virtual machine, but it is common to modify jsp after the program runs. Otherwise, what do you need? Therefore, the web container needs to support jsp modifications without restarting.

 

Let's look at our question again: Would Tomcat be okay if it used the default class loading mechanism?
The answer is No. Why? Let's see, the first problem is that if you use the default class loader mechanism, you can't load two different versions of the same class library. The default class adder doesn't care what version you have, it only cares about your fully qualified class name and only has one copy. The second problem is that the default class loader is achievable because it's his job to ensure uniqueness. The third question is the same as the first one. Let's look at the fourth question. We want to know how to implement the hot modification of jsp files (the name of the owner of the building). jsp files are actually class files. If the modification is made, but the class names are the same, the class loader will directly fetch the existing jsp in the method area, and the modified jsp will not be reloaded. So what? We can uninstall the class loader of the jsp file directly, so you should think that each jsp file corresponds to a unique class loader. When a jsp file is modified, the class loader of the jsp file is uninstalled directly. Recreate the class loader and reload the jsp file.

How does Tomcat implement its own unique class loading mechanism?

Let's take a look at their blueprints.

 

We see a lot of class loaders in this diagram. Apart from the class loaders that come with Jdk, we are particularly concerned about Tomcat's own class loaders. Carefully, we can easily find that Catalina class loaders and Shared class loaders are not father-son relationships, but brotherly relationships. Why do we design this way? We need to analyze the purpose of each class loader before we can know it.

  1. Common class loader, responsible for loading classes reused by Tomcat and Web applications
  2. Catalina class loader, responsible for loading Tomcat-specific classes, which will not be visible in Web applications
  3. Shared class loader, responsible for loading classes reused by all Web applications under Tomcat, and these loaded classes will not be visible in Tomcat
  4. WebApp class loader is responsible for loading classes used by a specific Web application, which will not be visible in Tomcat and other Web applications.
  5. Jsp class loader, each JSP page has a class loader, different JSP pages have different class loaders, easy to achieve hot plugging JSP pages

Source Code Reading

Tomcat starts the entry in the main() method of Bootstrap. Before the main() method is executed, its static {} block must be executed first. So we first analyze the static {} block and then the main() method.

Bootstrap.static{}

static {
    // Get the user directory
    // Will always be non-null
    String userDir = System.getProperty("user.dir");

    // The first step is to extract from environmental variables catalina.home,Later acquisition operations will be performed when no acquisition is made.
    // Home first
    String home = System.getProperty(Globals.CATALINA_HOME_PROP);
    File homeFile = null;

    if (home != null) {
        File f = new File(home);
        try {
            homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
            homeFile = f.getAbsoluteFile();
        }
    }

    // The second step, when the first step is not acquired, is from bootstrap.jar Upper level directory acquisition of the directory
    if (homeFile == null) {
        // First fall-back. See if current directory is a bin directory
        // in a normal Tomcat install
        File bootstrapJar = new File(userDir, "bootstrap.jar");

        if (bootstrapJar.exists()) {
            File f = new File(userDir, "..");
            try {
                homeFile = f.getCanonicalFile();
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }
    }

    // Step 3, Step 2 bootstrap.jar Maybe it doesn't exist, and then we'll go straight to it. user.dir As our home Catalog
    if (homeFile == null) {
        // Second fall-back. Use current directory
        File f = new File(userDir);
        try {
            homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
            homeFile = f.getAbsoluteFile();
        }
    }

    // Reset catalinaHome attribute
    catalinaHomeFile = homeFile;
    System.setProperty(
            Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());

    // Next, get CATALINA_BASE(Obtain from system variables, if not, the CATALINA_BASE Keep and CATALINA_HOME identical
    // Then base
    String base = System.getProperty(Globals.CATALINA_BASE_PROP);
    if (base == null) {
        catalinaBaseFile = catalinaHomeFile;
    } else {
        File baseFile = new File(base);
        try {
            baseFile = baseFile.getCanonicalFile();
        } catch (IOException ioe) {
            baseFile = baseFile.getAbsoluteFile();
        }
        catalinaBaseFile = baseFile;
    }
   // Reset catalinaBase attribute
    System.setProperty(
            Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}

Let's summarize the comments in the code:

  1. Get the user directory
  2. The first step is to get catalina.home from the environment variable and perform the subsequent fetch operation when it is not fetched.
  3. Second, when the first step is not retrieved, retrieve from the upper directory of the bootstrap.jar directory
  4. The third step, bootstrap.jar in the second step may not exist, at which point we directly use user.dir as our home directory
  5. Resetting catalinaHome property
  6. Next, get CATALINA_BASE (from system variables), and if it does not exist, keep CATALINA_BASE the same as CATALIINA_HOME.
  7. Resetting catalinaBase property

To sum up, it is to load and set up the information related to Catalina Home and Catalina Base for future use.

main()

The main method is roughly divided into two parts, one is init and the other is load+start.

public static void main(String args[]) {
    // The first piece, main When the method is first executed, daemon Definitely null,So direct new One Bootstrap Object, and then execute it init()Method
    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 The daemon object is set to bootstrap
        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);
    }

    // Second, execute the daemon's load Methods and methods start Method
    try {
        String command = "start";
        if (args.length > 0) {
            command = args[args.length - 1];
        }

        if (command.equals("startd")) {
            args[args.length - 1] = "start";
            daemon.load(args);
            daemon.start();
        } else if (command.equals("stopd")) {
            args[args.length - 1] = "stop";
            daemon.stop();
        } else if (command.equals("start")) {
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
            if (null == daemon.getServer()) {
                System.exit(1);
            }
        } else if (command.equals("stop")) {
            daemon.stopServer(args);
        } else if (command.equals("configtest")) {
            daemon.load(args);
            if (null == daemon.getServer()) {
                System.exit(1);
            }
            System.exit(0);
        } else {
            log.warn("Bootstrap: command \"" + command + "\" does not exist.");
        }
    } catch (Throwable t) {
        // Unwrap the Exception for clearer error reporting
        if (t instanceof InvocationTargetException &&
                t.getCause() != null) {
            t = t.getCause();
        }
        handleThrowable(t);
        t.printStackTrace();
        System.exit(1);
    }
}

Let's go inside init() and have a look.~

public void init() throws Exception {
    // The key point is to initialize the class loader s,We will analyze this method in detail later.
    initClassLoaders();

    // Set the context class loader to catalinaLoader,This class loader is responsible for loading Tomcat Specialized classes
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    // Let's skip it for a moment. I'll talk about it later.
    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // Use catalinaLoader Load our Catalina class
    // Load our startup class and call its process() method
    if (log.isDebugEnabled())
        log.debug("Loading startup class");
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // Set up Catalina Class parentClassLoader Attribute is sharedLoader
    // Set the shared extensions class loader
    if (log.isDebugEnabled())
        log.debug("Setting startup class properties");
    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);

    // catalina The daemon object is just used catalinaLoader Loading classes and initializing them Catalina object
    catalinaDaemon = startupInstance;
}

The key method, initClassLoaders, is responsible for initializing Tomcat's class loader. In this way, we can easily verify the Tomcat class loading diagram mentioned in the previous section.

private void initClassLoaders() {
    try {
        // Establish commonLoader,If no results are created, use the application class loader as the commonLoader
        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();
        }
        // Establish catalinaLoader,The parent loader is commonLoader
        catalinaLoader = createClassLoader("server", commonLoader);
        // Establish sharedLoader,The parent loader is commonLoader
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        // If an exception occurs during the creation process, the system exits directly after the log is completed
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}
All class loaders are created using the method createClassLoader, so let's take a closer look at this method. CreateClassLoader uses the Catalina Properties. getProperty ("xxx") method, which is used to obtain attribute values from the conf/catalina.properties file.
private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {
    // Gets the location of the class loader to be loaded. If it is empty, it does not need to load a specific location, and returns using the parent class loading.
    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;
    // Replace attribute variables, such as: ${catalina.base},${catalina.home}
    value = replace(value);

    List<Repository> repositories = new ArrayList<>();

   // Resolve attribute path variable to warehouse path array
    String[] repositoryPaths = getPaths(value);

    // For each warehouse Path repositories Set up. We can do this. repositories Think of it as a location object to be loaded, and it can be a location object. classes Catalogue, one jar File directory, etc.
    for (String repository : repositoryPaths) {
        // Check for a JAR URL repository
        try {
            @SuppressWarnings("unused")
            URL url = new URL(repository);
            repositories.add(
                    new Repository(repository, RepositoryType.URL));
            continue;
        } catch (MalformedURLException e) {
            // Ignore
        }

        // Local repository
        if (repository.endsWith("*.jar")) {
            repository = repository.substring
                (0, repository.length() - "*.jar".length());
            repositories.add(
                    new Repository(repository, RepositoryType.GLOB));
        } else if (repository.endsWith(".jar")) {
            repositories.add(
                    new Repository(repository, RepositoryType.JAR));
        } else {
            repositories.add(
                    new Repository(repository, RepositoryType.DIR));
        }
    }
    // Create a class loader using the class loader factory
    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

Let's analyze ClassLoaderFactory.createClassLoader -- the class loader factory creates the class loader.

public static ClassLoader createClassLoader(List<Repository> repositories,
                                            final ClassLoader parent)
    throws Exception {

    if (log.isDebugEnabled())
        log.debug("Creating new class loader");

    // Construct the "class path" for this class loader
    Set<URL> set = new LinkedHashSet<>();
    // ergodic repositories,For each repository Type judgment and generation URL,each URL We all need to verify its validity and effectiveness. URL We'll put it in URL In a set
    if (repositories != null) {
        for (Repository repository : repositories)  {
            if (repository.getType() == RepositoryType.URL) {
                URL url = buildClassLoaderUrl(repository.getLocation());
                if (log.isDebugEnabled())
                    log.debug("  Including URL " + url);
                set.add(url);
            } else if (repository.getType() == RepositoryType.DIR) {
                File directory = new File(repository.getLocation());
                directory = directory.getCanonicalFile();
                if (!validateFile(directory, RepositoryType.DIR)) {
                    continue;
                }
                URL url = buildClassLoaderUrl(directory);
                if (log.isDebugEnabled())
                    log.debug("  Including directory " + url);
                set.add(url);
            } else if (repository.getType() == RepositoryType.JAR) {
                File file=new File(repository.getLocation());
                file = file.getCanonicalFile();
                if (!validateFile(file, RepositoryType.JAR)) {
                    continue;
                }
                URL url = buildClassLoaderUrl(file);
                if (log.isDebugEnabled())
                    log.debug("  Including jar file " + url);
                set.add(url);
            } else if (repository.getType() == RepositoryType.GLOB) {
                File directory=new File(repository.getLocation());
                directory = directory.getCanonicalFile();
                if (!validateFile(directory, RepositoryType.GLOB)) {
                    continue;
                }
                if (log.isDebugEnabled())
                    log.debug("  Including directory glob "
                        + directory.getAbsolutePath());
                String filenames[] = directory.list();
                if (filenames == null) {
                    continue;
                }
                for (int j = 0; j < filenames.length; j++) {
                    String filename = filenames[j].toLowerCase(Locale.ENGLISH);
                    if (!filename.endsWith(".jar"))
                        continue;
                    File file = new File(directory, filenames[j]);
                    file = file.getCanonicalFile();
                    if (!validateFile(file, RepositoryType.JAR)) {
                        continue;
                    }
                    if (log.isDebugEnabled())
                        log.debug("    Including glob jar file "
                            + file.getAbsolutePath());
                    URL url = buildClassLoaderUrl(file);
                    set.add(url);
                }
            }
        }
    }

    // Construct the class loader itself
    final URL[] array = set.toArray(new URL[set.size()]);
    if (log.isDebugEnabled())
        for (int i = 0; i < array.length; i++) {
            log.debug("  location " + i + " is " + array[i]);
        }

    // From this point of view, ultimately all class loaders are URLClassLoader Objects~~
    return AccessController.doPrivileged(
            new PrivilegedAction<URLClassLoader>() {
                @Override
                public URLClassLoader run() {
                    if (parent == null)
                        return new URLClassLoader(array);
                    else
                        return new URLClassLoader(array, parent);
                }
            });
}

Now that we've analyzed initClassLoaders, let's look at SecurityClassLoad. Security ClassLoad and see what's going on inside.

public static void securityClassLoad(ClassLoader loader) throws Exception {
    securityClassLoad(loader, true);
}

static void securityClassLoad(ClassLoader loader, boolean requireSecurityManager) throws Exception {

    if (requireSecurityManager && System.getSecurityManager() == null) {
        return;
    }

    loadCorePackage(loader);
    loadCoyotePackage(loader);
    loadLoaderPackage(loader);
    loadRealmPackage(loader);
    loadServletsPackage(loader);
    loadSessionPackage(loader);
    loadUtilPackage(loader);
    loadValvesPackage(loader);
    loadJavaxPackage(loader);
    loadConnectorPackage(loader);
    loadTomcatPackage(loader);
}

 private static final void loadCorePackage(ClassLoader loader) throws Exception {
    final String basePackage = "org.apache.catalina.core.";
    loader.loadClass(basePackage + "AccessLogAdapter");
    loader.loadClass(basePackage + "ApplicationContextFacade$PrivilegedExecuteMethod");
    loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedForward");
    loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedInclude");
    loader.loadClass(basePackage + "ApplicationPushBuilder");
    loader.loadClass(basePackage + "AsyncContextImpl");
    loader.loadClass(basePackage + "AsyncContextImpl$AsyncRunnable");
    loader.loadClass(basePackage + "AsyncContextImpl$DebugException");
    loader.loadClass(basePackage + "AsyncListenerWrapper");
    loader.loadClass(basePackage + "ContainerBase$PrivilegedAddChild");
    loadAnonymousInnerClasses(loader, basePackage + "DefaultInstanceManager");
    loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntry");
    loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntryType");
    loader.loadClass(basePackage + "ApplicationHttpRequest$AttributeNamesEnumerator");
}

This is actually the use of Catalina Loader to load the various dedicated classes in tomcat source code. Let's outline the package where the class to be loaded is located:

  1. org.apache.catalina.core.*
  2. org.apache.coyote.*
  3. org.apache.catalina.loader.*
  4. org.apache.catalina.realm.*
  5. org.apache.catalina.servlets.*
  6. org.apache.catalina.session.*
  7. org.apache.catalina.util.*
  8. org.apache.catalina.valves.*
  9. javax.servlet.http.Cookie
  10. org.apache.catalina.connector.*
  11. org.apache.tomcat.*

Okay, so far we've analyzed several key methods involved in init.

WebApp class loader

Here, we feel a little less analysis! Yes, it's the WebApp class loader. After analyzing the whole boot process, we still haven't seen this class loader. Where did it appear?

We know that WebApp class loaders are private to Web applications, and each Web application is actually a Context, so we should be able to discover through the implementation class of Context. In Tomcat, the default implementation of Context is StandardContext. Let's look at the startInternal() method of this class, where we find the WebApp class loader of interest.

protected synchronized void startInternal() throws LifecycleException {
    if (getLoader() == null) {
        WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
        webappLoader.setDelegate(getDelegate());
        setLoader(webappLoader);
    }
}

The entry code is very simple, that is, when webappLoader does not exist, create one and call the setLoader method.

summary

Finally, we have completed the whole starting process + class loading process of Tomcat. We also understand and learn why Tomcat's different class loading mechanisms are designed in this way and what additional effects they bring.

Posted by Mark Nordstrom on Thu, 15 Aug 2019 00:58:09 -0700