學習Tomcat(六)之類加載器

通過前面的文章我們知道,Tomcat的請求最終都會交給用戶配置的servlet實例來處理。Servlet類是配置在配置文件中的,這就需要類加載器對Servlet類進行加載。Tomcat容器自定義了類加載器,有以下特殊功能:1. 在載入類中指定某些規則;2.緩存已經載入的類;3.實現類的預加載。 本文會對Tomcat的類加載器進行詳細介紹。

Java類加載雙親委派模型

Java類加載器是用戶程序和JVM虛擬機之間的橋樑,在Java程序中起了至關重要的作用,關於其詳細實現可以參考了java官方文檔關於虛擬機加載的教程,點此直達官方參考文檔。java中的類加載默認是採用雙親委派模型,即加載一個類時,首先判斷自身define加載器有沒有加載過此類,如果加載了直接獲取class對象,如果沒有查到,則交給加載器的父類加載器去重複上面過程。我在另外一篇文章中詳細介紹了Java的類加載機制,此處不做詳細介紹。

java-class-load-2021-10-05-18-35-03

Loader接口

在載入Web應用程序中需要的servlet類及其相關類時要遵守一些明確的規則,例如應用程序中的servlet只能引用部署在WEB-INF/classes目錄及其子目錄下的類。但是,servlet類不能訪問其它路徑中的類,即使這些累包含在運行當前Tomcat的JVM的CLASSPATH環境變量中。此外,servlet類只能訪問WEB-INF/LIB目錄下的庫,其它目錄的類庫均不能訪問。Tomcat中的載入器值得是Web應用程序載入器,而不僅僅是類載入器,載入器必須實現Loader接口。Loader接口的定義如下所示:

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

後台任務:Loader接口需要進行在servlet類變更的時候實現類的重新加載,這個任務就是在backgroundProcess()中實現的,WebApploader中backgroundProcess()的實現如下所示。可以看到,當Context容器開啟了Reload功能並且倉庫變更的情況下,Loaders會先把類加載器設置為Web類加載器,重啟Context容器。重啟Context容器會重啟所有的子Wrapper容器,會銷毀並重新創建servlet類的實例,從而達到動態加載servlet類的目的。

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

類加載器:Loader的實現中,會使用一個自定義類載入器,它是WebappClassLoader類的一個實例。可以使用Loader接口的getClassLoader()方法來獲取Web載入器中的ClassLoader的實例。默認的類加載器的實現有兩種種:ParallelWebappClassLoader和WebappClassLoader

Context容器:Tomcat的載入器通常會與一個Context級別的servelt容器相關聯,Loader接口的getContainer()方法和setContainer()方法用來將載入器和某個servlet容器關聯。如果Context容器中的一個或者多個類被修改了,載入器也可以支持對類的重載。這樣,servlet程序員就可以重新編譯servlet類及其相關類,並將其重新載入而不需要重新啟動Tomcat。Loader接口使用modified()方法來支持類的自動重載。

類修改檢測:在載入器的具體實現中,如果倉庫中的一個或者多個類被修改了,那麼modified()方法必須放回true,才能提供自動重載的支持

父載入器:載入器的實現會指明是否要委託給父類的載入器,可以通過setDelegate()和getDelegate方法配置。

WebappLoader類

Tomcat中唯一實現Loader接口的類就是WebappLoader類,其實例會用作Web應用容器的載入器,負責載入Web應用程序中所使用的類。在容器啟動的時候,WebApploader會執行以下工作:

  • 創建類加載器
  • 設置倉庫
  • 設置類的路徑
  • 設置訪問權限
  • 啟動新線程來支持自動重載

創建類加載器

為了完成類加載功能,WebappLoader會按照配置創建類加載器的實例,Tomcat默認有兩種類加載器:WebappClassLoader和ParallelWebappClassLoader,默認情況下使用ParallelWebappClassLoader作為類加載器。用戶可以通過setLoaderClass()設置類加載器的名稱。WebappLoader創建類加載器的源碼如下所示,我們可以看到類加載器的實例必須是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;
    }

設置倉庫

WebappLoader會在啟動的時候調用類加載器的初始化方法,類加載器在初始化的時候會設置類加載的倉庫地址。默認的倉庫地址為”/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;
    }

設置類路徑

設置類路徑是在初始化的時候調用setClassPath()方法完成的(源碼如下)。setClassPath()方法會在servlet上下文中為Jasper JSP編譯器設置一個字符串類型的屬性來指明類路徑信息。此處不詳細介紹JSP相關內容。

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

設置訪問權限

若是運行Tomcat的時候,使用了安全管理器,則setPermissions()方法會為類載入器設置訪問相關目錄的權限,比如只能訪問WEB-INF/classes和WEB-INF/lib的目錄。若是沒有使用安全管理器,則setPermissions()方法只是簡單地返回,什麼也不做。其源碼如下:


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

開啟新線程執行類的重新載入

