Cglib及其基本使用

五月的倉頡發表於2017-04-03

前言

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

相關文章