Java動態代理設計模式
本文主要介紹Java
中兩種常見的動態代理方式:JDK原生動態代理
和CGLIB動態代理
。
什麼是代理模式
就是為其他對象提供一種代理以控制對這個對象的訪問。代理可以在不改動目標對象的基礎上,增加其他額外的功能(擴展功能)。
代理模式角色分為 3 種:
Subject
(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;RealSubject
(真實主題角色):真正實現業務邏輯的類;Proxy
(代理主題角色):用來代理和封裝真實主題;
如果根據位元組碼的創建時機來分類,可以分為靜態代理和動態代理:
- 所謂靜態也就是在程式運行前就已經存在代理類的位元組碼文件,代理類和真實主題角色的關係在運行前就確定了。
- 而動態代理的源碼是在程式運行期間由JVM根據反射等機制動態的生成,所以在運行前並不存在代理類的位元組碼文件
靜態代理
學習動態代理前,有必要來學習一下靜態代理。
靜態代理在使用時,需要定義介面或者父類,被代理對象(目標對象)與代理對象(Proxy)一起實現相同的介面或者是繼承相同父類。
來看一個例子,模擬小貓走路的時間。
// 介面
public interface Walkable {
void walk();
}
// 實現類
public class Cat implements Walkable {
@Override
public void walk() {
System.out.println("cat is walking...");
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果我想知道走路的時間怎麼辦?可以將實現類Cat
修改為:
public class Cat implements Walkable {
@Override
public void walk() {
long start = System.currentTimeMillis();
System.out.println("cat is walking...");
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("walk time = " + (end - start));
}
}
這裡已經侵入了源程式碼,如果源程式碼是不能改動的,這樣寫顯然是不行的,這裡可以引入時間代理類CatTimeProxy
。
public class CatTimeProxy implements Walkable {
private Walkable walkable;
public CatTimeProxy(Walkable walkable) {
this.walkable = walkable;
}
@Override
public void walk() {
long start = System.currentTimeMillis();
walkable.walk();
long end = System.currentTimeMillis();
System.out.println("Walk time = " + (end - start));
}
}
如果這時候還要加上常見的日誌功能,我們還需要創建一個日誌代理類CatLogProxy
。
public class CatLogProxy implements Walkable {
private Walkable walkable;
public CatLogProxy(Walkable walkable) {
this.walkable = walkable;
}
@Override
public void walk() {
System.out.println("Cat walk start...");
walkable.walk();
System.out.println("Cat walk end...");
}
}
如果我們需要先記錄日誌,再獲取行走時間,可以在調用的地方這麼做:
public static void main(String[] args) {
Cat cat = new Cat();
CatLogProxy p1 = new CatLogProxy(cat);
CatTimeProxy p2 = new CatTimeProxy(p1);
p2.walk();
}
這樣的話,計時是包括打日誌的時間的。
靜態代理的問題
如果我們需要計算SDK
中100個方法的運行時間,同樣的程式碼至少需要重複100次,並且創建至少100個代理類。往小了說,如果Cat
類有多個方法,我們需要知道其他方法的運行時間,同樣的程式碼也至少需要重複多次。因此,靜態代理至少有以下兩個局限性問題:
- 如果同時代理多個類,依然會導致類無限制擴展
- 如果類中有多個方法,同樣的邏輯需要反覆實現
所以,我們需要一個通用的代理類來代理所有的類的所有方法,這就需要用到動態代理技術。
動態代理
學習任何一門技術,一定要問一問自己,這到底有什麼用。其實,在這篇文章的講解過程中,我們已經說出了它的主要用途。你發現沒,使用動態代理我們居然可以在不改變源碼的情況下,直接在方法中插入自定義邏輯。這有點不太符合我們的一條線走到底的編程邏輯,這種編程模型有一個專業名稱叫AOP
。所謂的AOP
,就像刀一樣,抓住時機,趁機插入。
Jdk動態代理
JDK實現代理只需要使用newProxyInstance方法,但是該方法需要接收三個參數:
@CallerSensitive
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);
}
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
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});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
方法是在Proxy
類中是靜態方法,且接收的三個參數依次為:
ClassLoader loader
//指定當前目標對象使用類載入器Class<?>[] interfaces
//目標對象實現的介面的類型,使用泛型方式確認類型InvocationHandler h
//事件處理器
主要是完成InvocationHandler h
的編寫工作。
介面類UserService
:
public interface UserService {
public void select();
public void update();
}
介面實現類,即要代理的類UserServiceImpl
:
public class UserServiceImpl implements UserService {
@Override
public void select() {
System.out.println("查詢 selectById");
}
@Override
public void update() {
System.out.println("更新 update");
}
}
代理類UserServiceProxy
:
public class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target){
this.target = target;
}
@Override
public void select() {
before();
target.select();
after();
}
@Override
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 UserServiceProxyJDKMain {
public static void main(String[] args) {
// 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()創建代理實例
*/
// 會動態生成UserServiceProxy代理類,並且用代理對象實例化LogHandler,調用代理對象的.invoke()方法即可
UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
// 調用代理的方法
proxy.select();
proxy.update();
// 生成class文件的名稱
ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceJDKProxy");
}
}
這裡可以保存下來代理生成的實現了介面的代理對象:
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();
}
}
}
}
動態代理實現過程
- 通過
getProxyClass0()
生成代理類。JDK
生成的最終真正的代理類,它繼承自Proxy
並實現了我們定義的介面. - 通過
Proxy.newProxyInstance()
生成代理類的實例對象,創建對象時傳入InvocationHandler
類型的實例。 - 調用新實例的方法,即原
InvocationHandler
類中的invoke()
方法。
代理對象不需要實現介面,但是目標對象一定要實現介面,否則不能用動態代理
Cglib動態代理
JDK
的動態代理機制只能代理實現了介面的類,而不能實現介面的類就不能實現JDK
的動態代理,cglib
是針對類來實現代理的,他的原理是對指定的目標類生成一個子類,並覆蓋其中方法實現增強,但因為採用的是繼承,所以不能對final
修飾的類進行代理。
Cglib
代理,也叫作子類代理,它是在記憶體中構建一個子類對象從而實現對目標對象功能的擴展。
Cglib
子類代理實現方法:
- 需要引入
cglib
的jar
文件,但是Spring
的核心包中已經包括了Cglib
功能,所以直接引入Spring-core.jar
即可. - 引入功能包後,就可以在記憶體中動態構建子類
- 代理的類不能為
final
,否則報錯 - 目標對象的方法如果為
final/static
,那麼就不會被攔截,即不會執行目標對象額外的業務方法.
基本使用
<!-- //mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency>
方法攔截器
public class LogInterceptor implements MethodInterceptor{
/*
* @param o 要進行增強的對象
* @param method 要攔截的方法
* @param objects 參數列表,基本數據類型需要傳入其包裝類
* @param methodProxy 對方法的代理,
* @return 執行結果
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(o, objects);
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()));
}
}
測試用例
這裡保存了代理類的.class
文件
public class CglibMain {
public static void main(String[] args) {
// 創建Enhancer對象,類似於JDK動態代理的Proxy類
Enhancer enhancer = new Enhancer();
// 設置目標類的位元組碼文件
enhancer.setSuperclass(UserDao.class);
// 設置回調函數
enhancer.setCallback(new LogInterceptor());
// create會創建代理類
UserDao userDao = (UserDao)enhancer.create();
userDao.update();
userDao.select();
}
}
結果
log start time [Mon Nov 30 17:26:39 CST 2020]
UserDao 更新 update
log end time [Mon Nov 30 17:26:39 CST 2020]
log start time [Mon Nov 30 17:26:39 CST 2020]
UserDao 查詢 selectById
log end time [Mon Nov 30 17:26:39 CST 2020]
JDK動態代理與CGLIB動態代理對比
JDK 動態代理
- 為了解決靜態代理中,生成大量的代理類造成的冗餘;
JDK
動態代理只需要實現InvocationHandler
介面,重寫invoke
方法便可以完成代理的實現,- jdk的代理是利用反射生成代理類
Proxyxx.class
代理類位元組碼,並生成對象 - jdk動態代理之所以只能代理介面是因為代理類本身已經
extends
了Proxy
,而java是不允許多重繼承的,但是允許實現多個介面
優點:解決了靜態代理中冗餘的代理實現類問題。
缺點:JDK
動態代理是基於介面設計實現的,如果沒有介面,會拋異常。
CGLIB 代理
- 由於
JDK
動態代理限制了只能基於介面設計,而對於沒有介面的情況,JDK
方式解決不了; CGLib
採用了非常底層的位元組碼技術,其原理是通過位元組碼技術為一個類創建子類,並在子類中採用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯,來完成動態代理的實現。- 實現方式實現
MethodInterceptor
介面,重寫intercept
方法,通過Enhancer
類的回調方法來實現。 - 但是
CGLib
在創建代理對象時所花費的時間卻比JDK多得多,所以對於單例的對象,因為無需頻繁創建對象,用CGLib
合適,反之,使用JDK
方式要更為合適一些。 - 同時,由於
CGLib
由於是採用動態創建子類的方法,對於final
方法,無法進行代理。
優點:沒有介面也能實現動態代理,而且採用位元組碼增強技術,性能也不錯。
缺點:技術實現相對難理解些。