Spring_3 - AOP

Spring_3 AOP

一、AOP 的相關概念

1、AOP概述

(1) 什麼是 AOP

AOP:全稱是 Aspect Oriented Programming 即:面向切面編程。
簡單的說它就是把我們程序重複的代碼抽取出來,在需要執行的時候,使用動態代理的技術,在不修改源碼的
基礎上,對我們的已有方法進行增強。

(2) AOP 的作用及優勢

作用:
在程序運行期間,不修改源碼對已有方法進行增強。
優勢:
減少重複代碼  提高開發效率  維護方便

(3) AOP 的實現方式

(解決重複代碼,提取重複代碼,在方法執行時加進去)使用動態代理技術

2、AOP 的具體應用

(1) 動態代理回顧

特點:字節碼隨用隨創建,隨用隨加載。
它與靜態代理的區別也在於此。因爲靜態代理是字節碼一上來就創建好,並完成加載。
裝飾者模式就是靜態代理的一種體現。

(2) 動態代理常用的有兩種方式

基於接口的動態代理
提供者:JDK 官方的 Proxy 類。

如何創建代理對象:使用Proxy類中的newProxyInstance方法

要求:被代理類最少實現一個接口。

基於子類的動態代理
提供者:第三方的 CGLib,如果報 asmxxxx 異常,需要導入 asm.jar。
要求:被代理類不能用 final 修飾的類(最終類)。

(3) 使用 JDK 官方的 Proxy 類創建代理對象

對生產廠家要求的接口

public interface IProducer {

    /**
     * 銷售
     * @param money
     */
    public void saleProduct(float money);

    /**
     * 售後
     * @param money
     */
    public void afterService(float money);
}

實現要求接口的廠家類

public class Producer implements IProducer{

    /**
     * 銷售
     * @param money
     */
    @Override
    public void saleProduct(float money){
        System.out.println("銷售產品,並拿到錢:"+money);
    }

    /**
     * 售後
     * @param money
     */
    @Override
    public void afterService(float money){
        System.out.println("提供售後服務,並拿到錢:"+money);
    }
}

模擬一個消費者

/**
 * 模擬一個消費者
 */
public class Client {

    public static void main(String[] args) {
        
        //new一個廠家
        final Producer producer = new Producer();

        /*
         *  如何創建代理對象:
         *      使用Proxy類中的newProxyInstance方法
         *  創建代理對象的要求:
         *      被代理類最少實現一個接口,如果沒有則不能使用
         *  newProxyInstance方法的參數:
         *      ClassLoader:類加載器
         *          它是用於加載代理對象字節碼的。和被代理對象使用相同的類加載器。固定寫法。
         *      class[]:字節碼數組
         			它是用於讓代理對象和被代理對象具有相同的方法。實現相同的接口。
         *          
         *      InvocationHandler:用於提供增強的代碼
         *          它是讓我們寫如何代理。我們一般都是些一個該接口的實現類,通常情況下都是匿名內部類,但不是必須的。
         *          此接口的實現類都是誰用誰寫。
         */
       IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 作用:執行被代理對象的任何接口方法都會經過該方法
                     * 方法參數的含義
                     * @param proxy   代理對象的引用
                     * @param method  當前執行的方法
                     * @param args    當前執行方法所需的參數
                     * @return        和被代理對象方法有相同的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //提供增強的代碼
                        Object returnValue = null;

                        //1.獲取方法執行的參數
                        Float money = (Float)args[0];
                        //2.判斷當前方法是不是銷售
                        if("saleProduct".equals(method.getName())) {
                            returnValue = method.invoke(producer, money*0.8f);
                        }
                        return returnValue;
                    }
                });
        
        
        proxyProducer.saleProduct(10000f);
    }
}

(4) 使用 CGLib 的 的 Enhancer 類創建代理對象

不實現接口,子類的動態代理

/**
 * 一個生產者
 */
public class Producer {

    //銷售
    public void saleProduct(float money){
        System.out.println("銷售產品,並拿到錢:"+money);
    }

    //售後
    public void afterService(float money){
        System.out.println("提供售後服務,並拿到錢:"+money);
    }
}
/**
 * 模擬一個消費者
 */
