Learn loaders like Tomcat

From the previous article, we know that Tomcat requests will eventually be handed over to the user configured servlet instance to process. The servlet class is configured in the configuration file, which requires the class loader to load the servlet class. Tomcat container customizes the class loader and has the following special functions: 1. Specify some rules in the loading class; 2. Cache the loaded classes; 3. Implement class preloading. This article will introduce Tomcat's class loader in detail.

Java class loading parent delegation model

Java class loader is a bridge between user programs and JVM virtual machines, and plays a vital role in Java programs. For its detailed implementation, please refer to the tutorial on virtual machine loading in the official JAVA document, Click here to go directly to the official reference documents . Class loading in Java adopts the parental delegation model by default, that is, when loading a class, first judge whether its own define loader has loaded this class. If it has loaded, directly obtain the class object. If not, hand it to the loader's parent class loader to repeat the above process. In another article, I introduced Java's class loading mechanism in detail, which is not described in detail here.

Loader interface

When loading the servlet classes and their related classes required in the web application, some clear rules should be observed. For example, the servlet in the application can only refer to the classes deployed in the WEB-INF/classes directory and its subdirectories. However, servlet classes cannot access classes in other paths, even if they are contained in the CLASSPATH environment variable of the JVM running the current Tomcat. In addition, the servlet class can only access the Libraries under the WEB-INF/LIB directory, and the class libraries of other directories cannot be accessed. The loader in Tomcat is worth the Web Application Loader, not just the class loader. The loader must implement the loader interface. The loader interface is defined as follows:

public interface Loader {

    public void backgroundProcess();
    public ClassLoader getClassLoader();
    public Context getContext();
    public void setContext(Context context);
    public boolean getDelegate();
    public void setDelegate(boolean delegate);
    public void addPropertyChangeListener(PropertyChangeListener listener);
    public boolean modified();
    public void removePropertyChangeListener(PropertyChangeListener listener);
}

Background task: the Loader interface needs to Reload the servlet class when the servlet class is changed. This task is implemented in backgroundProcess(). The implementation of backgroundProcess() in WebApploader is as follows. It can be seen that when the Reload function is enabled in the Context container and the warehouse is changed, Loaders will first set the class Loader to the Web class Loader and restart the Context container. Restarting the Context container will restart all child Wrapper containers, destroy and recreate the servlet class instance, so as to achieve the purpose of dynamically loading the servlet class.

    @Override
    public void backgroundProcess() {
        Context context = getContext();
        if (context != null) {
            if (context.getReloadable() && modified()) {
                ClassLoader originalTccl = Thread.currentThread().getContextClassLoader();
                try {
                    Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());
                    context.reload();
                } finally {
                    Thread.currentThread().setContextClassLoader(originalTccl);
                }
            }
        }
    }

Class Loader: in the implementation of Loader, a custom class Loader will be used, which is an instance of WebappClassLoader class. You can use the getClassLoader() method of the Loader interface to get an instance of ClassLoader in the Web Loader. There are two implementations of the default class Loader: ParallelWebappClassLoader and WebappClassLoader

Context container: Tomcat's Loader is usually associated with a context level servlet container. The getContainer() method and setContainer() method of the Loader interface are used to associate the Loader with a servlet container. If one or more classes in the context container are modified, the Loader can also support class overloading. In this way, servlet programmers can recompile servlet classes and their related classes and reload them without restarting Tomcat. The Loader interface uses the modified() method to support automatic overloading of classes.

Class modification detection: in the specific implementation of the loader, if one or more classes in the warehouse are modified, the modified() method must be put back to true to provide automatic overloading support

Parent loader: the implementation of the loader will indicate whether to delegate to the loader of the parent class, which can be configured through setDelegate() and getDelegate methods.

WebappLoader class

The only class in Tomcat that implements the Loader interface is the webapploader class. Its instance will be used as the Loader of the Web application container to load the classes used in the Web application. When the container starts, the webapploader performs the following tasks:

  • Create class loader
  • Set up warehouse
  • Set the path of the class
  • Set access rights
  • Start a new thread to support automatic overloading

Create class loader

In order to complete the class loading function, WebappLoader will create an instance of class loader according to the configuration. Tomcat has two kinds of loaders by default: WebappClassLoader and ParallelWebappClassLoader. ParallelWebappClassLoader is used as the class loader by default. The user can set the name of the class loader through setLoaderClass(). The source code of WebappLoader creating class loader is as follows. We can see that the instance of class loader must be a subclass of WebappClassLoaderBase.

    private WebappClassLoaderBase createClassLoader()
        throws Exception {

        if (classLoader != null) {
            return classLoader;
        }

        if (ParallelWebappClassLoader.class.getName().equals(loaderClass)) {
            return new ParallelWebappClassLoader(context.getParentClassLoader());
        }

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

        ClassLoader parentClassLoader = context.getParentClassLoader();

        Class<?>[] argTypes = { ClassLoader.class };
        Object[] args = { parentClassLoader };
        Constructor<?> constr = clazz.getConstructor(argTypes);
        classLoader = (WebappClassLoaderBase) constr.newInstance(args);

        return classLoader;
    }

