Tomcat類載入機制探析

k不是你的帝發表於2020-12-30

title: Tomcat類載入機制探析
comments: false
toc: true
categories:

  • Web伺服器
    tags:
  • Tomcat
  • 類載入
  • 雙親委派
    date: 2020-12-23 23:03:58

Java類載入機制,雙親委託模型相必大家已經熟的不能再熟了。本文就從Tomcat的原始碼級別上來探究一下類載入機制的祕密。

首先我們們還是老調重彈,看一下網上已經氾濫的一張Tomcat類載入關係圖 Tomcat類載入器關係圖

屬於JavaSE的BootstrapClassLoader、ExtClassLoader、AppClassLoader(這裡主要載入Tomcat啟動的Bootstrap類)在本文不再贅述;

  • CommonClassLoader載入common.loader屬性下的jar;一般是CATALINA_HOME/lib目錄下

  • CatalinaClassLoader載入server.loader屬性下的jar;預設未配置路徑,返回其父載入器即CommonClassLoader

  • SharedClassloader載入share.loader屬性下的jar;預設未配置路徑,返回其父載入器即CommonClassLoader

    由於WebAppClassLoader需要等Tomcat的各個元件初始化完成之後才載入對應的server.xml配置檔案,解析對應Host下的docBase目錄尋找WEB-INF下的類檔案,並且會將該根目錄下的每個直接子目錄當作一個web專案載入,為了確保各個專案之間的相互獨立,每個專案都是單獨的WebAppClassLoader載入的,我們們後文再講。

CommonClassLoader、CatalinaClassLoader、SharedClassloader

先從原始碼角度來看看CommonClassLoader、CatalinaClassLoader、SharedClassloader這三者是如何繫結關係的?

org.apache.catalina.startup.Bootstrap#initClassLoaders

// 初始化類載入器
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);
    }
}

呼叫了createClassLoader方法來建立物件,第二個引數是指定它的父級類載入器。可以看到catalinaLoader、sharedLoader均指明commonLoader為它的父級類載入器,這說明catalinaLoader、sharedLoader是同級類載入器,印證了上圖。

再看看createClassLoader方法怎麼做的:

// 建立基礎型別類載入器
private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {

    // 根據“name+.loader”從系統配置中讀取需要載入的jar路徑
    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
        }

        // 封裝路徑,指定其jar的型別:jar包、目錄等
        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));
        }
    }
    // 通過類載入工廠建立,repositories的值為當前Tomcat所在目錄下配置的jar子目錄,比如 Tomcat_Home/lib/
    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

ClassLoaderFactory.createClassLoader是一個典型的工廠模式,遮蔽了類載入器物件初始化的細節:

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

    if (repositories != null) {
        // 根據jar包型別解析其Url,並新增到Set<URL> set
        for (Repository repository : repositories)  {
            if (repository.getType() == RepositoryType.URL) {
                URL url = buildClassLoaderUrl(repository.getLocation());
                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);
               
                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);
                
                set.add(url);
            } else if (repository.getType() == RepositoryType.GLOB) {
                File directory=new File(repository.getLocation());
                directory = directory.getCanonicalFile();
                if (!validateFile(directory, RepositoryType.GLOB)) {
                    continue;
                }
               
                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()]);

    // 直接通過URLClassLoader建立ClassLoader
    return AccessController.doPrivileged(
            new PrivilegedAction<URLClassLoader>() {
                @Override
                public URLClassLoader run() {
                    if (parent == null)
                        return new URLClassLoader(array);
                    else
                        return new URLClassLoader(array, parent);
                }
            });
}

可以看到通過ClassLoaderFactory.createClassLoader(List repositories, final ClassLoader parent)此方法建立的ClassLoader其本身是一個URLClassLoader,通過人為手工的方式分別指定了其父級類載入。

上文提到catalinaLoader會載入Tomcat本身的類,其主要目的是進行類載入隔離,與SharedClassloader區分開,這樣我們們開發人員編寫的web專案就不能直接訪問到Tomcat的類,造成安全問題了。 那麼又是怎麼實現的呢?我們們接著看Tomcat的啟動前的初始方法org.apache.catalina.startup.Bootstrap#init()

public void init() throws Exception {
	// 初始化commonLoader、catalinaLoader、sharedLoader3個類載入器
    initClassLoaders();
	// 直接設定當前執行緒的類載入器為catalinaLoader
    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // 使用當前執行緒的catalinaLoader來載入Catalina類
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // 以下整段表示:Catalina初始化後得到的物件startupInstance呼叫setParentClassLoader方法,將sharedLoader設定為父載入器
    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;
}

從原始碼中可以看到步驟:

  • 用catalinaLoader載入了"org.apache.catalina.startup.Catalina"類;
  • 立即通過setParentClassLoader方法指定其父載入器為sharedLoader,這樣Catalina物件也可訪問sharedLoader下的類了;

如何為每一個webApp專案定義一個獨立的WebAppClassLoader?

我們們從原始碼角度來分析一下。

Tomcat的各容器呼叫流程不是本文關注的重點,直接檢視webApp的類載入器初始化的程式碼位置org.apache.catalina.loader.WebappLoader#startInternal()

// 每一個專案context均會執行此方法
protected void startInternal() throws LifecycleException {

    // 省略部分程式碼

    // Construct a class loader based on our current repositories list
    try {
		// 建立WebApp的類載入器
        classLoader = createClassLoader();
        // 獲取一個webapp專案的類載入路徑:WEB-INF/classes、WEB-INF/lib/
        classLoader.setResources(context.getResources());
        // delegate是否遵循類載入的雙親委派模型,預設為false
        classLoader.setDelegate(this.delegate);

		// 執行類載入
        ((Lifecycle) classLoader).start();

        // 省略

    } catch (Throwable t) {
        // 省略
    }

    // 省略
}


private String loaderClass = ParallelWebappClassLoader.class.getName();
/**
  * Create associated classLoader.
  */
private WebappClassLoaderBase createClassLoader()
    throws Exception {

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

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

    return classLoader;
}

當每一個HOST執行org.apache.catalina.startup.HostConfig#deployApps()方法釋出所有的專案時,其工作目錄下的每個webapp專案均會執行org.apache.catalina.loader.WebappLoader#startInternal()方法,並在其中使用createClassLoader()建立類載入器。可以從createClassLoader()方法看到 WebappClassLoaderBase的真正實現類為ParallelWebappClassLoader。

ParallelWebappClassLoader類繼承於WebappClassLoaderBase,類結構如下:

ParallelWebappClassLoader

WebappClassLoaderBase繼承於URLClassLoader,由URLClassLoader可指定載入位置的Path來進行類載入。

還記得上面的 this.delegate 欄位嗎?預設為false,表示不使用雙親委派機制,我們再看看是如何破壞雙親委派機制的。方法位置:org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean)

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

    synchronized (getClassLoadingLock(name)) {
        
        Class<?> clazz = null;

        // 省略一些檢查程式碼

        // (0) 從本地位元組碼快取中查詢
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        // (0.1) 從已經載入過的類中查詢
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        // (0.2) 嘗試使用系統類載入器載入該類,以防止webapp覆蓋Java SE類。This implements SRV.10.7.2
        String resourceName = binaryNameToPath(name, false);

        ClassLoader javaseLoader = getJavaseClassLoader();
        boolean tryLoadingFromJavaseLoader;
        try {
            //使用getResource,因為如果Java SE類載入器無法提供該資源,它將不會觸發昂貴的ClassNotFoundException。
            //但是,在極少數情況下在安全管理器下執行時(有關詳細資訊,請參見https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 //),此呼叫可能觸發ClassCircularityError。
            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;
        }
		// 嘗試通過JavaSE類載入器載入
        if (tryLoadingFromJavaseLoader) {
            try {
                clazz = javaseLoader.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }

        // (0.5) 使用SecurityManager時訪問此類的許可權
        if (securityManager != null) {
            int i = name.lastIndexOf('.');
            if (i >= 0) {
                try {
                    securityManager.checkPackageAccess(name.substring(0,i));
                } catch (SecurityException se) {
                    String error = "Security Violation, attempt to use " +
                        "Restricted Class: " + name;
                    log.info(error, se);
                    throw new ClassNotFoundException(error, se);
                }
            }
        }
		
        //預設為不使用雙親委派,呼叫filter進行過濾是否可以委派載入此方法,如果是JSP,EL表示式,Tomcat等一些類則可以委派
        boolean delegateLoad = delegate || filter(name, true);

        // (1) 如果需要,委託給我們的父載入器
        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
            }
        }

        // (2) 從本地搜尋查詢類即自我載入
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // (3) 實在找不到,無條件委託給父載入器
        if (!delegateLoad) {
            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
            }
        }
    }
	// 所有方法均載入失敗,返回ClassNotFoundException
    throw new ClassNotFoundException(name);
}

從原始碼中,可以提取為以下步驟:

  • (0) 從本地位元組碼快取中查詢
  • (0.1) 從已經載入過的類中查詢
  • (0.2) 嘗試使用系統類載入器載入該類,以防止webapp覆蓋Java SE類。
  • (0.3) 系統類載入器不適合載入此類
  • (0.4) 嘗試通過JavaSE類載入器載入
  • (0.5) 使用SecurityManager時訪問此類的許可權
  • (1) 判斷是否雙親委派(一般為false),如果需要,委託給我們的父載入器
  • (2) 使用自己載入器進行類載入,注意此步驟如果執行則破壞了雙親委派原則,因為沒有讓父類載入器進行載入。
  • (3) 實在找不到,無條件委託給父載入器
  • 所有方法均載入失敗,返回ClassNotFoundException

總結

Java的雙親委派機制無疑是一個非常優秀的設計,它有以下優點:

  1. 採用雙親委派模式的是好處是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,通過這種層級關可以避免類的重複載入,當父親已經載入了該類時,就沒有必要子ClassLoader再載入一次。
  2. 增加系統安全,java核心API中定義型別不會被隨意替換。

但是在Tomcat這種需要多專案部署時,其反而可能會有一些弊端,比如剛好部署了A,B兩個位元組碼完全相同的系統,如果是傳統的雙親委派機制,則後載入的系統的類不會載入成功,如果有類靜態變數則會被2個系統共享,引起系統異常。

相關文章