spring的AOP使用3

動態代理的第二種方式

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

  1. 創建maven,在pom.xml添加CGLib的依賴
 <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.1_3</version>
</dependency>
  1. 創建被代理的類
package com.cglib;

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

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

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

  1. 使用Enhancer 類創建代理對象
package com.cglib;

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

import java.lang.reflect.Method;

/**
 * 模擬一個消費者
 */
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);
    }
}

基於XML的AOP 配置

  1. 導入依賴
<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.0.2.RELEASE</version>
    </dependency>

    <!--aop依賴-->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.8.7</version>
    </dependency>

  1. 編寫業務代碼
package org.example.service;

public interface IAccountService {
    /**
     * 模擬保存賬戶
     */
    void saveAccount();

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

    /**
     * 刪除賬戶
     * @return
     */
    int  deleteAccount();
}

package org.example.service.impl;

import org.example.service.IAccountService;

public class AccountServiceImpl implements IAccountService {
    @Override
    public void saveAccount() {
        System.out.println("執行了保存");
    }

    @Override
    public void updateAccount(int i) {
        System.out.println("執行了更新"+i);

    }

    @Override
    public int deleteAccount() {
        System.out.println("執行了刪除");
        return 0;
    }
}

  1. 創建 spring 的配置文件並導入約束配置spring的ioc
<?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="org.example.service.impl.AccountServiceImpl"></bean>

</beans>
  1. 抽取公共代碼製作成通知
package org.example.utils;

/**
 * 用於記錄日誌的工具類,它裏面提供了公共的代碼
 */
public class Logger {
    /**
     * 用於打印日誌:計劃讓其在切入點方法執行之前執行(切入點方法就是業務層方法)
     */
    public  void printLog(){
        System.out.println("Logger類中的pringLog方法開始記錄日誌了。。。");
    }
}

  1. AOP配置

第一步:把通知類用 bean 標籤配置起來

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

第二步:使用 aop:config 聲明 aop 配置

aop:config:
作用:用於聲明開始 aop 的配置
<aop:config>
<!-- 配置的代碼都寫在此處 -->
</aop:config>

第三步:使用 aop:aspect 配置切面

aop:aspect:
作用:
用於配置切面。
屬性:
id:給切面提供一個唯一標識。
ref:引用配置好的通知類 bean 的 id。
<aop:aspect id="txAdvice" ref="txManager">
<!--配置通知的類型要寫在此處-->
</aop:aspect>

第 四 步:使用 aop:pointcut 配置切入點表達式

aop:pointcut :
作用:
用於配置切入點表達式。就是指定對哪些類的哪些方法進行增強。
屬性:
expression:用於定義切入點表達式。
id:用於給切入點表達式提供一個唯一標識
<aop:pointcut expression="execution(
public void com.itheima.service.impl.AccountServiceImpl.transfer(
java.lang.String, java.lang.String, java.lang.Float)
)" id="pt1"/>

第 五 步:使用 aop:xxx 配置 對應的通知類型

aop:before
作用:
用於配置前置通知。指定增強的方法在切入點方法之前執行
屬性:
method:用於指定通知類中的增強方法名稱
ponitcut-ref:用於指定切入點的表達式的引用
poinitcut:用於指定切入點表達式
執行時間點:
切入點方法執行之前執行
<aop:before method="beginTransaction" pointcut-ref="pt1"/>

aop:after-returning
作用:
用於配置後置通知
屬性:
	method :指定通知中方法的名稱。
	pointct :定義切入點表達式
	pointcut-ref :指定切入點表達式的引用
執行時間點:
切入點方法正常執行之後。它和異常通知只能有一個執行
<aop:after-returning method="commit" pointcut-ref="pt1"/>

aop:after-throwing
作用:
用於配置異常通知
屬性:
	method :指定通知中方法的名稱。
	pointct :定義切入點表達式
	pointcut-ref :指定切入點表達式的引用
執行時間點:
切入點方法執行產生異常後執行。它和後置通知只能執行一個
<aop:after-throwing method="rollback" pointcut-ref="pt1"/>

aop:after
作用:
用於配置最終通知
屬性:
	method :指定通知中方法的名稱。
	pointct :定義切入點表達式
	pointcut-ref :指定切入點表達式的引用
執行時間點:
無論切入點方法執行時是否有異常,它都會在其後面執行。
<aop:after method="release" pointcut-ref="pt1"/>

切入點表達式說明

