1. 程式人生 > >java開發必學知識:動態代理

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

目錄

  • 1. 引言
  • 2. 代理模式及靜態代理
    • 2.1 代理模式說明
    • 2.2 靜態代理
    • 2.3 靜態代理侷限性
  • 3. 動態代理
    • 3.1 JAVA反射機制
    • 3.2 JDK動態代理
      • 3.2.1 JDK動態代理
      • 3.2.2 JDK動態代理與限制
    • 3.4 CGLIB動態代理
  • 4. 動態代理在Spring的應用:AOP
    • 4.1 AOP 概念
    • 4.2 AOP程式設計
      • 4.2.1 引入aop依賴
      • 4.2.2 定義切面、切點與通知
  • 5. 總結
  • 參考資料
  • 往期文章

一句話概括: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,有興趣的可以跑一下示例來感受一下。

參考資料

  • 動態代理: https://zgljl2012.com/dong-tai-dai-li/
  • java動態代理: https://juejin.im/post/5ad3e6b36fb9a028ba1fee6a
  • 計算機程式的思維邏輯 (86) - 動態代理:https://juejin.im/post/591c5fe5a22b9d0058439333
  • 從原始碼入手,一文帶你讀懂Spring AOP面向切面程式設計:https://juejin.im/post/5b9b1c8be51d450e9942fae4

往期文章

  • springboot+apache前後端分離部署https
  • springboot+logback 日誌輸出企業實踐(下)
  • springboot+logback 日誌輸出企業實踐(上)
  • springboot+swagger 介面文件企業實踐(下)
  • springboot+swagger介面文件企業實踐(上)

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

相關推薦

java開發知識:動態代理

目錄 1. 引言 2. 代理模式及靜態代理 2.1 代理模式說明 2.2 靜態代理 2.3 靜態代理侷限性 3. 動態代理 3.1 JAVA反

架構之路—java開發知識點詳細梳理

編程語言 Java 設計模式 Redis MySQL 大家好,今天為大家帶來了java開發必學的知識點的梳理,希望對小夥伴們在技術成長的道路上有所幫助。 數據庫 mysql 1、sql基本語法(數據類型、增刪改查、join、函數等)。 2、索引(分類,失效條件,explain的使用,優化條

輕鬆理解-中高階java開發會之 代理模式和裝飾模式

代理模式和裝飾模式分別是什麼?有什麼區別? 這兩個設計模式看起來很像。對裝飾器模式來說,裝飾者(decorator)和被裝飾者(decoratee)都實現同一個 介面。對代理模式來說,代理類(proxy class)和真實處理的類(real class)都實現同一個介面。此外,不論我們使用哪一個

02.MyBatis在DAO層開發使用的Mapper動態代理方式

.get div 技術 before nco mes session list http   在實際開發中,Mybatis作用於DAO層,那麽Service層該如何調用Mybatis   Mybatis鼓勵使用Mapper動態代理的方式   Mapper接口開發方法只需要程

java入門:HTML和CSS

java ;入門 ; html; css Java開發已經悄無聲息的走進我們的生活中,無論是手機軟件、手機Java遊戲還是電腦軟件等,只要你使用到電子產品就會碰到和Java有關的東西,更多的企業正采用Java語言開發網站,也吸引了好多誌同道合的朋友開始加入Java開發的行列。 我們知道在

Java實現AOP切面(動態代理

定義 row ack tcl getc java的反射機制 div implement reat Java.lang.reflect包下,提供了實現代理機制的接口和類: public interface InvocationHandler InvocationHandl

java反射機制應用之動態代理

代理類 過多 size bject interface 並且 編譯期 代理 抽象 1.靜態代理類和動態代理類區別 靜態代理:要求被代理類和代理類同時實現相應的一套接口;通過代理類的對象調用重寫接口的方法時,實際上執行的是被代理類的同樣的 方法的調用。 動態代理:在程序運

【Mybtais】Mybatis 插件 Plugin開發(一)動態代理步步解析

發現 返回 交集 hand proc 攔截 and mybatis invoke 需求:   對原有系統中的方法進行‘攔截’,在方法執行的前後添加新的處理邏輯。 分析:   不是辦法的辦法就是,對原有的每個方法進行修改,添加上新的邏輯;如果需要攔截的方法比較少,選擇此方法到

Java高階特性—反射和動態代理

1).反射   通過反射的方式可以獲取class物件中的屬性、方法、建構函式等,一下是例項: 2).動態代理   使用場景:       在之前的程式碼呼叫階段,我們用action呼叫service的方法實現業務即可。     由於之前在service中實現的業務可能不能夠滿足當先客戶的要求,需要我們重

