1. 程式人生 > >Spring AOP之---基於JDK動態代理和CGLib動態代理的AOP實現

Spring AOP之---基於JDK動態代理和CGLib動態代理的AOP實現

AOP(面向切面程式設計)是OOP的有益補充,它只適合那些具有橫切邏輯的應用場合,如效能監測,訪問控制,事物管理,日誌記錄等。至於怎麼理解橫切邏輯,敲完例項程式碼也就明白了。

為什麼要使用AOP,舉個栗子:需要監測一些方法的執行所消耗的時間,在每個方法開始執行前呼叫一次記錄時間的方法beginTime,在每個方法執行結束後呼叫一次記錄時間的方法endTime,再endTime-beginTime就是該方法所消耗的時間。這樣要為每個需要監測的方法都加上這兩個記錄時間的方法。這樣重複的監測程式碼就與業務邏輯程式碼混雜在一起了,並且無法通過抽象父類的方式消除這樣的重複性橫切程式碼。AOP就是為此而生的,將這樣的重複性程式碼抽取出來,再動態的將這些程式碼切入到指定的執行點,實現同樣的效果。

一、AOP關鍵詞

1、目標物件—Target:需要動態切入程式碼的目標類。
2、連線點—Joinpoint:Spring只支援方法的連線點,方法呼叫前,方法呼叫後,方法呼叫前後(環繞),方法丟擲異常後。連線點是類中客觀存在的。每個方法都有這4個連線點。選擇在哪個連線點切入程式碼就是下面的“切點”了。
3、切點—Pointcut:選擇了某個連線點切入程式碼,這個連線點就是切點。例如準備在方法執行前記錄一次時間(切入並執行一次記錄時間的程式碼),那麼方法執行前這個連線點就是切點。
4、增強—Advice:增強是織入切點的一段程式程式碼。BeforeAdvice(方法呼叫前),AfterReturningAdvice(訪問返回後的位置),ThrowsAdvice(方法丟擲異常的位置)。結合切點和增強才能實施增強邏輯。
5、引介—Introduction

:一種特殊的增強,可以為類新增一些屬性和方法。可以動態的為目標類新增介面的實現邏輯,讓目標類成為這個介面的實現類。
6、織入—Weaving:織入是將增強新增到目標類的具體連線點上的過程。
7、代理—Proxy:目標類被織入增強後,就會產生一個結果類,它是融合了目標類和增強邏輯的代理類。可以採用呼叫原類相同的方式呼叫代理類。
8、切面—Aspect:切面由切點和增強/引介組成,包括橫切邏輯的定義和連線點定義。
實現AOP有許多工具:AspectJ、AspectWerkz、JBossAOP、SpringAop,這裡就學習SpringAOP。

需求:對某個目標業務類的方法進行效能監測,列印方法執行的時間。

通過代理的方式將業務類方法中開啟和結束效能監測的橫切程式碼從業務類中移除,通過JDK或CGLib動態代理技術將橫切程式碼動態織入目標方法的相應位置。
Spring AOP支援JDK動態代理和CGLib動態代理實現AOP,區別於使用場景下文會討論。

二、JDK動態代理實現AOP

CGLib的Maven依賴

 <!-- cglib依賴(spring依賴) -->
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm</artifactId>
      <version>4.0</version>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-util</artifactId>
      <version>4.0</version>
    </dependency>
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>3.0</version>
      <exclusions>
        <exclusion>
          <artifactId>asm</artifactId>
          <groupId>org.ow2.asm</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>aopalliance</groupId>
      <artifactId>aopalliance</artifactId>
      <version>1.0</version>
    </dependency>

Java提供了動態代理技術,允許開發者在執行期建立介面的代理例項。主要涉及java.lang.reflect包中的兩個類:Proxy和InvocationHandler。

1、目標類需要實現的介面 — ForumService

package com.mistra.aop;

/**
 * @author Mistra-WangRui
 * @create 2018-03-28 0:15
 * @desc 定義介面
 */
public interface ForumService {
    //兩個模擬方法
    public void removeTopic(int topicId);
    public void removeForum(int forumId);
}

2、目標類 — ForumServiceImpl

package com.mistra.aop;

