1. 程式人生 > 其它 >spring第三天---動態代理與AOP

spring第三天---動態代理與AOP

技術標籤:spring學習springjavaaop

Spring第三天—動態代理與AOP

文章目錄

1.1 轉賬方法演示事務問題

先是拿轉賬的案例,舉了一下異常時的不合理。


public void transfer(String sourceName, String targetName, Float money) {
        //1. 根據名稱查詢轉出賬戶
        Account source = accountDao.
findAccountByName(sourceName); //2. 根據名稱查詢轉入賬戶 Account target = accountDao.findAccountByName(targetName); //3. 轉出賬戶減錢 source.setMoney(source.getMoney() - money); //4. 轉入賬戶加錢 target.setMoney(target.getMoney() + money); //5. 更新轉出賬戶 accountDao.updateAccount
(source); int i = 1/0; //6. 更新轉入賬戶 accountDao.updateAccount(target); }

由於初始程式碼執行一次資料庫就獲取一個Connection,導致每個Connection的事務都是獨立的,多個事務。會出現有的事務成功了,有的事務失敗了。(轉出扣款成功、轉入加錢失敗)

  • 解決方法
    ThreadLocal
    每個執行緒繫結一個Connection + 手動commit 保證事務一致性。
    編寫事務管理工具類和ConnectionUtils。

/**
 * @author Ven
 * @version 1.0 2020/12/9
 *
 * 連線的工具類,它用於從資料來源中獲取一個連線,並且實現和執行緒的繫結
 */
public class ConnectionUtils {

    private  ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 獲取當前執行緒上的連線
     * @return
     */
    public Connection getThreadConnection(){

        try {
            //1. 先從ThreadLocal上獲取
            Connection conn = tl.get();
            //2.判斷當前執行緒上是否有連線
            if(conn == null){
                //3.從資料來源中獲取一個連線,並且存入ThreadLocal中
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.返回當前執行緒上的連線
            return conn;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 把連線和執行緒解綁
     */
    public void removeConnection(){

        tl.remove();
    }
}

執行緒解綁


/**
* 釋放連線--事務管理工具類中
*/
public void release(){
    try {

        connectionUtils.getThreadConnection().close(); // 還回連線池中,會有個問題就是服務時執行緒到時候也是還回執行緒池中,
        // 那麼下次獲取這個執行緒時,該執行緒綁定了資料庫連線,但資料庫連線被關閉了,不能用了,可能會有問題。
        connectionUtils.removeConnection();   //多一句這個,解綁
    } catch (Exception e) {
        e.printStackTrace();
    }

}

1.2 使用事務管理工具類保證事務一致性,並配置spring的ioc

注意:bean.xml中dataSource不再配置在QueryRunner下、dao層同步修改使用ConnectionUtils與資料庫互動,改為都由ConnectionUtils注入。
類成員提供set方法,以便spring注入。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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置Service-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <!-- 注入dao-->
        <property name="accountDao" ref="accountDao"></property>
        <!--注入事務管理器-->
        <property name="txManager" ref="txManager"></property>
    </bean>
    <!--配置Dao物件-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccoutDaoImpl">
        <!--注入QueryRunner-->
        <property name="runner" ref="runner"></property>
        <!--注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
    </bean>

    <!--配置資料來源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--連線資料庫的必備資訊-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>

    </bean>
    <!--    配置Connection的工具類 ConnectionUtils-->
    <bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils">
        <!--注入資料來源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事務管理器-->
    <bean id="txManager" class="com.itheima.utils.TranscationManager">
        <!--注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
</beans>

1.3 代理!!!

1.2 雖然實現了事務的一致性,但每個方法中都引用了事務工具類中的開啟事務、提交事務、回滾事務、釋放連線方法。 造成了方法之間的耦合,導致如果事務工具類修改下方法名,那都得相應改變。

以買電腦為例,原來消費者可以直接從廠家買,廠家直接提供貨、售後。
但多了後,發展成了 中間有代理商的模式。 代理商和廠家有約定,代理利潤是 銷售價的20%等等,有一定的約束。 然後消費者從代理商買電腦。

  • 動態代理:
    • 特點: 位元組碼隨用隨建立,隨用隨載入
    • 作用: 不修改原始碼的基礎上對方法增強
    • 分類:
      • 基於介面的動態代理
      • 基於子類的動態代理

1.3.1 基於介面的動態代理

  • 基於介面的動態代理
  • 涉及的類:Proxy
  • 提供者:JDK官方
  • 如何建立代理物件:
    使用Proxy類中的newProxyInstance方法
  • 建立代理物件的要求:
    被代理類最少實現一個介面,如果沒有則不能使用
  • newProxyInstance方法的引數:
    • ClassLoader:類載入器
      它是用於載入代理物件位元組碼的。和被代理物件使用相同的類載入器。固定寫法 : ***.getClass().getClassLoader()
    • Class[]:位元組碼陣列
      它是用於讓代理物件和被代理物件有相同方法。固定寫法: ***.getClass().getInterfaces()
    • InvocationHandler:用於提供增加的程式碼
      它是讓我們寫如何代理。我們一般都是寫一個該介面的實現類,通常情況下都是匿名內部類,但不是必須的。
      此介面的實現類都是誰用誰寫。

下面程式碼很重要!!!!


/**
 * @author Ven
 * @version 1.0 2020/12/9
 * 模擬一個消費者
 */
public class Client {

    public static void main(String[] args) {
        //匿名內部類訪問外部成員變數時,外部成員要求是 最終的 final的。
        //producer相當於廠家,被代理物件。 廠家需遵循一些約定(producer實現了IProducer介面)
        final Producer producer = new Producer();
        /**
         * 動態代理:
         *    特點: 位元組碼隨用隨建立,隨用隨載入
         *    作用: 不修改原始碼的基礎上對方法增強
         *    分類:
         *          基於介面的動態代理
         *          基於子類的動態代理
         *     基於介面的動態代理
         *          涉及的類:Proxy
         *          提供者:JDK官方
         *     如何建立代理物件:
         *          使用Proxy類中的newProxyInstance方法
         *     建立代理物件的要求:
         *          被代理類最少實現一個介面,如果沒有則不能使用
         *      newProxyInstance方法的引數:
         *          ClassLoader:類載入器
         *              它是用於載入代理物件位元組碼的。和被代理物件使用相同的類載入器。固定寫法 : ***.getClass().getClassLoader()
         *          Class[]:位元組碼陣列
         *              它是用於讓代理物件和被代理物件有相同方法。固定寫法。 ***.getClass().getInterfaces()
         *          InvocationHandler:用於提供增加的程式碼
         *              它是讓我們寫如何代理。我們一般都是寫一個該介面的實現類,通常情況下都是匿名內部類,但不是必須的。
         *              此介面的實現類都是誰用誰寫。
         *
        */

        IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
            /**
             * 作用: 執行被代理物件的任何介面方法都會經過該方法,相當於攔截
             * 方法引數的含義
             * @param proxy  代理物件的引用
             * @param method  當前執行的方法
             * @param args    當前執行方法所需的引數
             * @return        和被代理物件方法有相同的返回值
             * @throws Throwable
             */
            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);
    }
}

1.3.2 基於子類的動態代理

注:此時Producer不是IProducer介面的實現類

  • 基於子類的動態代理
  • 涉及的類:Enhancer
  • 提供者:第三方cglib庫
  • 如何建立代理物件:
    使用Enhancer類中的create方法
  • 建立代理物件的要求:
    被代理類不能是最終類 ,因為如果是最終類,就不能再建立子類了,也就不能建立代理物件了
  • create方法的引數:
    • Class:位元組碼
      它是用於被代理物件的位元組碼;
    • Callback:用於提供增加的程式碼
      它是讓我們寫如何代理。我們一般都是寫一個該介面的實現類,通常情況下都是匿名內部類,但不是必須的。
      此介面的實現類都是誰用誰寫。
      我們一般寫的都是該介面的子介面實現類

下面程式碼很重要!!!!


/**
 * @author Ven
 * @version 1.0 2020/12/9
 * 模擬一個消費者
 */
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
             */
            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);

    }
}