Python全棧知識:如何使用dict和set操作方法,正確的案例詳解!

Python內建了字典:dict的支援,dict全稱dictionary,在其他語言中也稱為map,使用鍵-值(key-value)儲存,具有極快的查詢速度。 舉個例子,假設要根據同學的名字查詢對應的成績,如果用list實現,需要兩個list: names = ['Michael', 'Bob

輕鬆理解-中高階java開發會 之 二分查詢

二分查詢也叫折半查詢,二分查詢就是將查詢的鍵和子陣列的中間鍵作比較,如果被查詢的鍵小於中間鍵,就在左子陣列繼續查詢;如果大於中間鍵,就在右子陣列中查詢,否則中間鍵就是要找的元素 但是這個查詢必須要求陣列中的數字是有順序性的其實還有很多關於這個二分查詢的變種演算法,可以自行拓展下。 而且此演算法在

輕鬆理解 - 中高階java開發會之 氣泡排序

其實氣泡排序演算法是非常經典的演算法,放在中高階開發中其實不太合適,但是實際工作後很少碰到這個氣泡排序演算法,漸漸地很多開發人員就開始遺忘了。 其核心思想就是將相臨近的值比較大小,大的放後面小的放前面,從人文角度來考慮好像不是很厚道啊,但是目的要做排序嘛~~ 此演算法求職面試的時候出鏡率又是

輕鬆理解 - 中高階java開發會之 HashMap擴容機制

簡單的介紹和原始碼分析 先看put操作的原始碼: public V put(K key, V value) {     //判斷當前Hashmap(底層是Entry陣列)是否存值(是否為空陣列)     if (table == EMPTY_TABLE) {       

輕鬆理解 - 中高階java開發會 之 掌握 java阻塞隊(ArrayBlockingQueue與LinkedBlockingQueue)

在java開發中有些特殊場景下適用於阻塞佇列如: 多執行緒環境中,通過佇列可以很容易實現資料共享,比如經典的“生產者”和“消費者”模型中,通過佇列可以很便利地實現兩者之間的資料共享。假設我們有若干生產者執行緒,另外又有若干個消費者執行緒。如果生產者執行緒需要把準備好的資料共享給消費者執行緒,利用

HTML5 video 視訊標籤屬性詳解——前端小白知識

童靴們上網的話都知道,現在很多網站都提供視訊展示。我們在上一篇關於HTML5文章中提到了HTML5支援視訊和音訊,現在小編帶大家學習一下吧! 建立簡單的HTML5檔案 HTML5檔名同樣字尾'.html',我們在sublime中可以使用輸入英文歎號(!),然後按tab鍵就能建立一個簡單的HT

JAVA程式設計119——事務控制/動態代理/程式碼優化

一、優化方案:將代理類單獨抽取出來封裝成為一個代理工廠 package com.mollen.config; import com.mollen.utils.TransactionManager; import net.sf.cglib.proxy.Enhancer; import n

JAVA程式設計118——事務控制/動態代理

一、目錄結構 二、資料庫結構及測試資料 三、程式碼詳解 1、xml配置檔案:pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.or

輕鬆理解-中高階java開發會之 事務@Transactional

事務,專案的重要部分,不可或缺。解決事務問題至關重要,即使出現異常情況,它也可以保證資料的一致性。。 在spring中對事務的操作有@Transactional註解去實現或者寫配置xml去實現,因為目前本人全面擁抱springboot框架,所以偏向於與註解的方式去實現; 一般我們會在serv

輕鬆理解-中高階java開發會之 遇見最好的單例模式

什麼是單例模式? 單例模式確保某個類只有一個例項,而且自行例項化並向整個系統提供這個例項。 很多教程裡都寫不好的寫法和好的寫法這裡我只介紹好的寫法; 使用語法糖是最好的寫法,站在巨人的肩膀上會減少很多問題 一、我們使用java的類級內部類和靜態初始化來保證單利模式的可行性:

輕鬆理解-中高階java開發會之 Callable和Runable

相對於Runable,Callable的出鏡率其實並不高,二者區別其實有限,下面是具體分析: 我們進行非同步執行的時候,如果需要知道執行的結果,就可以使用callable介面,並且可以通過Future類獲取到非同步執行的結果資訊。如果不關心執行的結果,直接使用runnable介面就可以了,因為它