1. 程式人生 > 其它 >動態代理大揭祕,帶你徹底弄清楚動態代理!

動態代理大揭祕,帶你徹底弄清楚動態代理!

前言

代理模式是一種設計模式,能夠使得在不修改源目標的前提下,額外擴充套件源目標的功能。即通過訪問源目標的代理類,再由代理類去訪問源目標。這樣一來,要擴充套件功能,就無需修改源目標的程式碼了。只需要在代理類上增加就可以了。

其實代理模式的核心思想就是這麼簡單,在java中,代理又分靜態代理和動態代理2種,其中動態代理根據不同實現又區分基於介面的的動態代理和基於子類的動態代理。

其中靜態代理由於比較簡單,面試中也沒啥問的,在代理模式一塊,問的最多就是動態代理,而且動態代理也是spring aop的核心思想,spring其他很多功能也是通過動態代理來實現的,比如攔截器,事務控制等。

熟練掌握動態代理技術,能讓你業務程式碼更加精簡而優雅。如果你需要寫一些中介軟體的話,那動態代理技術更是必不可少的技能包。

那此篇文章就帶大家一窺動態代理的所有細節吧。

靜態代理

在說動態代理前,還是先說說靜態代理。

所謂靜態代理,就是通過宣告一個明確的代理類來訪問源物件。

我們有2個介面,Person和Animal。每個介面各有2個實現類,UML如下圖:

每個實現類中程式碼都差不多一致,用Student來舉例(其他類和這個幾乎一模一樣)

public class Student implements Person{

    private String name;

    public Student() {
    }

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void wakeup() {
        System.out.println(StrUtil.format("學生[{}]早晨醒來啦",name));
    }

    @Override
    public void sleep() {
        System.out.println(StrUtil.format("學生[{}]晚上睡覺啦",name));
    }
}

假設我們現在要做一件事,就是在所有的實現類呼叫wakeup()前增加一行輸出早安~,呼叫sleep()前增加一行輸出晚安~。那我們只需要編寫2個代理類PersonProxyAnimalProxy

PersonProxy:

public class PersonProxy implements Person {

    private Person person;

    public PersonProxy(Person person) {
        this.person = person;
    }

    @Override
    public void wakeup() {
        System.out.println("早安~");
        person.wakeup();
    }

    @Override
    public void sleep() {
        System.out.println("晚安~");
        person.sleep();
    }
}

AnimalProxy:

public class AnimalProxy implements Animal {

    private Animal animal;

    public AnimalProxy(Animal animal) {
        this.animal = animal;
    }

    @Override
    public void wakeup() {
        System.out.println("早安~");
        animal.wakeup();
    }

    @Override
    public void sleep() {
        System.out.println("晚安~");
        animal.sleep();
    }
}

最終執行程式碼為:

public static void main(String[] args) {
    Person student = new Student("張三");
    PersonProxy studentProxy = new PersonProxy(student);
    studentProxy.wakeup();
    studentProxy.sleep();

    Person doctor = new Doctor("王教授");
    PersonProxy doctorProxy = new PersonProxy(doctor);
    doctorProxy.wakeup();
    doctorProxy.sleep();

    Animal dog = new Dog("旺旺");
    AnimalProxy dogProxy = new AnimalProxy(dog);
    dogProxy.wakeup();
    dogProxy.sleep();

    Animal cat = new Cat("咪咪");
    AnimalProxy catProxy = new AnimalProxy(cat);
    catProxy.wakeup();
    catProxy.sleep();
}

輸出:

早安~
學生[張三]早晨醒來啦
晚安~
學生[張三]晚上睡覺啦
早安~
醫生[王教授]早晨醒來啦
晚安~
醫生[王教授]晚上睡覺啦
早安~~
小狗[旺旺]早晨醒來啦
晚安~~
小狗[旺旺]晚上睡覺啦
早安~~
小貓[咪咪]早晨醒來啦
晚安~~
小貓[咪咪]晚上睡覺啦

結論:

靜態代理的程式碼相信已經不用多說了,程式碼非常簡單易懂。這裡用了2個代理類,分別代理了PersonAnimal介面。

這種模式雖然好理解,但是缺點也很明顯:

  • 會存在大量的冗餘的代理類,這裡演示了2個介面,如果有10個介面,就必須定義10個代理類。
  • 不易維護,一旦介面更改,代理類和目標類都需要更改。

JDK動態代理

動態代理,通俗點說就是:無需宣告式的建立java代理類,而是在執行過程中生成"虛擬"的代理類,被ClassLoader載入。從而避免了靜態代理那樣需要宣告大量的代理類。

