《深入理解Java虛擬機器器》讀書筆記(八)--類載入及執行子系統案例(Tomcat類載入、OSGI、動態代理)

2021-03-18 12:01:24

一、Tomcat類載入器架構

作為一個web伺服器,需要解決以下幾個問題:

  • 部署在同一個伺服器上的web應用程式所使用的Java類庫可以實現相互隔離。
  • 部署在同一個伺服器上的兩個web應用程式所使用的Java類庫可以互相共用。
  • 伺服器需要儘可能保證自身的安全不受部署的web應用程式影響。
  • JSP需要支援熱更新

由於存在上述問題,單獨的一個classpath無法滿足需求,所以web伺服器都提供了好幾個classpath路徑供使用者存放第三方類庫。在Tomcat目錄結構中,有3組目錄(「/common/*」、「/server/*」和「/shared/*」),再加上web應用程式自身的目錄」/WEB-INF/*「。

  • /common目錄:類庫可被Tomcat和所有的Web應用程式共同使用。

  • /server目錄:類庫可被Tomcat使用,對所有的Web應用程式都不可見。

  • /shared目錄:類庫可被所有的web應用程式共同使用,但對Tomcat自己不可見。

  • /webapp/WEB-INF目錄:類庫僅可以被此微web應用程式使用,對Tomcat和其它web應用程式都不可見 。

Tomcat自定義了多個類載入器,這些類載入按照經典的雙親委派模型來實現,其關係如圖所示:

Tomcat 5.x 類載入器結構(網圖)

其中,頂層的啟動類載入器、擴充套件類載入器、應用程式類載入器都是JDK預設提供的類載入器。另外的CommonClassLoader負責載入」/common/*「中的Java類庫,CatalinaClassLoader負責載入」/server/*「中的Java類庫,SharedClassLoader負責載入」/shared/*「中的Java類庫,而WebAppClassLoader負責載入各自的」/WebApp/WEB-INF/*「中的Java類庫,最後每一個jsp檔案都對應一個類載入器。

注:Tomcat的6.x版本後,common、server、shared都預設合併到了lib目錄,這個目錄裡的類庫相當於以前common目錄中類庫起到的作用。如果預設設定不能滿足要求,使用者可以通過修改組態檔指定server.loader和share.loader的方式重新啟用5.x的載入器結構

從上圖可以看出,CommonClassLoader能載入的類都可以被CatalinaClassLoader和SharedClassLoader類載入器載入,而CatalinaClassLoader和SharedClassloader自己能載入的類則與對方相互隔離。WebClassLoader可以使用SharedClassLoader(包括上層類載入器)載入到的類,但是各個WebAppClassLoader範例之間相互隔離。而JasperLoader的載入範圍僅是這個jsp檔案所編譯出來的那一個class,當伺服器檢測到jsp檔案被修改時,會替換掉目前的JasperLoader範例,並通過再建立一個新的jsp類載入來實現jsp的熱更新功能(關於jsp可以參考JSP是如何編譯成servlet並提供服務的)。

關於違反雙親委派

上面提到了,對於web容器來說,都需要解決的一個重要問題:對於不同的webapp,各自目錄下的jar包可能需要相互隔離。一個簡單的例子就是部署在同一個容器下的不同應用程式可能會依賴同一jar包的不同版本,所以必須要相互隔離。要做到隔離也很簡單,每個應用程式都有一個對應自己目錄的類載入器,而這些jar包存放在自己應用的目錄下,由各自的類載入器載入,即可做到相互隔離。但是從這個描述來說,其實並沒有違反雙親委派模型,主要看如何實現。

要達到這個目的,從實現上有兩種方案:

  • 如果某個共同父類別載入器和應用專屬的類載入器都能載入這些jar包,那麼就控制應用各自的類載入器不按照雙親委派的建議首先交給父類別載入器載入,而是直接先由自己載入

  • 遵循雙親委派模型,應用專屬的類載入器還是先委託父類別載入器載入,但是要保證它們所有共同的父類別載入器都不能載入這些jar包,這樣按照雙親委派回溯,還是會由自己載入

二、OSGI

OSGI(Open Service Gateway Initiative)是OSGI聯盟制定的一個基於Java語言的動態模組化規範。OSGI中的每個模組(稱為Bundle)可以宣告它所依賴的Java Package(通過Import-Package描述),也可以宣告它允許匯出釋出的Java-Package(通過Export-Package描述),每個模組從外觀上看來為平級之間的依賴。OSGI其中一個很大的優點就是基於OSGI的程式很可能可以實現模組級的熱插拔功能,這就要歸功於它靈活的類載入器架構。

OSGI的Bundle類載入器之前只有規則,沒有固定的委派關係。比如,某個Bundle宣告了一個它依賴的Package,如果有其他Bundle宣告發布了這個Package,那麼所有對這個Package的類載入動作都會委派給釋出它的Bundle類載入器去完成。不涉及某個具體的Package時,各個Bundle載入器之間都是平級的關係,只有具體使用某個Package和Class時,才會根據Package匯入匯出定義來構造Bundle間的委派和依賴。

假設存在BundleA、BundleB、BundleC三個模組,並且他們定義的依賴關係如下:

  • BundleA:釋出packageA,依賴java.*

  • BundleB:依賴packageA、packageC和java.*

  • BundleC:釋出packageC,依賴packageA

那麼這三個Bundle之間的類載入關係如下圖所示:

OSGI的類載入器架構

 

可以看出OSGI裡的載入器之間的關係不再是雙親委派模型的樹形結構,而是發展成了一種更為複雜的、執行時才能確定的網狀結構。在早期,這種非樹狀結構的類載入器架構出現相互依賴的情況就容易出現死鎖,以OSGI為例,如果BundleA依賴BundleB的packageB,BundleB依賴BundleA的packageA:當BundleA載入PackageB時,要先鎖定當前類載入器的範例物件(ClassLoader.loadClass()是一個synchronized方法),然後將請求委派給BundleB的類載入器處理,但如果這時BundleB也正好想要載入packageA的類,就需要先鎖定自己的類載入器再去請求BundleA的載入器處理,這樣兩個載入器都在等待對方處理自己的請求,但有各自持有己方的鎖,就造成了死鎖狀態。

在JDK1.7中,為非樹狀繼承關係的類載入器架構進行了一次專門的升級,參考:關於類載入的並行控制鎖(ClassLoadingLock)

三、位元組碼生成技術與動態代理

相對於動態代理,靜態代理實現比較簡單,直接讓代理類代替被代理類執行方法即可,當然前提是實現了相同介面或繼承有相同父類別。下面是一個簡單的例子:

public class StaticProxyTest {
    interface Runnable {
        void run();
    }

    class Dog implements Runnable {
        @Override
        public void run() {
            System.out.println("dog run.");
        }
    }

    static class StaticProxy implements Runnable {
        Runnable originObj;

        public Runnable bind(Runnable obj) {
            this.originObj = obj;
            return this;
        }

        @Override
        public void run() {
            System.out.println("before run.");
            originObj.run();
        }
    }

    public static void main(String[] args) {
        new StaticProxy().bind(new StaticProxyTest().new Dog()).run();
    }
}

可以看到,靜態代理實現很簡單,也沒有對被代理物件有入侵,但是也只限於這種簡單場景。靜態代理要求原始類和介面編譯期可知,不具備動態性,而且如果被代理類實現多個介面,也就不那麼容易維護了。Java中的動態代理實現了可以在原始類和介面還未知的時候,就確定代理類的代理行為,代理類與原始類脫離了直接聯絡,可以靈活地重用於不同的應用場景中。對應實現也較為簡單,主要依賴兩個類:Proxy和InvocationHandler,下面是一個簡單的例子:

public class DynamicProxyTest {
    interface Speakable {
        void speak();
    }

    interface Runnable {
        void run();
    }

    class Dog implements Runnable, Speakable {

        @Override
        public void run() {
            System.out.println("dog run.");
        }

        @Override
        public void speak() {
            System.out.println("dog speak.");
        }
    }

    static class DynamicProxy implements InvocationHandler {
        //原始物件
        Object originObj;

        public Object bind(Object obj) {
            this.originObj = obj;
            //生成代理物件
            return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("before method:" + method.getName());
            return method.invoke(originObj);
        }
    }

    public static void main(String[] args) {
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        Object proxy = (new DynamicProxy().bind(new DynamicProxyTest().new Dog()));
        ((Runnable) proxy).run();
        ((Speakable) proxy).speak();
    }
}

上述動態代理的邏輯很清晰,唯一不明瞭的就是Proxy.newProxyInstance方法,這個方法是如何返回代理物件的呢?都知道動態代理的原理是根據我們提供的代理介面和InvocationHandler範例建立了一個新的代理類(位元組碼)。由於原始碼分析不是這系列文章的重點,這裡只貼出部分核心邏輯:

Proxy.newProxyInstance方法最終會通過java.lang.reflect.Proxy類的一個靜態內部類ProxyClassFactory.apply(classLoader,interfaces)方法生成代理類位元組碼並完成位元組碼解析工作(部分程式碼):

private static final class ProxyClassFactory
        implements BiFunction<ClassLoader, Class<?>[], Class<?>>
    {
        // prefix for all proxy class names
        private static final String proxyClassNamePrefix = "$Proxy";

        // next number to use for generation of unique proxy class names
        private static final AtomicLong nextUniqueNumber = new AtomicLong();

        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
            .......
           /*
             * Generate the specified proxy class.
             * 這裡生成代理類位元組碼內容
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                /*
                 * A ClassFormatError here means that (barring bugs in the
                 * proxy class generation code) there was some other
                 * invalid aspect of the arguments supplied to the proxy
                 * class creation (such as virtual machine limitations
                 * exceeded).
                 */
                throw new IllegalArgumentException(e.toString());
            }
            ......
        }
    }

其間會呼叫ProxyGenerator.generateProxyClass生成代理類。在main方法中加入語句:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true")是為了輸出自動生成的代理類。執行之後發現目錄多了一個$Proxy0類:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.demo.aop;

import com.demo.aop.DynamicProxyTest.Runnable;
import com.demo.aop.DynamicProxyTest.Speakable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

final class $Proxy0 extends Proxy implements Runnable, Speakable {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m4;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void run() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void speak() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.demo.aop.DynamicProxyTest$Runnable").getMethod("run");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("com.demo.aop.DynamicProxyTest$Speakable").getMethod("speak");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

可以看到,自動生成的代理類預設繼承了java.lang.reflect.Proxy,實現了被代理類實現的介面,呼叫invoke方法最終呼叫我們自己實現的invoke方法。由於Java是單繼承,所以Java提供的動態代理要求被代理類必須實現介面,所以在spring的aop中才提供了InvocationHandler和cglib兩種代理方式。