設計模式【3.2】-- JDK動態代理原始碼分析有多香?

秦懷雜貨店發表於2021-11-07

前面文章有說到代理模式:http://aphysia.cn/archives/dy...

那麼回顧一下,代理模式怎麼來的?假設有個需求:

在系統中所有的 controller 類呼叫方法之前以及之後,列印一下日誌。

假設原來的程式碼:

public class Person{
    public void method(){
        // 表示自己的業務邏輯
        process();
    }
}

如果在所有的類裡面都新增列印方法,這樣肯定是不現實的,如果我有幾百個這樣的類,寫到崩潰,況且重複程式碼太多,冗餘,還耦合到一塊了,要是我下次不打日誌了,做其他的,那幾百個類又全部改一遍。

public class Person{
    public void method(){
        log();
        // 表示自己的業務邏輯
        process();
        log();
    }
}

靜態代理

怎麼樣寫比較優美呢?靜態代理 這時候出場了,先把方法抽象成為介面:

public class IProxy(){
    public void method();
}

讓具體的類去實現 IProxy,寫自己的業務邏輯,比如:

public class Person implements IProxy(){
    public void method(){
        // 表示自己的業務邏輯
        process();
    }
}

然後弄個代理類,對方法進行增強:

public class PersonProxy implements IProxy{
    private IProxy target;
    public PersonProxy(IProxy target){
        this.target = target;
    }
    @Override
    public void method() {
        log();
        target.method();
        log();
    }
}

呼叫的時候,把真實的物件放到代理類的構造器裡面,就可以得到一個代理類,對它的方法進行增強,好處就是,如果下次我要改,不打日誌,做其他事情,我改代理類就可以了,不用到處改我的目標類的方法,而壞處還是很明顯,要增強哪一個類,就要為它寫一個代理類,這樣好像不是很合理。

動態代理

怎麼樣能讓他自動生成代理物件呢? 動態代理做的就是這個事情,它可以 動態 的根據我們提供的類,生成代理類的物件。

最主要的,是在執行時,動態生成,只要傳入我們要代理增強的類相關的資訊,比如類物件本身,類載入器,類的介面等,就可以生成,不用提前知道它是 A 類,B 類還是 C 類。

動態代理主要有三種實現方法,今天我們重點分析 JDK 動態代理:

  • JDK 代理:使用 JDK 提供的官方的 Proxy
  • 第三方 CGlib 代理:使用 CGLib 的 Enhancer 類建立代理物件
  • javassit:Javassist 是一個開源的分析、編輯和建立 Java 位元組碼的類庫。是由東京工業大學的數學和電腦科學系的 Shigeru Chiba (千葉 滋)所建立的。

JDK 動態代理

使用步驟

  1. 新建一個介面
  2. 新建一個類,實現該介面
  3. 建立代理類,實現 java.lang.reflect.InvocationHandler 介面

程式碼如下:

IPlayDao.java(玩的介面)

public interface IPlayDao {
    void play();
}

StudentDao.java(實現了買東西,玩的介面的學生類)

public class StudentDao implements IPlayDao {
    @Override
    public void play() {
        System.out.println("我是學生,我想出去玩");
    }
}

MyProxy.java 代理類:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MyProxy {
    private Object target;
    public MyProxy(Object target){
        this.target=target;
    }
    public Object getProxyInstance(){
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    // 一個介面可能很多方法,要是需要針對某一個方法,那麼需要在函式裡判斷 method
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("開始事務 2");
                        // 執行目標物件方法
                        Object returnValue = method.invoke(target, args);
                        System.out.println("提交事務 2");
                        return returnValue;
                    }
                }
        );
    }
}

測試類(Test.java)

public class Test {
    public static void main(String [] args){
        // 儲存生成代理類的位元組碼檔案
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        IPlayDao target =new StudentDao();
        System.out.println(target.getClass());
        IPlayDao proxy = (IPlayDao) new MyProxy(target).getProxyInstance();
        System.out.println(proxy.getClass());
        // 執行方法   【代理物件】
        proxy.play();

    }
}

由於加了這句程式碼,我們可以把生成的代理類的位元組碼檔案儲存下來, 其實通過輸出也可以看到,兩個物件不是同一個類,代理類是動態生成的:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");


![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924001916.png)

### 原始碼分析

跟著原始碼一步步看,先從呼叫的地方 `Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)`:

![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924002757.png)

進入方法裡面,**省略各種異常處理**,主要剩下了**生成代理類位元組碼** 以及 **通過建構函式反射構造新物件**:
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    // 判空
    Objects.requireNonNull(h);
    // 安全檢查
    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * 查詢或者生成代理物件
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    if (sm != null) {
        checkNewProxyPermission(Reflection.getCallerClass(), cl);
    }

    // 獲取構造器
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    final InvocationHandler ih = h;
    if (!Modifier.isPublic(cl.getModifiers())) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                cons.setAccessible(true);
                return null;
            }
        });
    }
    // 反射構造代理物件
    return cons.newInstance(new Object[]{h});
}

上面註釋裡面說查詢或者生成代理物件,為什麼有查詢?因為並不是每一次都生成,生成的代理物件實際上會快取起來,如果沒有,才會生成,看原始碼 `Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces)`:
private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length> 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }
    // 呼叫快取代理類的 cache 來獲取類載入器
    return proxyClassCache.get(loader, interfaces);
}

