1. 程式人生 > >違反ClassLoader雙親委派機制三部曲第二部——Tomcat類載入機制

違反ClassLoader雙親委派機制三部曲第二部——Tomcat類載入機制

前言:
本文是基於 ClassLoader雙親委派機制原始碼分析 瞭解過正統JDK類載入機制及其實現原理的基礎上,進而分析這種思想如何應用到Tomcat這個web容器中,從原始碼的角度對 違反ClassLoader雙親委派機制三部曲之首部——JDBC驅動載入 中提出的Tomcat是如何完成多個web應用之間相互隔離,又如何保證多個web應用都能載入到基礎類庫的問題進行了解答,我們按如下的思路佈局整篇文章:

  • 先給出Tomcat整體的類載入體系結構
  • 通過檢視原始碼驗證該類載入體系的正確性
  • 總結Tomcat如何設計保證多應用隔離
    另外本文是基於Tomcat7的原始碼進行分析的,因此讀者最好先搭建一套基於Tomcat7的環境,以便查閱原始碼以及執行除錯,可以按照該文章的方式進行搭建:
    Tomcat原始碼匯入Idea

Tomcat類載入體系結構

圖1. Tomcat整體類載入體系結構

Tomcat本身也是一個java專案,因此其也需要被JDK的類載入機制載入,也就必然存在引導類載入器、擴充套件類載入器和應用(系統)類載入器。Tomcat自身定義的類載入器主要由圖中下半部分組成,Common ClassLoader作為Catalina ClassLoaderShared ClassLoader的parent,而Shared ClassLoader又可能存在多個children類載入器WebApp ClassLoader,一個WebApp ClassLoader實際上就對應一個Web應用,那Web應用就有可能存在Jsp頁面,這些Jsp頁面最終會轉成class類被載入,因此也需要一個Jsp的類載入器,就是圖中的JasperLoder
需要注意的是,在程式碼層面Catalina ClassLoader

Shared ClassLoaderCommon ClassLoader對應的實體類實際上都是URLClassLoader或者SecureClassLoader,一般我們只是根據載入內容的不同和載入父子順序的關係,在邏輯上劃分為這三個類載入器;而WebApp ClassLoaderJasperLoader都是存在對應的類載入器類的
下面我們從原始碼設計的角度驗證圖中類載入器的設計

原始碼分析Tomcat類載入機制

Tomcat的啟動入口在Bootstrap.class

圖2. Tomcat啟動入口

其中初始化類載入器的流程在bootstrap.init();中,如下“程式碼清單1


public void init()
        throws Exception
    {

        // Set Catalina path
        setCatalinaHome();
        setCatalinaBase();

        // (1)   初始化 classLoader
        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");

        //載入 org.apache.catalina.startup.Catalina class
        Class<?> startupClass =
            catalinaLoader.loadClass
            ("org.apache.catalina.startup.Catalina");

        // (2)  例項化 Catalina 例項
        Object startupInstance = startupClass.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;

    }

(1)處註釋的程式碼主要進行類載入的初始化以及形成類載入器的關係初始化,繼續跟進

圖3. initClassLoaders()方法

這裡紅線處的程式碼實際上建立了三個ClassLoader物件,其名稱和Tomcat類載入關係圖中的類載入器高度一致,那麼我們猜測createClassLoader(String,ClassLoader)方法可能就是建立Tomcat自定義類載入器的方法之一,繼續往下看 “ 程式碼清單2


private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {
        // (1) 根據名稱查詢特定的配置
        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;

        value = replace(value);

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

        StringTokenizer tokenizer = new StringTokenizer(value, ",");
        while (tokenizer.hasMoreElements()) {
            String repository = tokenizer.nextToken().trim();
            if (repository.length() == 0) {
                continue;
            }

            // 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));
            }
        }
        // (2) 類載入器工廠建立特定類載入器
        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }

程式碼清單中(1)處註釋是根據上圖中傳遞的“名稱”加上字尾.loader去某個配置檔案載入檔案,為了突出重點,這裡直接給出結論,其載入的內容為/org/apache/catalina/startup/catalina.properties,比如要載入 common.loader對應的value,其在檔案中的值為${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar,也就是說Common ClassLoader要載入的路徑是這些,是Tomcat執行要使用的公共元件,比如servlet-api.jarcatalina.jar等;而我們發現當要載入server.loadershared.loader時,其key在配置檔案中的value為空,也就是說,預設情況下Catalina ClassLoader和Shared ClassLoader(Tomcat整體類載入體系結構圖中紅色虛線內)都不存在,只有Common ClassLoader
方法中的第二個引數表示建立類載入器的父類載入器是哪個,再看initClassLoaders()方法圖中程式碼,在建立catalinaLoadersharedLoader時,父類載入器傳入的實際上就是commonLoader,以此可以驗證圖1中Catalina ClassLoaderShared ClassLoaderCommon ClassLoader的父子關係。而common ClassLoader的父類載入器引數傳遞的為null,為什麼null就會導致該類載入器的父類載入器為System ClassLoader呢?我們需要進入程式碼清單2中看註釋(2)處標識的程式碼 程式碼清單3


    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<URL>();
        // 載入指定路徑下的資源物件
        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]);
            }
        //  返回建立的類載入器
        return AccessController.doPrivileged(
                new PrivilegedAction<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null)
                            return new URLClassLoader(array);
                        else
                            return new URLClassLoader(array, parent);
                    }
                });
    }

大塊的if中的程式碼實際上是對資源進行轉化載入的過程,而return部分才是返回類載入器的部分,程式碼根據是否有parent呼叫了URLClassLoader不同的構造器,Common ClassLoader呼叫的是沒有parent的構造器

圖4. Common ClassLoader的parent建立的底層關鍵程式碼

按紅線所畫Common ClassLoader的parent實際上是JDK中sun.misc.Launcher.class類的loader成員變數,而在上一篇文章中已經知道該loader的值就是應用類載入器(系統類載入器)System ClassLoader。至此Tomcat中類載入機制和JDK的類載入機制也建立上了聯絡
現在Tomcat的類載入機制已完成了一大半,剩下用於載入每個web應用的類載入器WebApp ClassLoader的分析,這個時候需要重新回到程式碼清單1中看註釋(2)以下的部分,其主要做的事情是通過反射建立了org.apache.catalina.startup.Catalina類的例項,然後呼叫了簽名為void setParentClassLoader(ClassLoader parentClassLoader)的方法,並傳入了Shared ClassLoader,上面我們說過預設情況下Shared ClassLoader就是Common ClassLoader,因此其傳入的引數實際上是Common ClassLoader
我們思考既然有儲存parent的方法,必定使用時會呼叫獲得parent方法,那麼我們需要檢視Catalina類中ClassLoader getParentClassLoader()方法的呼叫棧(層級關係比較複雜,要緊跟主線不要迷失),最終定位到StandardContext中的synchronized void startInternal() throws LifecycleException方法(其中涉及到Tomcat的各種元件關係,生命週期管理等內容,將在下次分析Tomcat元件文章中詳細介紹),下面是隻保留核心邏輯的startInternal()方法 程式碼清單4


    protected synchronized void startInternal() throws LifecycleException {
        // 其他邏輯略......

        // Add missing components as necessary
        if (webappResources == null) {   // (1) Required by Loader
            if (log.isDebugEnabled())
                log.debug("Configuring default Resources");
            try {
                String docBase = getDocBase();
                if (docBase == null) {
                    setResources(new EmptyDirContext());
                } else if (docBase.endsWith(".war")
                        && !(new File(getBasePath())).isDirectory()) {
                    setResources(new WARDirContext());
                } else {
                    setResources(new FileDirContext());
                }
            } catch (IllegalArgumentException e) {
                log.error(sm.getString("standardContext.resourcesInit"), e);
                ok = false;
            }
        }
        if (ok) {
            if (!resourcesStart()) {
                throw new LifecycleException("Error in resourceStart()");
            }
        }

        // (1)  為每一個web應用建立一個WebappLoader
        if (getLoader() == null) {
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }

      // 其他邏輯略......

        try {

            if (ok) {
                // (2)  呼叫WebappLoader的start
                // Start our subordinate components, if any
                if ((loader != null) && (loader instanceof Lifecycle))
                    ((Lifecycle) loader).start();
                }

        // 其他邏輯省略......

        } finally {
            // Unbinding thread
            unbindThread(oldCCL);
        }
    }

