動態(tài)代理竟然如此簡單!
這篇文章我們來聊一下 Java 中的動態(tài)代理。
動態(tài)代理在 Java 中有著廣泛的應(yīng)用,比如 AOP 的實現(xiàn)原理、RPC遠(yuǎn)程調(diào)用、Java 注解對象獲取、日志框架、全局性異常處理、事務(wù)處理等。
在了解動態(tài)代理前,我們需要先了解一下什么是代理模式。
代理模式
代理模式(Proxy Pattern)是 23 種設(shè)計模式的一種,屬于結(jié)構(gòu)型模式。他指的是一個對象本身不做實際的操作,而是通過其他對象來得到自己想要的結(jié)果。這樣做的好處是可以在目標(biāo)對象實現(xiàn)的基礎(chǔ)上,增強額外的功能操作,即擴(kuò)展目標(biāo)對象的功能。
這里能體現(xiàn)出一個非常重要的編程思想:不要隨意去改源碼,如果需要修改,可以通過代理的方式來擴(kuò)展該方法。
如上圖所示,用戶不能直接使用目標(biāo)對象,而是構(gòu)造出一個代理對象,由代理對象作為中轉(zhuǎn),代理對象負(fù)責(zé)調(diào)用目標(biāo)對象真正的行為,從而把結(jié)果返回給用戶。
也就是說,代理的關(guān)鍵點就是代理對象和目標(biāo)對象的關(guān)系。
代理其實就和經(jīng)紀(jì)人一樣,比如你是一個明星,有很多粉絲。你的流量很多,經(jīng)常會有很多金主來找你洽談合作等,你自己肯定忙不過來,因為你要處理的不只是談合作這件事情,你還要懂才藝、拍戲、維護(hù)和粉絲的關(guān)系、營銷等。為此,你找了一個經(jīng)紀(jì)人,你讓他負(fù)責(zé)和金主談合作這件事,經(jīng)紀(jì)人做事很認(rèn)真負(fù)責(zé),他圓滿的完成了任務(wù),于是,金主找你談合作就變成了金主和你的經(jīng)紀(jì)人談合作,你就有更多的時間來忙其他事情了。如下圖所示
這是一種靜態(tài)代理,因為這個代理(經(jīng)紀(jì)人)是你自己親自挑選的。
但是后來隨著你的業(yè)務(wù)逐漸拓展,你無法選擇每個經(jīng)紀(jì)人,所以你索性交給了代理公司來幫你做。如果你想在 B 站火一把,那就直接讓代理公司幫你找到負(fù)責(zé)營銷方面的代理人,如果你想維護(hù)和粉絲的關(guān)系,那你直接讓代理公司給你找一些托兒就可以了,那么此時的關(guān)系圖會變?yōu)槿缦?/p>
此時你幾乎所有的工作都是由代理公司來進(jìn)行打理,而他們派出誰來幫你做這些事情你就不得而知了,這得根據(jù)實際情況來定,因為代理公司也不只是負(fù)責(zé)你一個明星,而且每個人所擅長的領(lǐng)域也不同,所以你只有等到有實際需求后,才會給你指定對應(yīng)的代理人,這種情況就叫做動態(tài)代理。
靜態(tài)代理
從編譯期是否能確定最終的執(zhí)行方法可以把代理模式分為靜態(tài)代理和動態(tài)代理,我們先演示一下動態(tài)代理,這里有一個需求,領(lǐng)導(dǎo)想在系統(tǒng)中添加一個用戶,但是他不自己添加,他讓下面的程序員來添加,我們看一下這個過程。
首先構(gòu)建一個用戶接口,定義一個保存用戶的模版方法。
- public interface UserDao {
- void saveUser();
- }
構(gòu)建一個用戶實現(xiàn)類,這個用戶實現(xiàn)類是真正進(jìn)行用戶操作的方法
- public class UserDaoImpl implements UserDao{
- @Override
- public void saveUser() {
- System.out.println(" ---- 保存用戶 ---- ");
- }
- }
構(gòu)建一個用戶代理類,用戶代理類也有一個保存用戶的方法,不過這個方法屬于代理方法,它不會執(zhí)行真正的保存用戶,而是內(nèi)部持有一個真正的用戶對象,進(jìn)行用戶保存。
- public class UserProxy {
- private UserDao userDao;
- public UserProxy(UserDao userDao){
- this.userDao = userDao;
- }
- public void saveUser() {
- System.out.println(" ---- 代理開始 ---- ");
- userDao.saveUser();
- System.out.println(" ---- 代理結(jié)束 ----");
- }
- }
下面是測試方法。
- public class UserTest {
- public static void main(String[] args) {
- UserDao userDao = new UserDaoImpl();
- UserProxy userProxy = new UserProxy(userDao);
- userProxy.saveUser();
- }
- }
新創(chuàng)建一個用戶實現(xiàn)類 (UserDaoImpl),它不執(zhí)行用戶操作。然后再創(chuàng)建一個用戶代理(UserProxy),執(zhí)行用戶代理的用戶保存(saveUser),其內(nèi)部會調(diào)用用戶實現(xiàn)類的保存用戶(saveUser)方法,因為我們 JVM 可以在編譯期確定最終的執(zhí)行方法,所以上面的這種代理模式又叫做靜態(tài)代理。
代理模式具有無侵入性的優(yōu)點,以后我們增加什么新功能的話,我們可以直接增加一個代理類,讓代理類來調(diào)用用戶操作,這樣我們就實現(xiàn)了不通過改源碼的方式增加了新的功能。然后生活很美好了,我們能夠直接添加我們想要的功能,在這美麗的日子里,cxuan 添加了用戶代理、日志代理等等無數(shù)個代理類。但是好景不長,cxuan 發(fā)現(xiàn)每次改代碼的時候都要改每個代理類,這就很煩啊!我寶貴的時光都浪費在改每個代理類上面了嗎?
動態(tài)代理
JDK 動態(tài)代理
于是乎 cxuan 上網(wǎng)求助,發(fā)現(xiàn)了一個叫做動態(tài)代理的概念,通讀了一下,發(fā)現(xiàn)有點意思,于是乎 cxuan 修改了一下靜態(tài)代理的代碼,新增了一個 UserHandler 的用戶代理,并做了一下 test,代碼如下
- public class UserHandler implements InvocationHandler {
- private UserDao userDao;
- public UserHandler(UserDao userDao){
- this.userDao = userDao;
- }
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- saveUserStart();
- Object obj = method.invoke(userDao, args);
- saveUserDone();
- return obj;
- }
- public void saveUserStart(){
- System.out.println("---- 開始插入 ----");
- }
- public void saveUserDone(){
- System.out.println("---- 插入完成 ----");
- }
- }
測試類如下
- public static void dynamicProxy(){
- UserDao userDao = new UserDaoImpl();
- InvocationHandler handler = new UserHandler(userDao);
- ClassLoader loader = userDao.getClass().getClassLoader();
- Class<?>[] interfaces = userDao.getClass().getInterfaces();
- UserDao proxy = (UserDao)Proxy.newProxyInstance(loader, interfaces, handler);
- proxy.saveUser();
- }
UserHandler 是用戶代理類,構(gòu)造函數(shù)中的 UserDao 是真實對象,通過把 UserDao 隱藏進(jìn) UserHandler ,通過 UserHandler 中的 UserDao 執(zhí)行真正的方法。
類加載器、接口數(shù)組你可以把它理解為一個方法樹,每棵葉子結(jié)點都是一個方法,通過后面的 proxy.saveUser() 來告訴 JVM 執(zhí)行的是方法樹上的哪個方法。
用戶代理是通過類加載器、接口數(shù)組、代理類來得到的。saveUser 方法就相當(dāng)于是告訴 proxy 你最終要執(zhí)行的是哪個方法,這個 proxy.saveUser 方法并不是最終直接執(zhí)行的 saveUser 方法,最終的 saveUser 方法是由 UserHandler 中的 invoke 方法觸發(fā)的。
上面這種在編譯期無法確定最終的執(zhí)行方法,而只能通過運行時動態(tài)獲取方法的代理模式被稱為 動態(tài)代理。
動態(tài)代理的優(yōu)勢是實現(xiàn)無侵入式的代碼擴(kuò)展,也可以對方法進(jìn)行增強。此外,也可以大大減少代碼量,避免代理類泛濫成災(zāi)的情況。
所以我們現(xiàn)在總結(jié)一下靜態(tài)代理和動態(tài)代理各自的特點。
靜態(tài)代理
- 靜態(tài)代理類:由程序員創(chuàng)建或者由第三方工具生成,再進(jìn)行編譯;在程序運行之前,代理類的 .class 文件已經(jīng)存在了。
- 靜態(tài)代理事先知道要代理的是什么。
- 靜態(tài)代理類通常只代理一個類。
動態(tài)代理
- 動態(tài)代理通常是在程序運行時,通過反射機(jī)制動態(tài)生成的。
- 動態(tài)代理類通常代理接口下的所有類。
- 動態(tài)代理事先不知道要代理的是什么,只有在運行的時候才能確定。
- 動態(tài)代理的調(diào)用處理程序必須事先繼承 InvocationHandler 接口,使用 Proxy 類中的 newProxyInstance 方法動態(tài)的創(chuàng)建代理類。
在上面的代碼示例中,我們是定義了一個 UserDao 接口,然后有 UserDaoImpl 接口的實現(xiàn)類,我們通過 Proxy.newProxyInstance 方法得到的也是 UserDao 的實現(xiàn)類對象,那么其實這是一種基于接口的動態(tài)代理。也叫做 JDK 動態(tài)代理。
是不是只有這一種動態(tài)代理技術(shù)呢?既然都這么問了,那當(dāng)然不是。
除此之外,還有一些其他代理技術(shù),不過是需要加載額外的 jar 包的,那么我們匯總一下所有的代理技術(shù)和它的特征
- JDK 的動態(tài)代理使用簡單,它內(nèi)置在 JDK 中,因此不需要引入第三方 Jar 包。
- CGLIB 和 Javassist 都是高級的字節(jié)碼生成庫,總體性能比 JDK 自帶的動態(tài)代理好,而且功能十分強大。
- ASM 是低級的字節(jié)碼生成工具,使用 ASM 已經(jīng)近乎于在使用字節(jié)碼編程,對開發(fā)人員要求最高。當(dāng)然,也是性能最好的一種動態(tài)代理生成工具。但 ASM 的使用很繁瑣,而且性能也沒有數(shù)量級的提升,與 CGLIB 等高級字節(jié)碼生成工具相比,ASM 程序的維護(hù)性較差,如果不是在對性能有苛刻要求的場合,還是推薦 CGLIB 或者 Javassist。
下面我們就來依次介紹一下這些動態(tài)代理工具的使用
CGLIB 動態(tài)代理
上面我們提到 JDK 動態(tài)代理是基于接口的代理,而 CGLIB 動態(tài)代理是針對類實現(xiàn)代理,主要是對指定的類生成一個子類,覆蓋其中的方法 ,也就是說 CGLIB 動態(tài)代理采用類繼承 -> 方法重寫的方式進(jìn)行的,下面我們先來看一下 CGLIB 動態(tài)代理的結(jié)構(gòu)。
如上圖所示,代理類繼承于目標(biāo)類,每次調(diào)用代理類的方法都會在攔截器中進(jìn)行攔截,攔截器中再會調(diào)用目標(biāo)類的方法。
下面我們通過一個示例來演示一下 CGLIB 動態(tài)代理的使用
首先導(dǎo)入 CGLIB 相關(guān) jar 包,我們使用的是 MAVEN 的方式
- <dependency>
- <groupId>cglib</groupId>
- <artifactId>cglib</artifactId>
- <version>3.2.5</version>
- </dependency>
然后我們新創(chuàng)建一個 UserService 類,為了和上面的 UserDao 和 UserDaoImpl 進(jìn)行區(qū)分。
- public class UserService {
- public void saveUser(){
- System.out.println("---- 保存用戶 ----");
- }
- }
之后我們創(chuàng)建一個自定義方法攔截器,這個自定義方法攔截器實現(xiàn)了攔截器類
- public class AutoMethodInterceptor implements MethodInterceptor {
- public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
- System.out.println("---- 方法攔截 ----");
- Object object = methodProxy.invokeSuper(obj, args);
- return object;
- }
- }
這里解釋一下這幾個參數(shù)都是什么含義
- Object obj: obj 是 CGLIB 動態(tài)生成代理類實例
- Method method: Method 為實體類所調(diào)用的被代理的方法引用
- Objectp[] args: 這個就是方法的參數(shù)列表
- MethodProxy methodProxy : 這個就是生成的代理類對方法的引用。
對于 methodProxy 參數(shù)調(diào)用的方法,在其內(nèi)部有兩種選擇:invoke() 和 invokeSuper() ,二者的區(qū)別不在本文展開說明,感興趣的讀者可以參考本篇文章:Cglib源碼分析 invoke和invokeSuper的差別
然后我們創(chuàng)建一個測試類進(jìn)行測試
- public static void main(String[] args) {
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(UserService.class);
- enhancer.setCallback(new AutoMethodInterceptor());
- UserService userService = (UserService)enhancer.create();
- userService.saveUser();
- }
測試類主要涉及 Enhancer 的使用,Enhancer 是一個非常重要的類,它允許為非接口類型創(chuàng)建一個 Java 代理,Enhancer 動態(tài)的創(chuàng)建給定類的子類并且攔截代理類的所有的方法,和 JDK 動態(tài)代理不一樣的是不管是接口還是類它都能正常工作。
JDK 動態(tài)代理與 CGLIB 動態(tài)代理都是將真實對象隱藏在代理對象的后面,以達(dá)到 代理 的效果。與 JDK 動態(tài)代理所不同的是 CGLIB 動態(tài)代理使用 Enhancer 來創(chuàng)建代理對象,而 JDK 動態(tài)代理使用的是 Proxy.newProxyInstance 來創(chuàng)建代理對象;還有一點是 CGLIB 可以代理大部分類,而 JDK 動態(tài)代理只能代理實現(xiàn)了接口的類。
Javassist 代理
Javassist是在 Java 中編輯字節(jié)碼的類庫;它使 Java 程序能夠在運行時定義一個新類, 并在 JVM 加載時修改類文件。我們使用最頻繁的動態(tài)特性就是 反射,而且反射也是動態(tài)代理的基礎(chǔ),我們之所以沒有提反射對動態(tài)代理的作用是因為我想在后面詳聊,反射可以在運行時查找對象屬性、方法,修改作用域,通過方法名稱調(diào)用方法等。實時應(yīng)用不會頻繁使用反射來創(chuàng)建,因為反射開銷比較大,另外,還有一種具有和反射一樣功能強大的特性那就是 Javaassist。
我們先通過一個簡單的示例來演示一下 Javaassist ,以及 Javaassist 如何創(chuàng)建動態(tài)代理。
我們?nèi)耘f使用上面提到的 UserDao 和 UserDaoImpl 作為基類。
我們新創(chuàng)建一個 AssistByteCode 類,它里面有一個 createByteCode 方法,這個方法主要做的事情就是通過字節(jié)碼生成 UserDaoImpl 實現(xiàn)類。我們下面來看一下它的代碼
- public class AssistByteCode {
- public static void createByteCode() throws Exception{
- ClassPool classPool = ClassPool.getDefault();
- CtClass cc = classPool.makeClass("com.cxuan.proxypattern.UserDaoImpl");
- // 設(shè)置接口
- CtClass ctClass = classPool.get("com.cxuan.proxypattern.UserDao");
- cc.setInterfaces(new CtClass[] {ctClass});
- // 創(chuàng)建方法
- CtMethod saveUser = CtMethod.make("public void saveUser(){}", cc);
- saveUser.setBody("System.out.println(\"---- 插入用戶 ----\");");
- cc.addMethod(saveUser);
- Class c = cc.toClass();
- cc.writeFile("/Users/mr.l/cxuan-justdoit");
- }
- }
由于本文并不是一個具體研究 Javaassist 的文章,所以我們不會過多研究細(xì)節(jié)問題,只專注于這個框架一些比較重要的類
ClassPool:ClassPool 就是一個 CtClass 的容器,而一個 CtClass 對象就是一個 class 對象的實例,這個實例和 class 對象一樣,包含屬性、方法等。
那么上面代碼主要做了哪些事兒呢?通過 ClassPool 來獲取 CtClass 所需要的接口、抽象類的 CtClass 實例,然后通過 CtClass 實例添加自己的屬性和方法,并通過它的 writeFile 把二進(jìn)制流輸出到當(dāng)前項目的根目錄路徑下。writeFile 其內(nèi)部是使用了 DataOutputStream 進(jìn)行輸出的。
流寫完后,我們打開這個 .class 文件如下所示
- public class UserDaoImpl implements UserDao {
- public void saveUser() {
- System.out.println("---- 插入用戶 ----");
- }
- public UserDaoImpl() {
- }
- }
可以對比一下上面發(fā)現(xiàn) UserDaoImpl 發(fā)現(xiàn)編譯器除了為我們添加了一個公有的構(gòu)造器,其他基本一致。
經(jīng)過這個簡單的示例后,cxuan 給你演示一下如何使用 Javaassist 動態(tài)代理。
首先我們先創(chuàng)建一個 Javaassist 的代理工廠,代碼如下
- public class JavaassistProxyFactory {
- public Object getProxy(Class clazz) throws Exception{
- // 代理工廠
- ProxyFactory proxyFactory = new ProxyFactory();
- // 設(shè)置需要創(chuàng)建的子類
- proxyFactory.setSuperclass(clazz);
- proxyFactory.setHandler((self, thisMethod, proceed, args) -> {
- System.out.println("---- 開始攔截 ----");
- Object result = proceed.invoke(self, args);
- System.out.println("---- 結(jié)束攔截 ----");
- return result;
- });
- return proxyFactory.createClass().newInstance();
- }
- }
上面我們定義了一個代理工廠,代理工廠里面創(chuàng)建了一個 handler,在調(diào)用目標(biāo)方法時,Javassist 會回調(diào) MethodHandler 接口方法攔截,來調(diào)用真正執(zhí)行的方法,你可以在攔截方法的前后實現(xiàn)自己的業(yè)務(wù)邏輯。最后的 proxyFactory.createClass().newInstance() 就是使用字節(jié)碼技術(shù)來創(chuàng)建了最終的子類實例,這種代理方式類似于 JDK 中的 InvocationHandler 接口。
測試方法如下
- public static void main(String[] args) throws Exception {
- JavaassistProxyFactory proxyFactory = new JavaassistProxyFactory();
- UserService userProxy = (UserService) proxyFactory.getProxy(UserService.class);
- userProxy.saveUser();
- }
ASM 代理
ASM 是一套 Java 字節(jié)碼生成架構(gòu),它可以動態(tài)生成二進(jìn)制格式的子類或其它代理類,或者在類被 Java 虛擬機(jī)裝入內(nèi)存之前,動態(tài)修改類。
下面我們使用 ASM 框架實現(xiàn)一個動態(tài)代理,ASM 生成的動態(tài)代理
以下代碼摘自 https://blog.csdn.net/lightj1996/article/details/107305662
- public class AsmProxy extends ClassLoader implements Opcodes {
- public static void createAsmProxy() throws Exception {
- // 目標(biāo)類類名 字節(jié)碼中類修飾符以 “/” 分割
- String targetServiceName = TargetService.class.getName().replace(".", "/");
- // 切面類類名
- String aspectServiceName = AspectService.class.getName().replace(".", "/");
- // 代理類類名
- String proxyServiceName = targetServiceName+"Proxy";
- // 創(chuàng)建一個 classWriter 它是繼承了ClassVisitor
- ClassWriter classWriter = new ClassWriter(0);
- // 訪問類 指定jdk版本號為1.8, 修飾符為 public,父類是TargetService
- classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, proxyServiceName, null, targetServiceName, null);
- // 訪問目標(biāo)類成員變量 為類添加切面屬性 “private TargetService targetService”
- classWriter.visitField(ACC_PRIVATE, "targetService", "L" + targetServiceName+";", null, null);
- // 訪問切面類成員變量 為類添加目標(biāo)屬性 “private AspectService aspectService”
- classWriter.visitField(ACC_PRIVATE, "aspectService", "L" + aspectServiceName+";", null, null);
- // 訪問默認(rèn)構(gòu)造方法 TargetServiceProxy()
- // 定義函數(shù) 修飾符為public 方法名為 <init>, 方法表述符為()V 表示無參數(shù),無返回參數(shù)
- MethodVisitor initVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
- // 從局部變量表取第0個元素 “this”
- initVisitor.visitVarInsn(ALOAD, 0);
- // 調(diào)用super 的構(gòu)造方法 invokeSpecial在這里的意思是調(diào)用父類方法
- initVisitor.visitMethodInsn(INVOKESPECIAL, targetServiceName, "<init>", "()V", false);
- // 方法返回
- initVisitor.visitInsn(RETURN);
- // 設(shè)置最大棧數(shù)量,最大局部變量表數(shù)量
- initVisitor.visitMaxs(1, 1);
- // 訪問結(jié)束
- initVisitor.visitEnd();
- // 創(chuàng)建有參構(gòu)造方法 TargetServiceProxy(TargetService var1, AspectService var2)
- // 定義函數(shù) 修飾符為public 方法名為 <init>, 方法表述符為(TargetService, AspectService)V 表示無參數(shù),無返回參數(shù)
- MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "(L" + targetServiceName + ";L"+aspectServiceName+";)V", null, null);
- // 從局部變量表取第0個元素 “this”壓入棧頂
- methodVisitor.visitVarInsn(ALOAD, 0);
- // this出棧 , 調(diào)用super 的構(gòu)造方法 invokeSpecial在這里的意思是調(diào)用父類方法。 <init>的owner是AspectService, 無參無返回類型
- methodVisitor.visitMethodInsn(INVOKESPECIAL, targetServiceName, "<init>", "()V", false);
- // 從局部變量表取第0個元素 “this”壓入棧頂
- methodVisitor.visitVarInsn(ALOAD, 0);
- // 從局部變量表取第1個元素 “targetService”壓入棧頂
- methodVisitor.visitVarInsn(ALOAD, 1);
- // this 和 targetService 出棧, 調(diào)用targetService put 賦值給this.targetService
- methodVisitor.visitFieldInsn(PUTFIELD, proxyServiceName, "targetService", "L" + targetServiceName + ";");
- // 從局部變量表取第0個元素 “this”壓入棧頂
- methodVisitor.visitVarInsn(ALOAD, 0);
- // 從局部變量表取第2個元素 “aspectService”壓入棧頂
- methodVisitor.visitVarInsn(ALOAD, 2);
- // this 和 aspectService 出棧 將 targetService put 賦值給this.aspectService
- methodVisitor.visitFieldInsn(PUTFIELD, proxyServiceName, "aspectService", "L" + aspectServiceName + ";");
- // 方法返回
- methodVisitor.visitInsn(RETURN);
- // 設(shè)置最大棧數(shù)量,最大局部變量表數(shù)量
- methodVisitor.visitMaxs(2, 3);
- // 方法返回
- methodVisitor.visitEnd();
- // 創(chuàng)建代理方法 修飾符為public,方法名為 demoQuest
- MethodVisitor visitMethod = classWriter.visitMethod(ACC_PUBLIC, "demoQuest", "()I", null, null);
- // 從局部變量表取第0個元素 “this”壓入棧頂
- visitMethod.visitVarInsn(ALOAD, 0);
- // this 出棧 將this.aspectService壓入棧頂
- visitMethod.visitFieldInsn(GETFIELD, proxyServiceName, "aspectService", "L"+aspectServiceName+";");
- // 取棧頂元素出棧 也就是targetService 調(diào)用其preOperation方法, demoQuest的owner是AspectService, 無參無返回類型
- visitMethod.visitMethodInsn(INVOKEVIRTUAL, aspectServiceName,"preOperation", "()V", false);
- // 從局部變量表取第0個元素 “this”壓入棧頂
- visitMethod.visitVarInsn(ALOAD, 0);
- // this 出棧, 取this.targetService壓入棧頂
- visitMethod.visitFieldInsn(GETFIELD, proxyServiceName, "targetService", "L"+targetServiceName+";");
- // 取棧頂元素出棧 也就是targetService調(diào)用其demoQuest方法, demoQuest的owner是TargetService, 無參無返回類型
- visitMethod.visitMethodInsn(INVOKEVIRTUAL, targetServiceName, "demoQuest", "()I", false);
- // 方法返回
- visitMethod.visitInsn(IRETURN);
- // 設(shè)置最大棧數(shù)量,最大局部變量表數(shù)量
- visitMethod.visitMaxs(1, 1);
- // 方法返回
- visitMethod.visitEnd();
- // 生成字節(jié)碼二進(jìn)制流
- byte[] code = classWriter.toByteArray();
- // 自定義classloader加載類
- Class<?> clazz = (new AsmProxy()).defineClass(TargetService.class.getName() + "Proxy", code, 0, code.length);
- // 取其帶參數(shù)的構(gòu)造方法
- Constructor constructor = clazz.getConstructor(TargetService.class, AspectService.class);
- // 使用構(gòu)造方法實例化對象
- Object object = constructor.newInstance(new TargetService(), new AspectService());
- // 使用TargetService類型的引用接收這個對象
- TargetService targetService;
- if (!(object instanceof TargetService)) {
- return;
- }
- targetService = (TargetService)object;
- System.out.println("生成代理類的名稱: " + targetService.getClass().getName());
- // 調(diào)用被代理方法
- targetService.demoQuest();
- // 這里可以不用寫, 但是如果想看最后生成的字節(jié)碼長什么樣子,可以寫 "ascp-purchase-app/target/classes/"是我的根目錄, 閱讀者需要將其替換成自己的
- String classPath = "/Users/mr.l/cxuan-justdoit/";
- String path = classPath + proxyServiceName + ".class";
- FileOutputStream fos =
- new FileOutputStream(path);
- fos.write(code);
- fos.close();
- }
- }
使用 ASM 生成動態(tài)代理的代碼比較長,上面這段代碼的含義就是生成類 TargetServiceProxy,用于代理TargetService ,在調(diào)用 targetService.demoQuest() 方法之前調(diào)用切面的方法 aspectService.preOperation();
測試類就直接調(diào)用 AsmProxy.createAsmProxy() 方法即可,比較簡單。
下面是我們生成 TargetServiceProxy 的目標(biāo)類
至此,我們已經(jīng)介紹了四種動態(tài)代理的方式,分別是JDK 動態(tài)代理、CGLIB 動態(tài)代理、Javaassist 動態(tài)代理、ASM 動態(tài)代理,那么現(xiàn)在思考一個問題,為什么會有動態(tài)代理的出現(xiàn)呢?或者說動態(tài)代理是基于什么原理呢?
其實我們上面已經(jīng)提到過了,沒錯,動態(tài)代理使用的就是反射 機(jī)制,反射機(jī)制是 Java 語言提供的一種基礎(chǔ)功能,賦予程序在運行時動態(tài)修改屬性、方法的能力。通過反射我們能夠直接操作類或者對象,比如獲取某個類的定義,獲取某個類的屬性和方法等。
關(guān)于 Java 反射的相關(guān)內(nèi)容可以參考 Java建設(shè)者的這一篇文章
給女同事講完代理后,女同事說:你好棒哦
另外還有需要注意的一點,從性能角度來講,有些人得出結(jié)論說是 Java 動態(tài)代理要比 CGLIB 和 Javaassist 慢幾十倍,其實,在主流 JDK 版本中,Java 動態(tài)代理可以提供相等的性能水平,數(shù)量級的差距不是廣泛存在的。而且,在現(xiàn)代 JDK 中,反射已經(jīng)得到了改進(jìn)和優(yōu)化。
我們在選型中,性能考量并不是主要關(guān)注點,可靠性、可維護(hù)性、編碼工作量同等重要。
本文轉(zhuǎn)載自微信公眾號「 Java建設(shè)者」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 Java建設(shè)者公眾號。