如果由實現給定介面的給定載入器定義的代理類存在,這將簡單地返回快取的副本; 否則,它將通過 ProxyClassFactory 建立代理類,`proxyClassCache` 其實就是個 `weakCache`:

private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());


初始化的時候,proxyClassCache 指定了兩個屬性,一個是 `KeyFactory`, 另外一個是 `ProxyClassFactory`, 從名字就是猜到 `ProxyClassFactory` 是代理類工廠:
public WeakCache(BiFunction<K, P, ?> subKeyFactory,
                 BiFunction<K, P, V> valueFactory) {
    this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
    this.valueFactory = Objects.requireNonNull(valueFactory);
}
**記住這裡的 subKeyFactory,實際上就是傳入的 ProxyClassFactory**,那前面 `proxyClassCache.get(loader, interfaces);` 到底是怎麼操作的?

![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924010200.png)

上面呼叫到了 `subKeyFactory.apply(key, parameter)`,這個 `subKeyFactory` 實際上是我們傳的 `ProxyClassFactory`, 進入裡面去看:
private static final class ProxyClassFactory
    implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
    // 生成的代理類字首
    private static final String proxyClassNamePrefix = "$Proxy";

    // 下一個生成的代理類的名字的計數器,一般是 $Proxy0,$Proxy1
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        for (Class<?> intf : interfaces) {
            /*
             * 檢驗類載入器是否能通過介面名稱載入該類
             */
            Class<?> interfaceClass = null;
            try {
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + "is not visible from class loader");
            }
            /*
             * 判斷介面型別
             */
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + "is not an interface");
            }
            /*
             * 判斷是否重複
             */
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface:" + interfaceClass.getName());
            }
        }
        // 代理包名字
        String proxyPkg = null;
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        /*
         * 記錄非公共代理介面的包,以便在同一個包中定義代理類。確認所有非公共代理介面都在同一個包中。
         */
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {
            // 如果沒有非公共代理介面,請使用 com.sun.proxy 包
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        /*
         * 為要生成的代理類選擇一個名稱。
         */
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        /*
         * 生成指定的代理類。(這裡是重點!!!)
         */
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            /*
             * 這裡的 ClassFormatError 意味著 (禁止代理類生成程式碼中的錯誤) 提供給代理類建立的引數有其他一些無效方面 (比如超出了虛擬機器限制)。
             */
            throw new IllegalArgumentException(e.toString());
        }
    }
}

上面呼叫一個方法生成代理類,我們看看 IDEA 反編譯的程式碼:
public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
    ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
    // 生成檔案
    final byte[] var4 = var3.generateClassFile();
    // 判斷是否要寫入磁碟!!!
    if (saveGeneratedFiles) {
        // 開啟高許可權寫入
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                try {
                    int var1 = var0.lastIndexOf(46);
                    Path var2;
                    if (var1> 0) {
                        Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
                        Files.createDirectories(var3);
                        var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
                    } else {
                        var2 = Paths.get(var0 + ".class");
                    }

                    Files.write(var2, var4, new OpenOption[0]);
                    return null;
                } catch (IOException var4x) {
                    throw new InternalError("I/O exception saving generated file:" + var4x);
                }
            }
        });
    }

    return var4;
}
生成代理檔案實際上和我們想的差不多,就是一些 hashCode(),toString(),equals(), 原方法,代理方法等:

![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924011447.png)

這與我們之前看到的檔案一致:

![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924011628.png)

然後之所以我們程式碼要設定寫入磁碟,是因為這個變數, 控制了寫磁碟操作:
private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));

**為什麼只支援介面實現,不支援繼承普通類?**
因為代理類繼承了 Proxy 類,並且實現了介面,Java 不允許多繼承,所以不能代理普通類的方式,並且在靜態程式碼塊裡面,用反射方式獲取了所有的代理方法。

JDK 代理看起來像是個黑盒,實際上,每一句程式碼,都有其緣由。其實本質上也是動態的為我們的原始類,動態生成代理類。

生成的代理類裡面其實對原始類進行增強(比如 `play()` 方法)的時候, 呼叫了 `super.h.invok()` 方法,其實這裡的 `h` 是什麼呢?

`h` 是父類的 `h`, 生成的代理類的父類是 `Proxy`,Proxy 的 `h`,就是我們傳入的 `InvocationHandler`:
public final void play() throws  {
    try {
        super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }

生成的程式碼裡面通過反射呼叫到的其實是我們自己重寫的那部分邏輯,所以就有了增強的功能,不得不說,這種設計確實巧妙:

![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210925020135.png)

## 動態代理有多香

動態代理是Java語言裡面的一個很強大的特性,可以用來做一些切面,比如攔截器,登入驗證等等,但是它並不是獨立存在的,任何一個知識點都不能獨立說明語言的強大,重要的是它們的組合。

動態代理要實現強大的功能,一般需要和反射,註解等一起合作,比如對某些請求進行攔截,攔截後做一些登入驗證,或者日誌功能。最重要的一點,它能夠在減少耦合度的前提下實現增強。

**【作者簡介】**:  
秦懷,公眾號【**秦懷雜貨店**】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:`Java原始碼解析`,`JDBC`,`Mybatis`,`Spring`,`redis`,`分散式`,`劍指Offer`,`LeetCode`等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。

[劍指Offer全部題解PDF](http://aphysia.cn/archives/jianzhiofferpdf)

[2020年我寫了什麼?](http://aphysia.cn/archives/2020)

[開源程式設計筆記](https://damaer.github.io/Coding/#/)

相關文章