動態代理竟然如此簡單!

程式設計師cxuan發表於2020-12-29

這篇文章我們來聊一下 Java 中的動態代理。

動態代理在 Java 中有著廣泛的應用,比如 AOP 的實現原理、RPC遠端呼叫、Java 註解物件獲取、日誌框架、全域性性異常處理、事務處理等

在瞭解動態代理前,我們需要先了解一下什麼是代理模式。

代理模式

代理模式(Proxy Pattern)是 23 種設計模式的一種,屬於結構型模式。他指的是一個物件本身不做實際的操作,而是通過其他物件來得到自己想要的結果。這樣做的好處是可以在目標物件實現的基礎上,增強額外的功能操作,即擴充套件目標物件的功能

這裡能體現出一個非常重要的程式設計思想:不要隨意去改原始碼,如果需要修改,可以通過代理的方式來擴充套件該方法。

如上圖所示,使用者不能直接使用目標物件,而是構造出一個代理物件,由代理物件作為中轉,代理物件負責呼叫目標物件真正的行為,從而把結果返回給使用者。

也就是說,代理的關鍵點就是代理物件和目標物件的關係

代理其實就和經紀人一樣,比如你是一個明星,有很多粉絲。你的流量很多,經常會有很多金主來找你洽談合作等,你自己肯定忙不過來,因為你要處理的不只是談合作這件事情,你還要懂才藝、拍戲、維護和粉絲的關係、營銷等。為此,你找了一個經紀人,你讓他負責和金主談合作這件事,經紀人做事很認真負責,它圓滿的完成了任務,於是,金主找你談合作就變成了金主和你的經紀人談合作,你就有更多的時間來忙其他事情了。如下圖所示

這是一種靜態代理,因為這個代理(經紀人)是你自己親自挑選的。

但是後來隨著你的業務逐漸擴充,你無法選擇每個經紀人,所以你索性交給了代理公司來幫你做。如果你想在 B 站火一把,那就直接讓代理公司幫你找到負責營銷方面的代理人,如果你想維護和粉絲的關係,那你直接讓代理公司給你找一些托兒就可以了,那麼此時的關係圖會變為如下

此時你幾乎所有的工作都是由代理公司來進行打理,而他們派出誰來幫你做這些事情你就不得而知了,這得根據實際情況來定,因為代理公司也不只是負責你一個明星,而且每個人所擅長的領域也不同,所以你只有等到有實際需求後,才會給你指定對應的代理人,這種情況就叫做動態代理

靜態代理

從編譯期是否能確定最終的執行方法可以把代理模式分為靜態代理和動態代理,我們先演示一下動態代理,這裡有一個需求,領導想在系統中新增一個使用者,但是他不自己新增,他讓下面的程式設計師來新增,我們看一下這個過程。

首先構建一個使用者介面,定義一個儲存使用者的模版方法。

public interface UserDao {

    void saveUser();
}

構建一個使用者實現類,這個使用者實現類是真正進行使用者操作的方法

public class UserDaoImpl implements UserDao{

    @Override
    public void saveUser() {
        System.out.println(" ---- 儲存使用者 ---- ");
    }
}

構建一個使用者代理類,使用者代理類也有一個儲存使用者的方法,不過這個方法屬於代理方法,它不會執行真正的儲存使用者,而是內部持有一個真正的使用者物件,進行使用者儲存。

public class UserProxy {

    private UserDao userDao;
    public UserProxy(UserDao userDao){
        this.userDao = userDao;
    }

    public void saveUser() {
        System.out.println(" ---- 代理開始 ---- ");
        userDao.saveUser();
        System.out.println(" ---- 代理結束 ----");
    }
}

下面是測試方法。

public class UserTest {

    public static void main(String[] args) {

        UserDao userDao = new UserDaoImpl();
        UserProxy userProxy = new UserProxy(userDao);
        userProxy.saveUser();

    }
}

新建立一個使用者實現類 (UserDaoImpl),它不執行使用者操作。然後再建立一個使用者代理(UserProxy),執行使用者代理的使用者儲存(saveUser),其內部會呼叫使用者實現類的儲存使用者(saveUser)方法,因為我們 JVM 可以在編譯期確定最終的執行方法,所以上面的這種代理模式又叫做靜態代理

