Cglib proxy探祕

Lorenzo君發表於2018-08-09

  Cglib是一個非常著名的位元組碼修改庫,廣泛的應用於一些開源框架中。spring 裡面的aop技術就用到了Cglib這個庫。這篇文章想通過程式碼簡單的介紹一些Cglib的使用。

1. maven 依賴

  maven 倉庫裡面展示了目前可用的版本,本文的依賴如下:

 <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.2.5</version>
</dependency>
複製程式碼

2.代理模式

  代理模式是一種比較常見的設計模式,為什麼要用代理模式?代理模式可以隔離底層和呼叫者,可以做許可權的控制。知乎上有一個關於什麼是java動態代理的回答,可以參考。jdk自帶的動態代理要求被代理的類實現了介面,這嚴重的制約了它的使用範圍。cglib沒有這樣的限制,所以在很多地方只能使用cglib來實現動態代理。

3. 使用cglib實現代理

3.1 Hello World

  下面是一個簡單的cglib實現代理的例子:

public class CglibTestExample {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(PersonService.class);
        enhancer.setCallback((FixedValue) () -> "Hello Tom!");
        PersonService proxy = (PersonService) enhancer.create();

        String res = proxy.sayHello(null);
        System.out.println(res);

    }
}
class PersonService {
    public String sayHello(String name) {
        return "Hello " + name;
    }

    public Integer lengthOfName(String name) {
        return name.length();
    }
}
複製程式碼

Enhancer是cglib用來生成proxy的類,生成的proxy類是被代理類的子類,所以可以看到enhancer.setSuperclass(PersonService.class); 當然setSuperClass也可接收interface,官方的說明如下:

Set the class which the generated class will extend. As a convenience, if the supplied superclass is actually an interface, setInterfaces will be called with the appropriate argument instead. A non-interface argument must not be declared as final, and must have an accessible constructor.

需要注意的就是如果傳的不是介面,這個類一定不能是final的(無法被繼承)並且一定有一個可以訪問的建構函式。

3.2 CallBack

  CallBack是方法被攔截的時候呼叫的,這也就是為什麼String res = proxy.sayHello(null); 會返回Hello Tom!FixedValue實現了CallBack這個介面,返回一個固定的值。當然實現了CallBack介面的有很多,其中有一個叫MethodInterceptor,它的功能非常的強大,可以看下面的例子:

 enhancer.setCallback((MethodInterceptor) (obj, method, arg, proxy) -> {
        if (method.getDeclaringClass() != Object.class && method.getReturnType() == String.class) {
             return "Hello Tom!";
        } else {
        return proxy.invokeSuper(obj, arg);
    }
});
複製程式碼

理解上面這段程式碼之前,我們可以先看一下MethodInterceptor.intercept的簽名,

public java.lang.Object intercept(java.lang.Object obj,
                                  java.lang.reflect.Method method,
                                  java.lang.Object[] args,
                                  MethodProxy proxy)
                                throws java.lang.Throwable
複製程式碼

引數的含義如下:

obj - "this", the enhanced object
method - intercepted Method
args - argument array; primitive types are wrapped
proxy - used to invoke super (non-intercepted method); may be called as many times as needed
複製程式碼

  上面MethodInterceptor的含義是,如果申明被攔截方法的類名字是Object並且被攔截的方法返回的結果是String,那麼就返回Hello Tom!,否則就呼叫被攔截的方法。可以用proxy.lengthOfName("Mary") 測試發現返回的結果是4.

  下面說明一下Callback的所有subinterface的含義

  1. NoOp

Methods using this Enhancer callback will delegate directly to the default (super) implementation in the base class.

也就是說會呼叫被代理類的實現

  1. Dispatcher

Dispatching Enhancer callback. This is identical to the LazyLoader interface but needs to be separate so that Enhancer knows which type of code to generate.

可以看到 DispatcherLazyLoader 可以放到一起去對比和分析,這裡有一篇講 LazyLoader文章,說的算比較清楚的了,把文章裡面的例子換成 Dispatcher 就知道Diapatcher 是幹啥的了,同時附上 DispatcherloadObject 方法的說明:

Return the object which the original method invocation should be dispatched. This method is called for every method invocation.

  1. LazyLoader

Dispatcher的解析

  1. FixedValue

Enhancer callback that simply returns the value to return from the proxied method. No information about what method is being called is available to the callback, and the type of the returned object must be compatible with the return type of the proxied method. This makes this callback primarily useful for forcing a particular method (through the use of a CallbackFilter to return a fixed value with little overhead.

有一點需要注意的是 FixedValue 返回的型別必須和被代理方法返回的型別相容,還是文章最開始的例子,如果用proxy呼叫 lengthOfName 方法,就會報錯,String無法轉成Long。

3.3 CallBackFilter

  有時候我們需要根據被攔截的方法來執行不同的邏輯。MethodInterceptor提供了一定的靈活性,但是還是不夠。這個時候就需要使用setCallbackssetCallbackFiltersetCallbacks會傳一組CallBack,而CallBackFilter會根據被攔截的方法返回一個index,這個index會去CallBack陣列裡面取,下面是CallbackFilter.accept的說明。

public int accept(java.lang.reflect.Method method)
    Map a method to a callback.
Parameters:
    method - the intercepted method
Returns:
    the index into the array of callbacks (as specified by Enhancer.setCallbacks(net.sf.cglib.proxy.Callback[])) to use for the method.
複製程式碼

下面是一個簡單的例子:

enhancer.setCallbacks(new Callback[]{
                (FixedValue) () -> "0", (FixedValue) () -> 1
        });
        enhancer.setCallbackFilter((Method method) -> {
            if (method.getName().equals("sayHello")) {
                return 0;
            } else {
            return 1;
        }
    });
複製程式碼

  可以看到如果我們呼叫proxy.sayHello(null),會返回字串“0”,如果我們呼叫proxy.lengthOfName("abc") 會返回1。

3.3 CallbackTypes

  從doc可以看到,Callback這個介面沒有定義任何的方法,那在 setCallback 方法的時候,需要知道具體傳入的是Callback的哪個子介面,以便在呼叫Callback的時候知道具體呼叫哪個方法。例如:我們上面的例子中傳入的是 FixedValue ,那麼就會呼叫 loadObject。cglib 是怎麼知道傳入的Callback是什麼型別的呢?看一下下面的程式碼

private void preValidate() {
        if (callbackTypes == null) {
            callbackTypes = CallbackInfo.determineTypes(callbacks, false);
            validateCallbackTypes = true;
        }
        if (filter == null) {
        if (callbackTypes.length > 1) {
            throw new IllegalStateException("Multiple callback types possible but no filter specified");
        }
        filter = ALL_ZERO;
    }
}
複製程式碼

在cglib 裡面有一個 callbackTypes 的變數,你可以顯示的進行設定。如果你沒有設定,cglib會根據你傳入的callbacks來確定。也就是上面的 CallbackInfo.determineTypes(callbacks, false),跟蹤這個方法我們可以找到這個地方,這個地方列舉了Callback的所有subinterface。每一個type對應了一個Generator,在構建proxy的時候會根據不同的CallbackType,呼叫不同的Generator。

 private static final CallbackInfo[] CALLBACKS = {
    new CallbackInfo(NoOp.class, NoOpGenerator.INSTANCE),
    new CallbackInfo(MethodInterceptor.class, MethodInterceptorGenerator.INSTANCE),
    new CallbackInfo(InvocationHandler.class, InvocationHandlerGenerator.INSTANCE),
    new CallbackInfo(LazyLoader.class, LazyLoaderGenerator.INSTANCE),
    new CallbackInfo(Dispatcher.class, DispatcherGenerator.INSTANCE),
    new CallbackInfo(FixedValue.class, FixedValueGenerator.INSTANCE),
    new CallbackInfo(ProxyRefDispatcher.class, DispatcherGenerator.PROXY_REF_INSTANCE),
};
複製程式碼

4.總結

  這篇文章介紹了spring在使用cglib時用到的一些方法,為之後更好的理解spring aop打下基礎。

相關文章