javaEE學習篇—從代理模式到SpringAOP

SpringAOP 就是Spring實現了AOP這種設計模式,AOP的全稱爲:Aspect Oriented Programming,意爲:面向切面編程。爲什麼會有面向切面編程呢。在java中,java語言是面向對象編程(OOP),但是在面向對象編程中,有時候會有一些問題,比如:當我們要給一些不具有繼承關係的對象引入一些公共行爲,比如安全日誌檢查,只能在每個對象中引入公共對象,這樣做非常不便於維護,而且可能會有大量重複的代碼。這個時候AOP的出現,剛好彌補了面向對象的這一弊端。

下面,爲了理清楚SpringAOP的東西,我將從下面幾個方面來進行討論:

一,代理模式簡介

二、靜態代理實現及分析

三、JDK動態代理的實現及分析

四、CGLIB動態代理實現及分析

五、SpringAOP

5.1 AOP簡介

5.2 AOP術語(白話)描述

5.2 SpringAOP的實現

5.3 SpringAOP源碼簡單分析


 

一,代理模式簡介


在說SpringAOP之前,先說說java的代理模式,代理模式是一種設計模式,那麼代理模式到底是什麼意思呢。下面,我舉一個例子,簡單說說代理模式的思想。

假如,你想要去一個地方買一個東西,但是那個地方太遠了,你又不想去,於是你就讓你男朋友去給你買,這就相當於是一個代理模式的過程。你不需要關心你男朋友是怎麼買到東西的,不用關心他在買東西的時候還幹了啥事,你只要結果,就是你男朋友吧東西遞到你的手上,這大概就是代理模式的思想。在你男朋友出去的過程中,他可能找他的好哥們聚了聚,可能見了見他的前女友,但是這都不是他到那個地方去的目的,他的目的始終是給你買東西,只是他在買東西的過程中還可能做一些別的事,然後回來之後把你想要的東西給你。過程可以用下面的圖表示:

在整個過程中,小明主要要乾的事是給他女朋友買喫的,小明就相當於是一個代理對象,代替他女朋友出去買喫的。只是在過程中,他還幹了一些其他的次要操作,比如陪前女友逛街,和小夥伴打球等,但是他的主要業務還是去買零食,就算他在這個過程中沒有配前女友逛街,沒有去打球,也不會影響他買零食這件事。而對於他女朋友來說,她不會關心小明出去幹了什麼,只關心結果,就算小明帶回來的零食。這就是代理模式的主要思想。

二、靜態代理實現及分析

2.1 實現靜態代理的步驟:

  • 寫一個接口:描述真正要乾的事(比如:買零食)
  • 兩個類,都繼承這個接口
    • 一個表示執行真實業務(比如:買零食)
    • 另一個就是代替第一個類,去真正的實現買零食的過程(比如:見前女友--->  買零食--->打球)
  • 測試結果(比如:給女朋友上交零食)

2.2 實現案例:

案例1:上面說的小明買零食的例子

首先寫一個接口: BuySnacks.java

然後一個類,實現真實業務方法 :  BuySnacksImpl.java

另一個類,也實現同樣的接口,具體實現真實業務的全過程:XiaoMing.java


// 小明去買零食
public class XiaoMing implements BuySnacks {

    // 給小明一個真實業務(要乾的事)的對象,調用具體要乾的事
    private BuySnacks buy;

    public XiaoMing(BuySnacks buy) {
        this.buy = buy;
    }

    // 實現他的主要業務,幹要乾的事
    @Override
    public void buySnacks() {
        // 買零食之前,和前女友逛街
        hangOutWithPreGirFrie();
        // 調用真實業務,買零食
        buy.buySnacks();
        // 買零食之後,陪小夥伴打球
        playBall();  
    }

    // 除了買零食,小明還見了前女友,陪前女友逛街
    public void hangOutWithPreGirFrie(){
        System.out.println("--------------和前女友逛街-----------------");
    }
    
    // 買完零食,和小夥伴打球
    public void playBall(){
        System.out.println("------------和小夥伴打球-------------------");
    }
    
}

最後,測試結果:Test.java

運行結果:

案例2:代理模式模擬增刪查改

一個表示真實業務的接口: IUserManager.java

實現真實業務的接口的類:UserManagerImpl.java

它實現了接口中的方法,也就是告訴我們這些真實業務具體是幹什麼的,但是它不會自己調用這些方法,而是交給代理類的對象去調用

代理類也要實現相同的接口:


/**
 * 代理類 用來代理真實類執行真實業務
 */
