從代理機制到Spring AOP

foofoo發表於2018-09-07

這篇文章準備從Java的代理機制講到Spring的AOP。

1.代理模式

代理模式是很常見的一種設計模式,代理一詞拆開來看就是代為受理,那顯然是要涉及到請求被代理的委託方,提供代理的代理方,以及想要通過代理來實際聯絡委託方的客戶三個角色。舉個生活中很常見的例子,各路的明星都會有個自己的經紀人來替自己打點各種各樣的事情,這種場景下,明星本身是委託方,經紀人是代理方,明星把自己安排演出、出席見面會的時間安排權利委託給經紀人,這樣當各個商家作為客戶想要請明星來代言時,就只能通過經紀人來進行。這樣明星本身不用暴露身份,而經濟人也可以在溝通中告知商家明星出席活動時要吃什麼飯,做什麼車的一些要求,省去了明星自己操心這些雞毛蒜皮小事兒。另一方面,當經紀人也可以給多個明星提供服務,這樣商家只接觸一個經紀人,可以聯絡到不同的明星,找個適合自己公司的人選。
通過上面的例子,代理模式的優點就顯而易見了:

優點一:可以隱藏委託類的實現;

優點二:可以實現客戶與委託類間的解耦,在不修改委託類程式碼的情況下能夠做一些額外的處理。

2.位元組碼與代理模式

Java程式設計師都應該知道,Java通過Java編譯器將.java原始檔編譯成.class位元組碼檔案,這種.class檔案是二進位制檔案,內容是隻有JVM虛擬機器能夠識別的機器碼,JVM虛擬機器讀取位元組碼檔案,取出二進位制資料,載入到記憶體中,解析.class檔案內的資訊,生成對應的Class物件,進而使Class物件建立類的具體例項來進行呼叫實現具體的功能。

從代理機制到Spring AOP

上圖說明了Java載入位元組碼的流程,但是Java的強大在於不僅僅可以載入在編譯期生成好的位元組碼,還可以在執行期系統中,遵循Java編譯系統組織.class檔案的格式和結構,生成相應的二進位制資料,然後再把這個二進位制資料載入轉換成對應的類,這樣,就完成了在程式碼中,動態建立一個類的能力了,如下圖流程。

從代理機制到Spring AOP

下面舉一個動態生成類的例項,通過Javassist實現,Javassist是一個開源的分析、編輯和建立Java位元組碼的類庫,我們可以使用Javasisst工具在執行時動態建立位元組碼並載入類,如下程式碼:

/**
 * Created by zhoujunfu on 2018/9/6.
 */
public class JavassistDemo {
    
    public static void main(String[] args) {
        makeNewClass();
    }
    