(1)處註釋下的程式碼邏輯就是為每一個web應用建立一個類載入器,該類載入器的父類載入器就是通過getParentClassLoader()得到的Shared ClassLoader(Common ClassLoader),(2)處程式碼呼叫了WebappLoaderstart方法,繼續跟進


    protected void startInternal() throws LifecycleException {
        // 其他邏輯省略.....
        try {
            //建立類載入器關鍵方法
            classLoader = createClassLoader();
            classLoader.setResources(container.getResources());
            classLoader.setDelegate(this.delegate);
            classLoader.setSearchExternalFirst(searchExternalFirst);
            if (container instanceof StandardContext) {
                classLoader.setAntiJARLocking(
                        ((StandardContext) container).getAntiJARLocking());
                classLoader.setClearReferencesRmiTargets(
                        ((StandardContext) container).getClearReferencesRmiTargets());
                classLoader.setClearReferencesStatic(
                        ((StandardContext) container).getClearReferencesStatic());
                classLoader.setClearReferencesStopThreads(
                        ((StandardContext) container).getClearReferencesStopThreads());
                classLoader.setClearReferencesStopTimerThreads(
                        ((StandardContext) container).getClearReferencesStopTimerThreads());
                classLoader.setClearReferencesHttpClientKeepAliveThread(
                        ((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
            }

        // 其他邏輯省略.....
    }

由於Tomcat的設計,WebappLoaderstart方法實際上呼叫的是父類的模板,而模板中的startinternal方法由各個子類具體實現,其中最關鍵的方法為createClassLoader()

圖5. WebappLoader中createClassLoader方法

上圖中的loadClass成員變數的值為org.apache.catalina.loader.WebappClassLoader,所以,實際上該類為每一個web應用建立了一個WebappClassLoader的例項,該例項的parent就是Shared ClassLoader或者Common ClassLoader,至此WebApp ClassLoader在圖1中的位置也得以驗證。
從理論上分析來看,由於類載入的“雙親委派”機制,一個類載入器只能載入本載入器指定的目錄以及使用有“繼承”關係的父類載入器載入過的類,而Tomcat為每一個Web應用建立了一個WebappClassLoader,不同的WebappClassLoader是同級關係,不會存在交叉訪問的問題,從而達到web應用相互隔離的目的。
那Tomcat是否沒有"破壞"雙親委派機制呢?我們通過檢視WebappClassLoader及其父類WebappClassLoaderBaseloadClass()findClass()分析一下Tomcat載入web應用相關類的策略


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

        synchronized (getClassLoadingLockInternal(name)) {
            if (log.isDebugEnabled())
                log.debug("loadClass(" + name + ", " + resolve + ")");
            Class<?> clazz = null;

            // Log access to stopped classloader
            if (!started) {
                try {
                    throw new IllegalStateException();
                } catch (IllegalStateException e) {
                    log.info(sm.getString("webappClassLoader.stopped", name), e);
                }
            }
            //                (1)          
            // 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);
            }

            //              (2)
            //  Check our previously loaded class cache
            clazz = findLoadedClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }

            //                (3)
            //  Try loading the class with the system class loader, to prevent
            //       the webapp from overriding J2SE classes
            try {
                clazz = j2seClassLoader.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            //  Permission to access this class when using a 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;
                        if (name.endsWith("BeanInfo")) {
                            // BZ 57906: suppress logging for calls from
                            // java.beans.Introspector.findExplicitBeanInfo()
                            log.debug(error, se);
                        } else {
                            log.info(error, se);
                        }
                        throw new ClassNotFoundException(error, se);
                    }
                }
            }

            //              (4)
            boolean delegateLoad = delegate || filter(name);

            //              (5)
            //  Delegate to our parent if requested
            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
                }
            }

            //            (6)
            //  Search local repositories
            if (log.isDebugEnabled())
                log.debug("  Searching local repositories");
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from local repository");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            // Delegate to parent unconditionally
            if (!delegateLoad) {
                if (log.isDebugEnabled())
                    log.debug("  Delegating to parent classloader at end: " + 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
                }
            }
        }

        throw new ClassNotFoundException(name);
    }