JDK從1.3版本就開始支援動態代理類的建立。主要核心類只有2個:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler

還是前面那個例子,用JDK動態代理類去實現的程式碼如下:

建立一個JdkProxy類,用於統一代理:

public class JdkProxy implements InvocationHandler {

    private Object bean;

    public JdkProxy(Object bean) {
        this.bean = bean;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if (methodName.equals("wakeup")){
            System.out.println("早安~~~");
        }else if(methodName.equals("sleep")){
            System.out.println("晚安~~~");
        }

        return method.invoke(bean, args);
    }
}

執行程式碼:

public static void main(String[] args) {
    JdkProxy proxy = new JdkProxy(new Student("張三"));
    Person student = (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Person.class}, proxy);
    student.wakeup();
    student.sleep();

    proxy = new JdkProxy(new Doctor("王教授"));
    Person doctor = (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Person.class}, proxy);
    doctor.wakeup();
    doctor.sleep();

    proxy = new JdkProxy(new Dog("旺旺"));
    Animal dog = (Animal) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Animal.class}, proxy);
    dog.wakeup();
    dog.sleep();

    proxy = new JdkProxy(new Cat("咪咪"));
    Animal cat = (Animal) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Animal.class}, proxy);
    cat.wakeup();
    cat.sleep();
}

講解:

可以看到,相對於靜態代理類來說,無論有多少介面,這裡只需要一個代理類。核心程式碼也很簡單。唯一需要注意的點有以下2點:

  • JDK動態代理是需要宣告介面的,建立一個動態代理類必須得給這個”虛擬“的類一個介面。可以看到,這時候經動態代理類創造之後的每個bean已經不是原來那個物件了。

  • 為什麼這裡JdkProxy還需要構造傳入原有的bean呢?因為處理完附加的功能外,需要執行原有bean的方法,以完成代理的職責。

    這裡JdkProxy最核心的方法就是

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    

    其中proxy為代理過之後的物件(並不是原物件),method為被代理的方法,args為方法的引數。

    如果你不傳原有的bean,直接用method.invoke(proxy, args)的話,那麼就會陷入一個死迴圈。

可以代理什麼

JDK的動態代理是也平時大家使用的最多的一種代理方式。也叫做介面代理。前幾天有一個小夥伴在群裡問我,動態代理是否一次可以代理一個類,多個類可不可以。

JDK動態代理說白了只是根據介面”憑空“來生成類,至於具體的執行,都被代理到了InvocationHandler 的實現類裡。上述例子我是需要繼續執行原有bean的邏輯,才將原有的bean構造進來。只要你需要,你可以構造進任何物件到這個代理實現類。也就是說,你可以傳入多個物件,或者說你什麼類都不代理。只是為某一個介面”憑空“的生成多個代理例項,這多個代理例項最終都會進入InvocationHandler的實現類來執行某一個段共同的程式碼。

所以,在以往的專案中的一個實際場景就是,我有多個以yaml定義的規則檔案,通過對yaml檔案的掃描,來為每個yaml規則檔案生成一個動態代理類。而實現這個,我只需要事先定義一個介面,和定義InvocationHandler的實現類就可以了,同時把yaml解析過的物件傳入。最終這些動態代理類都會進入invoke方法來執行某個共同的邏輯。

Cglib動態代理

Spring在5.X之前預設的動態代理實現一直是jdk動態代理。但是從5.X開始,spring就開始預設使用Cglib來作為動態代理實現。並且springboot從2.X開始也轉向了Cglib動態代理實現。

是什麼導致了spring體系整體轉投Cglib呢,jdk動態代理又有什麼缺點呢?

那麼我們現在就要來說下Cglib的動態代理。

Cglib是一個開源專案,它的底層是位元組碼處理框架ASM,Cglib提供了比jdk更為強大的動態代理。主要相比jdk動態代理的優勢有:

  • jdk動態代理只能基於介面,代理生成的物件只能賦值給介面變數,而Cglib就不存在這個問題,Cglib是通過生成子類來實現的,代理物件既可以賦值給實現類,又可以賦值給介面。
  • Cglib速度比jdk動態代理更快,效能更好。

那何謂通過子類來實現呢?

還是前面那個例子,我們要實現相同的效果。程式碼如下

建立CglibProxy類,用於統一代理:

public class CglibProxy implements MethodInterceptor {

    private Enhancer enhancer = new Enhancer();

    private Object bean;

    public CglibProxy(Object bean) {
        this.bean = bean;
    }