public class MyProxy implements IUserManager{

    // 先寫一個真實類的對象
    private IUserManager userManager ;

    public MyProxy(IUserManager userManager) {
        this.userManager = userManager;
    }

    // 在執行真實操作之前,先要進行一些輔助操作,比如安全性檢測
    private void MySecurityCheck()
    {
        System.out.println("check security first...");
    }

    // 覆寫真實業務方法
    public void addUser() {
        MySecurityCheck(); // 在執行添加用戶的方法之前先執行一個安全檢查
        userManager.addUser();
    }

    public void delUser() {
        MySecurityCheck();// 在執行刪除用戶的方法之前先執行一個安全檢查
        userManager.delUser();
    }

    public void modUser() {
        MySecurityCheck(); // 在執行修改用戶的方法之前先執行一個安全檢查
        userManager.modUser();
    }

    public void search() {
        MySecurityCheck(); // 在執行查詢用戶的方法之前先執行一個安全檢查
        userManager.search();
    }
}

最後,測試 Test.java

以上兩個案例就是我們自己實現的代理模式,又叫靜態代理

2.3  靜態代理總結:

靜態代理的優點:

可能有人會覺得上面這種操作是多此一舉,既然在增刪查改的時候需要進行安全檢查,那幹嘛不直接在一個類裏面實現了就行了,還要再多寫一個類。但是實際上,這樣做的好處有可以將主要業務和輔助方法分離開來,當我們需要加一些其他的輔助操作的時候直接在代理類中操作就可以了,不需要再去改動主要業務中的代碼;同時還可以使代碼結構看起來更清晰,試想一下如果將主要業務和輔助操作都放在一個類裏面,看起來就會特別複雜。

靜態代理的缺點:

由於代理類和目標類需要實現相同的接口,因此兩個類中要實現相同的方法,所以一旦接口中的方法有改動的時候,兩個子類都需要手動維護,那麼這個情況怎麼解決呢,答案是:JDK動態代理。

三、JDK動態代理的實現及分析

JKD動態代理是代理模式的一種實現方式,JDK動態代理只能代理接口,在JDK動態代理實現需要以下條件:

  • 需要實現一個接口 InvocationHandler(實現接口中的invoke方法,用來調用真實的業務方法)
  • 需要使用到一個類 Proxy ( Proxy裏面的一個靜態方法 newProxyInstance 用來返回一個代理對象,供客戶端調用)
  • 和上面的靜態代理一樣,還需要一個輔助真實業務執行的方法(比如上面的安全性檢查)

下面在代碼中再具體說這個類和接口怎麼用:

1、首先,依然要寫一個接口,裏面是真實業務的抽象方法:

2、然後,有一個真實業務的類實現該接口

3、自己寫一個代理類,使用上面說的接口和類:


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

/**
 * 基於JDK的動態代理
 *  需要實現一個接口:InvocationHandler 實現裏面的方法invoke,用來調用真實的業務方法(通過反射)
 *  需要使用一個類:Proxy裏面的一個靜態方法newProxyInstance去返回一個代理對象,供客戶端調用方法
 *  需要一個輔助真實業務的方法(比如安全性檢查)
 */

public class MyProxy implements InvocationHandler{

    // 這個目標對象就是上面這個實現了真實業務接口的類的對象
    private Object targetObject ;

    public MyProxy(Object targetObject) {
        this.targetObject = targetObject;
    }

    // 生成一個代理對象 最後我們在客戶端(測試類)調用的就是這個方法
    public Object returnProxy()
    {
        /**
         * 方法原型:
         * =======================================================================
         * public static Object newProxyInstance(ClassLoader loader,
         *                                       Class<?>[] interfaces,
         *                                        InvocationHandler h)
         *         throws IllegalArgumentException
         *=======================================================================
         *
         * Proxy.newProxyInstance 裏面的三個參數:
         * -----------------------------------------------------------------------------------------
         *   ClassLoader loader  目標類(UserManager)的ClassLoader
         *   Class<?>[] interfaces  目標類(UserManager)類實現的接口[有真實業務的接口]
         *   InvocationHandler h  實現InvocationHandler的類對象(在這裏就是當前類實現的這個接口)
         *   return 返回值Object就是系統幫我們生成的代理對象
         *-----------------------------------------------------------------------------------------
         */
        return Proxy.newProxyInstance(
                this.targetObject.getClass().getClassLoader(),
                this.targetObject.getClass().getInterfaces(),
                this);
    }

