Java動態代理之一CGLIB詳解

在上篇文章《Java代理模式及動態代理詳解》中我們介紹了Java中的靜態代理模式與動態代理模式,並以JDK原生動態代理作為示例進行講解。本篇文章我們來介紹一下基於CGLIB實現的動態代理,並與原生動態代理進行對比。

CGLIB介紹

CGLIB(Code Generation Library)是一個開源、高性能、高質量的Code生成類庫(代碼生成包)。

它可以在運行期擴展Java類與實現Java接口。Hibernate用它實現PO(Persistent Object 持久化對象)字節碼的動態生成,Spring AOP用它提供方法的interception(攔截)。

CGLIB的底層是通過使用一個小而快的字節碼處理框架ASM,來轉換字節碼並生成新的類。但不鼓勵大家直接使用ASM框架,因為對底層技術要求比較高。

使用實例

首先,引入CGLIB的依賴:

<code><dependency>
<groupid>cglib/<groupid>
<artifactid>cglib/<artifactid>
<version>3.2.5/<version>
/<dependency>/<code>

這裡我們以操作用戶數據的UserDao為例,通過動態代理來對其功能進行增強(執行前後添加日誌)。UserDao定義如下:

<code>public class UserDao {

public void findAllUsers(){
System.out.println("UserDao 查詢所有用戶");
}

public String findUsernameById(int id){
System.out.println("UserDao 根據ID查詢用戶");
return "公眾號:程序新視界";

}
}/<code>

創建一個攔截器,實現接口net.sf.cglib.proxy.MethodInterceptor,用於方法的攔截回調。

<code>import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
* @author sec
* @version 1.0
* @date 2020/3/24 8:14 上午
**/
public class LogInterceptor implements MethodInterceptor {

/**
*
* @param obj 表示要進行增強的對象
* @param method 表示攔截的方法
* @param objects 數組表示參數列表,基本數據類型需要傳入其包裝類型,如int-->Integer、long-Long、double-->Double
* @param methodProxy 表示對方法的代理,invokeSuper方法表示對被代理對象方法的調用
* @return 執行結果
* @throws Throwable 異常
*/
@Override
public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before(method.getName());
// 注意這裡是調用invokeSuper而不是invoke,否則死循環;
// methodProxy.invokeSuper執行的是原始類的方法;
// method.invoke執行的是子類的方法;
Object result = methodProxy.invokeSuper(obj, objects);
after(method.getName());
return result;
}

/**
* 調用invoke方法之前執行
*/
private void before(String methodName) {
System.out.println("調用方法" + methodName +"之【前】的日誌處理");
}

/**
* 調用invoke方法之後執行
*/
private void after(String methodName) {
System.out.println("調用方法" + methodName +"之【後】的日誌處理");
}
}/<code>

實現MethodInterceptor接口的intercept方法。該方法中參數:

  • obj:表示要進行增強的對象;
  • method:表示要被攔截的方法;
  • objects:表示要被攔截方法的參數;
  • methodProxy:表示要觸發父類的方法對象。

在方法的內部主要調用的methodProxy.invokeSuper,執行的原始類的方法。如果調用invoke方法否會出現死循環。

客戶端使用示例如下:

<code>import net.sf.cglib.proxy.Enhancer;

public class CglibTest {

public static void main(String[] args) {


// 通過CGLIB動態代理獲取代理對象的過程
// 創建Enhancer對象,類似於JDK動態代理的Proxy類
Enhancer enhancer = new Enhancer();
// 設置目標類的字節碼文件
enhancer.setSuperclass(UserDao.class);
// 設置回調函數
enhancer.setCallback(new LogInterceptor());
// create方法正式創建代理類
UserDao userDao = (UserDao) enhancer.create();
// 調用代理類的具體業務方法
userDao.findAllUsers();
userDao.findUsernameById(1);
}
}/<code>

執行客戶端的main方法打印結果如下:

<code>調用方法findAllUsers之【前】的日誌處理
UserDao 查詢所有用戶
調用方法findAllUsers之【後】的日誌處理
調用方法findUsernameById之【前】的日誌處理
UserDao 根據ID查詢用戶
調用方法findUsernameById之【後】的日誌處理/<code>

可以看到,我們方法前後已經被添加上對應的“增強處理”。

反編譯class

在main方法內的第一行我們也可以添加如下設置,來存儲代理類的class文件。

<code>// 代理類class文件存入本地磁盤,可反編譯查看源碼
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/zzs/temp");
/<code>

再次執行程序,我們可以看到在對應目錄下生成三個class文件:

<code>UserDao$$EnhancerByCGLIB$$1169c462.class
UserDao$$EnhancerByCGLIB$$1169c462$$FastClassByCGLIB$$22cae79c.class
UserDao$$FastClassByCGLIB$$197ae7fa.class/<code>

部分反編譯代碼如下:

<code>import java.lang.reflect.Method;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class UserDao$$EnhancerByCGLIB$$1169c462 extends UserDao implements Factory {
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
private MethodInterceptor CGLIB$CALLBACK_0;
private static Object CGLIB$CALLBACK_FILTER;
private static final Method CGLIB$findAllUsers$0$Method;
private static final MethodProxy CGLIB$findAllUsers$0$Proxy;
private static final Object[] CGLIB$emptyArgs;
private static final Method CGLIB$findUsernameById$1$Method;
private static final MethodProxy CGLIB$findUsernameById$1$Proxy;
private static final Method CGLIB$equals$2$Method;
private static final MethodProxy CGLIB$equals$2$Proxy;
private static final Method CGLIB$toString$3$Method;
private static final MethodProxy CGLIB$toString$3$Proxy;
private static final Method CGLIB$hashCode$4$Method;
private static final MethodProxy CGLIB$hashCode$4$Proxy;
private static final Method CGLIB$clone$5$Method;
private static final MethodProxy CGLIB$clone$5$Proxy;

public final void findAllUsers() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);

var10000 = this.CGLIB$CALLBACK_0;
}

if (var10000 != null) {
var10000.intercept(this, CGLIB$findAllUsers$0$Method, CGLIB$emptyArgs, CGLIB$findAllUsers$0$Proxy);
} else {
super.findAllUsers();
}
}

final String CGLIB$findUsernameById$1(int var1) {
return super.findUsernameById(var1);
}

public final String findUsernameById(int var1) {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}

return var10000 != null ? (String)var10000.intercept(this, CGLIB$findUsernameById$1$Method, new Object[]{new Integer(var1)}, CGLIB$findUsernameById$1$Proxy) : super.findUsernameById(var1);
}

// ...
}/<code>

從反編譯的源碼可以看出,代理對象繼承UserDao,攔截器調用intercept()方法,intercept()方法由自定義的LogInterceptor實現,所以最後調用LogInterceptor中的intercept()方法,從而完成了由代理對象訪問到目標對象的動態代理實現。

CGLIB創建動態代理類過程

(1)查找目標類上的所有非final的public類型的方法定義;

(2)將符合條件的方法定義轉換成字節碼;

(3)將組成的字節碼轉換成相應的代理的class對象;

(4)實現MethodInterceptor接口,用來處理對代理類上所有方法的請求。

JDK動態代理與CGLIB對比

JDK動態代理:基於Java反射機制實現,必須要實現了接口的業務類才生成代理對象。

CGLIB動態代理:基於ASM機制實現,通過生成業務類的子類作為代理類。

JDK Proxy的優勢:

最小化依賴關係、代碼實現簡單、簡化開發和維護、JDK原生支持,比CGLIB更加可靠,隨JDK版本平滑升級。而字節碼類庫通常需要進行更新以保證在新版Java上能夠使用。

基於CGLIB的優勢:

無需實現接口,達到代理類無侵入,只操作關心的類,而不必為其他相關類增加工作量。高性能。

小結

關於動態代理相關的實現就講這麼多,在具體的業務場景中如何選擇可根據它們的優缺點進行針對性的比對。不過作為Java領域的標杆項目Spring,很多功能都選擇使用CGLIB來實現。

本文首發來自微信公眾號:程序新視界。一個軟實力、硬技術同步學習的平臺。


分享到:


相關文章: