1. 程式人生 > 其它 >動態代理學習記錄

動態代理學習記錄

JDK動態代理

使用

  1. 首先定義一個介面類
// Person.java
package com.may.learning;

public interface Person {
    void check();
    void checkout();
}
  1. 再定義實現該介面的類,用以生成需要被代理的例項物件
// Employee.java
package com.may.learning;

public class Employee implements Person{
    private String name;

    public Employee(String name) {
        this.name = name;
    }
    public void check() {
        System.out.printf("%s 今天也要好好上班!\n", name);
    }

    public void checkout() {
        System.out.printf("%s 明天見\n", name);
    }
}
  1. 使用InvocationHandler定義invoke方法,用以替代被代理物件執行時的方法,並且使用newProxyInstance建立一個代理類.
// Welcome.java
package com.may.learning;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Welcome {

    static class LogHandler implements InvocationHandler {
        Object obj;
        public LogHandler(Object obj) {
            this.obj = obj;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("準備打卡");
            method.invoke(obj, args);
            System.out.println("打卡完成");
            return null;
        }
    }

    public static void main(String[] args) {
        Person p = new Employee("littlemay");
        LogHandler logHandler = new LogHandler(p);
        p = (Person) Proxy.newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(), logHandler);
        p.check();
        p.checkout();
    }
}

執行結果:

準備打卡
littlemay 今天也要好好上班!
打卡完成
準備打卡
littlemay 明天見
打卡完成

說明

  1. 進行呼叫的物件p事實上是根據Person介面的位元組碼臨時構造的一個物件.通過Proxy.newProxyInstance方法進行構造,接收三個引數:
  • loader: 代理類的ClassLoader
  • interfaces:代理類需要實現的介面
  • handler:呼叫處理器例項,在這裡是logHandler
  1. InvocationHandlerinvoke方法接收三個引數:
  • proxy: 代理後的物件,在這裡是p
  • method: 被代理物件需要代理的方法
  • args
    : method執行時需要的引數

在實現InvocationHandler中,傳入的proxy物件,和方法呼叫時的obj,需要區分開,並不是一個東西:
雖然proxy看似無用,但是卻是可以實現鏈式呼叫進行返回的.參照:Understanding “proxy” arguments of the invoke method of java.lang.reflect.InvocationHandler.
obj是需要傳入的被代理物件例項.
比如在此基礎上做一個邊上班邊增加工資的無聊功能:

// Person.java
package com.may.learning;

public interface Person {
    void check();
    void checkout();
    // new
    Person addSalary(double money);
    double getSalary();
}

// Employee.java
package com.may.learning;


public class Employee implements Person{
    private String name;
    private Double money;

    public Employee(String name, Double money) {
        this.name = name;
        this.money = money;
    }
    public void check() {
        System.out.printf("%s 今天也要好好上班!\n", name);
    }

    public void checkout() {
        System.out.printf("%s 明天見\n", name);
    }
    // new
    public Person addSalary(double money){
        this.money += money;
        return this;
    }

    public double getSalary() {
        return money;
    }
}

// Welcome.java
package com.may.learning;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Welcome {

    public static void main(String[] args) throws IOException {
        Person p = new Employee("littlemay", 10000.0);
        LogHandler logHandler = new LogHandler(p);
        p = (Person) Proxy
            .newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(),
                logHandler);
        p.check();
        System.out.println("工資餘額: "+ p.getSalary());
        // 鏈式呼叫
        p.addSalary(100).addSalary(100).addSalary(1000);
        p.checkout();
        System.out.println("工資餘額: "+ p.getSalary());
    }

    static class LogHandler implements InvocationHandler {

        Object obj;

        public LogHandler(Object obj) {
            this.obj = obj;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("check".equals(method.getName())) {
                System.out.println("準備打卡");
                method.invoke(obj, args);
                System.out.println("打卡成功");
            } else if ("addSalary".equals(method.getName())) {
                method.invoke(obj, args);
                System.out.println("工資增加了");
            } else{
                return method.invoke(obj, args);
            }
            return proxy;
        }
    }
}

輸出結果:

準備打卡
littlemay 今天也要好好上班!
打卡成功
工資餘額: 10000.0
工資增加了
工資增加了
工資增加了
littlemay 明天見
工資餘額: 11200.0

為什麼說JDK代理只能基於介面實現呢

newProxyInstance是生成代理物件的class檔案,更改程式碼獲得生成的class檔案:

    public static void main(String[] args) throws IOException {
        Person p = new Employee("littlemay");
        LogHandler logHandler = new LogHandler(p);
        p = (Person) Proxy.newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(), logHandler);
        // 獲取位元組碼
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass("com.may.learning.$Proxy0", p.getClass().getInterfaces(),
            Modifier.PUBLIC);
        File file = new File("$Proxy0.class");
        OutputStream outputStream = new FileOutputStream(file);
        outputStream.write(proxyClassFile);
        // end
        p.check();
        p.checkout();
    }

檢視位元組碼:

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

package com.may.learning;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public class $Proxy0 extends Proxy implements Person {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m3;
    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 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 check() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void checkout() throws  {
        try {
            super.h.invoke(this, m3, (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"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("com.may.learning.Person").getMethod("check");
            m3 = Class.forName("com.may.learning.Person").getMethod("checkout");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

可以發現$Proxy0已經繼承了Proxy類,因為java不支援多繼承,所以只能採用實現Person介面的方式來實現.

參考資料:
你真的完全瞭解Java動態代理嗎?看這篇就夠了
Java動態代理
為什麼JDK的動態代理要基於介面實現而不能基於繼承實現?
Java中InvocationHandler介面中第一個引數proxy詳解

cglib動態代理

由於JDK動態代理只能基於介面實現,如果要對類進行代理,那麼可以使用cglib動態代理.它是一個基於ASM的位元組碼生成庫,允許在執行時對位元組碼進行修改和動態生成。

使用

  1. 新增依賴
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2.2</version>
        </dependency>
  1. 編寫一個類
// UserDao.java
package com.may.learning;

public class UserDao {

    public void update() {
        System.out.println("update...");
    }

    public void select() {
        System.out.println("select...");
    }

    public final void show(){
        System.out.println("show");
    }
}
  1. 編寫代理,intercept有四個引數,obj表示代理物件,method表示目標類中的方法,args表示方法引數,proxy表示代理方法的MethodProxy物件
// UserDaoInterceptor.java
package com.may.learning;

import java.lang.reflect.Method;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

public class UserDaoInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // System.out.println(obj.getClass().getName());
        System.out.println("transaction open");
        Object o = proxy.invokeSuper(obj, args);
        System.out.println("transaction close");
        return o;
    }
}

需要呼叫invokeSuper方法,如果呼叫invoke會死迴圈導致棧溢位.原因在於:

  • invoke最終執行的是fci.f1.invoke(fci.i1, obj, args);
  • invokeSuper最終執行的是 fci.f2.invoke(fci.i2, obj, args)
    至於f1,f2分別代表什麼,需要檢視FastClassInfo.(FastClass機制就是對一個類的方法建立索引,通過索引來直接呼叫相應的方法.)
    private static class FastClassInfo {
        FastClass f1;  // 被代理物件的fastclass
        FastClass f2;  // 代理後物件的fastclass
        int i1; // 被代理方法在f1中的索引
        int i2; // 代理後方法在f2中的索引
}

因此如果使用invoke,最終還是呼叫被代理物件的被代理方法,仍然會進入到intercept中,如此迴圈下去.

  1. 利用enhancer.create()獲取代理物件,進行呼叫
package com.may.learning;

import org.springframework.cglib.core.DebuggingClassWriter;
import org.springframework.cglib.proxy.Enhancer;

public class TestCglib {

    public static void main(String[] args) {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/home/may/learning/java");
        Enhancer enhancer = new Enhancer();
        // 設定要代理的類
        enhancer.setSuperclass(UserDao.class);
         //設定回撥
        enhancer.setCallback(new UserDaoInterceptor());
         // 生成代理物件
        UserDao userDao = (UserDao)enhancer.create();
        userDao.select();
        userDao.update();
        userDao.show();
    }
}

可以利用System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/home/may/learning/java");檢視生成的位元組碼

說明

所有非final的方法都會轉發到UserDaoInterceptor.invoke()方法中.所以show方法不會有事務開啟關閉的語句輸出.呼叫結果為:

transaction open
select...
transaction close
transaction open
update...
transaction close
show

因為每一次列印都會呼叫toString方法,所以如果在invoke方法中執行列印obj,會導致stackoverflow,這在兩種動態代理方法都會出現.並且打斷點除錯會輸出更多的語句.

檢視生成的UserDao$$EnhancerByCGLIB$$8a7c533a.class檔案,發現如果存在攔截器,會使用攔截器執行:

    public final void update() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$update$1$Method, CGLIB$emptyArgs, CGLIB$update$1$Proxy);
        } else {
            super.update();
        }
    }

參考資料:
Java Proxy和CGLIB動態代理原理
cglib原始碼分析(四):cglib 動態代理原理分析
CGLib動態代理