    /**
     * 這個是實現InvocationHandler接口中的 invoke() 方法 用來執行接口中的真實業務方法 
     * 這個方法是系統調用,不用我們自己調用
     * @param proxy 代理類對象
     * @param method 代理的真實方法
     * @param args 方法裏面的參數
     * @return
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object obj = null;
        try {
            MySecurityCheck(); //安全監測
            obj = method.invoke(this.targetObject, args); // 通過反射調用真實方法
            return obj;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }
    // 在執行真實操作之前,先要進行一些輔助操作,比如安全性檢測
    private void MySecurityCheck()
    {
        System.out.println("check security first...");
    }
}

最後,測試運行結果:

JDK動態代理總結

動態代理和靜態代理相比,JDK動態代理大大降低了我們的開發任務,同時減少了對業務接口的依賴,大大降低了代碼的耦合度,底層原理就是使用反射機制來調用的業務方法。但是JDK動態態代理任然有一個缺陷,就是實現類必須使用接口定義業務方法,如果沒有實現接口,就無法使用JDK動態代理。

那麼對於那些沒有實現接口的類,又是如何實現動態代理的呢???

下面,就要用到CGLIB動態代理。

四、CGLIB動態代理實現及分析

cglib的動態代理底層採用的是字節碼技術,通過字節碼技術爲一個類創建子類,並在子類中採用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯,從而達到動態代理的效果。下面用代碼來實現一下

4.1 要使用cglib的動態代理,首先需要導入cglib所要依賴的jar包,這裏我創建的是maven項目,所以直接在pom文件中導入jar包:

        <!--cglib jar包-->
        <!-- https://mvnrepository.com/artifact/cglib/cglib -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2.2</version>
        </dependency>

4.2 這裏依然需要一個類,用來說明主要業務有哪些(不需要實現接口)

4.3 需要一個代理類,進行一些輔助操作

/**
 * cglib實現的動態代理 要實現MethodInterceptor接口 , 底層原理是採用字節碼技術爲一個類創建子類
 * 並在子類中採用方法攔截的技術攔截所有父類方法的調用,順勢植入橫切邏輯,因爲使用的是繼承,所以
 * 不能對final修飾的方法進行代理
 */

public class MyProxy implements MethodInterceptor {

    // 維護一個目標對象
    private Object target;

    public Object getProxyInstance(final Object target) {
        this.target = target;
        //Enhancer 是cglib中的一個字節碼增強器,他可以方便的對你想要處理的類進行擴展
        Enhancer enhancer = new Enhancer();
        // 將被代理的對象設置爲父類
        enhancer.setSuperclass(this.target.getClass());
        // 回調方法,設置攔截器
        enhancer.setCallback(this);
        // 動態創建一個代理類
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 執行業務方法之前先進行安全檢查
        securityCheck();
        // 執行主要業務方法
        Object obj = methodProxy.invokeSuper(o, objects);
        return obj;
    }
    //進行安全檢查的輔助方法
    private void securityCheck()
    {
        System.out.println("------securityCheck()-------");
    }

}

4.4 最後,進行方法的測試:

通過上面的分析,接下來在看看SpringAOP

五、SpringAOP

5.1 AOP簡介

在文章最開始的時候,已經大概介紹了一下AOP,說過的部分這裏就不重複了。

在AOP中,通常把軟件系統分爲兩部分:核心關注點 和 橫切關注點

  • 業務處理的主要流程是核心關注點
  • 與之關係不大的部分是橫切關注點。橫切關注點有一個特點:經常發生在覈心關注點的多處,而各處都基本相似(比如:權限認證,安全監測,事務處理等)

核心關注點和橫切關注點的關係如下圖:

AOP的主要作用也就是將系統中的核心關注點和橫切關注點分離開來。


5.2 AOP術語(白話)描述

AOP術語的專業解釋網上到處都是,但是對於初學者而言,個人感覺那個解釋不是太友好,比較難理解,所以在這裏我就不貼那些專業的解釋了,就自己對AOP專業術語的理解,用白話的方式進行了總結,具體如下圖:

5.2.1 Aspect(切面):

在我們的主要業務中會有許多橫切關注點,這些橫切關注點通俗的來說就是一些方法,用來輔助主要業務,我們通常會把這些方法放到一個專門的類中,這個類就叫做切面類。

注:這張圖以及下面這幾張圖是根據個人理解所畫,如果有不正確的地方,希望大佬們可以指出來

5.2.2 Advice(通知)

切面裏面的方法又叫做通知,通知就是說明了這個方法是幹什麼的(比如是安全監測的方法),通知還決定了這個方法什麼時候執行。

Spring切面中的五類通知(這裏先做了解,下面代碼中在具體看怎麼用):