public class Client {

    public static void main(String[] args) {
        final Producer producer = new Producer();

        /**
         *  基於子類的動態代理:
         *      涉及的類:Enhancer
         *      提供者:第三方cglib庫
         *  如何創建代理對象:
         *      使用Enhancer類中的create方法
         *  創建代理對象的要求:
         *      被代理類不能是最終類
         *  create方法的參數:
         *      Class:字節碼
         *          它是用於指定被代理對象的字節碼。
         *
         *      Callback:用於提供增強的代碼
         *          它是讓我們寫如何代理。我們一般都是些一個該接口的實現類,通常情況下都是匿名內部類,但不是必須的。
         *          此接口的實現類都是誰用誰寫。
         *          我們一般寫的都是該接口的子接口實現類:MethodInterceptor
         */
        Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 執行被代理對象的任何方法都會經過該方法
             * @param proxy
             * @param method
             * @param args
             *    以上三個參數和基於接口的動態代理中invoke方法的參數是一樣的
             * @param methodProxy :當前執行方法的代理對象
             * @return
             * @throws Throwable
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //提供增強的代碼
                Object returnValue = null;

                //1.獲取方法執行的參數
                Float money = (Float)args[0];
                //2.判斷當前方法是不是銷售
                if("saleProduct".equals(method.getName())) {
                    returnValue = method.invoke(producer, money*0.8f);
                }
                return returnValue;
            }
        });
        cglibProducer.saleProduct(12000f);
    }
}

二、Spring 中的 AOP

1、 Spring 中 AOP 的細節

(1) AOP 相關術語

Joinpoint (連接點):
所謂連接點是指那些被攔截到的點。在 spring 中,這些點指的是方法,因爲 spring 只支持方法類型的連接點。

Pointcut (切入點):
所謂切入點是指我們要對哪些 Joinpoint 進行攔截的定義。

Advice (通知/增強):
所謂通知是指攔截到 Joinpoint 之後所要做的事情就是通知。
通知的類型:前置通知,後置通知,異常通知,最終通知,環繞通知。

Introduction (引介):
引介是一種特殊的通知在不修改類代碼的前提下, Introduction 可以在運行期爲類動態地添加一些方法或 Field。

Target (目標對象):
代理的目標對象。

Weaving (織入):
是指把增強應用到目標對象來創建新的代理對象的過程。
spring 採用動態代理織入,而 AspectJ 採用編譯期織入和類裝載期織入。

Proxy (代理):
一個類被 AOP 織入增強後,就產生一個結果代理類。

Aspect (切面):
是切入點和通知(引介)的結合。

(2)spring AOP要明確的事

a、開發階段(我們做的

編寫核心業務代碼(開發主線):大部分程序員來做,要求熟悉業務需求。

把公用代碼抽取出來,製作成通知。(開發階段最後再做):AOP 編程人員來做。

在配置文件中,聲明切入點與通知間的關係,即切面。:AOP 編程人員來做。

b、運行階段(Spring框架完成的)

Spring 框架監控切入點方法的執行。一旦監控到切入點方法被運行,使用代理機制,動態創建目標對

象的代理對象,根據通知類別,在代理對象的對應位置,將通知對應的功能織入,完成完整的代碼邏輯運行。

(3) 關於代理的選擇

在 spring 中,框架會根據目標類是否實現了接口來決定採用哪種動態代理的方式。

2、基於 XML 的 AOP 配置

(1) 導入座標

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

        <!--解析切入點表達式-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>
    </dependencies>

(2) service包中的IAccountService和其實現類impl.AccountServiceImpl

public interface IAccountService {

    /**
     * 模擬保存賬戶
     */
    void saveAccount();

    /**
     * 模擬更新賬戶
     * @param i
     */
    void updateAccount(int i);

    /**
     * 刪除賬戶
     * @return
     */
    int deleteAccount();
}
public class AccountServiceImpl implements IAccountService {

    /**
     * 模擬保存賬戶
     */
    @Override
    public void saveAccount() {
        System.out.println("保存...");
    }

    /**
     * 模擬更新賬戶
     * @param i
     */
    @Override
    public void updateAccount(int i) {
        System.out.println("更新...");

    }

    /**
     * 刪除賬戶
     * @return
     */
    @Override
    public int deleteAccount() {
        System.out.println("刪除...");
        return 0;
    }
}

(3) Logger通知類

/**
 * 用戶記錄日誌的工具類,它裏面提供了公共的代碼
 */
public class Logger {

    /**
     * 前置通知
     */
    public void beforePrintLog() {
        System.out.println("logger類中的 前置通知");
    }

    /**
     * 後置通知
     */
    public void afterReturningPrintLog() {
        System.out.println("logger類中的 後置通知");
    }

    /**
     * 異常通知
     */
    public void afterThrowingPrintLog() {
        System.out.println("logger類中的 異常通知");
    }

    /**
     * 最終通知
     */
    public void afterPrintLog() {
        System.out.println("logger類中的 最終通知");
    }

    /**
     * 環繞通知
     * 問題:
     *      當我們配置了環繞通知之後,切入點方法沒有執行,而通知方法執行了。
     * 分析:
     *      通過對比動態代理中的環繞通知代碼,發現動態代理的環繞通知有明確的切入點方法調用,而我們的代碼中沒有。
     * 解決:
     *      Spring框架爲我們提供了一個接口:ProceedingJoinPoint。該接口有一個方法proceed(),此方法就相當於明確調用切入點方法。
     *      該接口可以作爲環繞通知的方法參數,在程序執行時,spring框架會爲我們提供該接口的實現類供我們使用。
     *
     * spring中的環繞通知:
     *      它是spring框架爲我們提供的一種可以在代碼中手動控制(寫入的位置)增強方法何時執行的方式。
     */
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();//得到方法執行所需的參數

            System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。前置");

            rtValue = pjp.proceed(args);//明確調用業務層方法(切入點方法)

            System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。後置");

            return rtValue;
        } catch (Throwable t) {
            System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。異常");
            throw new RuntimeException(t);
        } finally {
            System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。最終");
        }
    }
}

(4) bean.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">

    <!-- 配置srping的Ioc,把service對象配置進來-->
    <bean id="accountService" class="top.zoick.service.impl.AccountServiceImpl"></bean>


    <!-- 配置Logger類 -->
    <bean id="logger" class="top.zoick.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切入點表達式,id屬性用於指定表達式唯一標識 expression屬性用於指定表達式內容
                此標籤寫在aop:aspect標籤內部只能當前切面使用。
                它還可以寫在aop:aspect外面,此時就變成了所有切面可用。注意:由於約束的要求,若要寫在外面一定要放在aop:aspect前面
            -->
        <aop:pointcut id="pt1" expression="execution(* top.zoick.service.impl.*.*(..))"/>

        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置前置通知: 在切入點方法執行之前執行
            <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>-->

            <!-- 配置後置通知:在切入點方法正常執行之後執行,它和異常通知永遠只能執行一個
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>-->

            <!-- 配置異常通知:在切入點方法執行產生異常後執行。他和後置通知永遠只能執行一個
            <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>-->

            <!-- 配置最終通知,無論切入點方法是否正常執行它都會在其後面執行
            <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>-->

            <!--配置環繞通知 詳細的註釋在Logger類中-->
            <aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

spring中基於XML的AOP配置步驟

1、把通知Bean也交給spring來管理

2、使用aop:config標籤表明開始AOP的配置

3、使用aop:aspect標籤表明配置切面
    id屬性:是給切面提供一個唯一標識
    ref屬性:是指定通知類bean的Id。

4、在aop:aspect標籤的內部使用對應標籤來配置通知的類型
  aop:before:表示配置前置通知
    method屬性:用於指定Logger類中哪個方法是前置通知
    ponitcut-ref:用於指定切入點的表達式的引用
    poinitcut:用於指定切入點表達式
  aop:after-returning: 後置通知:在切入點方法正常執行之後執行,它和異常通知永遠只能執行一個
  aop:after-throwing: 異常通知:在切入點方法執行產生異常後執行。他和後置通知永遠只能執行一個
  aop:after: 最終通知,無論切入點方法是否正常執行它都會在其後面執行