代理模式具有無侵入性的優點,以後我們增加什麼新功能的話,我們可以直接增加一個代理類,讓代理類來呼叫使用者操作,這樣我們就實現了不通過改原始碼的方式增加了新的功能。然後生活很美好了,我們能夠直接新增我們想要的功能,在這美麗的日子裡,cxuan 新增了使用者代理、日誌代理等等無數個代理類。但是好景不長,cxuan 發現每次改程式碼的時候都要改每個代理類,這就很煩啊!我寶貴的時光都浪費在改每個代理類上面了嗎?

動態代理

JDK 動態代理

於是乎 cxuan 上網求助,發現了一個叫做動態代理的概念,通讀了一下,發現有點意思,於是乎 cxuan 修改了一下靜態代理的程式碼,新增了一個 UserHandler 的使用者代理,並做了一下 test,程式碼如下

public class UserHandler implements InvocationHandler {

    private UserDao userDao;

    public UserHandler(UserDao userDao){
        this.userDao = userDao;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        saveUserStart();
        Object obj = method.invoke(userDao, args);
        saveUserDone();
        return obj;
    }

    public void saveUserStart(){
        System.out.println("---- 開始插入 ----");
    }

    public void saveUserDone(){
        System.out.println("---- 插入完成 ----");
    }
}

測試類如下

public static void dynamicProxy(){

  UserDao userDao = new UserDaoImpl();
  InvocationHandler handler = new UserHandler(userDao);

  ClassLoader loader = userDao.getClass().getClassLoader();
  Class<?>[] interfaces = userDao.getClass().getInterfaces();

  UserDao proxy = (UserDao)Proxy.newProxyInstance(loader, interfaces, handler);
  proxy.saveUser();
}

UserHandler 是使用者代理類,建構函式中的 UserDao 是真實物件,通過把 UserDao 隱藏進 UserHandler ,通過 UserHandler 中的 UserDao 執行真正的方法。

類載入器、介面陣列你可以把它理解為一個方法樹,每棵葉子結點都是一個方法,通過後面的 proxy.saveUser() 來告訴 JVM 執行的是方法樹上的哪個方法。

使用者代理是通過類載入器、介面陣列、代理類來得到的。saveUser 方法就相當於是告訴 proxy 你最終要執行的是哪個方法,這個 proxy.saveUser 方法並不是最終直接執行的 saveUser 方法,最終的 saveUser 方法是由 UserHandler 中的 invoke 方法觸發的。

上面這種在編譯期無法確定最終的執行方法,而只能通過執行時動態獲取方法的代理模式被稱為 動態代理

動態代理的優勢是實現無侵入式的程式碼擴充套件,也可以對方法進行增強。此外,也可以大大減少程式碼量,避免代理類氾濫成災的情況。

所以我們現在總結一下靜態代理和動態代理各自的特點。

靜態代理

  • 靜態代理類:由程式設計師建立或者由第三方工具生成,再進行編譯;在程式執行之前,代理類的 .class 檔案已經存在了。
  • 靜態代理事先知道要代理的是什麼。
  • 靜態代理類通常只代理一個類。

動態代理

  • 動態代理通常是在程式執行時,通過反射機制動態生成的。
  • 動態代理類通常代理介面下的所有類。
  • 動態代理事先不知道要代理的是什麼,只有在執行的時候才能確定。
  • 動態代理的呼叫處理程式必須事先繼承 InvocationHandler 介面,使用 Proxy 類中的 newProxyInstance 方法動態的建立代理類。

在上面的程式碼示例中,我們是定義了一個 UserDao 介面,然後有 UserDaoImpl 介面的實現類,我們通過 Proxy.newProxyInstance 方法得到的也是 UserDao 的實現類物件,那麼其實這是一種基於介面的動態代理。也叫做 JDK 動態代理

是不是隻有這一種動態代理技術呢?既然都這麼問了,那當然不是。