  • 前置通知(Before):在目標方法被調用之前調用通知
  • 後置通知(After):在目標方法完成之後調用通知,此時不會關心方法的輸出結果是什麼
  • 返回通知(After-returning):在目標方法成功執行之後調用通知
  • 異常通知(After-throwing):在目標方法拋出異常之後調用通知
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法通知之前和調用之後執行之定義的行爲

5.2.3 PointCut(切點):

切點:和上面的通知相似,通知定義的是什麼時候調用方法,而切點定義的是在什麼地方(哪一個業務方法上)調用輔助方法(比如要在增加用戶的方法上調用安全檢查的輔助方法);

5.2.4 Join Point (連接點)和 Weave (織入)

連接點:是一個抽象的概念,在我們要將一個切面 ‘放入’ 我們的主要業務(增刪查該等)中的時候,在我們眼中,我們的業務那部分代碼是一個整體,我們不能把業務代碼一下分成兩部分,然後把切面放在中間,而是在主要業務中的某個位置(通知定義的什麼時候,切點定義的什麼位置)有一個 點 ,就在這個點的位置調用我們的輔助方法(橫切關注點)。

織入:上面說的吧切面 ‘放入’ 主要業務,這個 '放入' 的過程就叫做 織入, 最後所有術語關係如下圖所示。

以上就是AOP術語的大概意思,如果是一個初學者,看來可能還是不太明白,接下來再結合下面的SpringAOP的實現代碼看看應該就差不多理解了。下面提供兩種方式實現SpringAOP,分別是配置文件的方式和註解的方式

5.2 SpringAOP的實現

5.2.1 通過註解的方式實現

1.首先,因爲這是一個Spring的項目,所以需要導入Spring相關的jar包,要使用AOP,也好導入AOP的jar包,pom文件如下,有單元測試的話,導入junit的jar包:

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.sonatype.sisu</groupId>
            <artifactId>sisu-inject-bean</artifactId>
            <version>1.4.2</version>
        </dependency>

        <!--這個是AOP的jar包-->
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.3</version>
        </dependency>

2、同樣的是一個接口和一個實現類

3、一個代理類:MyProxy.java

4、由於這是一個Spring的項目,所以需要用Spring配置文件:contextApplication-anno.xml

<?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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd ">

    <!--這個表示Spring只是AOP-->
    <aop:aspectj-autoproxy/>
    <!--將類交給Spring管理-->
    <bean id="myProxy" class="per.fei.proxy.SpringAOP.annotation.MyProxy"/>
    <bean id="realImpl" class="per.fei.proxy.SpringAOP.annotation.RealImpl"/>

</beans>

5、最後,寫出測試代碼

5.2.2 通過配置文件的方式實現

這裏同樣需要上面的jar包和一個接口一個類,這裏就不重複截屏了,不同的只是代理類 MyProxy.java 和 Spring的配置文件

代理類:MyProxy.java 此時的MyProxy.java中只需要放一些輔助方法,別的都不需要放了,其他的都在配置文件中定義

配置文件:contextApplication-configFile.xml

<?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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd ">

    <aop:aspectj-autoproxy/>
    <bean id="realImpl" class="per.fei.proxy.SpringAOP.config_file.RealImpl"/>
    <bean id="myProxy" class="per.fei.proxy.SpringAOP.config_file.MyProxy"/>
    
    <aop:config>
        <!--定義切面-->
        <aop:aspect id="myAspect" ref="myProxy">
            <!--定義切點--> <!--給add開頭的方法上定義切點-->
            <aop:pointcut id="myPointCut" expression="execution(* add*(..))"/>
            <!--定義通知--> <!-- Before表示在調用業務方法之前調用橫切關注點 -->
            <aop:before method="securityCheck" pointcut-ref="myPointCut"/>
        </aop:aspect>
    </aop:config>
</beans>

測試方法:

5.3 SpringAOP源碼簡單分析

其實SpringAOP使用的是動態代理模式,上面說到兩種動態代理,那麼它到底是用的哪種代理模式呢,下面這段Spring的源碼+註釋會給我們答案

看到這差不多就可以總結SpringAOP了:如果使用了接口,就默認使用JDK動態代理,如果沒有使用接口,就使用CGLIB的動態代理

以上是我學完SpringAOP後結合書籍+理解+網上看相關資料後的總結,希望可以幫助到需要的人,同時,如果有總結的不對的地方,希望大家可以指出來.

 

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