1.3.3 基於動態代理實現事務控制

AccountServiceImpl 裡剝離了開啟事務、提交事務等操作方法和事務管理器物件,只有最初始的dao層相應操作。事務控制交由BeanFactory中返回的accountService動態代理實現。

BeanFactory動態代理中:
//2. 執行操作
rtValue = method.invoke(accountService, args);
//以及需要加個final
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}


/**
 * @author Ven
 * @version 1.0 2020/12/9
 * 用於建立service的代理物件的工廠
 */
public class BeanFactory {

    private IAccountService accountService;

    private TranscationManager txManager;

    public void setTxManager(TranscationManager txManager) {
        this.txManager = txManager;
    }

    //需要加個final
    public final void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    /**
     * 獲取Service代理物件
     * @return
     */
    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {

            /**
             * 新增事務的支援
             * @param proxy
             * @param method
             * @param args
             * @return
             * @throws Throwable
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object rtValue = null;
                try {
                    //1. 開啟事務
                    txManager.beginTransaction();
                    //2. 執行操作
                    rtValue = method.invoke(accountService, args);
                    //3.提交事務
                    txManager.commit();
                    //4.返回結果
                    return rtValue;
                } catch (Exception e) {
                    //5.回滾操作
                    txManager.rollback();
                    throw new RuntimeException(e);
                } finally {
                    //6. 釋放連線
                    txManager.release();
                }
            }
        });
    }
}

同時bean.xml ioc也要相應改變,注意代理service物件建立方式


<!--配置代理的service-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>

<!--配置beanfactory-->
<bean id="beanFactory" class="com.itheima.factory.BeanFactory">
    <!--注入service-->
    <property name="accountService" ref="accountService"></property>
    <!--注入事務管理器-->
    <property name="txManager" ref="txManager"></property>
</bean>

然後測試類AccountServiceTest,Qualifier指明使用動態代理返回的物件。


@Autowired
@Qualifier("proxyAccountService")
private IAccountService as;

1.4 Spring中AOP術語

  • Joinpoint(連線點): 被攔截到的方法.

    • 理解:業務層方法全是連線點,連線業務和動態代理增強的方法的點。增強只能通過這些方法。
  • Pointcut(切入點): 我們對其進行增強的方法.

舉例 IAccountService.java 介面 和 BeanFactory.java (建立service的代理物件的工廠):


 /**
     * 刪除
     * @param accountId
     */
    void deleteAccount(Integer accountId);

    /**
     * 轉賬
     * @param sourceName   轉出賬戶名稱
     * @param targetName   轉入賬戶名稱
     * @param money        轉賬金額
     */
    void transfer(String sourceName, String targetName, Float money);

    void test();    //它是連線點 Joinpint,因為在介面,業務層方法全是連線點,連線業務和動態代理增強的方法的點。  增強只能通過這些方法
    //切入點: 增強的方法才是,test不是切入點,只是連線點。
    //所有的切入點Pointcut,都是連線點。  但不是所有的連線點都是切入點。 只有被增強的才是切入點

/**
     * 獲取Service代理物件
     * @return
     */
    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {

            /**
             * 新增事務的支援
             * @param proxy
             * @param method
             * @param args
             * @return
             * @throws Throwable
             */
             //整個的invoke方法在執行就是環繞通知
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                //這個方法沒有被增強,不是切入點
                if("test".equals(method.getName())){
                    return method.invoke(accountService, args);
                }

                Object rtValue = null;
                try {
                    //1. 開啟事務
                    txManager.beginTransaction();  //--->前置通知
                    //2. 執行操作
                    rtValue = method.invoke(accountService, args);  //--->環繞通知中有明確的切入點方法呼叫
                    //3.提交事務
                    txManager.commit();  //--->後置通知
                    //4.返回結果
                    return rtValue;
                } catch (Exception e) {
                    //5.回滾操作
                    txManager.rollback();  //--->異常通知
                    throw new RuntimeException(e);
                } finally {
                    //6. 釋放連線
                    txManager.release();  //--->最終通知
                }
            }
        });
    }

  • Advice(通知/增強): 對切入點進行的增強操作
    包括前置通知,後置通知,異常通知,最終通知,環繞通知
    以切入點方法為劃分依據。