除此之外,還有一些其他代理技術,不過是需要載入額外的 jar 包的,那麼我們彙總一下所有的代理技術和它的特徵

  • JDK 的動態代理使用簡單,它內建在 JDK 中,因此不需要引入第三方 Jar 包。

  • CGLIB 和 Javassist 都是高階的位元組碼生成庫,總體效能比 JDK 自帶的動態代理好,而且功能十分強大。

  • ASM 是低階的位元組碼生成工具,使用 ASM 已經近乎於在使用位元組碼程式設計,對開發人員要求最高。當然,也是效能最好的一種動態代理生成工具。但 ASM 的使用很繁瑣,而且效能也沒有數量級的提升,與 CGLIB 等高階位元組碼生成工具相比,ASM 程式的維護性較差,如果不是在對效能有苛刻要求的場合,還是推薦 CGLIB 或者 Javassist。

下面我們就來依次介紹一下這些動態代理工具的使用

CGLIB 動態代理

上面我們提到 JDK 動態代理是基於介面的代理,而 CGLIB 動態代理是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法 ,也就是說 CGLIB 動態代理採用類繼承 -> 方法重寫的方式進行的,下面我們先來看一下 CGLIB 動態代理的結構。

如上圖所示,代理類繼承於目標類,每次呼叫代理類的方法都會在攔截器中進行攔截,攔截器中再會呼叫目標類的方法。

下面我們通過一個示例來演示一下 CGLIB 動態代理的使用

首先匯入 CGLIB 相關 jar 包,我們使用的是 MAVEN 的方式

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.5</version>
</dependency>

然後我們新建立一個 UserService 類,為了和上面的 UserDao 和 UserDaoImpl 進行區分。

public class UserService {
   public void saveUser(){
       System.out.println("---- 儲存使用者 ----");
   }
}

之後我們建立一個自定義方法攔截器,這個自定義方法攔截器實現了攔截器類

public class AutoMethodInterceptor implements MethodInterceptor {

    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("---- 方法攔截 ----");
        Object object = methodProxy.invokeSuper(obj, args);
        return object;
    }
}

這裡解釋一下這幾個引數都是什麼含義

  • Object obj: obj 是 CGLIB 動態生成代理類例項
  • Method method: Method 為實體類所呼叫的被代理的方法引用
  • Objectp[] args: 這個就是方法的引數列表
  • MethodProxy methodProxy : 這個就是生成的代理類對方法的引用。

對於 methodProxy 引數呼叫的方法,在其內部有兩種選擇:invoke()invokeSuper() ,二者的區別不在本文展開說明,感興趣的讀者可以參考本篇文章:Cglib原始碼分析 invoke和invokeSuper的差別

然後我們建立一個測試類進行測試

public static void main(String[] args) {

  Enhancer enhancer = new Enhancer();
  enhancer.setSuperclass(UserService.class);
  enhancer.setCallback(new AutoMethodInterceptor());

  UserService userService = (UserService)enhancer.create();

  userService.saveUser();
}

測試類主要涉及 Enhancer 的使用,Enhancer 是一個非常重要的類,它允許為非介面型別建立一個 Java 代理,Enhancer 動態的建立給定類的子類並且攔截代理類的所有的方法,和 JDK 動態代理不一樣的是不管是介面還是類它都能正常工作。

JDK 動態代理與 CGLIB 動態代理都是將真實物件隱藏在代理物件的後面,以達到 代理 的效果。與 JDK 動態代理所不同的是 CGLIB 動態代理使用 Enhancer 來建立代理物件,而 JDK 動態代理使用的是 Proxy.newProxyInstance 來建立代理物件;還有一點是 CGLIB 可以代理大部分類,而 JDK 動態代理只能代理實現了介面的類。

Javassist 代理

Javassist是在 Java 中編輯位元組碼的類庫;它使 Java 程式能夠在執行時定義一個新類, 並在 JVM 載入時修改類檔案。我們使用最頻繁的動態特性就是 反射,而且反射也是動態代理的基礎,我們之所以沒有提反射對動態代理的作用是因為我想在後面詳聊,反射可以在執行時查詢物件屬性、方法,修改作用域,通過方法名稱呼叫方法等。實時應用不會頻繁使用反射來建立,因為反射開銷比較大,另外,還有一種具有和反射一樣功能強大的特性那就是 Javaassist

