1. 程式人生 > 其它 >AOP之動態代理

AOP之動態代理

技術標籤:Springjavaspring

什麼是AOP

在上一章的學習中,我們知道Spring一直致力於簡化我們的Java開發,並且用到了依賴注入(Dependency Injection)與AOP(Aspect-Oriented Programming)這兩項非常重要的技術:

  • DI主要解決了在類和類之間有依賴關係的時候,如何通過注入的方式(屬性注入、構造器注入)形成鬆耦合

  • 而今天要學習的AOP則是考慮如何把散落在應用中多處相同的功能剝離出來,使得這些剝離出來的邏輯與業務邏輯相分離的問題。

讓我們先來看一個生活中的案例:

每家每戶都有一個電錶來監控用電量,這樣電力公司就知道應該收取多少費用了。雖然每一臺用電裝置都可以自裝一個用電統計的硬體,但這樣是極不合理,成本上也不划算的,因為每個用電裝置更多關注的是自身的功能是否完善的問題,吸塵器考慮的是清潔效果,微波爐考慮的是加熱效果等等。電力公司只需要在合適的地方,比如用電線路入戶的地方,統一安裝一個電錶,就能很好的解決監控所有用電裝置電量的問題。

軟體系統中的一些功能就像我們家裡的電錶一樣。這些功能需要用到應用程式的多個地方,但是我們又不想在每個點都明確呼叫它們。

在軟體開發中,散佈於應用中多處的功能,被稱為橫切關注點(cross-cutting concern)。通常來講,這些橫切關注點從概念上是與應用的業務邏輯相分離的(但往往會直接嵌入到應用的業務邏輯中)。把這些橫切關注點與業務邏輯相分離正是AOP所要解決的問題。

代理模式

  1. 代理模式的概念

    AOP在實現上採用了設計模式中的動態代理模式,因此,在深入學習SpringAOP之前,我們先來一起了解和學習一下這種強大的設計模式。

    代理模式的定義:為其他物件提供一種代理,以控制對這個物件的訪問。換句通俗的話來說,它是一種使用代理物件來執行目標物件的方法,並在代理物件中增強目標物件方法的一種設計模式。

    生活中最常見的代理模式就是”中介“。假如說我現在想買一輛二手車,雖然我可以自己去找車源,做質量檢測等一系列的車輛過戶流程,但是這確實太浪費我得時間和精力了。我只是想買一輛車而已為什麼我還要額外做這麼多事呢?於是我就通過中介公司來買車,他們來給我找車源,幫我辦理車輛過戶流程,我只是負責選擇自己喜歡的車,然後付錢就可以了。

代理模式的功能主要是起到增強方法和許可權攔截的作用。

  1. 代理模式的好處

    從上圖我們可以看出,在程式設計中使用代理模式的一些好處:

    • 中介隔離:在呼叫方(Caller)不能或不想直接與目標物件(Target)打交道的時候,代理物件(Proxy)可以起到兩者之間中介的作用。
    • 開閉原則:我們可以通過給代理物件增加新的功能來擴充套件目標物件的功能,這樣我們只需要修改代理類,而不需要修改目標類,符合程式碼設計的OCP原則(Open Closed Principle,對擴充套件是開放的,對修改是關閉的)。
  2. 代理模式的種類

    根據代理物件建立的不同,分為兩種代理模式:

    • 靜態代理:由程式設計師或者特定工具生成原始碼來產生代理物件。在程式執行前,代理類的位元組碼檔案(.class)就已經存在了
    • 動態代理:在程式執行期間,運用反射機制、位元組碼生成技術來產生代理類和例項。

靜態代理模式

Proxy—代理

​ 要實現靜態代理,我們首先使用介面的方式來封裝被代理的行為:

​ 介面:

public interface IUserDao {
    void save();
}

分別讓目標和代理都來實現這個介面,這樣,對於呼叫方來說,無論和代理還是目標打交道,執行的程式碼都是一致的,都可以執行相同的行為。

public class UserDaoImpl implements IUserDao {
    @Override
    public void save() {
        System.out.println("使用者資料儲存!");
    }
}

為目標方編寫對應的代理類,在其中增加新的服務功能:

package com.tuling.dao;
public class UserDaoProxy implements IUserDao{

    private IUserDao userDao;

    public UserDaoProxy(IUserDao userDao) {
        this.userDao = userDao;
    }
    @Override
    public void save() {
        System.out.println("開啟事務...");
        userDao.save();//執行目標物件方法
        System.out.println("提交事務...");
    }
}

從上面可以看到,我們最終讓客戶執行了購買行為,除此之外,為了讓客戶享受更為完善的服務,我們還擴充套件了尋找車源、質量檢測、售後諮詢等其它服務。在一個複雜的應用中,我們當然不是僅僅列印幾行字,我們可以封裝單獨的方法來做這些事情,甚至還可以呼叫其它的類來執行這些輔助邏輯。