-Target(目標物件):
代理的目標物件,即被代理物件。(accountService)

  • Weaving(織入): 是指把增強應用到目標物件來建立新的代理物件的過程。

    • 動態代理技術建立一個新的物件,返回一個代理物件,返回代理物件過程中從中增強方法實現事務支援,這個加入事務支援過程,叫織入。
  • Proxy(代理):
    一個類被AOP織入增強後,就產生一個結果代理類。

    • 即代理物件 : (IAccountService) Proxy
  • Aspect(切面): 是切入點和通知(引介)的結合

    • 建立切入點方法和通知方法在執行呼叫的對應關係,叫切面。
    • 如果配置方式,即 得有事務相關的方法、accountService某些方法
      配置說明哪個service、哪些方法、然後哪些方法在accountService方法前先執行、後執行,這叫切面。

執行階段(Spring 框架完成的)
Spring 框架監控切入點方法的執行。一旦監控到切入點方法被執行,使用代理機制,動態建立目標對
象的代理物件,根據通知類別,在代理物件的對應位置,將通知對應的功能織入,完成完整的程式碼邏輯執行。

1.5 spring中基於xml的AOP配置步驟

  • bean.xml檔案中

1.5.1 bean.xml引用約束xmlns: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"
       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">
</beans>

1.5.2 把通知Bean也交給spring來管理


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

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

1.5.4 使用aop:aspect 標籤表明配置切面

  • id屬性:是給切面提供一個唯一標識
  • ref屬性:是指定通知類bean的id