/**
 * @author Mistra-WangRui
 * @create 2018-03-28 0:17
 * @desc 目標類
 */
public class ForumServiceImpl implements ForumService {
    public void removeTopic(int topicId) {
        System.out.println("模擬刪除Topic記錄:" + topicId + "條。");
        try {
            Thread.currentThread().sleep(200);
        }catch (Exception e){
            throw new RuntimeException();
        }
    }
    public void removeForum(int forumId) {
        System.out.println("模擬刪除Forum記錄:" + forumId + "條。");
        try {
            Thread.currentThread().sleep(40);
        }catch (Exception e){
            throw new RuntimeException();
        }
    }
}

目標類—需要織入增強程式碼的類,只編寫具體的業務邏輯程式碼,效能監測的記錄時間的程式碼通過AOP動態織入。

3、記錄效能監測資訊的類 — MethodPerformance

package com.mistra.aop;

/**
 * @author Mistra-WangRui
 * @create 2018-03-28 0:23
 * @desc 記錄效能監視資訊
 */
public class MethodPerformance {
    private long begin;
    private long end;
    private String serviceMethod;//儲存監測的方法的名稱
    public MethodPerformance(String serviceMethod){
        this.serviceMethod = serviceMethod;
        this.begin = System.currentTimeMillis();//記錄目標類方法開始執行的系統時間
    }
    public void printPerformance(){
        end = System.currentTimeMillis();//記錄目標類方法執行完畢後的系統時間
        long elapse = end - begin;
        System.out.println(serviceMethod+"耗時"+elapse+"毫秒。");
    }
}

4、效能監測實現類 — PerfomanceMonitor

package com.mistra.aop;

/**
 * @author Mistra-WangRui
 * @create 2018-03-28 0:22
 * @desc 效能監視實現類
 */
public class PerfomanceMonitor {
    //通過一個ThreadLocal保存於呼叫執行緒相關的效能監視資訊----保證執行緒安全(查閱ThreadLocal相關知識)
    private static ThreadLocal<MethodPerformance> performanceRecord = 
                                new ThreadLocal<MethodPerformance>();
    //啟動對某一目標方法的效能監控
    public static void begin(String method){
        System.out.println("begin monitor。。。。");
        MethodPerformance mp = new MethodPerformance(method);
        performanceRecord.set(mp);
    }
    //方法執行結束時呼叫
    public static void end(){
        System.out.println("end monitor。。。。");
        MethodPerformance mp = performanceRecord.get();
        mp.printPerformance();//列印方法效能監視的結果
    }
}

5、效能監測橫切程式碼 — PerformanceHandler

package com.mistra.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @author Mistra-WangRui
 * @create 2018-03-28 0:21
 * @desc 效能監視橫切程式碼
 */
public class PerformanceHandler implements InvocationHandler {
    private Object target;
    public PerformanceHandler(Object target){//target為目標業務類
        this.target = target;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        PerfomanceMonitor.begin(target.getClass().getName()+"."+method.getName());
        Object obj = method.invoke(target,args);//通過Java反射方法呼叫業務類的目標方法
        PerfomanceMonitor.end();
        return obj;
    }
}

在建構函式裡傳入希望被代理的目標類。
InvocationHandler只有一個invoke方法,就是通過此方法將橫切邏輯程式碼和業務類方法的業務邏輯程式碼編織到一起。proxy是最終生成的代理類例項(一般不會用到),method是被代理目標類的某個具體方法,args是method的入參。
PerfomanceMonitor.begin(),PerfomanceMonitor.end()方法就是效能監測的橫切程式碼了。
method.invoke(target,args)通過Java反射方法呼叫目標類的目標方法。

6、建立代理例項 — ForumServiceTest

package com.mistra.aop;

import org.testng.annotations.Test;

import java.lang.reflect.Proxy;

/**
 * @author Mistra-WangRui
 * @create 2018-03-28 0:35
 * @desc 建立代理例項
 */

public class ForumServiceTest {

    @Test
    public void proxy(){
        ForumService target = new ForumServiceImpl();//被代理的目標類
        PerformanceHandler handler = new PerformanceHandler(target);//將目標類與橫切程式碼編織到一起
        ForumService proxy = (ForumService) Proxy.newProxyInstance(//建立代理例項
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                handler);
        proxy.removeForum(10);//呼叫代理例項的方法
        proxy.removeTopic(1012);
    }
}