測試程式碼:

package com.tuling.dao;
public class MainTest {
    public static void main(String[] args) {
        //目標物件
        UserDaoImpl userDao = new UserDaoImpl();
        //代理物件
        UserDaoProxy proxy = new UserDaoProxy(userDao);
        //執行代理物件的方法
        proxy.save();
    }

    /*
 * 靜態代理總結:
 * 1、可以做到在不修改目標物件的功能前提下,增強目標物件。
 * 2、但是由於要和目標物件實現相同的介面,如果目標方法過多,或
者
 * 目標物件一旦新增方法,代理物件都要一併維護。
 */
}

動態代理模式

靜態代理模式最大的缺陷就是,我們需要為每一個被代理的目標類都編寫一個代理類。而動態代理可以很好的解決這個問題。JDK的Proxy和開源框架CGLIB可以分別在不同的情況幫助我們生成代理。

  • JDK動態代理

當目標的被代理方法抽取了介面時,可以使用JDK Proxy。

介面:

package com.tuling.proxy;
/**
* 介面
* @author fred
*
*/
public interface IUserDao {
    void save();
}

目標類:

package com.tuling.proxy;
public class UserDaoImpl implements IUserDao {
    @Override
    public void save() {
        System.out.println("使用者資料儲存!");
    }
}

測試類:

package com.tuling.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MainTest {
    public static void main(String[] args) {
        //目標物件
        UserDaoImpl userDao = new UserDaoImpl();
        //代理物件
        IUserDao proxy = (IUserDao) Proxy.newProxyInstance(
            userDao.getClass().getClassLoader(), //目標對
            象的類載入器
            userDao.getClass().getInterfaces(), //目標對
            象的介面陣列
            new InvocationHandler() {//事件處理,執行目標對
                象的方法時,會觸invoke方法

                    @Override
                    public Object invoke(Object proxy, Method
                                         method, Object[] args) throws Throwable {
                    System.out.println("開啟事務...");
                    //執行目標物件方法
                    Object obj = method.invoke(userDao,
                                               args);
                    System.out.println("提交事務...");
                    return obj;
                }
            });
        //呼叫代理物件方法
        proxy.save();

        /*
 * JDK動態代理總結:
 * 1、目標物件必須要有介面,否則不能實現動態代理。
 * 2、代理物件必須強轉為介面型別。
 */
    }
}

invoke方法會在代理物件的被代理方法呼叫的時候觸發。

CGLIB

當目標類沒有實現介面時,我們可以通過開源的CGLIB來實現。

使用maven引入cglib庫:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

也可以引入spring-core庫,該庫中包含了CGLIB。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.1.2.RELEASE</version>
</dependency>

目標類:

package com.tuling.cglib;
/**
* 目標物件,沒有任何介面
* @author fred
*
*/
public class UserDaoImpl{
    public void save() {
        System.out.println("使用者資料儲存!");
    }
}

代理工廠類:

package com.tuling.cglib;
import java.lang.invoke.MethodHandleInfo;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class ProxyFactory implements MethodInterceptor{

    //目標物件
    private Object target;
    public ProxyFactory(Object target) {
        super();
        this.target = target;
    }

    //給目標物件建立一個代理物件
    public Object getProxyInstance(){
        //1.增強器
        Enhancer en = new Enhancer();
        //2.設定目標物件的類載入器
        en.setClassLoader(target.getClass().getClassLoader());
        //3.設定這個動態代理類的父類
        en.setSuperclass(target.getClass());
        //4.設定要傳入的攔截器
        en.setCallback(this);
        //5.建立子類(代理物件)
        return en.create();
    }
    @Override
    public Object intercept(Object arg0, Method method,
                            Object[] args, MethodProxy arg3) throws Throwable {
        System.out.println("開啟事務...");
        //執行目標物件的方法
        Object obj = method.invoke(target, args);
        System.out.println("提交事務...");
        return obj;
    }
}

測試類:

package com.tuling.cglib;
public class MainTest {
    public static void main(String[] args) {
        //目標物件
        UserDaoImpl userDao = new UserDaoImpl();
        //代理物件
        UserDaoImpl proxy =
            (UserDaoImpl) new ProxyFactory(userDao).getProxyInstance();
        //執行代理物件方法
        proxy.save();
    }
}

總結

  • 靜態代理需要自己手動編寫代理類和目標方法。

  • 動態代理就不需要自己手動實現代理類和目標方法,但動態代理的目標類要必須實現介面!

  • Cglib 代理的目標類就不需要實現介面!但目標類不能被final修飾!

Spring AOP 程式設計的實現原理就是 動態代理和Cglib 代理,當目標類實現介面時使用動態代理,沒有則Cglib代理。