1.5.5 在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()
      • 類名和方法名都可以使用*來實現通配 此時只有兩個有增強,因為update有引數,不適配
        * *..*.*()
      • 引數列表:
        可以直接寫資料型別;
        • 基本型別直接寫名稱 int
        • 引用型別寫包名.類名的方式 java.lang.String
        • 可以使用萬用字元表示任意型別,但是必須有引數
        • 可以使用…表示有無引數均可,有引數可以是任意型別
          全統配寫法: 不暫同,不然所有方法都增強了
          * *..*.*(..)
        • 實際開發中切入點表示式的通常寫法:
          切到業務層實現類下的所有方法
          * com.itheima.service.impl.*.*(..)

具體示例,重要!!:


<?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的Ioc,把service物件配置進來-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

    <!--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()
                類名和方法名都可以使用*來實現通配   此時只有兩個有增強,因為update有引數,不適配
                    * *..*.*()
                引數列表:
                    可以直接寫資料型別;
                        基本型別直接寫名稱  int
                        引用型別寫包名.類名的方式   java.lang.String
                    可以使用萬用字元表示任意型別,但是必須有引數
                    可以使用..表示有無引數均可,有引數可以是任意型別
                全統配寫法:    不暫同,不然所有方法都增強了
                * *..*.*(..)

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

    -->

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

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


</beans>

1.5.6 進一步改造

前置通知<aop:before>

1.5.6.1其他通知型別:
  • 後置通知(在切入點方法正常執行之後執行 它和異常通知永遠只能執行一個)
    <aop:after-returning>

  • 異常通知(在切入點方法產生異常之後執行 它和後置通知永遠只能執行一個)
    <aop:after-throwing>

  • 最終通知(無論切入點方法是否正常執行它都會在其後面執行)
    <aop:after>

  • 配置環繞通知(spring框架為我們提供的一種可以在程式碼中手動控制增強方法何時執行的方式)
    <aop:around>

同時,切入點表示式也可以提煉:
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/> 此標籤可寫在aop:aspect標籤內部(只能當前切面使用),也可以寫在aspect外面,此時就所有切面可用。不過寫外面時一定要寫在切面前面。

具體示例:


<!--配置AOP-->
<aop:config>

    <!--配置切入點表示式:id屬性用於指定表示式的唯一標識。 expression屬性用於指定表示式內容
            此標籤寫在aop:aspect標籤內部只能當前切面使用
            它還可以寫在aop:aspect外面,此時就變成了所有切面可用。  注意要按約束來,必須配置在切面之前,之後會報錯。
        -->
    <aop:pointcut id="pt1" expression="execution(* com.itheima.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>

1.5.6.2環繞通知

<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around> 如果只配置這個,那麼會發現有問題,切入點方法還沒執行,而通知方法執行了。需手動指定切入點方法呼叫。

  • Spring框架為我們提供了一個介面:ProceedingJoinPoint。 該介面有一個方法proceed(),此方法就相當於明確呼叫切入點方法
    該介面可以作為環繞通知的方法引數,在程式執行時,spring框架會為我們提供該介面的實現類供我們使用。

Object[] args = pjp.getArgs(); //得到方法執行所需的引數
Object rtValue = pjp.proceed(args); //明確呼叫業務層方法(切入點方法)


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

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

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

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

        return rtValue;
    } catch (Throwable t) {   //必須是 Throwable ,Exception不行 攔不住

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

}

1.6 Spring基於註解的AOP配置

  • 1.增加bean.xml中關於context的約束

<?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">
</beans>

  • 2.配置spring建立容器時要掃描的包

  • 3.相應類加上註解

    • @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
      private void pt1(){} 切入點表示式
    • @Before("pt1()") 前置通知
    • @AfterReturning("pt1()") 後置通知
    • @AfterThrowing("pt1()") 異常通知
    • @After("pt1()") 最終通知
    • @Around("pt1()") 環繞通知

@Component("logger")
@Aspect //表示當前類是一個切面類
public class Logger {

    @Pointcut("execution(* com.itheima.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中的環繞通知:
     *      它是spring框架為我們提供的一種可以在程式碼中手動控制增強方法何時執行的方式
     *
     */
    @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();  //得到方法執行所需的引數

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

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

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

            return rtValue;
        } catch (Throwable t) {   //必須是 Throwable ,Exception不行 攔不住

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

    }

  • 4.配置spring開啟註解AOP的支援
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

  • 5.不採用環繞通知,用spring註解的話,最終通知和後置通知會存在 順序執行的bug,不建議。儘量用環繞通知,手動控制執行順序