Java 動態代理詳解
動態代理在Java中有著廣泛的應用,比如Spring AOP、Hibernate資料查詢、測試框架的後端mock、RPC遠端呼叫、Java註解物件獲取、日誌、使用者鑑權、全域性性異常處理、效能監控,甚至事務處理等。
本文主要介紹Java中兩種常見的動態代理方式:JDK原生動態代理和CGLIB動態代理。
由於Java動態代理與java反射機制關係緊密,請讀者確保已經瞭解了Java反射機制,可參考上一篇文章《Java反射機制詳解》
代理模式
本文將介紹的Java動態代理與設計模式中的代理模式有關,什麼是代理模式呢?
代理模式:給某一個物件提供一個代理,並由代理物件來控制對真實物件的訪問。代理模式是一種結構型設計模式。
代理模式角色分為 3 種:
Subject(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;
RealSubject(真實主題角色):真正實現業務邏輯的類;
Proxy(代理主題角色):用來代理和封裝真實主題;
代理模式的結構比較簡單,其核心是代理類,為了讓客戶端能夠一致性地對待真實物件和代理物件,在代理模式中引入了抽象層
代理模式按照職責(使用場景)來分類,至少可以分為以下幾類:1、遠端代理。 2、虛擬代理。 3、Copy-on-Write 代理。 4、保護(Protect or Access)代理。 5、Cache代理。 6、防火牆(Firewall)代理。 7、同步化(Synchronization)代理。 8、智慧引用(Smart Reference)代理等等。
如果根據位元組碼的建立時機來分類,可以分為靜態代理和動態代理:
- 所謂靜態也就是在程式執行前就已經存在代理類的位元組碼檔案,代理類和真實主題角色的關係在執行前就確定了。
- 而動態代理的原始碼是在程式執行期間由JVM根據反射等機制動態的生成,所以在執行前並不存在代理類的位元組碼檔案
靜態代理
我們先通過例項來學習靜態代理,然後理解靜態代理的缺點,再來學習本文的主角:動態代理
編寫一個介面 UserService ,以及該介面的一個實現類 UserServiceImpl
public interface UserService {
public void select();
public void update();
}
public class UserServiceImpl implements UserService {
public void select() {
System.out.println("查詢 selectById");
}
public void update() {
System.out.println("更新 update");
}
}
我們將通過靜態代理對 UserServiceImpl 進行功能增強,在呼叫 select
和 update
之前記錄一些日誌。寫一個代理類 UserServiceProxy,代理類需要實現 UserService
public class UserServiceProxy implements UserService {
private UserService target; // 被代理的物件
public UserServiceProxy(UserService target) {
this.target = target;
}
public void select() {
before();
target.select(); // 這裡才實際呼叫真實主題角色的方法
after();
}
public void update() {
before();
target.update(); // 這裡才實際呼叫真實主題角色的方法
after();
}
private void before() { // 在執行方法之前執行
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() { // 在執行方法之後執行
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
客戶端測試
public class Client1 {
public static void main(String[] args) {
UserService userServiceImpl = new UserServiceImpl();
UserService proxy = new UserServiceProxy(userServiceImpl);
proxy.select();
proxy.update();
}
}
輸出
log start time [Thu Dec 20 14:13:25 CST 2018]
查詢 selectById
log end time [Thu Dec 20 14:13:25 CST 2018]
log start time [Thu Dec 20 14:13:25 CST 2018]
更新 update
log end time [Thu Dec 20 14:13:25 CST 2018]
通過靜態代理,我們達到了功能增強的目的,而且沒有侵入原始碼,這是靜態代理的一個優點。
靜態代理的缺點
雖然靜態代理實現簡單,且不侵入原始碼,但是,當場景稍微複雜一些的時候,靜態代理的缺點也會暴露出來。
1、 當需要代理多個類的時候,由於代理物件要實現與目標物件一致的介面,有兩種方式:
- 只維護一個代理類,由這個代理類實現多個介面,但是這樣就導致代理類過於龐大
- 新建多個代理類,每個目標物件對應一個代理類,但是這樣會產生過多的代理類
2、 當介面需要增加、刪除、修改方法的時候,目標物件與代理類都要同時修改,不易維護。
如何改進?
當然是讓代理類動態的生成啦,也就是動態代理。
為什麼類可以動態的生成?
這就涉及到Java虛擬機器的類載入機制了,推薦翻看《深入理解Java虛擬機器》7.3節 類載入的過程。
Java虛擬機器類載入過程主要分為五個階段:載入、驗證、準備、解析、初始化。其中載入階段需要完成以下3件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
- 在記憶體中生成一個代表這個類的
java.lang.Class
物件,作為方法區這個類的各種資料訪問入口
由於虛擬機器規範對這3點要求並不具體,所以實際的實現是非常靈活的,關於第1點,獲取類的二進位制位元組流(class位元組碼)就有很多途徑:
- 從ZIP包獲取,這是JAR、EAR、WAR等格式的基礎
- 從網路中獲取,典型的應用是 Applet
- 執行時計算生成,這種場景使用最多的是動態代理技術,在 java.lang.reflect.Proxy 類中,就是用了 ProxyGenerator.generateProxyClass 來為特定介面生成形式為
*$Proxy
的代理類的二進位制位元組流 - 由其它檔案生成,典型應用是JSP,即由JSP檔案生成對應的Class類
- 從資料庫中獲取等等
所以,動態代理就是想辦法,根據介面或目標物件,計算出代理類的位元組碼,然後再載入到JVM中使用。但是如何計算?如何生成?情況也許比想象的複雜得多,我們需要藉助現有的方案。
常見的位元組碼操作類庫
這裡有一些介紹:https://java-source.net/open-source/bytecode-libraries
- Apache BCEL (Byte Code Engineering Library):是Java classworking廣泛使用的一種框架,它可以深入到JVM組合語言進行類操作的細節。
- ObjectWeb ASM:是一個Java位元組碼操作框架。它可以用於直接以二進位制形式動態生成stub根類或其他代理類,或者在載入時動態修改類。
- CGLIB(Code Generation Library):是一個功能強大,高效能和高質量的程式碼生成庫,用於擴充套件JAVA類並在執行時實現介面。
- Javassist:是Java的載入時反射系統,它是一個用於在Java中編輯位元組碼的類庫; 它使Java程式能夠在執行時定義新類,並在JVM載入之前修改類檔案。
- …
實現動態代理的思考方向
為了讓生成的代理類與目標物件(真實主題角色)保持一致性,從現在開始將介紹以下兩種最常見的方式:
- 通過實現介面的方式 -> JDK動態代理
- 通過繼承類的方式 -> CGLIB動態代理
注:使用ASM對使用者要求比較高,使用Javassist會比較麻煩
JDK動態代理
JDK動態代理主要涉及兩個類:java.lang.reflect.Proxy
和 java.lang.reflect.InvocationHandler
,我們仍然通過案例來學習
編寫一個呼叫邏輯處理器 LogHandler 類,提供日誌增強功能,並實現 InvocationHandler 介面;在 LogHandler 中維護一個目標物件,這個物件是被代理的物件(真實主題角色);在 invoke
方法中編寫方法呼叫的邏輯處理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;
public class LogHandler implements InvocationHandler {
Object target; // 被代理的物件,實際的方法執行者
public LogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args); // 呼叫 target 的 method 方法
after();
return result; // 返回方法的執行結果
}
// 呼叫invoke方法之前執行
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
// 呼叫invoke方法之後執行
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
編寫客戶端,獲取動態生成的代理類的物件須藉助 Proxy 類的 newProxyInstance 方法,具體步驟可見程式碼和註釋
import proxy.UserService;
import proxy.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Client2 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
// 設定變數可以儲存動態代理類,預設名稱以 $Proxy0 格式命名
// System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 1. 建立被代理的物件,UserService介面的實現類
UserServiceImpl userServiceImpl = new UserServiceImpl();
// 2. 獲取對應的 ClassLoader
ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
// 3. 獲取所有介面的Class,這裡的UserServiceImpl只實現了一個介面UserService,
Class[] interfaces = userServiceImpl.getClass().getInterfaces();
// 4. 建立一個將傳給代理類的呼叫請求處理器,處理所有的代理物件上的方法呼叫
// 這裡建立的是一個自定義的日誌處理器,須傳入實際的執行物件 userServiceImpl
InvocationHandler logHandler = new LogHandler(userServiceImpl);
/*
5.根據上面提供的資訊,建立代理物件 在這個過程中,
a.JDK會通過根據傳入的引數資訊動態地在記憶體中建立和.class 檔案等同的位元組碼
b.然後根據相應的位元組碼轉換成對應的class,
c.然後呼叫newInstance()建立代理例項
*/
UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
// 呼叫代理的方法
proxy.select();
proxy.update();
// 儲存JDK動態代理生成的代理類,類名儲存為 UserServiceProxy
// ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");
}
}
執行結果
log start time [Thu Dec 20 16:55:19 CST 2018]
查詢 selectById
log end time [Thu Dec 20 16:55:19 CST 2018]
log start time [Thu Dec 20 16:55:19 CST 2018]
更新 update
log end time [Thu Dec 20 16:55:19 CST 2018]
InvocationHandler 和 Proxy 的主要方法介紹如下:
java.lang.reflect.InvocationHandler
Object invoke(Object proxy, Method method, Object[] args)
定義了代理物件呼叫方法時希望執行的動作,用於集中處理在動態代理類物件上的方法呼叫
java.lang.reflect.Proxy
static InvocationHandler getInvocationHandler(Object proxy)
用於獲取指定代理物件所關聯的呼叫處理器
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
返回指定介面的代理類
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
構造實現指定介面的代理類的一個新例項,所有方法會呼叫給定處理器物件的 invoke 方法
static boolean isProxyClass(Class<?> cl)
返回 cl 是否為一個代理類
代理類的呼叫過程
生成的代理類到底長什麼樣子呢?藉助下面的工具類,把代理類儲存下來再探個究竟
(通過設定環境變數sun.misc.ProxyGenerator.saveGeneratedFiles=true也可以儲存代理類)
import sun.misc.ProxyGenerator;
import java.io.FileOutputStream;
import java.io.IOException;
public class ProxyUtils {
/**
* 將根據類資訊動態生成的二進位制位元組碼儲存到硬碟中,預設的是clazz目錄下
* params: clazz 需要生成動態代理類的類
* proxyName: 為動態生成的代理類的名稱
*/
public static void generateClassFile(Class clazz, String proxyName) {
// 根據類資訊和提供的代理類名稱,生成位元組碼
byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
String paths = clazz.getResource(".").getPath();
System.out.println(paths);
FileOutputStream out = null;
try {
//保留到硬碟中
out = new FileOutputStream(paths + proxyName + ".class");
out.write(classFile);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
然後在 Client2 測試類的main的最後面加入一行程式碼
// 儲存JDK動態代理生成的代理類,類名儲存為 UserServiceProxy
ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");
IDEA 再次執行之後就可以在 target 的類路徑下找到 UserServiceProxy.class,雙擊後IDEA的反編譯外掛會將該二進位制class檔案
UserServiceProxy 的程式碼如下所示:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.UserService;
public final class UserServiceProxy extends Proxy implements UserService {
private static Method m1;
private static Method m2;
private static Method m4;
private static Method m0;
private static Method m3;
public UserServiceProxy(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
// 省略...
}
public final String toString() throws {
// 省略...
}
public final void select() 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 {
// 省略...
}
public final void update() throws {
try {
super.h.invoke(this, m3, (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("proxy.UserService").getMethod("select");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m3 = Class.forName("proxy.UserService").getMethod("update");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
從 UserServiceProxy 的程式碼中我們可以發現:
- UserServiceProxy 繼承了 Proxy 類,並且實現了被代理的所有介面,以及equals、hashCode、toString等方法
- 由於 UserServiceProxy 繼承了 Proxy 類,所以每個代理類都會關聯一個 InvocationHandler 方法呼叫處理器
- 類和所有方法都被
public final
修飾,所以代理類只可被使用,不可以再被繼承 - 每個方法都有一個 Method 物件來描述,Method 物件在static靜態程式碼塊中建立,以
m + 數字
的格式命名 - 呼叫方法的時候通過
super.h.invoke(this, m1, (Object[])null);
呼叫,其中的super.h.invoke
實際上是在建立代理的時候傳遞給Proxy.newProxyInstance
的 LogHandler 物件,它繼承 InvocationHandler 類,負責實際的呼叫處理邏輯
而 LogHandler 的 invoke 方法接收到 method、args 等引數後,進行一些處理,然後通過反射讓被代理的物件 target 執行方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args); // 呼叫 target 的 method 方法
after();
return result; // 返回方法的執行結果
}
JDK動態代理執行方法呼叫的過程簡圖如下:
代理類的呼叫過程相信大家都明瞭了,而關於Proxy的原始碼解析,還請大家另外查閱其他文章或者直接看原始碼
CGLIB動態代理
maven引入CGLIB包,然後編寫一個UserDao類,它沒有介面,只有兩個方法,select() 和 update()
public class UserDao {
public void select() {
System.out.println("UserDao 查詢 selectById");
}
public void update() {
System.out.println("UserDao 更新 update");
}
}
編寫一個 LogInterceptor ,繼承了 MethodInterceptor,用於方法的攔截回撥
import java.lang.reflect.Method;
import java.util.Date;
public class LogInterceptor implements MethodInterceptor {
/**
* @param object 表示要進行增強的物件
* @param method 表示攔截的方法
* @param objects 陣列表示引數列表,基本資料型別需要傳入其包裝型別,如int-->Integer、long-Long、double-->Double
* @param methodProxy 表示對方法的代理,invokeSuper方法表示對被代理物件方法的呼叫
* @return 執行結果
* @throws Throwable
*/
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(object, objects); // 注意這裡是呼叫 invokeSuper 而不是 invoke,否則死迴圈,methodProxy.invokesuper執行的是原始類的方法,method.invoke執行的是子類的方法
after();
return result;
}
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
測試
import net.sf.cglib.proxy.Enhancer;
public class CglibTest {
public static void main(String[] args) {
DaoProxy daoProxy = new DaoProxy();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Dao.class); // 設定超類,cglib是通過繼承來實現的
enhancer.setCallback(daoProxy);
Dao dao = (Dao)enhancer.create(); // 建立代理類
dao.update();
dao.select();
}
}
執行結果
log start time [Fri Dec 21 00:06:40 CST 2018]
UserDao 查詢 selectById
log end time [Fri Dec 21 00:06:40 CST 2018]
log start time [Fri Dec 21 00:06:40 CST 2018]
UserDao 更新 update
log end time [Fri Dec 21 00:06:40 CST 2018]
還可以進一步多個 MethodInterceptor 進行過濾篩選
public class LogInterceptor2 implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(object, objects);
after();
return result;
}
private void before() {
System.out.println(String.format("log2 start time [%s] ", new Date()));
}
private void