aop:around: 環繞通知 詳細的註釋在Logger類中,通常情況下,環繞通知都是獨立使用的

切入點表達式相關的注意:

切入點表達式的寫法:
    關鍵字:execution(表達式)
    表達式:
        訪問修飾符  返回值  包名.包名.包名...類名.方法名(參數列表)
    標準的表達式寫法:
        public void top.zoick.service.impl.AccountServiceImpl.saveAccount()
    訪問修飾符可以省略
        void top.zoick.service.impl.AccountServiceImpl.saveAccount()
    返回值可以使用通配符,表示任意返回值
        * top.zoick.service.impl.AccountServiceImpl.saveAccount()
    包名可以使用通配符,表示任意包。但是有幾級包,就需要寫幾個*.
        * *.*.*.*.AccountServiceImpl.saveAccount())
    包名可以使用..表示當前包及其子包
        * *..AccountServiceImpl.saveAccount()
    類名和方法名都可以使用*來實現通配
        * *..*.*()
    參數列表:
        可以直接寫數據類型:
            基本類型直接寫名稱           int
            引用類型寫包名.類名的方式   java.lang.String
        可以使用通配符表示任意類型,但是必須有參數
        可以使用..表示有無參數均可,有參數可以是任意類型
    全通配寫法:
        * *..*.*(..)

    實際開發中切入點表達式的通常寫法:
        切到業務層實現類下的所有方法
            * top.zoick.service.impl.*.*(..)


<aop:pointcut>配置切入點表達式,id屬性用於指定表達式唯一標識 expression屬性用於指定表達式內容
    此標籤寫在aop:aspect標籤內部只能當前切面使用。
    它還可以寫在aop:aspect外面,此時就變成了所有切面可用。注意:由於約束的要求,若要寫在外面一定要放在aop:aspect前面
<aop:pointcut id="pt1" expression="execution(* top.zoick.service.impl.*.*(..))"/>
(5) 測試
public class AOTest {
    public static void main(String[] args) {
        //1.獲取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.獲取對象
        IAccountService accountService = (IAccountService) ac.getBean("accountService");
        //3.執行方法
        accountService.saveAccount();
    }
}

3、基於註解的 AOP 配置

(1) service類中impl.AccountServiceImpl

@Service("accountService")
public class AccountServiceImpl implements IAccountService {

    /**
     * 模擬保存賬戶
     */
    @Override
    public void saveAccount() {

        System.out.println("保存...");

    }

    /**
     * 模擬更新賬戶
     * @param i
     */
    @Override
    public void updateAccount(int i) {
        System.out.println("更新...");

    }

    /**
     * 刪除賬戶
     *
     * @return
     */
    @Override
    public int deleteAccount() {
        System.out.println("刪除...");
        return 0;
    }
}

(2) utils中的Logger

/**
 * 用戶記錄日誌的工具類,它裏面提供了公共的代碼
 */
@Component("logger")
@Aspect
public class Logger {

    @Pointcut("execution(* top.zoick.service.impl.*.*(..))")//切入點表達式
    private void pt1(){}

    /**
     * 前置通知
     */
//    @Before("pt1()")
    public void beforePrintLog() {
        System.out.println("logger類中的 前置通知");
    }

    /**
     * 後置通知
     */
//    @AfterReturning("pt1()")
    public void afterReturningPrintLog() {
        System.out.println("logger類中的 後置通知");
    }

    /**
     * 異常通知
     */
//    @AfterThrowing("pt1()")
    public void afterThrowingPrintLog() {
        System.out.println("logger類中的 異常通知");
    }

    /**
     * 最終通知
     */
//    @After("pt1()")
    public void afterPrintLog() {
        System.out.println("logger類中的 最終通知");
    }

    @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();//得到方法執行所需的參數

            System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。前置");

            rtValue = pjp.proceed(args);//明確調用業務層方法(切入點方法)

            System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。後置");

            return rtValue;
        } catch (Throwable t) {
            System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。異常");
            throw new RuntimeException(t);
        } finally {
            System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。最終");
        }
    }
}

(3) bean.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"
       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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--註解掃描-->
    <context:component-scan base-package="top.zoick"/>


    <!--配置spring開啓AOP註解-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>


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