前言
最近一直在看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,才能更好地使用這個優秀的位元組碼生成框架。