雙親委派模型與Tomcat類載入架構

Setanta發表於2019-03-22

雙親委派模型

類載入器的劃分

  • 啟動類載入器(Bootstrap ClassLoader)
  • 擴充套件類載入器(Extension ClassLoader)
  • 應用程式類載入器(Application Classloader)

啟動類載入器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的類庫載入到虛擬機器記憶體中。啟動類載入器無法被Java程式直接引用。
擴充套件類載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。
應用程式類載入器由sun.misc.Launcher$App-ClassLoader實現。應用程式類載入器也稱之為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫。應用程式中如果沒有自定義類載入器,那一般情況下應用程式類載入器就是程式中預設的類載入器。

雙親委派模型的工作過程

雙親委派模型與Tomcat類載入架構
雙親委派模型要求除了頂層的啟動類載入器外,其餘的父類載入器都應當有自己的父類載入器。這裡的父類載入器之前的父子關係一般不會以繼承的關係來實現,而都使用組合關係來複用父載入器的程式碼。
如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。
雙親委派模型的程式碼都集中在java.lang.ClassLoader的loadClass()方法中:先檢查是否已經被載入過,若沒有載入則呼叫父載入器的loadClass()方法,若父載入器為空則預設使用啟動類載入器作為父載入器。如果父載入器載入失敗,丟擲ClassNotFoundException異常後,再呼叫自己的findClass()方法進行載入。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
複製程式碼

雙親委派模型的作用

Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式在程式的各種類載入器環境中都是同一個類。

破壞雙親委派模型

Java有許多核心庫提供的SPI介面,允許第三方為這些介面提供實現,常見的有JDBC、JNDI等。問題是SPI介面是核心庫的一部分,是由啟動類載入器來載入的,而SPI介面的實現類是由應用程式類載入器來載入的。根據雙親委派模型,Bootstrap ClassLoader無法委派Application Classloader來載入類(有關Java的SPI可以參考Dubbo SPI中2.1節的Java SPI例子)。為了解決這個問題,我們引入一個執行緒上下文類載入器(Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過的話,那這個類載入器預設就是應用程式類載入器。
我們來看下JDBC的驅動管理類DriverManager的程式碼:
初始化jdbc驅動類

/**
 * Load the initial JDBC drivers by checking the System property
 * jdbc.properties and then use the {@code ServiceLoader} mechanism
 */
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
複製程式碼
private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            /* Load these drivers, so that they can be instantiated.
             * It may be the case that the driver class may not be there
             * i.e. there may be a packaged driver with the service class
             * as implementation of java.sql.Driver but the actual class
             * may be missing. In that case a java.util.ServiceConfigurationError
             * will be thrown at runtime by the VM trying to locate
             * and load the service.
             *
             * Adding a try catch block to catch those runtime errors
             * if driver not available in classpath but it's
             * packaged as service and that service is there in classpath.
             */
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}
複製程式碼

注意上面的ServiceLoader的load方法,這裡就用到了執行緒上下文類載入器。

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
複製程式碼

Tomcat的類載入架構

Tomcat作為一個web容器需要解決下面幾個問題:

  1. 部署在同一伺服器上的兩個Web應用程式所使用的Java類庫可以實現相互隔離,因為兩個不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求一個類庫在一個伺服器中只有一份,伺服器應當保證兩個應用程式的類庫可以互相獨立使用。
  2. 部署在同一服務上的兩個Web應用程式所使用的Java類庫可以互相共享。
  3. 伺服器需要儘可能地保證自身的安全不受部署的Web應用程式影響。一般來說,伺服器所使用的類庫應該與應用程式的類庫互相獨立。
  4. 支援JSP應用的Web伺服器,大多數需要支援Hotswap功能。

Tomcat類載入架構如下圖:

雙親委派模型與Tomcat類載入架構
CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader分別載入/common/*、/server/*、/shared/*(Tomcat6已經合併到根目錄下的lib目錄下)和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類載入器和JSP類載入器通常會存在多個例項,每個Web應用程式對應一個WebApp類載入器,每一個JSP檔案對應一個JSP類載入器。

  1. CommonClassLoader載入路徑中的class可以被Tomcat和所有的Web應用程式共同使用
  2. CatalinaClassLoader載入路徑中的class可被Tomcat使用,對所有的Web應用程式都不可見
  3. SharedClassLoader載入路徑中的Class可被所有的Web應用程式共同使用,但對Tomcat自己不可見
  4. WebappClassLoader載入路徑中的Class只對當前webapp可見,對Tomcat和其他Web應用程式不可見

從委派關係可以看出:

  1. 各個WebappClassLoader例項之間相互隔離
  2. CommonClassLoader和SharedClassLoader實現了公有類庫的共用
  3. CatalinaClassLoader和SharedClassLoader自己能載入的類與對方相互隔離
  4. JasperLoader的載入範圍僅僅是這個JSP檔案所編譯出來的那一個Class

先從Tomcat的啟動方法bootstrap.main方法入手

/**
 * Main method and entry point when starting Tomcat via the provided
 * scripts.
 *
 * @param args Command line arguments to be processed
 */
public static void main(String args[]) {

    synchronized (daemonLock) {
        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 = 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);
        }
    }

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

}
複製程式碼

看下init方法,init方法中執行Thread.currentThread().setContextClassLoader方法,將當前執行緒上下文類載入器設為catalinaLoader。

/**
 * Initialize daemon.
 * @throws Exception Fatal initialization error
 */
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;

}
複製程式碼

看下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);
    }
}
複製程式碼

可以看到commonLoader、catalinaLoader、sharedLoader都是在initClassLoaders方法中初始化的。對於Tomcat6.x版本,只有指定了tomcat/conf/catalina.properties配置檔案的server.loader和shared.loader項後才會真正建立CatalinaClassLoader和SharedClassLoader的例項,否則會用到這兩個類載入器的地方都會用CommonClassLoader的例項代替,而預設的配置檔案沒有設定這兩個loader項,所以Tomcat6.x把/common、/server和/shared三個目錄預設合併到一起變成一個/lib目錄,這個目錄裡的類庫相當於以前/common目錄中類庫的作用。

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
複製程式碼
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);
}
複製程式碼

相關文章