前言
最近一直在看Spring源碼,其實我之前一直知道AOP的基本實現原理:
- 如果針對接口做代理默認使用的是JDK自帶的Proxy+InvocationHandler
- 如果針對類做代理使用的是Cglib
- 即使針對接口做代理,也可以將代理方式配置成走Cglib的
之后要看AOP源碼了,Proxy+InvocationHandler這套我已經很熟了,想着如果把Cglib研究研究,應該看AOP源碼的時候會更快一些,因此本文就研究一下Cglib是什么、如何一些基本的使用、底層實現原理。
Cglib是什么
Cglib是一個強大的、高性能的代碼生成包,它廣泛被許多AOP框架使用,為他們提供方法的攔截。下圖是我網上找到的一張Cglib與一些框架和語言的關系:
對此圖總結一下:
- 最底層的是字節碼Bytecode,字節碼是Java為了保證“一次編譯、到處運行”而產生的一種虛擬指令格式,例如iload_0、iconst_1、if_icmpne、dup等
- 位於字節碼之上的是ASM,這是一種直接操作字節碼的框架,應用ASM需要對Java字節碼、Class結構比較熟悉
- 位於ASM之上的是CGLIB、Groovy、BeanShell,后兩種並不是Java體系中的內容而是腳本語言,它們通過ASM框架生成字節碼變相執行Java代碼,這說明在JVM中執行程序並不一定非要寫Java代碼----只要你能生成Java字節碼,JVM並不關心字節碼的來源,當然通過Java代碼生成的JVM字節碼是通過編譯器直接生成的,算是最“正統”的JVM字節碼
- 位於CGLIB、Groovy、BeanShell之上的就是Hibernate、Spring AOP這些框架了,這一層大家都比較熟悉
- 最上層的是Applications,即具體應用,一般都是一個Web項目或者本地跑一個程序
使用Cglib代碼對類做代理
下面演示一下Cglib代碼示例----對類做代理。首先定義一個Dao類,里面有一個select()方法和一個update()方法:
public class Dao { public void update() { System.out.println("PeopleDao.update()"); } public void select() { System.out.println("PeopleDao.select()"); } }
創建一個Dao代理,實現MethodInterceptor接口,目標是在update()方法與select()方法調用前后輸出兩句話:
public class DaoProxy implements MethodInterceptor { @Override public Object intercept(Object object, Method method, Object[] objects, MethodProxy proxy) throws Throwable { System.out.println("Before Method Invoke"); proxy.invokeSuper(object, objects); System.out.println("After Method Invoke"); return object; } }
intercept方法的參數名並不是原生的參數名,我做了自己的調整,幾個參數的含義為:
- Object表示要進行增強的對象
- Method表示攔截的方法
- Object[]數組表示參數列表,基本數據類型需要傳入其包裝類型,如int-->Integer、long-Long、double-->Double
- MethodProxy表示對方法的代理,invokeSuper方法表示對被代理對象方法的調用
寫一個測試類:
public class CglibTest { @Test public void testCglib() { DaoProxy daoProxy = new DaoProxy(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Dao.class); enhancer.setCallback(daoProxy); Dao dao = (Dao)enhancer.create(); dao.update(); dao.select(); } }
這是使用Cglib的通用寫法,setSuperclass表示設置要代理的類,setCallback表示設置回調即MethodInterceptor的實現類,使用create()方法生成一個代理對象,注意要強轉一下,因為返回的是Object。最后看一下運行結果:
Before Method Invoke
PeopleDao.update()
After Method Invoke
Before Method Invoke
PeopleDao.select()
After Method Invoke
符合我們的期望。
使用Cglib定義不同的攔截策略
再擴展一點點,比方說在AOP中我們經常碰到的一種復雜場景是:我們想對類A的B方法使用一種攔截策略、類A的C方法使用另外一種攔截策略。
在本例中,即我們想對select()方法與update()方法使用不同的攔截策略,那么我們先定義一個新的Proxy:
public class DaoAnotherProxy implements MethodInterceptor { @Override public Object intercept(Object object, Method method, Object[] objects, MethodProxy proxy) throws Throwable { System.out.println("StartTime=[" + System.currentTimeMillis() + "]"); method.invoke(object, objects); System.out.println("EndTime=[" + System.currentTimeMillis() + "]"); return object; } }
方法調用前后輸出一下開始時間與結束時間。為了實現我們的需求,實現一下CallbackFilter:
public class DaoFilter implements CallbackFilter { @Override public int accept(Method method) { if ("select".equals(method.getName())) { return 0; } return 1; } }
返回的數值表示順序,結合下面的代碼解釋,測試代碼要修改一下:
public class CglibTest { @Test public void testCglib() { DaoProxy daoProxy = new DaoProxy(); DaoAnotherProxy daoAnotherProxy = new DaoAnotherProxy(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Dao.class); enhancer.setCallbacks(new Callback[]{daoProxy, daoAnotherProxy, NoOp.INSTANCE}); enhancer.setCallbackFilter(new DaoFilter()); Dao dao = (Dao)enhancer.create(); dao.update(); dao.select(); } }
意思是CallbackFilter的accept方法返回的數值表示的是順序,順序和setCallbacks里面Proxy的順序是一致的。再解釋清楚一點,Callback數組中有三個callback,那么:
- 方法名為"select"的方法返回的順序為0,即使用Callback數組中的0位callback,即DaoProxy
- 方法名不為"select"的方法返回的順序為1,即使用Callback數組中的1位callback,即DaoAnotherProxy
因此,方法的執行結果為:
StartTime=[1491198489261] PeopleDao.update() EndTime=[1491198489275] Before Method Invoke PeopleDao.select() After Method Invoke
符合我們的預期,因為update()方法不是方法名為"select"的方法,因此返回1,返回1使用DaoAnotherProxy,即打印時間;select()方法是方法名為"select"的方法,因此返回0,返回0使用DaoProxy,即方法調用前后輸出兩句話。
這里要額外提一下,Callback數組中我特意定義了一個NoOp.INSTANCE,這表示一個空Callback,即如果不想對某個方法進行攔截,可以在DaoFilter中返回2,具體效果可以自己嘗試一下。
構造函數不攔截方法
如果Update()方法與select()方法在構造函數中被調用,那么也是會對這兩個方法進行相應的攔截的,現在我想要的是構造函數中調用的方法不會被攔截,那么應該如何做?先改一下Dao代碼,加一個構造方法Dao(),調用一下update()方法:
public class Dao { public Dao() { update(); } public void update() { System.out.println("PeopleDao.update()"); } public void select() { System.out.println("PeopleDao.select()"); } }
如果想要在構造函數中調用update()方法時,不攔截的話,Enhancer中有一個setInterceptDuringConstruction(boolean interceptDuringConstruction)方法設置為false即可,默認為true,即構造函數中調用方法也是會攔截的。那么測試方法這么寫:
public class CglibTest { @Test public void testCglib() { DaoProxy daoProxy = new DaoProxy(); DaoAnotherProxy daoAnotherProxy = new DaoAnotherProxy(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Dao.class); enhancer.setCallbacks(new Callback[]{daoProxy, daoAnotherProxy, NoOp.INSTANCE}); enhancer.setCallbackFilter(new DaoFilter()); enhancer.setInterceptDuringConstruction(false); Dao dao = (Dao)enhancer.create(); dao.update(); dao.select(); } }
運行結果為:
PeopleDao.update() StartTime=[1491202022297] PeopleDao.update() EndTime=[1491202022311] Before Method Invoke PeopleDao.select() After Method Invoke
看到第一次update()方法的調用,即Dao類構造方法中的調用沒有攔截,符合預期。
后記
本文演示了一些Cglib的基本用法,由於Cglib的原理探究篇幅比較長,就不放在本文寫了,會在下一篇文章中寫。
想要深入使用Cglib的朋友還需要多多嘗試Cglib的各種API,才能更好地使用這個優秀的字節碼生成框架。