我們先通過一個簡單的示例來演示一下 Javaassist ,以及 Javaassist 如何建立動態代理。

我們仍舊使用上面提到的 UserDao 和 UserDaoImpl 作為基類。

我們新建立一個 AssistByteCode 類,它裡面有一個 createByteCode 方法,這個方法主要做的事情就是通過位元組碼生成 UserDaoImpl 實現類。我們下面來看一下它的程式碼

public class AssistByteCode {

    public static void createByteCode() throws Exception{
        ClassPool classPool = ClassPool.getDefault();
        CtClass cc = classPool.makeClass("com.cxuan.proxypattern.UserDaoImpl");

        // 設定介面
        CtClass ctClass = classPool.get("com.cxuan.proxypattern.UserDao");
        cc.setInterfaces(new CtClass[] {ctClass});

        // 建立方法
        CtMethod saveUser = CtMethod.make("public void saveUser(){}", cc);
        saveUser.setBody("System.out.println(\"---- 插入使用者 ----\");");
        cc.addMethod(saveUser);

        Class c = cc.toClass();
        cc.writeFile("/Users/mr.l/cxuan-justdoit");

    }
}

由於本文並不是一個具體研究 Javaassist 的文章,所以我們不會過多研究細節問題,只專注於這個框架一些比較重要的類

ClassPool:ClassPool 就是一個 CtClass 的容器,而一個 CtClass 物件就是一個 class 物件的例項,這個例項和 class 物件一樣,包含屬性、方法等。

那麼上面程式碼主要做了哪些事兒呢?通過 ClassPool 來獲取 CtClass 所需要的介面、抽象類的 CtClass 例項,然後通過 CtClass 例項新增自己的屬性和方法,並通過它的 writeFile 把二進位制流輸出到當前專案的根目錄路徑下。writeFile 其內部是使用了 DataOutputStream 進行輸出的。

流寫完後,我們開啟這個 .class 檔案如下所示

public class UserDaoImpl implements UserDao {
    public void saveUser() {
        System.out.println("---- 插入使用者 ----");
    }

    public UserDaoImpl() {
    }
}

可以對比一下上面發現 UserDaoImpl 發現編譯器除了為我們新增了一個公有的構造器,其他基本一致。

經過這個簡單的示例後,cxuan 給你演示一下如何使用 Javaassist 動態代理。

首先我們先建立一個 Javaassist 的代理工廠,程式碼如下

public class JavaassistProxyFactory {

    public Object getProxy(Class clazz) throws Exception{

        // 代理工廠
        ProxyFactory proxyFactory = new ProxyFactory();
        // 設定需要建立的子類
        proxyFactory.setSuperclass(clazz);
        proxyFactory.setHandler((self, thisMethod, proceed, args) -> {

            System.out.println("---- 開始攔截 ----");
            Object result = proceed.invoke(self, args);
            System.out.println("---- 結束攔截 ----");

            return result;
        });
        return proxyFactory.createClass().newInstance();

    }
}

上面我們定義了一個代理工廠,代理工廠裡面建立了一個 handler,在呼叫目標方法時,Javassist 會回撥 MethodHandler 介面方法攔截,來呼叫真正執行的方法,你可以在攔截方法的前後實現自己的業務邏輯。最後的 proxyFactory.createClass().newInstance() 就是使用位元組碼技術來建立了最終的子類例項,這種代理方式類似於 JDK 中的 InvocationHandler 介面。

測試方法如下

public static void main(String[] args) throws Exception {

  JavaassistProxyFactory proxyFactory = new JavaassistProxyFactory();
  UserService userProxy = (UserService) proxyFactory.getProxy(UserService.class);
  userProxy.saveUser();
}

ASM 代理

ASM 是一套 Java 位元組碼生成架構,它可以動態生成二進位制格式的子類或其它代理類,或者在類被 Java 虛擬機器裝入記憶體之前,動態修改類。

下面我們使用 ASM 框架實現一個動態代理,ASM 生成的動態代理

以下程式碼摘自 https://blog.csdn.net/lightj1996/article/details/107305662

public class AsmProxy extends ClassLoader implements Opcodes {

