1. 程式人生 > 實用技巧 >【Spring】Spring AOP

【Spring】Spring AOP

Spring AOP

SpringCRUD 存在的問題

  • 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: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/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="cn.parzulpan"/>
    
        <!-- 配置 QueryRunner -->
        <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
            <!-- 注入資料來源,建構函式形式-->
            <constructor-arg name="ds" ref="dataSource"/>
        </bean>
    
        <!-- 配置 資料來源 -->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="com.mysql.jdbc.Driver"/>
            <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/springT?useSSL=false"/>
            <property name="user" value="root"/>
            <property name="password" value="root"/>
        </bean>
    
    </beans>
    
  • BankAccountDAOImpl.java

    package cn.parzulpan.dao;
    
    import cn.parzulpan.domain.BankAccount;
    import cn.parzulpan.utils.ConnectionUtil;
    import org.apache.commons.dbutils.QueryRunner;
    import org.apache.commons.dbutils.handlers.BeanHandler;
    import org.apache.commons.dbutils.handlers.BeanListHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Repository;
    
    import java.sql.SQLException;
    import java.util.List;
    
    /**
     * @Author : parzulpan
     * @Time : 2020-12
     * @Desc : 銀行賬戶的持久層介面的實現類
     */
    
    @Repository("bankAccountDAO")
    public class BankAccountDAOImpl implements BankAccountDAO {
        @Autowired
        private QueryRunner runner;
    
        public List<BankAccount> findAll() {
            try {
                return runner.query("select * from bankAccount",
                        new BeanListHandler<BankAccount>(BankAccount.class));
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    
        public BankAccount findByName(String accountName) {
            try {
                List<BankAccount> accounts = runner.query("select * from bankAccount where name = ?",
                        new BeanListHandler<BankAccount>(BankAccount.class), accountName);
                if (accounts == null || accounts.size() == 0) {
                    return null;
                }
                if (accounts.size() > 1) {
                    throw new RuntimeException("結果集不一致,請檢查賬戶名稱!");
                }
                return accounts.get(0);
    
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    
  • BankAccountServiceImpl.java

    package cn.parzulpan.service;
    
    import cn.parzulpan.dao.BankAccountDAO;
    import cn.parzulpan.domain.BankAccount;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    /**
     * @Author : parzulpan
     * @Time : 2020-12
     * @Desc : 銀行賬戶的業務層介面的實現類
     */
    
    @Service("bankAccountService")
    public class BankAccountServiceImpl implements BankAccountService {
        @Autowired
        private BankAccountDAO bankAccountDAO;
    
        public List<BankAccount> findAll() {
            return accounts =  bankAccountDAO.findAll();
    }
    
        public void transfer(String sourceName, String targetName, Double money) {
            BankAccount source = bankAccountDAO.findByName(sourceName);
            BankAccount target = bankAccountDAO.findByName(targetName);
            source.setMoney(source.getMoney() - money);
            target.setMoney(target.getMoney() + money);
            bankAccountDAO.update(source);
            int i = 1 / 0;  //  模擬轉賬異常
            bankAccountDAO.update(target);
        }
    }
    

當執行 轉賬操作 時,由於執行有異常,轉賬失敗。但是因為每次執行持久層方法都是獨立事務,導致無法實現事務控制,不符合事務的一致性。

歸根結底,整個轉賬操作應該使用同一個連線。可以使用 ThreadLocal 物件把 Connection 和 當前執行緒繫結,使一個執行緒中只有一個能控制事務的物件。

ThreadLocal

  • ThreadLocal 可以解決多執行緒的資料安全問題。
  • ThreadLocal 可以給當前執行緒關聯一個數據,這個資料可以是普通變數,可以是物件,也可以是陣列和集合等。

ThreadLocal 特點

  • ThreadLocal 可以為當前執行緒關聯一個數據,它可以像 Map 一樣存取資料,key 為當前執行緒
  • 每一個 ThreadLocal 物件,只能為當前執行緒關聯一個數據,如果要為當前執行緒關聯多個數據,就需要使用 多個 ThreadLocal 例項,所以是執行緒安全的
  • 每個 ThreadLocal 物件例項定義的時候,一般都是 Static 型別
  • ThreadLocal 中儲存資料,線上程銷燬後,會由 JVM 自動釋放

利用事務控制解決 轉賬問題

小節原始碼

  • ConnectionUtil.java

    package cn.parzulpan.utils;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.SQLException;
    
    /**
    * @Author : parzulpan
    * @Time : 2020-12
    * @Desc : 連線物件的工具類,它用於從資料中獲取一個連線,並且實現和執行緒的繫結。
    */
    
    @Component
    public class ConnectionUtil {
        private ThreadLocal<Connection> conns = new ThreadLocal<Connection>();
    
        @Autowired
        private DataSource dataSource;
    
        /**
        * 獲取一個連線
        * @return connection
        */
        public Connection getThreadConnection() {
            // 1. 從 ThreadLocal 中獲取
            Connection connection = conns.get();
            // 2. 判斷當前執行緒上是否有連線
            if (connection == null) {
                try {
                    // 3. 從資料來源中獲取一個連線,並且存入 ThreadLocal
                    connection = dataSource.getConnection();
                    conns.set(connection);
                    connection.setAutoCommit(false);    // 設定這個連線為手動管理事務
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            // 4. 返回當前執行緒上的連線
            return connection;
        }
    
        /**
        * 提交事務並關閉連線
        */
        public void commitAndClose() {
            Connection connection = conns.get();
            if (connection != null) {   // 如果不等於 null,說明之前使用過這個連線,操作過資料庫
                try {
                    connection.commit();    // 提交事務
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        connection.close(); // 關閉連線,資源資源
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
            conns.remove(); // 對於用了執行緒池技術的,需要將連線與執行緒解綁
        }
    
        /**
        * 回滾事務並關閉連線
        */
        public void rollbackAndClose() {
            Connection connection = conns.get();
            if (connection != null) {   // 如果不等於 null,說明之前使用過這個連線,操作過資料庫
                try {
                    connection.rollback();    // 回滾事務
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        connection.close(); // 關閉連線,資源資源
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
            conns.remove(); // 對於用了執行緒池技術的,需要將連線與執行緒解綁
        }
    }
    
  • 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: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/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="cn.parzulpan"/>
    
        <!-- 配置 QueryRunner -->
        <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
            <!-- 注入資料來源,建構函式形式-->
    <!--        <constructor-arg name="ds" ref="dataSource"/>-->
            <!-- 註釋掉 注入資料來源,不需要自己獲取連線,在 ConnectionUtil 中注入,並由 ConnectionUtil 進行事務控制 -->
        </bean>
    
        <!-- 配置 資料來源 -->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="com.mysql.jdbc.Driver"/>
            <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/springT?useSSL=false"/>
            <property name="user" value="root"/>
            <property name="password" value="root"/>
        </bean>
        
    </beans>
    
  • BankAccountDAOImpl.java

    package cn.parzulpan.dao;
    
    import cn.parzulpan.domain.BankAccount;
    import cn.parzulpan.utils.ConnectionUtil;
    import org.apache.commons.dbutils.QueryRunner;
    import org.apache.commons.dbutils.handlers.BeanHandler;
    import org.apache.commons.dbutils.handlers.BeanListHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Repository;
    
    import java.sql.SQLException;
    import java.util.List;
    
    /**
     * @Author : parzulpan
     * @Time : 2020-12
     * @Desc : 銀行賬戶的持久層介面的實現類,使用 ConnectionUtil 事務控制
     */
    
    @Repository("bankAccountDAO")
    public class BankAccountDAOImpl implements BankAccountDAO {
        @Autowired
        private QueryRunner runner;
        @Autowired
        private ConnectionUtil connectionUtil;
    
        public List<BankAccount> findAll() {
            try {
                return runner.query(connectionUtil.getThreadConnection(), "select * from bankAccount",
                        new BeanListHandler<BankAccount>(BankAccount.class));
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    
        public BankAccount findByName(String accountName) {
            try {
                List<BankAccount> accounts = runner.query(connectionUtil.getThreadConnection(), "select * from bankAccount where name = ?",
                        new BeanListHandler<BankAccount>(BankAccount.class), accountName);
                if (accounts == null || accounts.size() == 0) {
                    return null;
                }
                if (accounts.size() > 1) {
                    throw new RuntimeException("結果集不一致,請檢查賬戶名稱!");
                }
                return accounts.get(0);
    
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    
  • BankAccountServiceImpl.java

    package cn.parzulpan.service;
    
    import cn.parzulpan.dao.BankAccountDAO;
    import cn.parzulpan.domain.BankAccount;
    import cn.parzulpan.utils.ConnectionUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    /**
    * @Author : parzulpan
    * @Time : 2020-12
    * @Desc : 銀行賬戶的業務層介面的實現類,使用 ConnectionUtil 事務控制
    */
    
    @Service("bankAccountService")
    public class BankAccountServiceImpl implements BankAccountService {
        @Autowired
        private BankAccountDAO bankAccountDAO;
        @Autowired
        private ConnectionUtil connectionUtil;
    
        public List<BankAccount> findAll() {
            List<BankAccount> accounts = null;
            try {
                accounts =  bankAccountDAO.findAll();
                connectionUtil.commitAndClose();
            } catch (Exception e) {
                connectionUtil.rollbackAndClose();
                throw new RuntimeException(e);
            }
            return accounts;
        }
    
        public void transfer(String sourceName, String targetName, Double money) {
            try {
                BankAccount source = bankAccountDAO.findByName(sourceName);
                BankAccount target = bankAccountDAO.findByName(targetName);
                source.setMoney(source.getMoney() - money);
                target.setMoney(target.getMoney() + money);
                bankAccountDAO.update(source);
                int i = 1 / 0;  //  模擬轉賬異常
                bankAccountDAO.update(target);
                connectionUtil.commitAndClose();
            } catch (Exception e) {
                connectionUtil.rollbackAndClose();
                throw new RuntimeException(e);
            }
        }
    }
    
    

雖然通過事務控制對業務層進行了改造,但是也產生了新的問題:業務層方法變得臃腫了,裡面充斥著很多重複程式碼。並且存在很多依賴注入。這個問題可以通過 Spring 事務管理 來解決!

更加嚴重的是,業務層方法和事務控制方法嚴重耦合了,試想一下,比如 提交事務並關閉連線 commitAndClose() 等方法名更改,那麼所有業務層的程式碼都需要更改。這個問題可以通過 動態代理 來解決!

動態代理

推薦檢視 反射的應用:動態代理

動態代理是指客戶通過代理類來呼叫其它物件的方法,並且是在程式執行時根據需要 動態建立目標類(位元組碼在用時才建立和載入) 的代理物件,它可以在不修改原始碼的基礎上對方法進行增強。而靜態代理在編譯期間位元組碼就確定下來了。

動態代理實現的兩種方式

  • *基於介面的動態代理
    • 如何建立代理物件:JDK java.lang.reflect.Proxy,即使用 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
      • loader 類載入器,它是用於載入代理物件位元組碼的,和被代理物件使用相同的類載入器,即 被代理物件.getClass().getClassLoader()
      • interfaces 位元組碼陣列,它是用於讓代理物件和被代理物件有相同的方法,即 被代理物件.getClass().getInterfaces()
      • h 提供增強的程式碼,它是用於如何代理,通常是一個 InvocationHandler 介面的實現類,可以是匿名內部類
    • 建立代理物件要求:被代理類 實現 InvocationHandler 介面,要實現這個介面,必須重寫 Object invoke(Object proxy, Method method, Object[] args)
      • proxy 代理物件的引用
      • method 當前執行的方法
      • args 當前執行的方法所需的引數
      • @return method.invoke(被代理物件, args)
      • 可以在 invoke() 前後增加一些通用方法。注意,被代理物件必須是基於介面的
  • 基於子類的動態代理
    • 如何建立代理物件:cglib 2.2.2 net.sf.cglib.proxy.Enhancer 或者 Spring org.springframework.cglib.proxy.Enhancer,即使用 Enhancer.create(Class type, Callback callback)
      • type 它是用於指定被代理物件的位元組碼,即 被代理物件.getClass()
      • callback 提供增強的程式碼,它是用於如何代理,通常是一個 MethodInterceptor 介面的實現類,可以是匿名內部類
    • 建立代理物件要求:被代理類不能是最終類(不能用 final 修飾的類)。必須重寫 Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
      • o 代理物件的引用
      • method 當前執行的方法
      • args 當前執行的方法所需的引數
      • methodProxy 當前執行方法的代理物件

解決 SpringCRUD 存在的問題

  • BeanFactory.java

    package cn.parzulpan.factory;
    
    import cn.parzulpan.service.BankAccountService;
    import cn.parzulpan.utils.ConnectionUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    /**
     * @Author : parzulpan
     * @Time : 2020-12
     * @Desc : 用於建立 業務層實現類 的 代理物件工廠
     */
    
    @Component
    public class BeanFactory {
        @Autowired
        private BankAccountService bankAccountService;  // 被代理類
        @Autowired
        private ConnectionUtil connectionUtil;
    
        /**
         * 獲取 業務層實現類 的 代理物件
         * @return
         */
        public BankAccountService getBankAccountService() {
            System.out.println("獲取 業務層實現類 的 代理物件");
            return (BankAccountService) Proxy.newProxyInstance(bankAccountService.getClass().getClassLoader(),
                    bankAccountService.getClass().getInterfaces(),
                    new InvocationHandler() {
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            // 新增事務控制
                            Object rtValue = null;
                            try {
    //                            accounts =  bankAccountDAO.findAll();
                                rtValue = method.invoke(bankAccountService, args);
                                connectionUtil.commitAndClose();
                            } catch (Exception e) {
                                connectionUtil.rollbackAndClose();
                                throw new RuntimeException(e);
                            }
                            return rtValue;
                        }
                    });
        }
    }
    
  • BankAccountServiceImpl.java

    package cn.parzulpan.service;
    
    import cn.parzulpan.dao.BankAccountDAO;
    import cn.parzulpan.domain.BankAccount;
    import cn.parzulpan.utils.ConnectionUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    /**
     * @Author : parzulpan
     * @Time : 2020-12
     * @Desc : 銀行賬戶的業務層介面的實現類,使用 ConnectionUtil 事務控制,使用動態代理
     */
    
    @Service("bankAccountService")
    public class BankAccountServiceImpl implements BankAccountService {
        @Autowired
        private BankAccountDAO bankAccountDAO;
    //    @Autowired
    //    private ConnectionUtil connectionUtil;
    
        public List<BankAccount> findAll() {
    //        使用事務管理
    //        List<BankAccount> accounts = null;
    //        try {
    //            accounts =  bankAccountDAO.findAll();
    //            connectionUtil.commitAndClose();
    //        } catch (Exception e) {
    //            connectionUtil.rollbackAndClose();
    //            throw new RuntimeException(e);
    //        }
    //        return accounts;
    
            // 使用動態代理
            return bankAccountDAO.findAll();
        }
    
        public void transfer(String sourceName, String targetName, Double money) {
    //        try {
    //            BankAccount source = bankAccountDAO.findByName(sourceName);
    //            BankAccount target = bankAccountDAO.findByName(targetName);
    //            source.setMoney(source.getMoney() - money);
    //            target.setMoney(target.getMoney() + money);
    //            bankAccountDAO.update(source);
    //            int i = 1 / 0;  //  模擬轉賬異常
    //            bankAccountDAO.update(target);
    //            connectionUtil.commitAndClose();
    //        } catch (Exception e) {
    //            connectionUtil.rollbackAndClose();
    //            throw new RuntimeException(e);
    //        }
    
            BankAccount source = bankAccountDAO.findByName(sourceName);
            BankAccount target = bankAccountDAO.findByName(targetName);
            source.setMoney(source.getMoney() - money);
            target.setMoney(target.getMoney() + money);
            bankAccountDAO.update(source);
            int i = 1 / 0;  //  模擬轉賬異常
            bankAccountDAO.update(target);
        }
    }
    
  • 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: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/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="cn.parzulpan"/>
    
        <!-- 配置代理的 Service -->
        <bean id="proxyBankAccountService" factory-bean="beanFactory" factory-method="getBankAccountService"/>
    
        <!-- 配置 QueryRunner -->
        <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
            <!-- 注入資料來源,建構函式形式-->
    <!--        <constructor-arg name="ds" ref="dataSource"/>-->
            <!-- 註釋掉 注入資料來源,不需要自己獲取連線,在 ConnectionUtil 中注入,並由 ConnectionUtil 進行事務控制 -->
        </bean>
    
        <!-- 配置 資料來源 -->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="com.mysql.jdbc.Driver"/>
            <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/springT?useSSL=false"/>
            <property name="user" value="root"/>
            <property name="password" value="root"/>
        </bean>
    
    </beans>
    
  • BankAccountServiceImplTest.java

    package cn.parzulpan.service;
    
    import cn.parzulpan.domain.BankAccount;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    /**
     * @Author : parzulpan
     * @Time : 2020-12
     * @Desc : 測試 銀行賬戶的業務層介面的實現類
     */
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "classpath:bean.xml")
    public class BankAccountServiceImplTest {
    
    //    @Autowired
    //    private BankAccountService as;
    
        // 指定 BankAccountService 的代理物件
        @Resource(name = "proxyBankAccountService")
        private BankAccountService as;
    
        @Test
        public void findAllTest() {
            List<BankAccount> accounts = as.findAll();
            for (BankAccount account : accounts) {
                System.out.println(account);
            }
        }
    
        @Test
        public void transfer() {
            as.transfer("aaa", "bbb", 100.0);
        }
    }
    

AOP 概念

在軟體行業中,AOP(Aspect Oriented Programming,面向切面程式設計),是通過預編譯方式和執行期動態代理實現程式功能的統一維護技術,是 OOP 的延續,也是函數語言程式設計的一個衍生範型。

利用 AOP 可以對業務邏輯的各個部分進行隔離,從而使業務邏輯各部分之間的耦合度降低,提供程式的可重用性和開發效率。

AOP 實際是 GoF 設計模式 的延續,設計模式孜孜不倦追求的是呼叫者和被呼叫者之間的解耦,提高程式碼的靈活性和可擴充套件性。

主要功能(應用範圍):

  • 日誌記錄
  • 效能統計
  • 安全控制
  • 事務處理
  • 異常處理

AOP 相關術語

AOP 相關術語:

  • Joinpoint 連線點 指哪些被攔截到的點,比如 業務層中所有的方法
  • Pointcut 切入點 指對連線點進行攔截的點,比如 業務層中增強的方法
  • Advice 通知 指攔截到連線點後要做的事情,通知的型別分為前置通知、後置通知、異常通知、最終通知、環繞通知(有明確的切入點方法呼叫)。比如事務控制
  • Introduction 引導 指一種特殊的通知,在不修改類程式碼的前提下,它可以在執行期為類動態地新增一些方法或屬性
  • Target 目標物件 指代理的目標物件,比如 bankAccountService
  • Weaving 織入 指把增強應用到目標物件來建立新的代理物件的過程,比如 Spring 採用動態代理
  • Proxy 代理 指一個類被 AOP 織入增強後,就產生一個結果代理類
  • Aspect 切面 指切入點和通知的結合

Spring AOP 的分工

  • 開發階段,開發者 做的:
    • 編寫核心業務程式碼
    • 把公用程式碼抽取出來,即通知
    • 在配置檔案中,宣告切入點和通知間的關係,即切面
  • 執行階段,Spring 做的:
    • Spring 監控切入點方法的執行
    • 一旦監控到切入點方法被執行,使用代理機制,動態建立目標物件的代理物件,根據通知型別,在代理物件的相應位置,將通知對應的功能織入,完成程式碼邏輯

代理的選擇

  • 在 Spring 中,會根據目標物件是否實現了介面來決定採用哪種動態代理

XML 的 AOP 配置

小節原始碼

步驟一

步驟一 編寫核心業務程式碼:AccountServiceImpl.java

package cn.parzulpan.service;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 賬戶的業務層介面的實現類
 */

public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("執行了儲存操作...");
    }

    public void updateAccount(int id) {
        System.out.println("執行了更新操作... " + id);
    }

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

步驟二

步驟二 抽取公共程式碼,組成通知:Logger.java

package cn.parzulpan.utils;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 用於記錄日誌的工具類,它提供了公共的方法,即 Advice 通知
 */

public class Logger {

    /**
     * 列印日誌
     * 前置通知,在 切入點方法(業務層中增強的方法)之前執行
     */
    public void printLogBefore() {
        System.out.println("Logger 類中的 printLogBefore 方法開始記錄日誌了...");
    }

    /**
     * 列印日誌
     * 最終通知,在 切入點方法(業務層中增強的方法)之後執行
     */
    public void printLogAfter() {
        System.out.println("Logger 類中的 printLogAfter 方法開始記錄日誌了...");
    }

    /**
     * 環繞通知
     * 問題:當配置了環繞通知之後,切入點方法沒有執行,而通知方法執行了
     * 分析:通過對比動態代理中的環繞通知,發現動態代理的環繞通知有明確的切入點方法呼叫
     * 解決:Spring 提供了一個介面 ProceedingJoinPint,它有一個 proceed(),此方法相當於明確呼叫切入點方法
     * 該介面可以作為環繞通知的方法的引數,在程式執行時,Spring 會提供該介面的實現類
     */
    public Object printLogAround(ProceedingJoinPoint pjp) {
//        System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...");
        Object rtValue = null;

        try {
            Object[] args = pjp.getArgs();  //  得到方法執行所需的引數
            System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...  前置通知");
            rtValue = pjp.proceed(args);    // 切入點方法
            System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...  後置通知");
        } catch (Throwable throwable) {
            System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...  異常通知");
            throwable.printStackTrace();
        } finally {
            System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...  最終通知");
        }
        return rtValue;
    }
}

步驟三

*AOP 配置檔案編寫步驟

  • 先配置 Spring IOC,將 業務層 物件 和 Advice 通知 物件配置進來
  • 然後配置 Spring AOP:
    • 第一步:使用 aop:config 宣告 AOP 配置
    • 第二步:使用 aop:aspect 配置切面
      • id 屬性 給切面提供一個唯一標識
      • ref 屬性 指定配置好的通知類 bean 的 id
    • 第三步:配置通知的型別
      • method 屬性 用於指定通知類中的增強方法名稱
      • pointcut-ref 屬性 用於指定切入點的表示式的引用
      • pointcut 屬性 用於指定切入點表示式,使用 aop:pointcut 配置切入點表示式,指定對哪些類的哪些方法進行增強。當它在 aop:aspect 標籤 內部時,只能用於當前切面。在外部時,就能用於所有切面,但是要求它在 aop:aspect 標籤 前面
        • expression 屬性 用於定義切入點表示式。
        • id 屬性 用於給切入點表示式提供一個唯一標識

切入點表示式:指定對哪些類的哪些方法進行增強

  • 語法: execution([修飾符] 返回值型別 包名.類名.方法名(引數))
  • 全匹配方式: public void cn.parzulpan.service.AccountServiceImpl.saveAccount(),其中訪問修飾符可以省略
  • 返回值使用 * 號,表示任意返回值: * cn.parzulpan.service.AccountServiceImpl.saveAccount()
  • 包名使用 * 號,表示任意包,但是有幾級包,就需要寫幾個 * 號 : *.*.*.AccountServiceImpl.saveAccount()
  • 使用 .. 號 來表示當前包,及其子包
  • 類名使用 * 號,表示任意類,方法名使用 * 號,表示任意方法
  • 引數列表使用 * 號,表示引數可以是任意資料型別,但是必須有引數
  • 引數列表使用 .. 號,表示有無引數均可,有引數可以是任意型別
  • 通常用法:切到業務層實現類下的所有方法,* cn.parzulpan.service.*.*(..)

通知型別

  • aop:before 用於配置前置通知,指定增強的方法在切入點方法之前執行。執行時間點為 切入點方法執行之前執行
  • aop:after-returning 用於配置後置通知。執行時間點為 切入點方法正常執行之後,它和異常通知只能有一個執行
  • aop:after-throwing 用於配置異常通知。執行時間點為 切入點方法執行產生異常後執行,它和後置通知只能有一個執行
  • aop:after 用於配置最終通知。執行時間點為 無論切入點方法執行時是否有異常,它都會在其後面執行
  • aop:around 用於配置環繞通知(環繞通知指有明確的切入點方法呼叫),它是 Spring 提供的一種可以在程式碼中手動控制增強程式碼什麼時候執行的方式

值得注意的是,Spring 執行時,後置通知或異常通知總是在最終通知後面。所以,推薦使用環繞通知,自定義執行順序。

步驟三 編寫 AOP 配置檔案: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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置 Spring IOC -->
    <!-- 將 AccountService 物件配置進來 -->
    <bean id="accountService" class="cn.parzulpan.service.AccountServiceImpl"/>
    <!-- 將 Logger 物件配置進來,是一個 Advice 通知 -->
    <bean id="logger" class="cn.parzulpan.utils.Logger"/>

    <!-- 配置 Spring AOP -->
    <!-- 1. 使用 aop:config 宣告 AOP 配置 -->
    <aop:config>
        <aop:pointcut id="allMethodPCRGlobal"
                      expression="execution(* cn.parzulpan.service.*.*(..))"/>
        
        <!-- 2. 使用 aop:aspect 配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 3. 配置通知的型別 -->
            <aop:before method="printLogBefore"
                        pointcut="execution(public void cn.parzulpan.service.AccountServiceImpl.saveAccount())"/>
            <aop:after method="printLogAfter"
                       pointcut-ref="allMethodPCR"/>
            <aop:around method="printLogAround"
                        pointcut-ref="allMethodPCRGlobal"/>

            <aop:pointcut id="allMethodPCR"
                          expression="execution(* cn.parzulpan.service.*.*(..))"/>
        </aop:aspect>
    </aop:config>
</beans>

步驟四 測試

步驟四 測試 AOP XML 配置:XmlAOPTest.java

package cn.parzulpan;

import cn.parzulpan.service.AccountService;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 測試 AOP XML 配置
 */

public class XmlAOPTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        AccountService as = ac.getBean("accountService", AccountService.class);
        as.saveAccount();
        System.out.println();
        as.updateAccount(1024);
        System.out.println();
        as.deleteAccount();
    }
}

註解 的 AOP 配置

小節原始碼

配置步驟

  • 第一步 在配置檔案中匯入 context 的名稱空間
  • 第二步 所有資源使用註解配置
  • 第三步 在配置檔案中指定 Spring 要掃描的包
  • 第四步 在配置檔案中指定 Spring AOP 支援
  • 第五步 在通知類上使用 @Aspect 註解宣告為切面
  • 第六步 編寫切入點表示式註解
  • 第七步 在增強的方法上使用註解配置通知

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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 配置 Spring 建立容器時要掃描的包  -->
    <context:component-scan base-package="cn.parzulpan"/>

    <!-- 配置 Spring AOP 支援-->
    <aop:aspectj-autoproxy/>

</beans>

Logger.java

package cn.parzulpan.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 用於記錄日誌的工具類,它提供了公共的方法,即 Advice 通知,使用註解
 */

@Component
@Aspect
public class Logger {
    // 編寫切入點表示式註解
    @Pointcut("execution(* cn.parzulpan.service.*.*(..))")
    private void allMethodPCRGlobal(){}

    /**
     * 列印日誌
     * 前置通知,在 切入點方法(業務層中增強的方法)之前執行
     */
    @Before("allMethodPCRGlobal()")
    public void printLogBefore() {
        System.out.println("Logger 類中的 printLogBefore 方法開始記錄日誌了...");
    }

    /**
     * 列印日誌
     * 最終通知,在 切入點方法(業務層中增強的方法)之後執行
     */
    @After("allMethodPCRGlobal()")
    public void printLogAfter() {
        System.out.println("Logger 類中的 printLogAfter 方法開始記錄日誌了...");
    }

    /**
     * 環繞通知
     * 問題:當配置了環繞通知之後,切入點方法沒有執行,而通知方法執行了
     * 分析:通過對比動態代理中的環繞通知,發現動態代理的環繞通知有明確的切入點方法呼叫
     * 解決:Spring 提供了一個介面 ProceedingJoinPint,它有一個 proceed(),此方法相當於明確呼叫切入點方法
     * 該介面可以作為環繞通知的方法的引數,在程式執行時,Spring 會提供該介面的實現類
     */
    @Around("allMethodPCRGlobal()")
    public Object printLogAround(ProceedingJoinPoint pjp) {
//        System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...");
        Object rtValue = null;

        try {
            Object[] args = pjp.getArgs();  //  得到方法執行所需的引數
            System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...  前置通知");
            rtValue = pjp.proceed(args);    // 切入點方法
            System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...  後置通知");
        } catch (Throwable throwable) {
            System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...  異常通知");
            throwable.printStackTrace();
        } finally {
            System.out.println("Logger 類中的 printLogAround 方法開始記錄日誌了...  最終通知");
        }
        return rtValue;
    }
}

不使用 XML 的配置方式,直接純註解,雖然不推薦,但是也可以實現。

SpringConfiguration.java

@Configuration
@ComponentScan(basePackages="cn.parzulpan")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}

XmlAOPTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AnnotationAOPTest {
}

總結和練習