前言
《設計模式自習室》系列,顧名思義,本系列文章帶你溫習常見的設計模式。主要內容有:
- 該模式的介紹,包括:
- 引子、意圖(大白話解釋)
- 類圖、時序圖(理論規范)
- 該模式的代碼示例:熟悉該模式的代碼長什么樣子
- 該模式的優缺點:模式不是萬金油,不可以濫用模式
- 該模式的應用案例:了解它在哪些重要的源碼中被使用
系列文章回顧
- 【設計模式自習室】開篇:為什么我們要用設計模式?
- 【設計模式自習室】建造者模式
- 【設計模式自習室】原型模式
- 【設計模式自習室】透徹理解單例模式
- 【設計模式自習室】理解工廠模式的三種形式
- 【設計模式自習室】適配器模式
- 【設計模式自習室】裝飾模式
- 【設計模式自習室】橋接模式 Bridge Pattern:處理多維度變化
- 【設計模式自習室】門面模式 Facade Pattern
- 【設計模式自習室】享元模式 Flyweight Pattern:減少對象數量
結構型——代理模式 Proxy Pattern
引子
通俗的來講,代理模式就是我們生活中常見的中介。在某些情況下,一個客戶不想或者不能直接引用一個對象,此時可以通過一個稱之為“代理”的第三者來實現間接引用。
為什么要用代理模式
- 中介隔離作用:在某些情況下,一個客戶類不想或者不能直接引用一個委托對象,而代理類對象可以在客戶類和委托對象之間起到中介的作用,其特征是代理類和委托類實現相同的接口。
- 開閉原則,增加功能:真正的業務功能還是由委托類來實現,但是可以在業務功能執行的前后加入一些公共的服務。例如我們想給項目加入緩存、日志這些功能,我們就可以使用代理類來完成,而沒必要打開已經封裝好的委托類。
定義
代理模式給某一個對象提供一個代理對象,并由代理對象控制對原對象的引用。
常見的代理區分為靜態代理和動態代理:
1. 靜態代理
在程序運行前就已經存在代理類的字節碼文件,代理類和真實主題角色的關系在運行前就確定了。
是由程序員創建或特定工具自動生成源代碼,在對其編譯。在程序員運行之前,代理類.class文件就已經被創建了。
2. 動態代理
為什么類可以動態的生成?
這就涉及到Java虛擬機的類加載機制了
Java虛擬機類加載過程主要分為五個階段:加載、驗證、準備、解析、初始化。其中加載階段需要完成以下3件事情:
- 通過一個類的全限定名來獲取定義此類的二進制字節流
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
- 在內存中生成一個代表這個類的 java.lang.Class 對象,作為方法區這個類的各種數據訪問入口
由于虛擬機規范對這3點要求并不具體,所以實際的實現是非常靈活的,關于第1點,獲取類的二進制字節流(class字節碼)就有很多途徑:
- 從ZIP包獲取,這是JAR、EAR、WAR等格式的基礎
- 從網絡中獲取,典型的應用是 Applet
- 運行時計算生成,這種場景使用最多的是動態代理技術,在 java.lang.reflect.Proxy 類中,就是用了 ProxyGenerator.generateProxyClass 來為特定接口生成形式為 *$Proxy 的代理類的二進制字節流
- 由其它文件生成,典型應用是JSP,即由JSP文件生成對應的Class類
- 從數據庫中獲取等等
所以,動態代理就是想辦法,根據接口或目標對象,計算出代理類的字節碼,然后再加載到JVM中使用。
更多Java類加載機制可以查看:
動態代理又有兩種典型的實現方式:JDK動態代理和CGLib動態代理
- 通過實現接口的方式 -> JDK動態代理
- 通過繼承類的方式 -> CGLIB動態代理
2.1 JDK反射機制(接口代理)
- 是在程序運行時通過反射機制動態創建的。
- 為需要攔截的接口生成代理對象以實現接口方法攔截功能。
2.2 CGLIB代理
- 其原理是通過字節碼技術為一個類創建子類,并在子類中采用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯。
- 但因為采用的是繼承,所以不能對final修飾的類進行代理。
- JDK動態代理與CGLib動態代理均是實現Spring AOP的基礎。
類圖
如果看不懂UML類圖,可以先粗略瀏覽下該圖,想深入了解的話,可以繼續谷歌,深入學習:
代理模式包含如下角色:
- Subject(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;
- RealSubject(真實主題角色):真正實現業務邏輯的類;
- Proxy(代理主題角色):用來代理和封裝真實主題;
時序圖
代碼實現和使用場景
代理模式得到了非常廣泛的應用,最常用的便是我們在Spring中使用的CGlib代理,所以我們將代碼示例和使用場景再次融合在一起來講解。
主要分為四個代碼小Demo,分別是:
- 靜態代理代碼示例
- JDK動態代理代碼示例
- CGLIB底層使用的ASM字節碼插入技術代碼示例
- CGLIB動態代理代碼示例
1. 靜態代理示例代碼
編寫一個接口 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();
}
}
通過靜態代理,我們達到了功能增強的目的,而且沒有侵入原代碼,這是靜態代理的一個優點。
2. 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]
上方1和2中的示例代碼來自,文中有詳細的細節分析:
http://laijianfeng.org/2018/12/Java-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E8%AF%A6%E8%A7%A3/
3. CGLib動態代理
CGLIB代理則是通過繼承的方式來生成代理類。
字節碼修改示例代碼
首先,我們先通過代碼來了解一下字節碼修改技術是如何實現的
我們使用ASM字節碼操作類庫(cglib底層使用的是ASM)來給出一段示例代碼,小伙伴們也可以自己在本地運行試試。
ASM 可以直接產生二進制 class 文件,它能被用來動態生成類或者增強既有類的功能。
ASM從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。
ASM相對于其他類似工具如BCEL、SERP、Javassist、CGLIB,它的最大的優勢就在于其性能更高,其jar包僅30K。Hibernate和Spring都使用了cglib代理,而cglib底層使用的是ASM,可見ASM在各種開源框架都有廣泛的應用。
Base類:被修改的類,該類實現了每3秒輸出一個process,用來模擬正在處理的請求。
package asm;
import java.lang.management.ManagementFactory;
public class Base {
public static void main(String[] args) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String s = name.split("@")[0];
//打印當前Pid
System.out.println("pid:"+s);
while (true) {
try {
Thread.sleep(3000L);
} catch (Exception e) {
break;
}
process();
}
}
public static void process() {
System.out.println("process");
}
}
執行字節碼修改和轉換的類:該類中,我們實現在被修改類前后都輸出start和end語句的方法
public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("Transforming " + className);
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("asm.Base");
CtMethod m = cc.getDeclaredMethod("process");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
接著我們生成字節碼修改的Jar包
public class TestAgent {
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new TestTransformer(), true);
try {
inst.retransformClasses(TransformTarget.class);
System.out.println("Agent Load Done.");
} catch (Exception e) {
System.out.println("agent load failed!");
}
}
}
我們將生成的agent.jar通過JVM Tool寫入正在執行的Base進程中。可以看出,本來只是3秒輸出procss的類變成了從前后輸出start和end的類,類被成功修改了。
上面字節碼修改的Demo代碼我放在了自己的Github倉庫中:
https://github.com/qqxx6661/Java_Practise/tree/master/ASMDemo
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();
}
}
運行結果和上面相同。
優缺點
靜態代理優缺點
- 優點:可以做到在符合開閉原則的情況下對目標對象進行功能擴展。
- 缺點:當需要代理多個類的時候,由于代理對象要實現與目標對象一致的接口,有兩種方式:
- 只維護一個代理類,由這個代理類實現多個接口,但是這樣就導致代理類過于龐大
- 新建多個代理類,每個目標對象對應一個代理類,但是這樣會產生過多的代理類
JDK動態代理優缺點
- 優勢:雖然相對于靜態代理,動態代理大大減少了我們的開發任務,同時減少了對業務接口的依賴,降低了耦合度。
- 劣勢:只能對接口進行代理
CGLIB動態代理優缺點
CGLIB創建的動態代理對象比JDK創建的動態代理對象的性能更高,但是CGLIB創建代理對象時所花費的時間卻比JDK多得多。
- 所以對于單例的對象,因為無需頻繁創建對象,用CGLIB合適,反之使用JDK方式要更為合適一些。
- 同時由于CGLib由于是采用動態創建子類的方法,對于final修飾的方法無法進行代理。
參考
- https://www.cnblogs.com/daniels/p/8242592.html
- http://laijianfeng.org/2018/12/Java-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E8%AF%A6%E8%A7%A3/
- https://www.cnblogs.com/jie-y/p/10732347.html
補充
裝飾模式與代理模式的區別
裝飾器模式關注于在一個對象上動態的添加方法,然而代理模式關注于控制對對象的訪問。
- 使用代理模式的時候,我們常常在一個代理類中創建一個對象的實例。
- 當我們使用裝飾器模式的時候,我們通常的做法是將原始對象作為一個參數傳給裝飾者的構造器。