    public static void createAsmProxy() throws Exception {

        // 目標類類名 位元組碼中類修飾符以 “/” 分割
        String targetServiceName = TargetService.class.getName().replace(".", "/");
        // 切面類類名
        String aspectServiceName = AspectService.class.getName().replace(".", "/");
        // 代理類類名
        String proxyServiceName = targetServiceName+"Proxy";
        // 建立一個 classWriter 它是繼承了ClassVisitor
        ClassWriter classWriter = new ClassWriter(0);
        // 訪問類 指定jdk版本號為1.8, 修飾符為 public,父類是TargetService
        classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, proxyServiceName, null, targetServiceName, null);
        // 訪問目標類成員變數 為類新增切面屬性 “private TargetService targetService”
        classWriter.visitField(ACC_PRIVATE, "targetService", "L" + targetServiceName+";", null, null);
        // 訪問切面類成員變數 為類新增目標屬性 “private AspectService aspectService”
        classWriter.visitField(ACC_PRIVATE, "aspectService", "L" + aspectServiceName+";", null, null);

        // 訪問預設構造方法 TargetServiceProxy()
        // 定義函式 修飾符為public 方法名為 <init>, 方法表述符為()V 表示無引數,無返回引數
        MethodVisitor initVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        // 從區域性變數表取第0個元素 “this”
        initVisitor.visitVarInsn(ALOAD, 0);
        // 呼叫super 的構造方法 invokeSpecial在這裡的意思是呼叫父類方法
        initVisitor.visitMethodInsn(INVOKESPECIAL, targetServiceName, "<init>", "()V", false);
        // 方法返回
        initVisitor.visitInsn(RETURN);
        // 設定最大棧數量,最大區域性變數表數量
        initVisitor.visitMaxs(1, 1);
        // 訪問結束
        initVisitor.visitEnd();

        // 建立有參構造方法 TargetServiceProxy(TargetService var1, AspectService var2)
        // 定義函式 修飾符為public 方法名為 <init>, 方法表述符為(TargetService, AspectService)V 表示無引數,無返回引數
        MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "(L" + targetServiceName + ";L"+aspectServiceName+";)V", null, null);
        // 從區域性變數表取第0個元素 “this”壓入棧頂
        methodVisitor.visitVarInsn(ALOAD, 0);
        // this出棧 , 呼叫super 的構造方法 invokeSpecial在這裡的意思是呼叫父類方法。 <init>的owner是AspectService, 無參無返回型別
        methodVisitor.visitMethodInsn(INVOKESPECIAL, targetServiceName, "<init>", "()V", false);
        // 從區域性變數表取第0個元素 “this”壓入棧頂
        methodVisitor.visitVarInsn(ALOAD, 0);
        // 從區域性變數表取第1個元素 “targetService”壓入棧頂
        methodVisitor.visitVarInsn(ALOAD, 1);
        // this 和 targetService 出棧, 呼叫targetService put 賦值給this.targetService
        methodVisitor.visitFieldInsn(PUTFIELD, proxyServiceName, "targetService", "L" + targetServiceName + ";");
        // 從區域性變數表取第0個元素 “this”壓入棧頂
        methodVisitor.visitVarInsn(ALOAD, 0);
        // 從區域性變數表取第2個元素 “aspectService”壓入棧頂
        methodVisitor.visitVarInsn(ALOAD, 2);
        // this 和 aspectService 出棧 將 targetService put 賦值給this.aspectService
        methodVisitor.visitFieldInsn(PUTFIELD, proxyServiceName, "aspectService", "L" + aspectServiceName + ";");
        // 方法返回
        methodVisitor.visitInsn(RETURN);
        // 設定最大棧數量,最大區域性變數表數量
        methodVisitor.visitMaxs(2, 3);
        // 方法返回
        methodVisitor.visitEnd();

        // 建立代理方法 修飾符為public,方法名為 demoQuest
        MethodVisitor visitMethod = classWriter.visitMethod(ACC_PUBLIC, "demoQuest", "()I", null, null);
        // 從區域性變數表取第0個元素 “this”壓入棧頂
        visitMethod.visitVarInsn(ALOAD, 0);
        // this 出棧 將this.aspectService壓入棧頂
        visitMethod.visitFieldInsn(GETFIELD, proxyServiceName, "aspectService", "L"+aspectServiceName+";");
        // 取棧頂元素出棧 也就是targetService 呼叫其preOperation方法, demoQuest的owner是AspectService, 無參無返回型別
        visitMethod.visitMethodInsn(INVOKEVIRTUAL, aspectServiceName,"preOperation", "()V", false);
        // 從區域性變數表取第0個元素 “this”壓入棧頂
        visitMethod.visitVarInsn(ALOAD, 0);
        // this 出棧, 取this.targetService壓入棧頂
        visitMethod.visitFieldInsn(GETFIELD, proxyServiceName, "targetService", "L"+targetServiceName+";");
        // 取棧頂元素出棧 也就是targetService呼叫其demoQuest方法, demoQuest的owner是TargetService, 無參無返回型別
        visitMethod.visitMethodInsn(INVOKEVIRTUAL, targetServiceName, "demoQuest", "()I", false);
        // 方法返回
        visitMethod.visitInsn(IRETURN);
        // 設定最大棧數量,最大區域性變數表數量
        visitMethod.visitMaxs(1, 1);
        // 方法返回
        visitMethod.visitEnd();

        // 生成位元組碼二進位制流
        byte[] code = classWriter.toByteArray();
        // 自定義classloader載入類
        Class<?> clazz = (new AsmProxy()).defineClass(TargetService.class.getName() + "Proxy", code, 0, code.length);
        // 取其帶引數的構造方法
        Constructor constructor = clazz.getConstructor(TargetService.class, AspectService.class);
        // 使用構造方法例項化物件
        Object object = constructor.newInstance(new TargetService(), new AspectService());

        // 使用TargetService型別的引用接收這個物件
        TargetService targetService;
        if (!(object instanceof TargetService)) {
            return;
        }
        targetService = (TargetService)object;

        System.out.println("生成代理類的名稱: " + targetService.getClass().getName());
        // 呼叫被代理方法
        targetService.demoQuest();

        // 這裡可以不用寫, 但是如果想看最後生成的位元組碼長什麼樣子,可以寫 "ascp-purchase-app/target/classes/"是我的根目錄, 閱讀者需要將其替換成自己的
        String classPath = "/Users/mr.l/cxuan-justdoit/";
        String path = classPath + proxyServiceName + ".class";
        FileOutputStream fos =
                new FileOutputStream(path);
        fos.write(code);
        fos.close();

    }
}