WebappLoader類支持自動重載功能。如果WEB-INF/classes目錄或者WEB-INF/lib目錄下的某些類被重新編譯了,那麼這個類會自動重新載入,而無需重啟Tomcat。為了實現此目的,WebappLoader類使用一個線程周期性的檢查每個資源的時間戳。間隔時間由變量checkInterval指定,單位為s,默認情況下,checkInterval的值為15s,每隔15s會檢查依次是否有文件需要自動重新載入。頂層容器在啟動的時候,會啟動定時線程池循環調用backgroundProcess任務。

    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類加載器

Web應用程序中負責載入類的類載入器有兩種:ParallelWebappClassLoader和WebappClassLoaderBase,二者實現大同小異,本節以WebappClassLoader類加載器為例,介紹Tomcat的類加載器。

WebappClassLoader的設計方案考慮了優化和安全兩方面。例如,它會緩存之前已經載入的類來提升性能,還會緩存加載失敗的類的名字,這樣,當再次請求加載同一個類的時候,類加載器就會直接拋出ClassNotFindException異常,而不是再次去查找這個類。WebappClassLoader會在倉庫列表和指定的JAR文件中搜索需要在載入的類。

類緩存

為了達到更好的性能,WebappClassLoader會緩存已經載入的類,這樣下次再使用該類的時候,會直接從緩存中獲取。由WebappClassLoader載入的類都會被視為資源進行緩存,對應的類為「ResourceEntry」類的實例。ResourceEndty保存了其所代表的class文件的位元組流、最後一次修改日期,Manifest信息等。如下為類加載過程中讀取緩存的部分代碼和ResourceEntry的定義源碼。

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

    // 省略部分邏輯
    // (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;
    }
     // 省略部分邏輯
}

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

載入類

載入類的時候,WebappClassLoader要遵循如下規則:

  1. 因為所有已經載入的類都會緩存起來,所以載入類的時候要先檢查本地緩存。
  2. 若本地緩存沒有,則檢查父類加載器的緩存,調用ClassLoader接口的findLoadedClass()方法。
  3. 若兩個緩存總都沒有,則使用系統類加載器進行加載,防止Web應用程序中的類覆蓋J2EE中的類。
  4. 若啟用了SecurityManager,則檢查是否允許載入該類。若該類是禁止載入的類,拋出ClassNotFoundException異常。
  5. 若打開了標誌位delegate,或者待載入的在類不能用web類加載器加載的類,則使用父類加載器來加載器來加載相關類。如果父類加載器為null,則使用系統類加載器。
  6. 從當前倉庫載入類。
  7. 當前倉庫沒有需要載入的類,而且delegate關閉,則是用父類載入器來載入相關的類。
  8. 若沒有找到需要加載的類,則拋出ClassNotFindException。

Tomcat類加載結構

Tomcat容器在啟動的時候會初始化類加載器,Tomcat的類加載器分為四種類型:Common類加載器,Cataline類加載器和Shared類加載器,此外每個應用都會有自己的Webapp類加載器,也就是我們上文介紹的WebappClassLoader,四者之間的關係如下所示。

tomcat-class-loader-2021-10-05-18-38-17

Common類加載器,Cataline類加載器和Shared類加載器會在Tomcat容器啟動的時候就初始化完成,初始化代碼如下所示:

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

而Webapp類加載器則是在Context容器啟動時候有WebappLoader初始化,Webapp類加載器的父類加載器是Tomcat容器在初始化階段通過反射設置的,反射設置父類加載器的源碼如下所示:

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

Tomcat類加載結構的目的

  1. 一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。所以每個應用需要自身的Webapp類加載器。
  2. 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果服務器有10個應用程序,那麼要有10份相同的類庫加載進虛擬機。所以需要Shared類加載器
  3. web容器也有自己依賴的類庫,不能於應用程序的類庫混淆。基於安全考慮,應該讓容器的類庫和程序的類庫隔離開來。所以需要Cataline類加載器。
  4. web容器要支持jsp的修改,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行後修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支持 jsp 修改後不用重啟。

還有最後一個類的共享的問題,如果十個web應用都引入了spring的類,由於web類加載器的隔離,那麼對內存的開銷是很大的。此時我們可以想到shared類加載器,我們肯定都會選擇將spring的jar放於shared目錄底下,但是此時又會存在一個問題,shared類加載器是webapp類加載器的parent,若spring中的getBean方法需要加載web應用底下的類,這種過程是違反雙親委託機制的。

打破雙親委託機制的桎梏:線程上下文類加載器線程上下文類加載器是指的當前線程所用的類加載器,可以通過Thread.currentThread().getContextClassLoader()獲得或者設置。在spring中,他會選擇線程上下文類加載器去加載web應用底下的類,如此就打破了雙親委託機制。

參考文檔列表

我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

本文最先發佈至微信公眾號,版權所有,禁止轉載!

Tags: