java開發必學知識:動態代理

一句話概括:java動態代理通過反射機制,可在不修改原代碼的情況下添加新的功能,應用於多種場景,簡單、實用、靈活,是java開發必學知識,本文將對動態代理使用進行詳細介紹。

1. 引言

最近開發項目過程中需要使用動態代理來實現功能,趁此機會把動態代理的知識點進行了一次梳理。在java開發過程中,當需要對已有的代碼(方法)前後添加新功能,而不想修改或不方便修改原代碼的情況下,如需要在執行某個已有的方法前後輸出日誌,以記錄方法執行的記錄,這個時候,動態代理就派上用場了。動態代理可以有以下幾使用場景:

  • 記錄跟蹤:對函數執行前後統一輸出日誌跟蹤執行情況
  • 計時:統一對函數執行用時進行計算(前後時間記錄之差)
  • 權限校驗:統一在函數執行前進行權限校驗
  • 事務:統一對函數作爲事務處理
  • 異常處理:對某一類函數執行輸出的異常進行統一捕獲處理
  • 動態切換數據源:多數據源切換或動態添加數據源

動態代理首先是代理,對應的有靜態代理,然後是動態代理,在Spring中還有動態代理的應用-AOP(面向切面編程)。本文針對這些內容,梳理了以下幾個知識點:

  • 瞭解代理模式並實現靜態代理
  • 針對動態代理,描述它使用到的反射機制,以及JDK、CGLIB兩種動態代理的實現
  • 描述如何在在Spring中,使用AOP實現面向切面編程

本文所涉及到的靜態代理、反射、動態代理及AOP使用有相應的示例代碼:https://github.com/mianshenglee/my-example/tree/master/dynamic-proxy-demo,讀者可結合一起看。

2. 代理模式及靜態代理

2.1 代理模式說明

代理,即調用者不需要跟實際的對象接觸,只跟代理打交道。現實中典型的例子就是買房者,賣房者及房產中介。賣房者作爲委託方,委託房產中介作爲代理幫賣房,而買房者只需要跟房產中介打交道即可。這樣就可以做到委託者與買房者解耦。再來看以下的圖,就可以瞭解代理模式(定義:爲其它對象提供代理以控制這個對象的訪問)了:

代理模式

Proxy相當於房產中介,RealSubject就是賣房者,Client就是買房者,operation方法就是委託的內容,ProxyRealSubject共同實現一個接口,以表示他們的操作一致。

2.2 靜態代理

按照上面的代理模式的代碼實現,其實就是靜態代理了。靜態意思是代理類Proxy是在代碼在編譯時就確定了,而不是在代碼中動態生成。如下,我們在示例代碼中,接口中有兩個函數(doAction1doAction2),對應的實現類:

/**
 * 服務實現類:委託類
 **/
public class ServiceImpl implements IService {
    @Override
    public void doAction1() { System.out.println(" do action1 ");}

    @Override
    public void doAction2() { System.out.println(" do action2 ");}
}

現在的需求是需要在doAction1方法執行前和執行後輸出日誌以便於跟蹤,然後對doAction2方法的執行時間進行計算,但又不允許使用修改ServiceImpl類,這個時候,通過一個代理類就可以實現。如下:

/**
 * 服務代理類:代理類
 **/
public class ServiceProxy implements IService {
    /**
     * 關聯實際委託類
     **/
    private ServiceImpl serviceImpl;
    
    public ServiceProxy(ServiceImpl serviceImpl) {this.serviceImpl = serviceImpl;}

    @Override
    public void doAction1() {
        System.out.println(" proxy log begin ");
        serviceImpl.doAction1();
        System.out.println(" proxy log end ");
    }

    @Override
    public void doAction2() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("timeCalculation");

        serviceImpl.doAction2();

        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
    }
}

客戶端執行時,只需要使用代理類執行對應的方法即可,如下:

ServiceProxy serviceProxy = new ServiceProxy(new ServiceImpl());
serviceProxy.doAction1();

執行的結果如下:

日誌輸出

計時輸出

2.3 靜態代理侷限性

從上面代碼可以發現,靜態代理很簡單,可以很快實現我們打印日誌及計算執行用時的需求。但靜態需求有它的侷限性,就是當接口中的函數增加的時候,代理類中會出現很多臃腫、重複的代碼。比如上述接口若有100個函數,其中50個函數需要打印日誌,50個函數需要計算用時,那麼,代理類中,像doAction1這樣的日誌輸出代碼就要寫50次,像doAction2這樣使用StopWatch計時的代碼同樣需要需要寫50次。一旦出現重複的代碼,就應該知道這個代碼需要優化了。既然多個函數用了相同的代碼,有沒有一種方式只需要把這代碼寫一次,然後應用到多個函數?這個時候就需要動態代理。

3. 動態代理

前面提到,使用動態代理解決靜態代理中重複代碼的問題,其實就像是把全部需要代理執行的函數看成是一個可以動態執行的函數,把這個函數像針線一樣,織入到需要執行的額外代碼中間。如前面的日誌輸出,把函數織入到日誌輸出的代碼中間。怎樣能把函數動態執行?這就需要用到JAVA的反射技術了,這也是動態代理的關鍵。

3.1 JAVA反射機制

JAVA的反射技術其實就是在運行狀態中,動態獲取類的屬性和方法,也可以夠調用和操作這個類對象的方法和屬性,這種功能就叫做反射。使用反射,可以動態生成類對象,而不用像之前的代碼(使用new)靜態生成。也可以動態地執行類對象的方法。在示例代碼中的reflection包及ReflectionTest類展示瞭如何動態執行某個類對象的方法。如下,定義了某個類及它的方法:

public class ReflectionService {
    public void doSomething(){
        System.out.println(" logging reflection service");
    }
}

使用反射,動態生成這個類對象,並使用invoke來執行doSomething方法。

//加載類
Class<?> refClass = Class.forName("me.mason.demo.proxy.refrection.ReflectionService");
//生成類對象
Object refClassObject = refClass.getConstructor().newInstance();
//調用類對象方法
Method method = refClass.getDeclaredMethod("doSomething");
method.invoke(refClassObject);

從以上代碼可知道,只要知道類路徑和它定義的方法名,就可以動態來執行這個方法了,這裏動態的意思就是不把需要執行的代碼寫死在代碼中(編譯時即確定),而是靈活的在運行時才生成。

3.2 JDK動態代理

3.2.1 JDK動態代理

知道了反射機制可以動態執行類對象,就容易理解動態代理了。在JDK中,已默認提供了動態代理的實現,它的關鍵點也是在於通過反射執行invoke來動態執行方法,主要實現流程如下:

  • 實現InvocationHandler,由它來實現invoke方法,執行代理函數
  • 使用Proxy類根據類的加載器及接口說明,創建代理類,同時關聯委託類
  • 使用代理類執行代理函數,則會調用invoke方法,完成代理

在示例代碼中JdkLogProxyHandler類是日誌輸出代理類,代碼如下:

/**
 * 日誌動態代理類:JDK實現
 **/
public class JdkLogProxyHandler implements InvocationHandler {
    private Object targetObject;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(" jdk dynamic proxy log begin ");
        Object result = method.invoke(targetObject, args);
        System.out.println(" jdk dynamic proxy log end ");
        return result;
    }

    /**
     * 根據委託類動態產生代理類
     * @param targetObject 委託類對象
     * @return 代理類
     */
    public Object createPorxy(Object targetObject){
        this.targetObject = targetObject;
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader()
        ,targetObject.getClass().getInterfaces(),this);
    }
}

在客戶端使用時,需要產生代理類,對的日誌輸出,執行如下(執行輸出結果與靜態代理功能一致):

@Test
void testLogProxy() {
    JdkLogProxyHandler logProxyHandler = new JdkLogProxyHandler();
    IService proxy = (IService)logProxyHandler.createPorxy(new ServiceImpl());
    proxy.doAction1();
    System.out.println("############");
    proxy.doAction2();
}

這裏把日誌輸出代理作爲一類,把函數執行計時作爲一類(JdkTimeProxyHandler),關注代理內容本身,而不是針對委託類的函數。這裏的日誌輸出和函數執行計時,就是切面(後面會提到)。

可以比較一下,使用這種動態代理,與前面靜態代理的區別:

  • 代理不是固定在某個接口或固定的某個類,而在根據參數動態生成,不是固定(靜態)的
  • 在代理中無需針對接口的函數來一個一個實現,只需要針對代理的功能寫一次即可
  • 若有多個函數需要寫日誌輸出,代理類無需再做修改,執行函數時會自動invoke來完成,就像把函數織入到代碼中。這樣就解決了前面靜態代理的侷限。

3.2.2 JDK動態代理與限制

JDK默認提供的動態代理機制使用起來很簡單方便,但它也有相應的限制,就是隻能動態代理實現了接口的類,如果類沒有實現接口,只是單純的一個類,則沒有辦法使用InvocationHandler的方式來動態代理了。此時,就需要用到CGLIB來代理。

3.4 CGLIB動態代理

CGLIB(Code Generator Library)是一個強大的、高性能的代碼生成庫。CGLIB代理主要通過對字節碼的操作,爲對象引入間接級別,以控制對象的訪問。針對上面沒有實現接口的類,CGLIB主要是通過繼承來完成動態代理的。在使用方法上,主要也是有3個步驟:

  • 實現MethodInterceptor接口,在intercept方法中實現代理內容(如日誌輸出)
  • 使用Enhancer及委託類生成代理類
  • 使用代理類執行函數,就會動態調用intercept方法的實現

如下所示是使用CGLIB來實現類的動態代理:

/**
 * 日誌動態代理:cglib實現
 **/
public class CglibLogProxyInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println(" cglib dynamic proxy log begin ");
        Object result = methodProxy.invokeSuper(object, args);
        System.out.println(" cglib dynamic proxy log begin ");
        return result;
    }

    /**
     * 動態創建代理
     *
     * @param cls 委託類
     * @return
     */
    public static <T> T createProxy(Class<T> cls) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(cls);
        enhancer.setCallback(new CglibLogProxyInterceptor());
        return (T) enhancer.create();
    }
}

從上面代碼可知道,代理類是通過Enhancer設置委託類爲父類(setsuperclass),並把當前的intercept方法作爲回調,以此創建代理類,在客戶端執行代理時,則會執行回調,從而達到代理效果,客戶端執行如下:

@Test
void testLogProxy() {
    CglibService proxy = CglibLogProxyInterceptor.createProxy(CglibService.class);
    proxy.doAction1();
    System.out.println("############");
    proxy.doAction2();
}

4. 動態代理在Spring的應用:AOP

前面提到JDK的默認動態代理和CGLIB動態代理,在Spring中,AOP(面向切面編程)就是使用這兩個技術實現的(如果有實現接口的類使用JDK動態代理,沒有實現接口的類則使用CGLIB)。具體到在Spring應用中,如何使用AOP進行切面編程,示例代碼中使用springboot工程,模擬提供user的增刪改查的REST接口,通過切面對所有Service類的函數統一進行日誌輸出。

4.1 AOP 概念

關於AOP的概念,從理解這兩個問題開始,即代理髮生在什麼地方,以什麼樣的形式添加額外功能代碼。

  • 切面(Aspect):前面提到的日誌輸出代理和函數執行計時代理,它們其實都是與業務邏輯無關,只是在各個業務邏輯中都添加功能,這種代理就是切面。將橫切關注點與業務邏輯分離的編程方式,每個橫切關注點都集中在一個地方,而不是分散在多處代碼中。
  • 切點(PointCut):明確什麼地方需要添加額外功能,這些地方有可能是一類函數(比如有多個函數都需要輸出日誌),因此需要使用一定的規則定義是哪一類函數。
  • 連接點(JoinPoint):就是具體被攔截添加額外功能的地方,其實就是執行的某一個具體函數,是前面切點定義的其中一個函數。
  • 通知(advice):明確以什麼樣的形式添加額外功能,可以在函數執行前(before),後(after),環繞(around),函數正常返回後通知(afterReturning)和異常返回後通知(afterThrowing)。

在AOP編程中,上面提到的概念,都有對應的註解進行使用,通過註解,就可以實現切面功能。

4.2 AOP編程

4.2.1 引入aop依賴

Springboot有提供aop的starter,添加以下依賴,即可使用AOP相關功能。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

4.2.2 定義切面、切點與通知

本示例的需求是對service包下所有類的全部函數統一進行日誌輸出。因此我們定義一個LogAopAspect作爲這個日誌輸出功能的切面(使用註解@Aspect),使用@Pointcut來確定輸出點的匹配規則是service這個包下所有類的全部函數。當真正某個函數執行時,通過動態代理執行通知(使用註解@Before@After@Around等)。具體的輸出動作,也就是在這些通知裏。

@Slf4j
@Aspect
@Component
public class LogAopAspect {

    /**
     * 切點:對service包中所有方法進行織入
     */
    @Pointcut("execution(* me.mason.demo.proxy.springaop.service.*.*(..))")
    private void allServiceMethodPointCut() {}

    @Before("allServiceMethodPointCut()")
    public void before() { log.info(" spring aop before log begin ");}

    @AfterReturning("allServiceMethodPointCut()")
    public void after() { log.info(" spring aop before log end ");}

    /**
     * 環繞通知,需要返回調用結果
     */
    @Around("allServiceMethodPointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info(" spring aop around log begin ");
        try { 
            return proceedingJoinPoint.proceed();
        } finally { 
            log.info(" spring aop around log end ");
        }
    }

}

通過上面的類定義,即可完成動態代理,而不需要像上面的JDK和GCLIB那樣自己實現接口來操作。

  • AOP的底層實現依然是使用JDK和CGLIB來實現動態代理的,若類有實現接口則使用JDK,沒有則使用CGLIB。
  • Pointcut的定義規則是指示器+正則式,指示器有參數定義(agrs),執行方法(execution),指定對象(target),指定類型(within)及相應的註解(使用@開頭)。正則式中*表示任何內容,(..)表示任意參數匹配。示例中execution(* me.mason.demo.proxy.springaop.service.*.*(..))表示對執行方法進行攔截,攔截的是me.mason.demo.proxy.springaop.service包下的所有類的所有函數,返回值不限,參數不限。
  • 環繞通知(Around)需要有返回值來返回連接點執行後的結果。

5. 總結

本文對JAVA的動態代理知識進行了梳理,先從代理模式說起,使用靜態代理實現簡單的外加功能,接着通過講述了JAVA動態代理使用到的反射機制,並通過示例實現JDK和CGLIB兩種動態代理功能,最後結合springboot示例,使用AOP編程,實現對關心的類進行日誌輸出的切面功能。通過動態代理,我們可以把一些輔助性的功能抽取出來,在不修改業務邏輯的情況下,完成輔助功能的添加。所以當你需要添加新功能,又不想修改原代碼的情況下,就用動態代理吧!

本文配套的示例,demo地址https://github.com/mianshenglee/my-example/tree/master/dynamic-proxy-demo,有興趣的可以跑一下示例來感受一下。

參考資料

往期文章

關注我的公衆號(搜索Mason技術記錄),獲取更多技術記錄:

mason

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章