Set up warehouse

WebappLoader will call the initialization method of the class loader at startup, and the class loader will set the warehouse address of class loading at initialization. The default warehouse addresses are "/ WEB-INF/classes" and "/ WEB-INF/lib". The initialization source code of class loader is as follows:

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

Set classpath

Setting the classpath is completed by calling the setClassPath() method during initialization (the source code is as follows). The setClassPath() method sets a string type attribute for the Jasper JSP compiler in the servlet context to indicate the classpath information. JSP related content is not described in detail here.

  private void setClassPath() {

        // Validate our current state information
        if (context == null)
            return;
        ServletContext servletContext = context.getServletContext();
        if (servletContext == null)
            return;

        StringBuilder classpath = new StringBuilder();

        // Assemble the class path information from our class loader chain
        ClassLoader loader = getClassLoader();

        if (delegate && loader != null) {
            // Skip the webapp loader for now as delegation is enabled
            loader = loader.getParent();
        }

        while (loader != null) {
            if (!buildClassPath(classpath, loader)) {
                break;
            }
            loader = loader.getParent();
        }

        if (delegate) {
            // Delegation was enabled, go back and add the webapp paths
            loader = getClassLoader();
            if (loader != null) {
                buildClassPath(classpath, loader);
            }
        }

        this.classpath = classpath.toString();

        // Store the assembled class path as a servlet context attribute
        servletContext.setAttribute(Globals.CLASS_PATH_ATTR, this.classpath);
    }

Set access rights

If the security manager is used when running Tomcat, the setPermissions() method will set the permission for the class loader to access the relevant directories, such as the directories of WEB-INF/classes and WEB-INF/lib. If the security manager is not used, the setPermissions() method simply returns and does nothing. Its source code is as follows:

    /**
     * Configure associated class loader permissions.
     */
    private void setPermissions() {

        if (!Globals.IS_SECURITY_ENABLED)
            return;
        if (context == null)
            return;

        // Tell the class loader the root of the context
        ServletContext servletContext = context.getServletContext();

        // Assigning permissions for the work directory
        File workDir =
            (File) servletContext.getAttribute(ServletContext.TEMPDIR);
        if (workDir != null) {
            try {
                String workDirPath = workDir.getCanonicalPath();
                classLoader.addPermission
                    (new FilePermission(workDirPath, "read,write"));
                classLoader.addPermission
                    (new FilePermission(workDirPath + File.separator + "-",
                                        "read,write,delete"));
            } catch (IOException e) {
                // Ignore
            }
        }

        for (URL url : context.getResources().getBaseUrls()) {
           classLoader.addPermission(url);
        }
    }

Start a new thread to perform class reload

The WebappLoader class supports automatic overloading. If some classes in the WEB-INF/classes directory or WEB-INF/lib directory are recompiled, the class will be automatically reloaded without restarting Tomcat. To achieve this, the WebappLoader class uses a thread to periodically check the timestamp of each resource. The interval is specified by the variable checkInterval in S. by default, the value of checkInterval is 15s. It will check whether there are files that need to be automatically reloaded every 15s. When the top-level container is started, it will start the timed thread pool to call the backgroundProcess task circularly.

    protected void threadStart() {
        if (backgroundProcessorDelay > 0
                && (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState()))
                && (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) {
            if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) {
                // There was an error executing the scheduled task, get it and log it
                try {
                    backgroundProcessorFuture.get();
                } catch (InterruptedException | ExecutionException e) {
                    log.error(sm.getString("containerBase.backgroundProcess.error"), e);
                }
            }
            backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
                    .scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
                            backgroundProcessorDelay, backgroundProcessorDelay,
                            TimeUnit.SECONDS);
        }
    }

    @Override
    public void backgroundProcess() {
        Context context = getContext();
        if (context != null) {
            if (context.getReloadable() && modified()) {
                ClassLoader originalTccl = Thread.currentThread().getContextClassLoader();
                try {
                    Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());
                    context.reload();
                } finally {
                    Thread.currentThread().setContextClassLoader(originalTccl);
                }
            }
        }
    } 

WebappClassLoader class loader

There are two kinds of class loaders responsible for loading classes in Web applications: ParallelWebappClassLoader and WebappClassLoaderBase. Their implementation is similar. This section takes WebappClassLoader as an example to introduce Tomcat's class loader.

The design scheme of WebappClassLoader considers two aspects: optimization and security. For example, it will cache the previously loaded classes to improve performance, and also cache the name of the class that failed to load. In this way, when the same class is requested to be loaded again, the class loader will directly throw a ClassNotFindException exception instead of looking for the class again. WebappClassLoader searches the repository list and the specified JAR file for the classes that need to be loaded.

Class cache

In order to achieve better performance, WebappClassLoader will cache the loaded class, so that the class will be directly obtained from the cache the next time it is used. All classes loaded by WebappClassLoader will be cached as resources, and the corresponding class is an instance of "ResourceEntry" class. ResourceEndty saves the byte stream, last modification date, Manifest information, etc. of the class file it represents. The following is part of the code to read the cache during class loading and the definition source code of ResourceEntry.

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

    // Omit partial logic
    // (0) Check our previously loaded local class cache
    clazz = findLoadedClass0(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
        return clazz;
    }
     // Omit partial logic
}

protected Class<?> findLoadedClass0(String name) {

    String path = binaryNameToPath(name, true);

    ResourceEntry entry = resourceEntries.get(path);
    if (entry != null) {
        return entry.loadedClass;
    }
     return null;
}


public class ResourceEntry {
    /**
     * The "last modified" time of the origin file at the time this resource
     * was loaded, in milliseconds since the epoch.
     */
    public long lastModified = -1;

    /**
     * Loaded class.
     */
    public volatile Class<?> loadedClass = null;
}

Load class

When loading classes, WebappClassLoader should follow the following rules:

  1. Because all loaded classes will be cached, you should first check the local cache when loading classes.
  2. If there is no local cache, check the cache of the parent class loader and call the findLoadedClass() method of the ClassLoader interface.
  3. If there are no two caches, the system class loader is used to load to prevent the classes in the Web application from overwriting the classes in J2EE.
  4. If SecurityManager is enabled, check whether the class is allowed to load. If the class is a prohibited class, a ClassNotFoundException exception is thrown.
  5. If the flag delegate is turned on, or if the class to be loaded cannot be loaded by the web class loader, the parent class loader is used to load the relevant class. If the parent class loader is null, the system class loader is used.
  6. Load the class from the current warehouse.
  7. If the current warehouse has no classes to load and delegate is closed, the parent class loader is used to load the relevant classes.
  8. If no class to load is found, ClassNotFindException is thrown.

Tomcat class loading structure

Tomcat container initializes the classloader when it is started. Tomcat classloaders are divided into four types: Common classloader, Cataline classloader and Shared classloader. In addition, each application will have its own Webapp classloader, that is, the WebappClassLoader described above. The relationship between the four is as follows.

The Common class loader, Cataline class loader and Shared class loader will be initialized when the Tomcat container is started. The initialization code is as follows:

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


    private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {

        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;

        value = replace(value);

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

        String[] repositoryPaths = getPaths(value);

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

        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }

The Webapp class loader is initialized by the WebappLoader when the Context container is started. The parent class loader of the Webapp class loader is set by the Tomcat container through reflection in the initialization stage. The source code of the reflection setting parent class loader is as follows:

    public void init() throws Exception {

        initClassLoaders();

        Thread.currentThread().setContextClassLoader(catalinaLoader);

        SecurityClassLoad.securityClassLoad(catalinaLoader);

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

        catalinaDaemon = startupInstance;
    }

Purpose of Tomcat class loading structure

  1. A web container may need to deploy two applications. Different applications may depend on different versions of the same third-party class library. It is not necessary to have only one copy of the same class library on the same server. Therefore, it is necessary to ensure that the class libraries of each application are independent and isolated from each other. Therefore, each application needs its own Webapp class loader.
  2. Deployed in the same web container, the same class library and the same version can be Shared. Otherwise, if the server has 10 applications, 10 copies of the same class library should be loaded into the virtual machine. Therefore, the Shared class loader is required
  3. The web container also has its own dependent class library, which can not be confused with the class library of the application. For security reasons, the class library of the container should be isolated from the class library of the program. Therefore, the Cataline class loader is required.
  4. The web container should support the modification of jsp. We know that the jsp file must be compiled into a class file to run in the virtual machine, but it is common to modify jsp after the program runs. Otherwise, what's your use? Therefore, the web container needs to support jsp modification without restarting.

There is also the problem of sharing the last class. If spring classes are introduced into ten web applications, the memory overhead is great due to the isolation of Web class loaders. At this time, we can think of the shared class loader. We will certainly choose to put the spring jar under the shared directory, but there will be another problem. The shared class loader is the parent of the webapp class loader. If the getBean method in spring needs to load the classes under the web application, this process violates the parental delegation mechanism.

Break the shackles of the two parent delegation mechanism: thread context class loader thread context class loader refers to the class loader used by the current thread, which can be obtained or set through Thread.currentThread().getContextClassLoader(). In spring, he will select the thread context class loader to load the classes under the web application, thus breaking the parental delegation mechanism.

List of reference documents

I am the fox fox. Welcome to my WeChat official account: wzm2zsd

This article is first released to WeChat official account, all rights reserved, no reprint!

Tags: Java

Posted on Wed, 06 Oct 2021 11:47:37 -0400 by plusnplus