我們首先定位到WebappClassLoaderBaseloadClass方法,(1)處首先看name對應的類是否存在快取中,快取是一個ConcurrentHashMap<String, ResourceEntry>的集合,如果沒有快取執行(2)處邏輯,從JVM中查詢是否曾今載入過該類,(3)中的程式碼保證自定義類不會覆蓋java基礎類庫中的類,(4)的邏輯就是是否進行雙親委派的分叉口,其中delegate預設為false,那麼就要看filter(String)方法,該方法的內部實際上將待載入類的全路徑名稱和一個成員變數protected static final String[] packageTriggers中的類名進行比較,如果待載入的類名和packageTriggers陣列中的內容字首匹配,則需要委派父類載入,即執行(5)處程式碼,否則執行(6),呼叫重寫的findClass(String)方法載入該類


public Class<?> findClass(String name) throws ClassNotFoundException {
        // 其他程式碼略去.....
       
        // Ask our superclass to locate this class, if possible
        // (throws ClassNotFoundException if it is not found)
        Class<?> clazz = null;
        try {
            if (log.isTraceEnabled())
                log.trace("      findClassInternal(" + name + ")");
            //        (1)
            if (hasExternalRepositories && searchExternalFirst) {
                try {
                    clazz = super.findClass(name);
                } catch(ClassNotFoundException cnfe) {
                    // Ignore - will search internal repositories next
                } catch(AccessControlException ace) {
                    log.warn("WebappClassLoaderBase.findClassInternal(" + name
                            + ") security exception: " + ace.getMessage(), ace);
                    throw new ClassNotFoundException(name, ace);
                } catch (RuntimeException e) {
                    if (log.isTraceEnabled())
                        log.trace("      -->RuntimeException Rethrown", e);
                    throw e;
                }
            }
            //            (2)
            if ((clazz == null)) {
                try {
                    clazz = findClassInternal(name);
                } catch(ClassNotFoundException cnfe) {
                    if (!hasExternalRepositories || searchExternalFirst) {
                        throw cnfe;
                    }
                } catch(AccessControlException ace) {
                    log.warn("WebappClassLoaderBase.findClassInternal(" + name
                            + ") security exception: " + ace.getMessage(), ace);
                    throw new ClassNotFoundException(name, ace);
                } catch (RuntimeException e) {
                    if (log.isTraceEnabled())
                        log.trace("      -->RuntimeException Rethrown", e);
                    throw e;
                }
            }
            
      //其他程式碼略去........
        return (clazz);

    }

(1)處由於hasExternalRepositoriessearchExternalFirst預設為false,因此執行(2)處邏輯,呼叫findClassInternal(String)方法

圖6. WebappClassLoader類的findClassInternal方法

其主要的思想是根據待載入類的全路徑讀取該類的二進位制資料,進而進行類的預定義、class source的解析等,將該類載入到JVM中
綜上所述,我認為Tomcat的類載入機制不能算完全“正統”的雙親委派,WebappClassLoader內部重寫了loadClassfindClass方法,實現了繞過“雙親委派”直接載入web應用內部的資源,當然可以通過在Context.xml檔案中加上<Loader delegate = "true">開啟正統的“雙親委派”載入機制