new PerformanceHandler(target):將效能監測橫切邏輯編織到ForumService例項中。
Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),handler):通過Proxy的newProxyInstance()靜態方法為編織了業務邏輯和效能監測邏輯的handler建立一個符合ForumService介面的代理例項。第一個入參是類載入器,第二個入參為目標業務類(要被代理的物件,這裡就是ForumServiceImpl)實現的介面列表,第三個入參是整合了業務邏輯和橫切邏輯的編織物件。
這個代理例項proxy實現了目標業務類(ForumServiceImpl)所實現的所有介面,這裡就一個,即ForumService。就可以按照呼叫ForumService介面例項相同的方式呼叫代理例項。
執行proxy(),輸出一下資訊,成功在方法前後進行了橫切邏輯織入:
這裡寫圖片描述

三、CGLib動態代理實現AOP

1、效能監測橫切程式碼 — CglibProxy

package com.mistra.aop;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * @author Mistra-WangRui
 * @create 2018/3/28 14:41
 * @desc CGLib代理
 */
public class CglibProxy implements MethodInterceptor{

    private Enhancer enhancer = new Enhancer();
    public Object getProxy(Class clazz){
        enhancer.setSuperclass(clazz);//設定需要建立子類的類
        enhancer.setCallback(this);//設定回撥
        return enhancer.create();//通過位元組碼技術冬天建立子類例項
    }

    public Object intercept(Object o, Method method, Object[] objects,
                            MethodProxy methodProxy) throws Throwable {//攔截父類所有方法的呼叫
        PerfomanceMonitor.begin(o.getClass().getName()+"."+method.getName());
        Object result=methodProxy.invokeSuper(o,objects);//通過代理類呼叫父類中的方法
        PerfomanceMonitor.end();
        return result;
    }
}

通過getProxy()為一個目標類建立動態代理物件。
intercept()方法會攔截所有目標類方法的呼叫。o是目標類例項,method是目標類方法的反射物件,objects是方法的入參,methodProxy是代理類例項。
會攔截父類所有方法的呼叫,都執行橫切邏輯織入。

2、建立代理例項 — ForumServiceTest2

package com.mistra.aop;

import org.testng.annotations.Test;

/**
 * @author Mistra-WangRui
 * @create 2018/3/28 14:56
 * @desc CGLib代理建立代理物件例項
 */
public class ForumServiceTest2 {
    @Test
    public void proxy(){
        CglibProxy proxy = new CglibProxy();
        ForumServiceImpl forumService = (ForumServiceImpl)proxy.getProxy(ForumServiceImpl.class);
        forumService.removeForum(10);
        forumService.removeTopic(1023);
    }
}

通過proxy.getProxy()方法為ForumServiceImpl 建立了一個織入了效能監測邏輯的代理物件,並呼叫代理類的業務方法。

輸出資訊:
這裡寫圖片描述
圖中的 com.mistra.aop.ForumServiceImplEnhancerByCGLIB$$74e237b4 就是CGLib動態建立的子類。

四、JDK動態代理和CGLib動態代理的區別與使用場景

1、JDK動態代理只能建立介面物件的代理例項,CGLib動態代理能建立介面物件和普通類的代理例項。
2、CGLib建立的動態代理物件的效能比JDK動態代理建立的動態代理物件效能高。
3、但CGLib建立代理物件所耗費的時間比JDK動態代理多。
對於singleton的代理物件或者具有例項池的代理,因為不用頻繁的建立代理物件,所以比較適合用CGLib動態代理技術,反之則適合採用JDK動態代理技術。

五、缺點

1、使用JDK動態代理和CGLib動態代理的話,目標類的所有方法都添加了效能監測橫切邏輯,有時候只想對部分方法新增橫切邏輯。
2、手工編寫代理例項的建立過程,為不同的目標類建立代理時無法通用。
3、通過硬編碼的方式指定織入橫切邏輯的位置。
下篇文章介紹了更方便快捷的方式: Spring AOP之—基於ProxyFactory的類編碼方式和XML配置方式實現AOP