使用 ASM 生成動態代理的程式碼比較長,上面這段程式碼的含義就是生成類 TargetServiceProxy,用於代理TargetService ,在呼叫 targetService.demoQuest() 方法之前呼叫切面的方法 aspectService.preOperation();

測試類就直接呼叫 AsmProxy.createAsmProxy() 方法即可,比較簡單。

下面是我們生成 TargetServiceProxy 的目標類

至此,我們已經介紹了四種動態代理的方式,分別是JDK 動態代理、CGLIB 動態代理、Javaassist 動態代理、ASM 動態代理,那麼現在思考一個問題,為什麼會有動態代理的出現呢?或者說動態代理是基於什麼原理呢?

其實我們上面已經提到過了,沒錯,動態代理使用的就是反射 機制,反射機制是 Java 語言提供的一種基礎功能,??賦予程式在執行時動態修改屬性、方法的能力。通過反射我們能夠直接操作類或者物件,比如獲取某個類的定義,獲取某個類的屬性和 方法等。

關於 Java 反射的相關內容可以參考 Java建設者的這一篇文章

精講 Java 反射

另外還有需要注意的一點,從效能角度來講,有些人得出結論說是 Java 動態代理要比 CGLIB 和 Javaassist 慢幾十倍,其實,在主流 JDK 版本中,Java 動態代理可以提供相等的效能水平,數量級的差距不是廣泛存在的。而且,在現代 JDK 中,反射已經得到了改進和優化。

我們在選型中,效能考量並不是主要關注點,可靠性、可維護性、編碼工作量同等重要。

我自己肝了六本 PDF,微信搜尋「程式設計師cxuan」關注公眾號後,在後臺回覆 cxuan ,領取全部 PDF,這些 PDF 如下

六本 PDF 連結

相關文章