    public static Class<?> makeNewClass() {
        try {
            // 獲取ClassPool
            ClassPool pool = ClassPool.getDefault();
            // 建立Student類
            CtClass ctClass = pool.makeClass("com.fufu.aop.Student");
            // 建立Student類成員變數name
            CtField name = new CtField(pool.get("java.lang.String"), "name", ctClass);
            // 設定name為私有
            name.setModifiers(Modifier.PRIVATE);
            // 將name寫入class
            ctClass.addField(name, CtField.Initializer.constant("")); //寫入class檔案
            //增加set方法,名字為"setName"
            ctClass.addMethod(CtNewMethod.setter("setName", name));
            //增加get方法,名字為getname
            ctClass.addMethod(CtNewMethod.getter("getName", name));
            // 新增無參的構造體
            CtConstructor cons = new CtConstructor(new CtClass[] {}, ctClass);
            cons.setBody("{name = \"Brant\";}"); //相當於public Sclass(){this.name = "brant";}
            ctClass.addConstructor(cons);
            // 新增有參的構造體
            cons = new CtConstructor(new CtClass[] {pool.get("java.lang.String")}, ctClass);
            cons.setBody("{$0.name = $1;}");  //第一個傳入的形參$1,第二個傳入的形參$2,相當於public Sclass(String s){this.name = s;}
            ctClass.addConstructor(cons);

            //反射呼叫新建立的類
            Class<?> aClass =  ctClass .toClass();
            Object student = aClass.newInstance();
            Method getter = null;
            getter = student.getClass().getMethod("getName");
            System.out.println(getter.invoke(student));

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
複製程式碼

介紹靜態和動態載入位元組碼的兩種方式,是為了引出下面關於兩種代理方式的介紹,代理機制通過代理類建立的時間不同分為了靜態代理和動態代理:
靜態代理:代理類在編譯階段生成,程式執行前就已經存在,那麼這種代理方式被成為靜態代理,這種情況下的代理類通常都是我們在Java程式碼中定義的。
動態代理:代理類在程式執行時建立,也就是說,這種情況下,代理類並不是在Java程式碼中定義的,而是在執行時根據我們在Java程式碼中的“指示”動態生成的。

目前,靜態代理主要有AspectJ靜態代理、JDK靜態代理技術、而動態代理有JDK動態代理、Cglib動態代理技術,而Spring Aop是整合使用了JDK動態代理和Cglib動態代理兩種技術,下面我們結合例項一步一步介紹所有的概念。

3.靜態代理

3.1 AspectJ靜態代理

對於AspectJ,我們只會進行簡單的瞭解,為後續理解打下基礎,現在只需要知道下面這一句定義:

AspectJ是一個Java實現的面向切面的框架,它擴充套件了Java語言。AspectJ有自定義的語法,所以它有一個專門的編譯器用來生成遵守Java位元組編碼規範的Class檔案。

注意上面定義中的“專門的編譯器”這個描述,可以看出AspectJ是典型的靜態代理技術,因為是在編譯時期就生成了代理類,而使用AspectJ也肯定需要指定特定的編譯器,下面我們用AspectJ來實現上面的明星和經紀人的模型。

首先在maven工程中引入AspectJ依賴:

  <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>1.8.9</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjtools</artifactId>
      <version>1.8.9</version>
    </dependency>
複製程式碼

然後在idea中將javac編譯器改為acj編譯器來支援AspectJ語法:

從代理機制到Spring AOP

將明星的表演抽象成一個ShowService介面,包括了唱歌、跳舞的功能

public interface ShowService {
    // 歌唱表演
    void sing(String songName);
    // 舞蹈表演
    void dance();
}

複製程式碼

明星類實現了ShowService介面:

package com.fufu.aop;

/**
 * Created by zhoujunfu on 2018/9/6.
 * 明星類
 */
public class Star implements ShowService{
    private String name;

    @Override
    public void sing(String songName) {
        System.out.println(this.name + " sing a song: " + songName);
    }

    @Override
    public void dance() {
        System.out.println(this.name + "dance");
    }

    public Star(String name) {
        this.name = name;
    }

    public Star() {
    }

    public static void main(String[] args) {
        Star star = new Star("Eminem");
        star.sing("Mockingbird");
    }
}
複製程式碼

用AspectJ語法實現一個代理AgentAspectJ:

package com.fufu.aop;

/**
 * Created by zhoujunfu on 2018/9/7.
 */
public aspect AgentAspectJ {

    /**
     * 定義切點
     */
    pointcut sleepPointCut():call(* Star.sing(..));

    /**
     * 定義切點
     */
    pointcut eatPointCut():call(* Star.eat(..));

    /**
     * 定義前置通知
     *
     * before(引數):連線點函式{
     *     函式體
     * }
     */
    before():sleepPointCut(){
        getMoney();
    }

    /**
     * 定義後置通知
     * after(引數):連線點函式{
     *     函式體
     * }
     */
    after():sleepPointCut(){
        writeReceipt();
    }

    private void getMoney() {
        System.out.println("get money");
    }

    private void writeReceipt() {
        System.out.println("write receipt");
    }
}
複製程式碼

建立一個Star並執行方法:

public static void main(String[] args) {
        Star star = new Star("Eminem");
        star.sing("Mockingbird");
    }
複製程式碼

輸出:

get money
Eminem sing a song: Mockingbird
write receipt
複製程式碼

可以看到Star的sing()方法前後輸出了我們在AgentAspectJ中定義的前置通知和後置通知,所以是AspectJ在編譯期間,根據AgentAspectJ程式碼中定義的程式碼,生成了增強的Star類,而我們實際呼叫時,就會實現代理類的功能。具體的AspectJ語法我們不深究,只需要知道pointcut是定義代理要代理的切入點,這裡是定義了兩個pointcut,分別是Star類的sing()方法和dance()方法。而before()和after()分別可以定義具體在切入點前後需要的額外操作。

總結一下,AspctJ就是用特定的編譯器和語法,對類實現編譯期增強,實現靜態代理技術,下面我們看JDK靜態代理。

3.2 JDK靜態代理

通常情況下, JDK靜態代理更多的是一種設計模式,JDK靜態代理的代理類和委託類會實現同一介面或是派生自相同的父類,代理模式的基本類圖入下:

從代理機制到Spring AOP

我們接著通過把上面的明星和經紀人的例子寫成程式碼來實現一個JDK靜態代理模式。

經紀人類也實現了ShowService介面,持有了一個明星物件來提供真正的表演,並在各項表演的前後加入了經紀人需要處理的事情,如收錢、開發票等:

package com.fufu.aop;

/**
 * Created by zhoujunfu on 2018/9/6.
 * 經紀人
 */
public class Agent implements ShowService{

    private Star star;

    public Agent(Star star) {
        this.star = star;
    }

    private void getMoney() {
        System.out.println("get money");
    }

    private void writeReceipt() {
        System.out.println("write receipt");
    }
    @Override
    public void sing(String songName) {
        // 唱歌開始前收錢
        getMoney();
        // 明星開始唱歌
        star.sing(songName);
        // 唱歌結束後開發票
        writeReceipt();
    }

    @Override
    public void dance() {
        // 跳舞開始前收錢
        getMoney();
        // 明星開始跳舞
        star.dance();
        // 跳舞結束後開發票
        writeReceipt();
    }
}
複製程式碼

通過經紀人來請明星表演:

 public static void main(String[] args) {
        Agent agent = new Agent(new Star("Eminem"));
        agent.sing("Mockingbird");
 }
複製程式碼

輸出:

get money
Eminem sing a song: Mockingbird
write receipt
複製程式碼

以上就是一個典型的靜態代理的例項,很簡單但是也能說明問題,我們來看看靜態代理的優缺點:

優點: 業務類可以只關注自身邏輯,可以重用,通過代理類來增加通用的邏輯處理。

缺點: 1.代理物件的一個介面只服務於一種型別的物件,如果要代理的類很多,勢必要為每一個類都進行代理,靜態代理在程式規模稍大時就無法勝任了。

2.如果介面增加一個方法,除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了程式碼維護的複雜度

另外,如果要按照上述的方法使用代理模式,那麼真實角色(委託類)必須是事先已經存在的,並將其作為代理物件的內部屬性。但是實際使用時,一個真實角色必須對應一個代理角色,如果大量使用會導致類的急劇膨脹;此外,如果事先並不知道真實角色(委託類),該如何使用代理呢?這些問題可以通過Java的動態代理類來解決。

4.動態代理

動態代理類的原始碼是在程式執行期間由JVM根據反射等機制動態的生成,所以不存在代理類的位元組碼檔案。代理類和委託類的關係是在程式執行時確定。

4.1 動態代理思路

想弄明白動態代理類實現的思路是什麼,我們還需用從靜態代理的存在的問題入手,因為畢竟動態代理是為了解決靜態代理存在問題而出現的,回過頭來看靜態代理的問題:

  1. 類膨脹: 每個代理類都是一個需要程式設計師編寫的具體類,不現實。
  2. 方法級代理:代理類和實現類都實現相同介面,導致代理類每個方法都需要進行代理,你有幾個方法我就要有幾個,編碼複雜,無法維護。

動態代理如何解決:

  1. 第一個問題很容易回答,類似使用Javasisst的例子,在程式碼中動態的建立代理類的位元組碼,然後獲取到代理類物件。
  2. 第二問題就要引出InvocationHandler了,為了構造出具有通用性和簡單性的代理類,可以將所有的觸發真實角色動作交給一個觸發的管理器,讓這個管理器統一地管理觸發。這種管理器就是InvocationHandler。靜態代理中,代理類無非是在前後加入特定邏輯後,呼叫對應的實現類的方法,sleep()對應sleep(),run()對應run(),而在Java中,方法Method也是一個物件,所以,動態代理類可以將對自己的所有呼叫作為Method物件都交給InvocationHandler處理,InvocationHandler根據是什麼Method呼叫具體實現類的不同方法,InvocationHandler負責增加代理邏輯和呼叫具體的實現類的方法。

也就是說,動態代理類還是和實現類實現相同的介面,但是動態代理類是根據實現類實現的介面動態生成,不需要使用者關心,另外動態代理類的所有方法呼叫,統一交給InvocationHandler,不用處理實現類每個介面的每個方法。

在這種模式之中:代理Proxy和RealSubject應該實現相同的功能,這一點相當重要。(我這裡說的功能,可以理解為某個類的public方法)

在物件導向的程式設計之中,如果我們想要約定Proxy和RealSubject可以實現相同的功能,有兩種方式:

a.一個比較直觀的方式,就是定義一個功能介面,然後讓Proxy 和RealSubject來實現這個介面。
b.還有比較隱晦的方式,就是通過繼承。因為如果Proxy繼承自RealSubject,這樣Proxy則擁有了RealSubject的功能,Proxy還可以通過重寫RealSubject中的方法,來實現多型。

其中JDK中提供的建立動態代理的機制,是以a這種思路設計的,而cglib則是以b思路設計的。

4.1 JDK動態代理(通過介面)

先來看一個具體的例子,還是以上邊明星和經紀人的模型為例,這樣方便對比理解:

將明星的表演抽象成一個ShowService介面,包括了唱歌、跳舞的功能:

package com.fufu.aop;

/**
 * Created by zhoujunfu on 2018/9/6.
 */
public interface ShowService {
    // 歌唱表演
    void sing(String songName);
    // 舞蹈表演
    void dance();
}
複製程式碼

明星類實現了ShowService介面:

package com.fufu.aop;

/**
 * Created by zhoujunfu on 2018/9/6.
 * 明星類
 */
public class Star implements ShowService{
    private String name;

    @Override
    public void sing(String songName) {
        System.out.println(this.name + " sing a song: " + songName);
    }

    @Override
    public void dance() {
        System.out.println(this.name + "dance");
    }

    public Star(String name) {
        this.name = name;
    }

    public Star() {
    }
}
複製程式碼

實現一個代理類的請求處理器,處理對具體類的所有方法的呼叫:

package com.fufu.aop;

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

/**
 * Created by zhoujunfu on 2018/9/7.
 */
public class InvocationHandlerImpl implements InvocationHandler {

    ShowService target;

    public InvocationHandlerImpl(ShowService target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 表演開始前收錢
        getMoney();
        // 明星開始唱歌
        Object invoke = method.invoke(target, args);
        // 表演結束後開發票
        writeReceipt();

        return invoke;
    }

    private void getMoney() {
        System.out.println("get money");
    }

    private void writeReceipt() {
        System.out.println("write receipt");
    }
}
複製程式碼

通過JDK動態代理機制實現一個動態代理:

package com.fufu.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

/**
 * Created by zhoujunfu on 2018/9/7.
 */
public class JDKProxyDemo {

    public static void main(String[] args) {
        // 1.建立被代理的具體類
        Star star = new Star("Eminem");
        // 2.獲取對應的ClassLoader
        ClassLoader classLoader = star.getClass().getClassLoader();
        // 3.獲取被代理物件實現的所有介面
        Class[] interfaces = star.getClass().getInterfaces();
        // 4.設定請求處理器,處理所有方法呼叫
        InvocationHandler invocationHandler = new InvocationHandlerImpl(star);

        /**
         * 5.根據上面提供的資訊,建立代理物件 在這個過程中,
         *   a.JDK會通過根據傳入的引數資訊動態地在記憶體中建立和.class檔案等同的位元組碼
         *   b.然後根據相應的位元組碼轉換成對應的class,
         *   c.然後呼叫newInstance()建立例項
         */
        Object o = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
        ShowService showService = (ShowService)o;
        showService.sing("Mockingbird");
    }
}
複製程式碼

我們從代理的建立入手,看看JDK的動態代理都做了什麼:

Object o = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
複製程式碼
  1. Proxy.newProxyInstance()獲取Star類的所有介面列表(第二個引數:interfaces)
  2. 確定要生成的代理類的類名,預設為:com.sun.proxy.$ProxyXXXX
  3. 根據需要實現的介面資訊,在程式碼中動態建立該Proxy類的位元組碼;
  4. 將對應的位元組碼轉換為對應的class物件;
  5. 建立InvocationHandler例項handler,用來處理Proxy所有方法呼叫
  6. Proxy的class物件以建立的handler物件為引數(第三個引數:invocationHandler),例項化一個Proxy物件

而對於InvocationHandler,我們需要實現下列的invoke方法:

public Object invoke(Object proxy, Method method, Object[] args) 
複製程式碼

在呼叫代理物件中的每一個方法時,在程式碼內部,都是直接呼叫了InvocationHandler的invoke方法,而invoke方法根據代理類傳遞給自己的method引數來區分是什麼方法。

可以看出,Proxy.newProxyInstance()方法生成的物件也是實現了ShowService介面的,所以可以在程式碼中將其強制轉換為ShowService來使用,和靜態代理到達了同樣的效果。我們可以用下面程式碼把生成的代理類的位元組碼儲存到磁碟裡,然後反編譯看看JDK生成的動態代理類的結構。

package com.fufu.aop;

import sun.misc.ProxyGenerator;

import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Created by zhoujunfu on 2018/9/7.
 */
public class ProxyUtils {

    public static void main(String[] args) {
        Star star = new Star("Eminem");
        generateClassFile(star.getClass(), "StarProxy");
    }

    public static void generateClassFile(Class clazz, String proxyName) {

        //根據類資訊和提供的代理類名稱,生成位元組碼
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
        String paths = clazz.getResource(".").getPath();
        System.out.println(paths);
        FileOutputStream out = null;

        try {
            //保留到硬碟中
            out = new FileOutputStream(paths + proxyName + ".class");
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
複製程式碼

反編譯StarPoxy.class檔案後得到:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import com.fufu.aop.ShowService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

// 動態代理類StarPoxy實現了ShowService介面
public final class StarProxy extends Proxy implements ShowService {
    // 載入介面中定義的所有方法
    private static Method m1;
    private static Method m3;
    private static Method m4;
    private static Method m2;
    private static Method m0;

    //建構函式接入InvocationHandler,也就是持有了InvocationHandler物件h
    public StarProxy(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    // 自動生成的sing()方法,實際呼叫InvocationHandler物件h的invoke方法,傳入m3引數物件代表sing()方法
    public final void sing(String var1) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }
    
    //同理生成dance()方法
    public final void dance() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    // 載入介面中定義的所有方法
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m3 = Class.forName("com.fufu.aop.ShowService").getMethod("sing", new Class[]{Class.forName("java.lang.String")});
            m4 = Class.forName("com.fufu.aop.ShowService").getMethod("dance", new Class[0]);
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}
複製程式碼

通過上面反編譯後的程式碼可以看出,JDK生成的動態代理類實現和具體類相同的介面,並持有InvocationHandler物件(InvocationHandler物件又持有具體類),呼叫動態代理類中方法,會觸發傳入InvocationHandler的invoke()方法,通過method引數,來區分呼叫的是什麼具體的方法,具體如下圖所示:

從代理機制到Spring AOP

4.2 CGLIB動態代理(通過繼承)

JDK中提供的生成動態代理類的機制有個鮮明的特點是:

某個類必須有實現的介面,而生成的代理類也只能代理某個類介面定義的方法,比如:如果上面例子的Star實現了繼承自ShowService介面的方法外,另外實現了方法play(),則在產生的動態代理類中不會有這個方法了!更極端的情況是:如果某個類沒有實現介面,那麼這個類就不能用JDK產生動態代理了!

幸好我們有cglib,“CGLIB(Code Generation Library),是一個強大的,高效能,高質量的Code生成類庫,它可以在執行期擴充套件Java類與實現Java介面。”

cglib 建立某個類A的動態代理類的模式是:

1.查詢A上的所有非final 的public型別的方法定義;
2.將這些方法的定義轉換成位元組碼;
3.將組成的位元組碼轉換成相應的代理的class物件;
4.實現 MethodInterceptor介面,用來處理對代理類上所有方法的請求(這個介面和JDK動態代理InvocationHandler的功能和角色是一樣的)

有了上邊JDK動態代理的例子,cglib的理解起來就簡單了,還是先以例項說明,ShowService介面和Star類都複用之前的不變:

實現 MethodInterceptor介面:

package com.fufu.aop;

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

import java.lang.reflect.Method;

/**
 * Created by zhoujunfu on 2018/9/7.
 */
public class MethodInterceptorImpl implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 表演開始前收錢
        getMoney();
        // 明星開始唱歌
        Object invoke = methodProxy.invokeSuper(o, objects);
        // 表演結束後開發票
        writeReceipt();

        return invoke;
    }

    private void getMoney() {
        System.out.println("get money");
    }

    private void writeReceipt() {
        System.out.println("write receipt");
    }
}
複製程式碼

建立動態代理:

package com.fufu.aop;

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

/**
 * Created by zhoujunfu on 2018/9/7.
 */
public class CglibProxyDemo {

    public static void main(String[] args) {
        Star star = new Star("Eminem");

        MethodInterceptor methodInterceptor = new MethodInterceptorImpl();

        //cglib 中加強器,用來建立動態代理
        Enhancer enhancer = new Enhancer();
        //設定要建立動態代理的類
        enhancer.setSuperclass(star.getClass());
        // 設定回撥,這裡相當於是對於代理類上所有方法的呼叫,都會呼叫CallBack,而Callback則需要實行intercept()方法進行攔截
        enhancer.setCallback(methodInterceptor);

        ShowService showService = (ShowService) enhancer.create();
        showService.sing("Mockingbird");
    }
}
複製程式碼

通過以上例項可以看出,Cglib通過繼承實現動態代理,具體類不需要實現特定的介面,而且代理類可以呼叫具體類的非介面方法,更加靈活。

5.Spring AOP

5.1 概念

AOP的具體概念就不再說了,網上一搜一大把,這篇文章主要介紹Spring AOP低層使用的代理技術,因為平時在使用Spring AOP時,很多人都是copy配置,對上面介紹的這些技術概念並不清楚。

Spring AOP採用的是動態代理,在執行期間對業務方法進行增強,所以不會生成新類,對於動態代理技術,Spring AOP提供了對JDK動態代理的支援以及CGLib的支援,然而什麼時候用哪種代理呢?

1、如果目標物件實現了介面,預設情況下會採用JDK的動態代理實現AOP

2、如果目標物件實現了介面,可以強制使用CGLIB實現AOP

3、如果目標物件沒有實現了介面,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換

目前來看,Spring貌似和AspectJ沒半毛錢關係,那為什麼在許多應用了Spring AOP的專案中都出現了@AspectJ的註解呢?Spring是應用的動態代理,怎麼會還和AspectJ有關係呢,原因是Spring AOP基於註解配置的情況下,需要依賴於AspectJ包的標準註解,但是不需要額外的編譯以及AspectJ的織入器,而基於XML配置不需要,所以Spring AOP只是複用了AspectJ的註解,並沒有其他依賴AspectJ的地方。

當Spring需要使用@AspectJ註解支援時,需要在Spring配置檔案中如下配置:

<aop:aspectj-autoproxy/>
複製程式碼

而關於第二點強制使用CGLIB,可以通過在Spring的配置檔案如下配置實現:

<aop:aspectj-autoproxy proxy-target-class="true"/>
複製程式碼

proxy-target-class屬性值決定是基於介面的還是基於類的代理被建立。如果proxy-target-class 屬性值被設定為true,那麼基於類的代理將起作用(這時需要cglib庫)。如果proxy-target-class屬值被設定為false或者這個屬性被省略,那麼標準的JDK 基於介面的代理。

所以,雖然使用了Aspect的Annotation,但是並沒有使用它的編譯器和織入器。其實現原理是JDK動態代理或Cglib,在執行時生成代理類。

已經寫了這麼多了,下面再貼兩個Spring AOP的demo程式碼吧,分別是基於XML和註解的:

5.2 基於XML

切面類:

package com.fufu.spring.aop;

import org.springframework.stereotype.Component;

/**
 * Created by zhoujunfu on 2018/9/7.
 * 基於XML的Spring AOP
 */
@Component
public class AgentAdvisorXML {

    public void getMoney() {
        System.out.println("get money");
    }

    public void writeReceipt() {
        System.out.println("write receipt");
    }
}

複製程式碼

配置檔案:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="star" class="com.fufu.proxy.Star">
        <property name="name" value="Eminem"/>
    </bean>

    <bean id="agentAdvisorXML" class="com.fufu.spring.aop.AgentAdvisorXML"/>
    
     <!--Spring基於Xml的切面-->
     <aop:config>
         <!-- 定義切點函式 -->
         <aop:pointcut id="singPointCut" expression="execution(* com.fufu.proxy.Star.sing(..))"/>
         <!-- 定義切面 order 定義優先順序,值越小優先順序越大-->
         <aop:aspect ref="agentAdvisorXML" order="0">
             <!--前置通知-->
             <aop:before method="getMoney" pointcut-ref="singPointCut"/>
             <!--後置通知-->
             <aop:after method="writeReceipt" pointcut-ref="singPointCut"/>
         </aop:aspect>
     </aop:config>

</beans>
複製程式碼

測試類:

package com.fufu.spring.aop;

import com.fufu.proxy.ShowService;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Created by zhoujunfu on 2018/9/7.
 */
public class Main {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop.xml");

        Object star = applicationContext.getBean("star");

        ShowService showService = (ShowService)star;
        showService.sing("Mockingbird");
    }
}
複製程式碼

5.3 基於註解

切面類:

package com.fufu.spring.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * Created by zhoujunfu on 2018/9/7.
 * 基於註解的Spring AOP
 */
@Aspect
@Component
public class AgentAdvisor {

    @Before(value = "execution(* com.fufu.proxy.ShowService.sing(..))")
    public void getMoney() {
        System.out.println("get money");
    }

    @After(value = "execution(* com.fufu.proxy.ShowService.sing(..))")
    public void writeReceipt() {
        System.out.println("write receipt");
    }
}

複製程式碼

配置檔案:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">


    <context:component-scan base-package="com.fufu.proxy, com.fufu.spring.aop"/>

    <aop:aspectj-autoproxy  proxy-target-class="true"/>

</beans>
複製程式碼

測試類:

package com.fufu.spring.aop;

import com.fufu.proxy.ShowService;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Created by zhoujunfu on 2018/9/7.
 */
public class Main {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop-annotation.xml");

        Object star = applicationContext.getBean("star");

        ShowService showService = (ShowService)star;
        showService.sing("Mockingbird");
    }
}
複製程式碼

6.總結

以上內容,雖然比較淺顯易懂,但是可以對Java代理機制和Spring AOP會有一個全面的理解,如有錯誤,歡迎指正。

相關文章