    public Object getProxy(){
        //設定需要建立子類的類
        enhancer.setSuperclass(bean.getClass());
        enhancer.setCallback(this);
        //通過位元組碼技術動態建立子類例項
        return enhancer.create();
    }
    //實現MethodInterceptor介面方法
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        String methodName = method.getName();
        if (methodName.equals("wakeup")){
            System.out.println("早安~~~");
        }else if(methodName.equals("sleep")){
            System.out.println("晚安~~~");
        }

        //呼叫原bean的方法
        return method.invoke(bean,args);
    }
}

執行程式碼:

public static void main(String[] args) {
    CglibProxy proxy = new CglibProxy(new Student("張三"));
    Student student = (Student) proxy.getProxy();
    student.wakeup();
    student.sleep();

    proxy = new CglibProxy(new Doctor("王教授"));
    Doctor doctor = (Doctor) proxy.getProxy();
    doctor.wakeup();
    doctor.sleep();

    proxy = new CglibProxy(new Dog("旺旺"));
    Dog dog = (Dog) proxy.getProxy();
    dog.wakeup();
    dog.sleep();

    proxy = new CglibProxy(new Cat("咪咪"));
    Cat cat = (Cat) proxy.getProxy();
    cat.wakeup();
    cat.sleep();
}

講解:

在這裡用Cglib作為代理,其思路和jdk動態代理差不多。也需要把原始bean構造傳入。原因上面有說,這裡不多贅述。

關鍵的程式碼在這裡

//設定需要建立子類的類
enhancer.setSuperclass(bean.getClass());
enhancer.setCallback(this);
//通過位元組碼技術動態建立子類例項
return enhancer.create();

可以看到,Cglib"憑空"的創造了一個原bean的子類,並把Callback指向了this,也就是當前物件,也就是這個proxy物件。從而會呼叫intercept方法。而在intercept方法裡,進行了附加功能的執行,最後還是呼叫了原始bean的相應方法。

在debug這個生成的代理物件時,我們也能看到,Cglib是憑空生成了原始bean的子類:

javassist動態代理

Javassist是一個開源的分析、編輯和建立Java位元組碼的類庫,可以直接編輯和生成Java生成的位元組碼。相對於bcel, asm等這些工具,開發者不需要了解虛擬機器指令,就能動態改變類的結構,或者動態生成類。

在日常使用中,javassit通常被用來動態修改位元組碼。它也能用來實現動態代理的功能。

話不多說,還是一樣的例子,我用javassist動態代理來實現一遍

建立JavassitProxy,用作統一代理:

public class JavassitProxy {

    private Object bean;

    public JavassitProxy(Object bean) {
        this.bean = bean;
    }

    public Object getProxy() throws IllegalAccessException, InstantiationException {
        ProxyFactory f = new ProxyFactory();
        f.setSuperclass(bean.getClass());
        f.setFilter(m -> ListUtil.toList("wakeup","sleep").contains(m.getName()));

        Class c = f.createClass();
        MethodHandler mi = (self, method, proceed, args) -> {
            String methodName = method.getName();
            if (methodName.equals("wakeup")){
                System.out.println("早安~~~");
            }else if(methodName.equals("sleep")){
                System.out.println("晚安~~~");
            }
            return method.invoke(bean, args);
        };
        Object proxy = c.newInstance();
        ((Proxy)proxy).setHandler(mi);
        return proxy;
    }
}

執行程式碼:

public static void main(String[] args) throws Exception{
    JavassitProxy proxy = new JavassitProxy(new Student("張三"));
    Student student = (Student) proxy.getProxy();
    student.wakeup();
    student.sleep();

    proxy = new JavassitProxy(new Doctor("王教授"));
    Doctor doctor = (Doctor) proxy.getProxy();
    doctor.wakeup();
    doctor.sleep();

    proxy = new JavassitProxy(new Dog("旺旺"));
    Dog dog = (Dog) proxy.getProxy();
    dog.wakeup();
    dog.sleep();

    proxy = new JavassitProxy(new Cat("咪咪"));
    Cat cat = (Cat) proxy.getProxy();
    cat.wakeup();
    cat.sleep();
}

講解:

熟悉的配方,熟悉的味道,大致思路也是類似的。同樣把原始bean構造傳入。可以看到,javassist也是用”憑空“生成子類的方式類來解決,程式碼的最後也是呼叫了原始bean的目標方法完成代理。

javaassit比較有特點的是,可以對所需要代理的方法用filter來設定,裡面可以像Criteria構造器那樣進行構造。其他的程式碼,如果你仔細看了之前的程式碼演示,應該能很輕易看懂了。

ByteBuddy動態代理

ByteBuddy,位元組碼夥計,一聽就很牛逼有不。

ByteBuddy也是一個大名鼎鼎的開源庫,和Cglib一樣,也是基於ASM實現。還有一個名氣更大的庫叫Mockito,相信不少人用過這玩意寫過測試用例,其核心就是基於ByteBuddy來實現的,可以動態生成mock類,非常方便。另外ByteBuddy另外一個大的應用就是java agent,其主要作用就是在class被載入之前對其攔截,插入自己的程式碼。

ByteBuddy非常強大,是一個神器。可以應用在很多場景。但是這裡,只介紹用ByteBuddy來做動態代理,關於其他使用方式,可能要專門寫一篇來講述,這裡先給自己挖個坑。

來,還是熟悉的例子,熟悉的配方。用ByteBuddy我們再來實現一遍前面的例子

建立ByteBuddyProxy,做統一代理:

public class ByteBuddyProxy {

    private Object bean;

    public ByteBuddyProxy(Object bean) {
        this.bean = bean;
    }

    public Object getProxy() throws Exception{
        Object object = new ByteBuddy().subclass(bean.getClass())
                .method(ElementMatchers.namedOneOf("wakeup","sleep"))
                .intercept(InvocationHandlerAdapter.of(new AopInvocationHandler(bean)))
                .make()
                .load(ByteBuddyProxy.class.getClassLoader())
                .getLoaded()
                .newInstance();
        return object;
    }

    public class AopInvocationHandler implements InvocationHandler {

        private Object bean;

        public AopInvocationHandler(Object bean) {
            this.bean = bean;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            if (methodName.equals("wakeup")){
                System.out.println("早安~~~");
            }else if(methodName.equals("sleep")){
                System.out.println("晚安~~~");
            }
            return method.invoke(bean, args);
        }
    }
}

執行程式碼:

public static void main(String[] args) throws Exception{
    ByteBuddyProxy proxy = new ByteBuddyProxy(new Student("張三"));
    Student student = (Student) proxy.getProxy();
    student.wakeup();
    student.sleep();

    proxy = new ByteBuddyProxy(new Doctor("王教授"));
    Doctor doctor = (Doctor) proxy.getProxy();
    doctor.wakeup();
    doctor.sleep();

    proxy = new ByteBuddyProxy(new Dog("旺旺"));
    Dog dog = (Dog) proxy.getProxy();
    dog.wakeup();
    dog.sleep();

    proxy = new ByteBuddyProxy(new Cat("咪咪"));
    Cat cat = (Cat) proxy.getProxy();
    cat.wakeup();
    cat.sleep();
}

講解:

思路和之前還是一樣,通過仔細觀察程式碼,ByteBuddy也是採用了創造子類的方式來實現動態代理。

各種動態代理的效能如何

前面介紹了4種動態代理對於同一例子的實現。對於代理的模式可以分為2種:

  • JDK動態代理採用介面代理的模式,代理物件只能賦值給介面,允許多個介面
  • Cglib,Javassist,ByteBuddy這些都是採用了子類代理的模式,代理物件既可以賦值給介面,又可以複製給具體實現類

Spring5.X,Springboot2.X只有都採用了Cglib作為動態代理的實現,那是不是cglib效能是最好的呢?

我這裡做了一個簡單而粗暴的實驗,直接把上述4段執行程式碼進行單執行緒同步迴圈多遍,用耗時來確定他們4個的效能。應該能看出些端倪。

JDK PROXY迴圈10000遍所耗時:0.714970125秒
Cglib迴圈10000遍所耗時:0.434937833秒
Javassist迴圈10000遍所耗時:1.294194708秒
ByteBuddy迴圈10000遍所耗時:9.731999042秒

執行的結果如上

從執行結果來看,的確是cglib效果最好。至於為什麼ByteBuddy執行那麼慢,不一定是ByteBuddy效能差,也有可能是我測試程式碼寫的有問題,沒有找到正確的方式。所以這隻能作為一個大致的參考。

看來Spring選擇Cglib還是有道理的。

最後

動態代理技術對於一個經常寫開源或是中介軟體的人來說,是一個神器。這種特性提供了一種新的解決方式。從而使得程式碼更加優雅而簡單。動態代理對於理解spring的核心思想也有著莫大的幫助,希望對動態代理技術感興趣的童鞋能試著去跑一遍示例中的程式碼,來加強理解。

最後附上本篇文章中所用到的全部程式碼,我已經將其上傳到Gitee:

https://gitee.com/bryan31/proxy-demo

如果你已經看到這,覺得此篇文章能幫助到你的話,請給文章點贊且分享,也希望能關注我的公眾號。我是一個開源作者,會在空餘時間分享技術和生活,和你一起成長。