你必須會的 JDK 動態代理和 CGLIB 動態代理

ytao發表於2020-04-06

你必須會的 JDK 動態代理和 CGLIB 動態代理

我們在閱讀一些 Java 框架的原始碼時,基本上常會看到使用動態代理機制,它可以無感的對既有程式碼進行方法的增強,使得程式碼擁有更好的擴充性。 通過從靜態代理、JDK 動態代理、CGLIB 動態代理來進行本文的分析。

靜態代理

靜態代理就是在程式執行之前,代理類位元組碼.class就已編譯好,通常一個靜態代理類也只代理一個目標類,代理類和目標類都實現相同的介面。 接下來就先通過 demo 進行分析什麼是靜態代理,當前建立一個 Animal 介面,裡面包含call函式。

package top.ytao.demo.proxy;

/**
 * Created by YangTao
 */
public interface Animal {

    void call();

}
複製程式碼

建立目標類 Cat,同時實現 Animal 介面,下面是 Cat 發出叫聲的實現。

package top.ytao.demo.proxy;

/**
 * Created by YangTao
 */
public class Cat implements Animal {

    @Override
    public void call() {
        System.out.println("喵喵喵 ~");
    }
}
複製程式碼

由於 Cat 叫之前是因為肚子餓了,所以我們需要在目標物件方法Cat#call之前說明是飢餓,這是使用靜態代理實現貓飢餓然後發出叫聲。

package top.ytao.demo.proxy.jdk;

import top.ytao.demo.proxy.Animal;

/**
 * Created by YangTao
 */
public class StaticProxyAnimal implements Animal {

    private Animal impl;

    public StaticProxyAnimal(Animal impl) {
        this.impl = impl;
    }

    @Override
    public void call() {
        System.out.println("貓飢餓");
        impl.call();
    }
}
複製程式碼

通過呼叫靜態代理實現貓飢餓和叫行為。

public class Main {

    @Test
    public void staticProxy(){
        Animal staticProxy = new StaticProxyAnimal(new Cat());
        staticProxy.call();
    }
}  
複製程式碼

執行結果

你必須會的 JDK 動態代理和 CGLIB 動態代理

代理類、目標類、介面之間關係如圖:

你必須會的 JDK 動態代理和 CGLIB 動態代理

以上內容可以看到代理類中通過持有目標類物件,然後通過呼叫目標類的方法,實現靜態代理。 靜態代理雖然實現了代理,但在一些情況下存在比較明顯不足之處:

  1. 當我們在 Animal 介面中增加方法,這時不僅實現類 Cat 需要新增該方法的實現,同時,由於代理類實現了 Animal 介面,所以代理類也必須實現 Animal 新增的方法,這對專案規模較大時,在維護上就不太友好了。
  2. 代理類實現Animal#call是針對 Cat 目標類的物件進行設定的,如果再需要新增 Dog 目標類的代理,那就必須再針對 Dog 類實現一個對應的代理類,這樣就使得代理類的重用型不友好,並且過多的代理類對維護上也是比較繁瑣。

上面問題,在 JDk 動態代理中就得到了較友好的解決。

JDK 動態代理

動態代理類與靜態代理類最主要不同的是,代理類的位元組碼不是在程式執行前生成的,而是在程式執行時再虛擬機器中程式自動建立的。 繼續用上面 Cat 類和 Animal 介面實現 JDK 動態代理。

實現 InvocationHandler 介面

JDK 動態代理類必須實現反射包中的 java.lang.reflect.InvocationHandler 介面,在此介面中只有一個 invoker 方法:

你必須會的 JDK 動態代理和 CGLIB 動態代理

InvocationHandler#invoker中必須呼叫目標類被代理的方法,否則無法做到代理的實現。下面為實現 InvocationHandler 的程式碼。

/**
 * Created by YangTao
 */
public class TargetInvoker implements InvocationHandler {
    // 代理中持有的目標類
    private Object target;

    public TargetInvoker(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("jdk 代理執行前");
        Object result = method.invoke(target, args);
        System.out.println("jdk 代理執行後");
        return result;
    }
}
複製程式碼

在實現InvocationHandler#invoker時,該方法裡有三個引數:

  • proxy 代理目標物件的代理物件,它是真實的代理物件。
  • method 執行目標類的方法
  • args 執行目標類的方法的引數

建立 JDK 動態代理類

建立 JDK 動態代理類例項同樣也是使用反射包中的 java.lang.reflect.Proxy 類進行建立。通過呼叫Proxy#newProxyInstance靜態方法進行建立。

/**
 *
 * Created by YangTao
 */
public class DynamicProxyAnimal {

    public static Object getProxy(Object target) throws Exception {
        Object proxy = Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 指定目標類的類載入
                target.getClass().getInterfaces(),  // 代理需要實現的介面,可指定多個,這是一個陣列
                new TargetInvoker(target)   // 代理物件處理器
        );
        return proxy;
    }

}
複製程式碼

Proxy#newProxyInstance中的三個引數(ClassLoader loader、Class<?>[] interfaces、InvocationHandler h):

  • loader 載入代理物件的類載入器
  • interfaces 代理物件實現的介面,與目標物件實現同樣的介面
  • h 處理代理物件邏輯的處理器,即上面的 InvocationHandler 實現類。

最後實現執行 DynamicProxyAnimal 動態代理:

public class Main {

    @Test
    public void dynamicProxy() throws Exception {
        Cat cat = new Cat();
        Animal proxy = (Animal) DynamicProxyAnimal.getProxy(cat);
        proxy.call();
    }
}    
複製程式碼

執行結果:

你必須會的 JDK 動態代理和 CGLIB 動態代理

通過上面的程式碼,有兩個問題:代理類是怎麼建立的和代理類怎麼呼叫方法的?

分析

Proxy#newProxyInstance入口進行原始碼分析:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    // 查詢或生成指定的代理類
    Class<?> cl = getProxyClass0(loader, intfs);

    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        // 獲取代理的構造器
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        // 處理代理類修飾符,使得能被訪問
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        // 建立代理類例項化
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}
複製程式碼

newProxyInstance 方法裡面獲取到代理類,如果類的作用不能訪問,使其能被訪問到,最後例項化代理類。這段程式碼中最為核心的是獲取代理類的getProxyClass0方法。

private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    // 實現類的介面不能超過 65535 個
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // 獲取代理類
    return proxyClassCache.get(loader, interfaces);
}
複製程式碼

如果 proxyClassCache 快取中存在指定的代理類,則從快取直接獲取;如果不存在,則通過 ProxyClassFactory 建立代理類。 至於為什麼介面最大為 65535,這個是由位元組碼檔案結構和 Java 虛擬機器規定的,具體可以通過研究位元組碼檔案瞭解。

進入到proxyClassCache#get,獲取代理類:

你必須會的 JDK 動態代理和 CGLIB 動態代理

繼續進入Factory#get檢視,

你必須會的 JDK 動態代理和 CGLIB 動態代理

最後到ProxyClassFactory#apply,這裡實現了代理類的建立。

private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>>{
    // 所有代理類名稱都已此字首命名
    private static final String proxyClassNamePrefix = "$Proxy";

    // 代理類名的編號
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        for (Class<?> intf : interfaces) {
            
            // 校驗代理和目標物件是否實現同一介面
            Class<?> interfaceClass = null;
            try {
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }
            
            // 校驗 interfaceClass 是否為介面
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            
            // 判斷當前 interfaceClass 是否被重複
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }

        // 代理類的包名
        String proxyPkg = null;     
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        // 記錄非 public 修飾符代理介面的包,使生成的代理類與它在同一個包下
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                // 獲取介面類名
                String name = intf.getName();
                // 去掉介面的名稱,獲取所在包的包名
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {
            // 如果介面類是 public 修飾,則用 com.sun.proxy 包名
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        // 建立代理類名稱
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        // 生成代理類位元組碼檔案
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
            // 載入位元組碼,生成指定代理物件
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            throw new IllegalArgumentException(e.toString());
        }
    }
}
複製程式碼

以上就是建立位元組碼流程,通過檢查介面的屬性,決定代理類位元組碼檔案生成的包名及名稱規則,然後載入位元組碼獲取代理例項。操作生成位元組碼檔案在ProxyGenerator#generateProxyClass中生成具體的位元組碼檔案,位元組碼操作這裡不做詳細講解。 生成的位元組碼檔案,我們可以通過儲存本地進行反編譯檢視類資訊,儲存生成的位元組碼檔案可以通過兩種方式:設定jvm引數或將生成 byte[] 寫入檔案。

你必須會的 JDK 動態代理和 CGLIB 動態代理

上圖的ProxyGenerator#generateProxyClass方法可知,是通過 saveGeneratedFiles 屬性值控制,該屬性的值來源:

private static final boolean saveGeneratedFiles = ((Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))).booleanValue();
複製程式碼

所以通過設定將生成的代理類位元組碼儲存到本地。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
複製程式碼

反編譯檢視生成的代理類:

你必須會的 JDK 動態代理和 CGLIB 動態代理

生成的代理類繼承了 Proxy 和實現了 Animal 介面,呼叫call方法,是通過呼叫 Proxy 持有的 InvocationHandler 實現TargetInvoker#invoker的執行。

CGLIB 動態代理

CGLIB 動態代理的實現機制是生成目標類的子類,通過呼叫父類(目標類)的方法實現,在呼叫父類方法時再代理中進行增強。

實現 MethodInterceptor 介面

相比於 JDK 動態代理的實現,CGLIB 動態代理不需要實現與目標類一樣的介面,而是通過方法攔截的方式實現代理,程式碼實現如下,首先方法攔截介面 net.sf.cglib.proxy.MethodInterceptor。

/**
 * Created by YangTao
 */
public class TargetInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("CGLIB 呼叫前");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("CGLIB 呼叫後");
        return result;
    }
}
複製程式碼

通過方法攔截介面呼叫目標類的方法,然後在該被攔截的方法進行增強處理,實現方法攔截器介面的 intercept 方法裡面有四個引數:

  • obj 代理類物件
  • method 當前被代理攔截的方法
  • args 攔截方法的引數
  • proxy 代理類對應目標類的代理方法

建立 CGLIB 動態代理類

建立 CGLIB 動態代理類使用 net.sf.cglib.proxy.Enhancer 類進行建立,它是 CGLIB 動態代理中的核心類,首先建立個簡單的代理類:

/**
 * Created by YangTao
 */
public class CglibProxy {

    public static Object getProxy(Class<?> clazz){
        Enhancer enhancer = new Enhancer();
        // 設定類載入
        enhancer.setClassLoader(clazz.getClassLoader());
        // 設定被代理類
        enhancer.setSuperclass(clazz);
        // 設定方法攔截器
        enhancer.setCallback(new TargetInterceptor());
        // 建立代理類
        return enhancer.create();
    }

}
複製程式碼

設定被代理類的資訊和代理類攔截的方法的回撥執行邏輯,就可以實現一個代理類。 實現 CGLIB 動態代理呼叫:

public class Main {

    @Test
    public void dynamicProxy() throws Exception {
        Animal cat = (Animal) CglibProxy.getProxy(Cat.class);
        cat.call();
    }
}
複製程式碼

執行結果:

你必須會的 JDK 動態代理和 CGLIB 動態代理

CGLIB 動態代理簡單應用就這樣實現,但是 Enhancer 在使用過程中,常用且有特色功能還有回撥過濾器 CallbackFilter 的使用,它在攔截目標物件的方法時,可以有選擇性的執行方法攔截,也就是選擇被代理方法的增強處理。使用該功能需要實現 net.sf.cglib.proxy.CallbackFilter 介面。 現在增加一個方法攔截的實現:

/**
 * Created by YangTao
 */
public class TargetInterceptor2 implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("CGLIB 呼叫前 TargetInterceptor2");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("CGLIB 呼叫後 TargetInterceptor2");
        return result;
    }
}
複製程式碼

然後在 Cat 中增加 hobby 方法,因為 CGLIB 代理無需實現介面,可以直接代理普通類,所以不需再 Animal 介面中增加方法:

package top.ytao.demo.proxy;

/**
 * Created by YangTao
 */
public class Cat implements Animal {

    @Override
    public void call() {
        System.out.println("喵喵喵 ~");
    }
    
    public void hobby(){
        System.out.println("fish ~");
    }
}
複製程式碼

實現回撥過濾器 CallbackFilter

/**
 * Created by YangTao
 */
public class TargetCallbackFilter implements CallbackFilter {
    @Override
    public int accept(Method method) {
        if ("hobby".equals(method.getName()))
            return 1;
        else
            return 0;
    }
}
複製程式碼

為演示呼叫不同的方法攔截器,在 Enhancer 設定中,使用Enhancer#setCallbacks設定多個方法攔截器,引數是一個陣列,TargetCallbackFilter#accept返回的數字即為該陣列的索引,決定呼叫的回撥選擇器。

/**
 * Created by YangTao
 */
public class CglibProxy {

    public static Object getProxy(Class<?> clazz){
        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(clazz.getClassLoader());
        enhancer.setSuperclass(clazz);
        enhancer.setCallbacks(new Callback[]{new TargetInterceptor(), new TargetInterceptor2()});
        enhancer.setCallbackFilter(new TargetCallbackFilter());
        return enhancer.create();
    }

}
複製程式碼

按程式碼實現邏輯,call 方法會呼叫 TargetInterceptor 類,hobby 類會呼叫 TargetInterceptor2 類,執行結果:

你必須會的 JDK 動態代理和 CGLIB 動態代理

CGLIB 的實現原理是通過設定被代理的類資訊到 Enhancer 中,然後利用配置資訊在Enhancer#create生成代理類物件。生成類是使用 ASM 進行生成,本文不做重點分析。如果不關注 ASM 的操作原理,只看 CGLIB 的處理原理還是比較容易讀懂。這裡主要看生成後的代理類位元組碼檔案,通過設定

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "F:\\xxx");
複製程式碼

可儲存生成的位元組到 F:\xxx 資料夾中

你必須會的 JDK 動態代理和 CGLIB 動態代理

通過反編譯可看到

你必須會的 JDK 動態代理和 CGLIB 動態代理

代理類繼承了目標類 Cat,同時將兩個方法攔截器載入到了代理類中,通過 Callbacks 下標作為變數名字尾進行區分,最後呼叫指定的方法攔截器中的 intercept 實現代理的最終的執行結果。 這裡需要注意的是 CGLIB 動態代理不能代理 final 修飾的類和方法。

最後

通過反編譯生成的 JDK 代理類和 CGLIB 代理類,我們可以看到它們兩種不同機制的實現: JDK 動態代理是通過實現目標類的介面,然後將目標類在構造動態代理時作為引數傳入,使代理物件持有目標物件,再通過代理物件的 InvocationHandler 實現動態代理的操作。 CGLIB 動態代理是通過配置目標類資訊,然後利用 ASM 位元組碼框架進行生成目標類的子類。當呼叫代理方法時,通過攔截方法的方式實現代理的操作。 總的來說,JDK 動態代理利用介面實現代理,CGLIB 動態代理利用繼承的方式實現代理。

動態代理在 Java 開發中是非常常見的,在日誌,監控,事務中都有著廣泛的應用,同時在大多主流框架中的核心元件中也是少不了使用的,掌握其要點,不管是開發還是閱讀其他框架原始碼時,都是必須的。


個人部落格: ytao.top

關注公眾號 【ytao】,更多原創好文

我的公眾號

相關文章