execution:匹配方法的執行(常用)
execution(表達式)
表達式語法:execution([修飾符] 返回值類型 包名.類名.方法名(參數))
寫法說明:
全匹配方式:
public void
org.example.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)

訪問修飾符可以省略
void
org.example.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)

返回值可以使用*號,表示任意返回值
*
org.example.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)

包名可以使用*號,表示任意包,但是有幾級包,需要寫幾個*
* *.*.*.*.AccountServiceImpl.saveAccount(com.itheima.domain.Account)

使用..來表示當前包,及其子包
* org..AccountServiceImpl.saveAccount(com.itheima.domain.Account)

類名可以使用*號,表示任意類
* org..*.saveAccount(com.itheima.domain.Account)

方法名可以使用*號,表示任意方法
* org..*.*( com.itheima.domain.Account)

參數列表可以使用*,表示參數可以是任意數據類型,但是必須有參數
* org..*.*(*)

參數列表可以使用..表示有無參數均可,有參數可以是任意類型
* org..*.*(..)

全通配方式:
* *..*.*(..)

注:
通常情況下,我們都是對業務層的方法進行增強,所以切入點表達式都是切到業務層實現類。
execution(* org.example.service.impl.*.*(..))

AOP配置例子如下

 <!--spring中基於XML的AOP配置步驟
        1、把通知Bean也交給spring來管理
        2、使用aop:config標籤表明開始AOP的配置
        3、使用aop:aspect標籤表明配置切面
                id屬性:是給切面提供一個唯一標識
                ref屬性:是指定通知類bean的Id。
        4、在aop:aspect標籤的內部使用對應標籤來配置通知的類型
               我們現在示例是讓printLog方法在切入點方法執行之前之前:所以是前置通知
               aop:before:表示配置前置通知
                    method屬性:用於指定Logger類中哪個方法是前置通知
                    pointcut屬性:用於指定切入點表達式,該表達式的含義指的是對業務層中哪些方法增強

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

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

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

    <!--配置AOP-->
    <!--配置AOP-->
    <aop:config>
        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置通知的類型,並且建立通知方法和切入點方法的關聯-->
            <aop:before method="printLog" pointcut="execution(* org.example.service.impl.*.*(..))"></aop:before>
        </aop:aspect>
    </aop:config>

執行測試

@Test
    public void shouldAnswerWithTrue()
    {
        //1.獲取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.獲取對象
        IAccountService accountService = (IAccountService)ac.getBean("accountService");
        //3.執行方法
        accountService.saveAccount();
        accountService.updateAccount(1);
        accountService.deleteAccount();
    }

在這裏插入圖片描述

環繞通知

配置方式:
<aop:config>
<aop:pointcut expression="execution(* com.itheima.service.impl.*.*(..))"
id="pt1"/>
<aop:aspect id="txAdvice" ref="txManager">
<!-- 配置環繞通知 -->
<aop:around method="transactionAround" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
aop:around :
作用:
用於配置環繞通知
屬性:
method:指定通知中方法的名稱。
pointct:定義切入點表達式
pointcut-ref:指定切入點表達式的引用
說明:
它是 spring 框架爲我們提供的一種可以在代碼中手動控制增強代碼什麼時候執行的方式。
注意:
通常情況下,環繞通知都是獨立使用的

編寫通知類

package org.example.utils;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * 用於記錄日誌的工具類,它裏面提供了公共的代碼
 */
public class Logger {
    /**
     * 前置通知
     */
    public  void beforePrintLog(){
        System.out.println("前置通知Logger類中的beforePrintLog方法開始記錄日誌了。。。");
    }

    /**
     * 後置通知
     */
    public  void afterReturningPrintLog(){
        System.out.println("後置通知Logger類中的afterReturningPrintLog方法開始記錄日誌了。。。");
    }
    /**
     * 異常通知
     */
    public  void afterThrowingPrintLog(){
        System.out.println("異常通知Logger類中的afterThrowingPrintLog方法開始記錄日誌了。。。");
    }

    /**
     * 最終通知
     */
    public  void afterPrintLog(){
        System.out.println("最終通知Logger類中的afterPrintLog方法開始記錄日誌了。。。");
    }

    /**
     * 環繞通知
     * 問題:
     *      當我們配置了環繞通知之後,切入點方法沒有執行,而通知方法執行了。
     * 分析:
     *      通過對比動態代理中的環繞通知代碼,發現動態代理的環繞通知有明確的切入點方法調用,而我們的代碼中沒有。
     * 解決:
     *      Spring框架爲我們提供了一個接口:ProceedingJoinPoint。該接口有一個方法proceed(),此方法就相當於明確調用切入點方法。
     *      該接口可以作爲環繞通知的方法參數,在程序執行時,spring框架會爲我們提供該接口的實現類供我們使用。
     *
     * spring中的環繞通知:
     *      它是spring框架爲我們提供的一種可以在代碼中手動控制增強方法何時執行的方式。
     */
    public Object aroundPringLog(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方法開始記錄日誌了。。。最終");
        }
    }
}

配置所有通知類型

<!--配置AOP-->
    <aop:config>
        <!-- 配置切入點表達式 id屬性用於指定表達式的唯一標識。expression屬性用於指定表達式內容
              此標籤寫在aop:aspect標籤內部只能當前切面使用。
              它還可以寫在aop:aspect外面,此時就變成了所有切面可用
          -->
        <aop:pointcut id="pt1" expression="execution(* org.example.service.impl.*.*(..))"></aop:pointcut>
        <!--配置切面 -->
        <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="aroundPringLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>

註解配置AOP

  1. 創建項目添加依賴
<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.0.2.RELEASE</version>
    </dependency>

    <!--aop依賴-->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.8.7</version>
    </dependency>
  1. 在配置文件中導入 context 的名稱空間並配置創建spring容器掃描的包和開啓註解aop
<?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">

    <!-- 配置spring創建容器時要掃描的包-->
    <context:component-scan base-package="org.example"></context:component-scan>

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

</beans>
  1. 把資源和通知類都使用註解 配置
package org.example.service;

public interface IAccountService {
    /**
     * 模擬保存賬戶
     */
    void saveAccount();

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

    /**
     * 刪除賬戶
     * @return
     */
    int  deleteAccount();
}

package org.example.service.impl;

import org.example.service.IAccountService;
import org.springframework.stereotype.Service;


@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Override
    public void saveAccount() {
        System.out.println("執行了保存");
    }

    @Override
    public void updateAccount(int i) {
        System.out.println("執行了更新"+i);

    }

    @Override
    public int deleteAccount() {
        System.out.println("執行了刪除");
        return 0;
    }
}

package org.example.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 用於記錄日誌的工具類,它裏面提供了公共的代碼
 */
@Component("logger")
@Aspect//表示當前類是一個切面類
public class Logger {
    @Pointcut("execution(* org.example.service.impl.*.*(..))")
    private void pt1(){}

    /**
     * 前置通知
     */
//    @Before("pt1()")
    public  void beforePrintLog(){
        System.out.println("前置通知Logger類中的beforePrintLog方法開始記錄日誌了。。。");
    }

    /**
     * 後置通知
     */
//    @AfterReturning("pt1()")
    public  void afterReturningPrintLog(){
        System.out.println("後置通知Logger類中的afterReturningPrintLog方法開始記錄日誌了。。。");
    }
    /**
     * 異常通知
     */
//    @AfterThrowing("pt1()")
    public  void afterThrowingPrintLog(){
        System.out.println("異常通知Logger類中的afterThrowingPrintLog方法開始記錄日誌了。。。");
    }

    /**
     * 最終通知
     */
//    @After("pt1()")
    public  void afterPrintLog(){
        System.out.println("最終通知Logger類中的afterPrintLog方法開始記錄日誌了。。。");
    }

    /**
     * 環繞通知
     * 問題:
     *      當我們配置了環繞通知之後,切入點方法沒有執行,而通知方法執行了。
     * 分析:
     *      通過對比動態代理中的環繞通知代碼,發現動態代理的環繞通知有明確的切入點方法調用,而我們的代碼中沒有。
     * 解決:
     *      Spring框架爲我們提供了一個接口:ProceedingJoinPoint。該接口有一個方法proceed(),此方法就相當於明確調用切入點方法。
     *      該接口可以作爲環繞通知的方法參數,在程序執行時,spring框架會爲我們提供該接口的實現類供我們使用。
     *
     * spring中的環繞通知:
     *      它是spring框架爲我們提供的一種可以在代碼中手動控制增強方法何時執行的方式。
     */
    @Around("pt1()")
    public Object aroundPringLog(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方法開始記錄日誌了。。。最終");
        }
    }
}

測試

@Test
    public void shouldAnswerWithTrue()
    {
        //1.獲取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.獲取對象
        IAccountService accountService = (IAccountService)ac.getBean("accountService");
        //3.執行方法
        accountService.saveAccount();
    }

在這裏插入圖片描述

不使用bean.xml

@Configuration
@ComponentScan(basePackages="org.example")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章