Spring AOP 最全入門教程【新手向】

初識 AOP(傳統程序)

Tips:如果想要快速查閱的朋友,可以直接跳轉到 初識AOP(Spring 程序)這一大節

(一) AOP 術語

(二) AOP 入門案例:XML 、註解方式

(三) 完全基於 Spring 的事務控制:XML、註解方式、純註解方式

(一) AOP的簡單分析介紹

在軟件業,AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,通過預編譯方式和運行期間動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。

—— 百度百科

開篇就直接來看 Spring AOP 中的百科說明,我個人認爲是非常晦澀的,當回過來頭再看這段引言的時候,才恍然大悟,這段話的意思呢,說白了,就是說我們把程序中一些重複的代碼拿出來,在需要它執行的時候,可以通過預編譯或者運行期的動態代理實現不動源碼而動態的給程序進行增強或者添加功能的技術

拿出一些重複的代碼? 拿出的究竟是什麼代碼呢?舉個例子!

在下面的方法中,我們模擬的是程序中對事務的管理,下面代碼中的 A B都可以看做 “開啓事務”、“提交事務” 的一些事務場景,這些代碼就可以看做是上面所說的重複的代碼的一種

而還有一些重複代碼大多是關於權限管理或者說日誌登錄等一些雖然影響了我們 代碼業務邏輯的 “乾淨”,但是卻不得不存在,如果有什麼辦法能夠抽取出這些方法,使得我們的業務代碼更加簡潔,自然我們可以更專注與我們的業務,利於開發,這也就是我們今天想要說重點

最後不得不提的是,AOP 作爲 Spring 這個框架的核心內容之一,很顯然應用了大量的設計模式,設計模式,歸根結底,就是爲了解耦,以及提高靈活性可擴展性,而我們所學的一些框架,直接把這些東西封裝好,讓你直接用,說的白一點,就是爲了讓你偷懶,讓你既保持了良好的代碼結構,又不需要和你去自己編寫這些複雜的數據結構,提高了開發效率

一上來就直接談 AOP術語阿,面向切面等等,很顯然不是很合適,光聽名字總是能能讓人 “望文生怯” , 任何技術的名字只不過是一個名詞罷了,實際上對於入門來說,我們更需要搞懂的是,通過傳統的程序與使用 Spring AOP 相關技術的程序進行比較,使用 AOP 可以幫助我們解決哪些問題或者需求,通過知其然,然後應用其所以然,這樣相比較於,直接學習其基本使用方式,會有靈魂的多!

(二) 演示案例(傳統方式)

說明:下面的第一部分的例子是在上一篇文章的程序加以改進,爲了照顧到所有的朋友,我把從依賴到類的編寫都會提到,方便大家有需要來練習,看一下程序的整體結構,對後面的說明也有一定的幫助

(1) 添加必要的依賴

  • spring-context
  • mysql-connector-java
  • c3p0(數據庫連接池)
  • commons-dbutils(簡化JDBC的工具)—後面會簡單介紹一下
  • junit (單元自測)
  • spring-test

說明:由於我這裏創建的是一個Maven項目,所以在這裏修改 pom.xml 添加一些必要的依賴座標就可以

如果創建時沒有使用依賴的朋友,去下載我們所需要的 jar 包導入就可以了

<packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>

        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>

        <dependency>
            <groupId>commons-dbutils</groupId>
            <artifactId>commons-dbutils</artifactId>
            <version>1.4</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
    </dependencies>

簡單看一下,spring核心的一些依賴,以及數據庫相關的依賴,還有單元測試等依賴就都導入進來了

(2) 創建賬戶表以及實體

下面所要使用的第一個案例,涉及到兩個賬戶之間的模擬轉賬交易,所以我們創建出含有名稱以及餘額這樣幾個字段的表

A:創建 Account 表

-- ----------------------------
-- Table structure for account
-- ----------------------------
CREATE TABLE `account`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32),
  `balance` float,
  PRIMARY KEY (`id`)
)

B:創建 Account 類

沒什麼好說的,對應着我們的表創出實體

public class Account implements Serializable {
    private  Integer id;
    private String name;
    private Float balance;
    ......補充 get set toString 方法

(3) 創建Service以及Dao

下面我們演示事務問題,最主要還是使用 transfer 這個轉賬方法,當然還有一些增刪改查的方法,我只留了一個查詢所有的方法,到時候就可以看出傳統方法中一些代碼的重複以及複雜的工作度

A:AccountService 接口

public interface AccountService {
    /**
     * 查詢所有
     * @return
     */
    List<Account> findAll();

    /**
     * 轉賬方法
     * @param sourceName    轉出賬戶
     * @param targetName    轉入賬戶
     * @param money
     */
    void transfer(String sourceName,String targetName,Float money);
}

B:AccountServiceImpl 實現類

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    public List<Account> findAll() {
        return accountDao.findAllAccount();
    }
    
    public void transfer(String sourceName, String targetName, Float money) {
        //根據名稱分別查詢到轉入轉出的賬戶
        Account source = accountDao.findAccountByName(sourceName);
        Account target = accountDao.findAccountByName(targetName);

        //轉入轉出賬戶加減
        source.setBalance(source.getBalance() - money);
        target.setBalance(target.getBalance() + money);
        //更新轉入轉出賬戶
        accountDao.updateAccount(source);
        accountDao.updateAccount(target);
    }
}

C:AccountDao 接口

public interface AccountDao {

    /**
     * 更細賬戶信息(修改)
     * @param account
     */
    void updateAccount(Account account);

    /**
     * 查詢所有賬戶
     * @return
     */
    List<Account> findAllAccount();

    /**
     * 通過名稱查詢
     * @param accountName
     * @return
     */
    Account findAccountByName(String accountName);
}

D:AccountDaoImpl 實現類

我們引入了 DBUtils 這樣一個操作數據庫的工具,它的作用就是封裝代碼,達到簡化 JDBC 操作的目的,由於以後整合 SSM 框架的時候,持久層的事情就可以交給 MyBatis 來做,而今天我們重點還是講解 Spring 中的知識,所以這部分會用就可以了

用到的內容基本講解:

QueryRunner 提供對 sql 語句進行操作的 API (insert delete update)

ResultSetHander 接口,定義了查詢後,如何封裝結果集(僅提供了我們用到的)

  • BeanHander:將結果集中第第一條記錄封裝到指定的 JavaBean 中
  • BeanListHandler:將結果集中的所有記錄封裝到指定的 JavaBean 中,並且將每一個 JavaBean封裝到 List 中去
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private QueryRunner runner;

    public void updateAccount(Account account) {
        try {
            runner.update("update account set name=?,balance=? where id=?", account.getName(), account.getBalance(), account.getId());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public List<Account> findAllAccount() {
        try {
            return runner.query("select * from account", new BeanListHandler<Account>(Account.class));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public Account findAccountByName(String accountName) {
        try {
            List<Account> accounts = runner.query("select * from account where name = ?", new BeanListHandler<Account>(Account.class), accountName);

            if (accounts == null || accounts.size() == 0) {
                return null;
            }
            if (accounts.size() > 1) {
                throw new RuntimeException("結果集不唯一,數據存在問題");
            }
            return accounts.get(0);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

(4) 配置文件

A:bean.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
    <!--開啓掃描-->
    <context:component-scan base-package="cn.ideal"></context:component-scan>

    <!--配置 QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner">
        <!--注入數據源-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </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/ideal_spring"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root99"></property>
    </bean>
</beans>

B: jdbcConfig.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ideal_spring
jdbc.username=root
jdbc.password=root99

(5) 測試代碼

A:AccountServiceTest

在這裏,我們使用 Spring以及Junit 測試

說明:使用 @RunWith 註解替換原有運行器 然後使用 @ContextConfiguration 指定 spring 配置文件的位置,然後使用 @Autowired 給變量注入數據

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {

    @Autowired
    private AccountService as;

    @Test
    public void testFindAll() {
        List<Account> list = as.findAll();
        for (Account account : list) {
            System.out.println(account);
        }
    }

    @Test
    public void testTransfer() {
        as.transfer("李四", "張三", 500f);
    }

}

(6) 執行效果

先執行查詢所有:

再執行模擬轉賬方法:

方法中也就是李四向張三轉賬500,看到下面的結果,是沒有任何問題的

(三) 初步分析以及解決

(1) 分析事務問題

首先分析一下,我們並沒有顯式的進行事務的管理,但是不用否定,事務一定存在的,如果沒有提交事務,很顯然,查詢功能是不能夠測試成功的,我們的代碼事務隱式的被自動控制了,使用了 connection 對象的 setAutoCommit(true),即自動提交了

接着看一下配置文件中,我們只注入了了數據源,這樣做代表什麼呢?

<!--配置 QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner">
	<!--注入數據源-->
	<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

也就是說,每一條語句獨立事務:

說白了,就是各管各的,彼此沒任何溝通,例如在Service的轉賬方法中,下面標着 1 2 3 4 5 的位置處的語句,每一個調用時,都會創建一個新的 QueryRunner對象,並且從數據源中獲取一個連接,但是,當在某一個步驟中突然出現問題,前面的語句仍然會執行,但是後面的語句就因爲異常而終止了,這也就是我們開頭說的,彼此之間是獨立的

public void transfer(String sourceName, String targetName, Float money) {
        //根據名稱分別查詢到轉入轉出的賬戶
        Account source = accountDao.findAccountByName(sourceName); // 1
        Account target = accountDao.findAccountByName(targetName); // 2

        //轉入轉出賬戶加減
        source.setBalance(source.getBalance() - money); // 3
        target.setBalance(target.getBalance() + money);
        //更新轉入轉出賬戶
        accountDao.updateAccount(source); // 4
        //模擬轉賬異常
        int num = 100/0; // 異常
        accountDao.updateAccount(target); //5
}

很顯然這是非常不合適的,甚至是致命的,像我們代碼中所寫,轉出賬戶的賬戶信息已經扣款更新了,但是轉入方的賬戶信息卻由於前面異常的發生,導致並沒有成功執行,李四從2500 變成了 2000,但是張三卻沒有成功收到轉賬

(2) 初步解決事務問題

上面出現的問題,歸根結底是由於我們持久層中的方法獨立事務,所以無法實現整體的事務控制(與事務的一致性相悖)那麼我們解決問題的思路是什麼呢?

首先我們需要做的,就是使用 ThreadLocal 對象把 Connection 和當前線程綁定,從而使得一個線程中只有一個控制事務的對象,

簡單提一下Threadlocal:

Threadlocal 是一個線程內部的存儲類,可以在指定線程內存儲數據,也就相當於,這些數據就被綁定在這個線程上了,只能通過這個指定的線程,纔可以獲取到想要的數據

這是官方的說明:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copLy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

就是說,ThreadLoacl 提供了線程內存儲局部變量的方式,這些變量比較特殊的就是,每一個線程獲取到的變量都是獨立的,獲取數據值的方法就是 get 以及 set

A:ConnectionUtils 工具類

創建 utils 包 ,然後創建一個 ConnectionUtils 工具類,其中最主要的部分,其實也就是寫了一個簡單的判斷,如果這個線程中已經存在連接,就直接返回,如果不存在連接,就獲取數據源中的一個鏈接,然後存入,再返回

@Component
public class ConnectionUtils {
    private ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();

    @Autowired
    private DataSource dataSource;

    public Connection getThreadConnection() {

        try {
            // 從 ThreadLocal獲取
            Connection connection = threadLocal.get();
            //先判斷是否爲空
            if (connection == null) {
                //從數據源中獲取一個連接,且存入 ThreadLocal
                connection = dataSource.getConnection();
                threadLocal.set(connection);
            }
            return connection;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void removeConnection(){
        threadLocal.remove();
    }
}

B:TransactionManager 工具類

接着可以創建一個管理事務的工具類,其中包括,開啓、提交、回滾事務,以及釋放連接

@Component
public class TransactionManager {

    @Autowired
    private ConnectionUtils connectionUtils;

    /**
     * 開啓事務
     */
    public void beginTransaction() {
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事務
     */
    public void commit() {
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滾事務
     */
    public void rollback() {
        try {
            System.out.println("回滾事務" + connectionUtils.getThreadConnection());
            connectionUtils.getThreadConnection().rollback();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 釋放連接
     */
    public void release() {
        try {
            connectionUtils.getThreadConnection().close();//還回連接池中
            connectionUtils.removeConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

C:業務層增加事務代碼

在方法中添加事務管理的代碼,正常情況下執行開啓事務,執行操作(你的業務代碼),提交事務,捕獲到異常後執行回滾事務操作,最終執行釋放連接

在這種情況下,即使在某個步驟中出現了異常情況,也不會對數據造成實際的更改,這樣上面的問題就初步解決了

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    @Autowired
    private TransactionManager transactionManager;

    public List<Account> findAll() {
        try {
            //開啓事務
            transactionManager.beginTransaction();
            //執行操作
            List<Account> accounts = accountDao.findAllAccount();
            //提交事務
            transactionManager.commit();
            //返回結果
            return accounts;
        } catch (Exception e) {
            //回滾操作
            transactionManager.rollback();
            throw new RuntimeException(e);
        } finally {
            //釋放連接
            transactionManager.release();
        }
    }

    public void transfer(String sourceName, String targetName, Float money) {

        try {
            //開啓事務
            transactionManager.beginTransaction();
            //執行操作

            //根據名稱分別查詢到轉入轉出的賬戶
            Account source = accountDao.findAccountByName(sourceName);
            Account target = accountDao.findAccountByName(targetName);

            //轉入轉出賬戶加減
            source.setBalance(source.getBalance() - money);
            target.setBalance(target.getBalance() + money);

            //更新轉出轉入賬戶
            accountDao.updateAccount(source);
            //模擬轉賬異常
            int num = 100 / 0;
            accountDao.updateAccount(target);

            //提交事務
            transactionManager.commit();

        } catch (Exception e) {
            //回滾操作
            transactionManager.rollback();
            e.printStackTrace();
        } finally {
            //釋放連接
            transactionManager.release();
        }
    }
}

(四) 思考再改進方式

雖然上面,我們已經實現了在業務層進行對事務的控制,但是很顯然可以看見,我們在每一個方法中都存在着太多重複的代碼了,並且以業務層與事務管理的方法出現了耦合,打個比方,事務管理類中的隨便一個方法名進行更改,就會直接導致業務層中找不到對應的方法,全部需要修改,如果在業務層方法較多時,很顯然這是很麻煩的

這種情況下,我們可以通過使用靜態代理這一種方式,來進行對上面程序的改進,改進之前爲了照顧到所有的朋友,回顧一下動態代理的一個介紹以及基本使用方式

(五) 回顧動態代理

(1) 什麼是動態代理

動態代理,也就是給某個對象提供一個代理對象,用來控制對這個對象的訪問

簡單的舉個例子就是:買火車、飛機票等,我們可以直接從車站售票窗口進行購買,這就是用戶直接在官方購買,但是我們很多地方的店鋪或者一些路邊的亭臺中都可以進行火車票的代售,用戶直接可以在代售點購票,這些地方就是代理對象

(2) 使用代理對象有什麼好處呢?

  • 功能提供的這個類(火車站售票處),可以更加專注於主要功能的實現,比如安排車次以及生產火車票等等
  • 代理類(代售點)可以在功能提供類提供方法的基礎上進行增加實現更多的一些功能

這個動態代理的優勢,帶給我們很多方便,它可以幫助我們實現無侵入式的代碼擴展,也就是在不用修改源碼的基礎上,同時增強方法

動態代理分爲兩種:① 基於接口的動態代理 ② 基於子類的動態代理

(3) 動態代理的兩種方式

A:基於接口的動態代理方式

A:創建官方售票處(類和接口)

RailwayTicketProducer 接口

/**
 * 生產廠家的接口
 */
public interface RailwayTicketProducer {

    public void saleTicket(float price);

    public void ticketService(float price);

}

RailwayTicketProducerImpl 類

實現類中,我們後面只對銷售車票方法進行了增強,售後服務並沒有涉及到

/**
 * 生產廠家具體實現
 */
public class RailwayTicketProducerImpl implements RailwayTicketProducer{

    public void saleTicket(float price) {
        System.out.println("銷售火車票,收到車票錢:" + price);
    }

    public void ticketService(float price) {
        System.out.println("售後服務(改簽),收到手續費:" + price);
    }
}

Client 類

這個類,就是客戶類,在其中,通過代理對象,實現購票的需求

首先先來說一下如何創建一個代理對象:答案是 Proxy類中的 newProxyInstance 方法

注意:既然叫做基於接口的動態代理,這就是說被代理的類,也就是文中官方銷售車票的類最少必須實現一個接口,這是必要的!

public class Client {

    public static void main(String[] args) {
        RailwayTicketProducer producer = new RailwayTicketProducerImpl();

        //動態代理
        RailwayTicketProducer proxyProduce = (RailwayTicketProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),new MyInvocationHandler(producer));

        //客戶通過代理買票
        proxyProduce.saleTicket(1000f);
    }
}

newProxyInstance 共有三個參數 來解釋一下:

  • ClassLoader:類加載器

    • 用於加載代理對象字節碼,和被代理對象使用相同的類加載器
  • Class[]:字節碼數組

    • 爲了使被代理對象和的代理對象具有相同的方法,實現相同的接口,可看做固定寫法
  • InvocationHandler:如何代理,也就是想要增強的方式

    • 也就是說,我們主需要 new 出 InvocationHandler,然後書寫其實現類,是否寫成匿名內部類可以自己選擇

    • 如上述代碼中 new MyInvocationHandler(producer) 實例化的是我自己編寫的一個 MyInvocationHandler類,實際上可以在那裏直接 new 出 InvocationHandler,然後重寫其方法,其本質也是通過實現 InvocationHandler 的 invoke 方法實現增強

MyInvocationHandler 類

這個 invoke 方法具有攔截的功能,被代理對象的任何方法被執行,都會經過 invoke

public class MyInvocationHandler implements InvocationHandler {

    private  Object implObject ;

    public MyInvocationHandler (Object implObject){
        this.implObject=implObject;
    }

    /**
     * 作用:執行被代理對象的任何接口方法都會經過該方法
     * 方法參數的含義
     * @param proxy   代理對象的引用
     * @param method  當前執行的方法
     * @param args    當前執行方法所需的參數
     * @return        和被代理對象方法有相同的返回值
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object returnValue = null;
        //獲取方法執行的參數
        Float price = (Float)args[0];
        //判斷是不是指定方法(以售票爲例)
        if ("saleTicket".equals(method.getName())){
            returnValue = method.invoke(implObject,price*0.8f);
        }
        return returnValue;
    }
}

在此處,我們獲取到客戶購票的金額,由於我們使用了代理方進行購票,所以代理方會收取一定的手續費,所以用戶提交了 1000 元,實際上官方收到的只有800元,這也就是這種代理的實現方式,結果如下

銷售火車票,收到車票錢:800.0

B:基於子類的動態代理方式

上面方法簡單的實現起來也不是很難,但是唯一的標準就是,被代理對象必須提供一個接口,而現在所講解的這一種就是一種可以直接代理普通 Java 類的方式,同時在演示的時候,我會將代理方法直接以內部類的形式寫出,就不單獨創建類了,方便大家與上面對照

增加 cglib 依賴座標

<dependencies>
	<dependency>
		<groupId>cglib</groupId>
		<artifactId>cglib</artifactId>
        <version>3.2.4</version>
    </dependency>
</dependencies>

TicketProducer 類

/**
 * 生產廠家
 */
public class TicketProducer {

    public void saleTicket(float price) {
        System.out.println("銷售火車票,收到車票錢:" + price);
    }

    public void ticketService(float price) {
        System.out.println("售後服務(改簽),收到手續費:" + price);
    }
}

Enhancer 類中的 create 方法就是用來創建代理對象的

而 create 方法又有兩個參數

  • Class :字節碼
    • 指定被代理對象的字節碼
  • Callback:提供增強的方法
    • 與前面 invoke 作用是基本一致的
    • 一般寫的都是該接口的子接口實現類:MethodInterceptor
public class Client {

    public static void main(String[] args) {
        // 由於下方匿名內部類,需要在此處用final修飾
        final TicketProducer ticketProducer = new TicketProducer();

        TicketProducer cglibProducer =(TicketProducer) Enhancer.create(ticketProducer.getClass(), new MethodInterceptor() {

            /**
             * 前三個三個參數和基於接口的動態代理中invoke方法的參數是一樣的
             * @param o
             * @param method
             * @param objects
             * @param methodProxy   當前執行方法的代理對象
             * @return
             * @throws Throwable
             */
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Object returnValue = null;
                //獲取方法執行的參數
                Float price = (Float)objects[0];
                //判斷是不是指定方法(以售票爲例)
                if ("saleTicket".equals(method.getName())){
                    returnValue = method.invoke(ticketProducer,price*0.8f);
                }
                return returnValue;
            }
        });
        cglibProducer.saleTicket(900f);
    }

(六) 動態代理程序再改進

在這裏我們寫一個用於創建業務層對象的工廠

在這段代碼中,我們使用了前面所回顧的基於接口的動態代理方式,在執行方法的前後,分別寫入了開啓事務,提交事務,回滾事務等事務管理方法,這時候,業務層就可以刪除掉前面所寫的關於業務的重複代碼

@Component
public class BeanFactory {
    @Autowired
    private AccountService accountService;
    @Autowired
    private TransactionManager transactionManager;

    @Bean("proxyAccountService")
    public AccountService getAccountService() {
        return (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        Object returnValue = null;
                        try {
                            //開啓事務
                            transactionManager.beginTransaction();
                            //執行操作
                            returnValue = method.invoke(accountService, args);
                            //提交事務
                            transactionManager.commit();
                            //返回結果
                            return returnValue;
                        } catch (Exception e) {
                            //回滾事務
                            transactionManager.rollback();
                            throw new RuntimeException();
                        } finally {
                            //釋放連接
                            transactionManager.release();
                        }
                    }
                });
    }
}

AccountServiceTest 類

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")

public class AccountServiceTest {
    @Autowired
    @Qualifier("proxyAccountService")
    private AccountService as;

    @Test
    public void testFindAll() {
        List<Account> list = as.findAll();
        for (Account account : list) {
            System.out.println(account);
        }
    }

    @Test
    public void testTransfer() {
        as.transfer("李四", "張三", 500f);
    }

}

到現在,一個相對完善的案例就改造完成了,由於我們上面大體使用的是註解的方式,並沒有全部使用 XML 進行配置,如果使用 XML 進行配置,配置也是相對繁瑣的,那麼我們鋪墊這麼多的內容,實際上就是爲了引出 Spring 中 AOP 的概念,從根源上,一步一步,根據問題引出要學習的技術

讓我們一起來看一看!

初識 AOP(Spring 程序)

在前面,大篇幅的講解我們在傳統的程序中,是如何一步一步,改進以及處理例如事務這樣的問題的,而 Spring 中 AOP 這個技術,就可以幫助我們來在不修源碼的基礎上對已經存在的方法進行增強,同樣維護也是很方便,大大的提高了開發的效率,現在我們開始正式介紹 AOP 的知識,有了一定的知識鋪墊後,就可以使用 AOP 的方式繼續對前面的程序進行改進!

(一) AOP 術語

任何一門技術,都會有其特定的術語,實際上就是一些特定的名稱而已,事實上,我以前在學習的時候,感覺 AOP 的一些術語都是相對抽象的,並沒有很直觀的體現出它的意義,但是這些術語已經廣泛的被開發者熟知,成爲了在這個相關技術中,默認已知的一些概念,雖然更重要的是理解 AOP 的思想與使用方式,但是,我們還是需要講這樣一種 “共識” 介紹一下

《Spring 實戰》中有這樣一句話,摘出來:

在我們進入某個領域之前,必須學會在這個領域該如何說話

通知(Advice)

  • 將安全,事務,或日誌定義好,在某個方法前後執行一些通知、增強的處理
  • 也就是說:通知就是指,攔截到**連接點(Joinpoint)**後需要做的事情
  • 通知分爲五種類型:
    • 前置通知(Before):在目標方法被執行前調用
    • 後置通知(After):在目標方法完成後使用,輸出的結果與它無關
    • 返回通知(After-returning):在目標方法成功執行之後調用
    • 異常通知(After-throwing):在目標方法拋出異常後調用
    • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行爲(在註解中體現明顯,後面可以注意下)

連接點(Joinpoint)

  • 是在應用執行過程中能夠插入切面的一個點。這個點可以是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼可以利用這些點插入到應用的正常流程之中,並添加新的行爲
  • 例如我們前面對 Service 中的方法增加了事務的管理,事務層中的方法都會被動態代理所攔截到,這些方法就可以看做是這個連接點,在這些方法的前後,我們就可以增加一些通知
  • 一句話:方法的前後都可以看做是連接點

切入點(Pointcut)

  • 有的時候,類中方法有很多,但是我們並不想將所有的方法前後都增加通知,我們只想對指定的方法進行通過,這就是切入點的概念
  • 一句話:切入點就是對連接點進行篩選,選出最終要用的

切面(Aspect)

  • 切入點,告訴程序要在哪個位置進行增強或處理,通知告訴程序在這個點要做什麼事情,以及什麼時候去做,所以 切入點 + 通知 ≈ 切面
  • 切面事實上,就是將我們在業務模塊中重複的部分切分放大,大家可以對比前面我們直接在業務層中的每個方法上進行添加重複的事務代碼,理解一下
  • 一句話:切面就是切入點通知的結合

引入(Introduction)

  • 它是一種特殊的通知,在不修改源代碼的前提下,可以在運行期爲類動態的添加一些方法或者屬性

織入(Weaving)

  • 把切面(增強)應用到目標對象並且創建新的代理對象的過程
  • 實際上就是類似前面,在通過動態代理對某個方法進行增強,且添加事務方法的過程

(二) AOP 入門案例

首先,通過一個非常簡單的案例,來演示一下,如何在某幾個方法執行前,均執行一個日誌的打印方法,簡單模擬爲輸出一句話,前面的步驟我們都很熟悉,需要注意的就是 bean.xml 中配置的方法,我會代碼下面進行詳的講解

(1) 基於 XML 的方式

A:依賴座標

aspectjweaver,這個依賴用來支持切入點表達式等,後面配置中會提到這個知識

<packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>
    </dependencies>

B:業務層

AccountService 接口

public interface AccountService {
    /**
     * 保存賬戶
     */
    void addAccount();
    /**
     * 刪除賬戶
     * @return
     */
    int  deleteAccount();
    /**
     * 更新賬戶
     * @param i
     */
    void updateAccount(int i);
}

AccountServiceImpl 實現類

public class AccountServiceImpl implements AccountService {
    public void addAccount() {
        System.out.println("這是增加方法");
    }

    public int deleteAccount() {
        System.out.println("這是刪除方法");
        return 0;
    }

    public void updateAccount(int i) {
        System.out.println("這是更新方法");
    }
}

C:日誌類

public class Logger {
    /**
     * 用於打印日誌:計劃讓其在切入點方法執行之前執行(切入點方法就是業務層方法)
     */
    public void printLog(){
        System.out.println("Logger類中的printLog方法執行了");
    }
}

D:配置文件

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">
    
    <!--配置Spring的IOC,配置service進來-->
    <bean id="accountService" class="cn.ideal.service.impl.AccountServiceImpl"></bean>

    <!--配置 Logger 進來-->
    <bean id="logger" class="cn.ideal.utils.Logger"></bean>

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

(2) XML配置分析

A:基本配置

<?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>

首先需要引入的就是這個XML的頭部文件,一些約束,可以直接複製這裏的,也可以像以前一樣,去官網找對應的約束等

接着,將 Service 和 Logger 通過 bean 標籤配置進來

B:AOP基本配置

aop:config:表明開始 aop 配置,配置的代碼全部寫在這個標籤內

aop:aspect:表明開始配置切面

  • id屬性:給切面提供一個唯一的標識
  • ref屬性:用來引用已經配置好的通知類 bean,填入通知類的id即可

aop:aspect 標籤內部,通過對應的標籤,配置通知的類型

<aop:config>
	<!--配置切面-->
	<aop:aspect id="logAdvice" ref="logger">
    	<!--通知的類型,以及建立通知方法和切入點方法的關聯-->
	</aop:aspect>
</aop:config>

C:AOP四種常見通知配置

題目中我們是以在方法執行前執行通知,所以是使用了前置通知

aop:before:用於配置前置通知,指定增強的方法在切入點方法之前執行

aop:after-returning:用於配置後置通知,與異常通知只能執行其中一個

aop:after-throwing:用於配置異常通知,異常通知只能執行其中一個

aop:after:用於配置最終通知,無論切入點方法執行時是否有異常,它都會在其後面執行

參數:

  • method:用於指定通知類中的增強方法名稱,也就是我們上面的 Logger類中的 printLog 方法

  • poinitcut:用於指定切入點表達式(文中使用的是這個)指的是對業務層中哪些方法進行增強

  • ponitcut-ref:用於指定切入點的表達式的引用(調用次數過多時,更多的使用這個,減少了重複的代碼)

切入點表達式的寫法:

  • 首先,在poinitcut屬性的引號內 加入execution() 關鍵字,括號內書寫表達式

  • 基本格式:訪問修飾符 返回值 包名.包名.包名…類名.方法名(方法參數)

    • 說明:包名有幾個是根據自己的類所有在的包結構決定

    • 全匹配寫法

      • public void cn.ideal.service.impl.AccountServiceImpl.addAccount()
    • 訪問修飾符,如 public 可以省略,返回值可以使用通配符,表示任意返回值

      • void cn.ideal.service.impl.AccountServiceImpl.addAccount()
    • 包名可以使用通配符,表示任意包,有幾級包,就需要寫幾個*.

      • * *.*.*.*.AccountServiceImpl.addAccount()
    • 包名可以使用…表示當前包及其子包

      • cn..*.addAccount()
    • 類名和方法名都可以使用*來實現通配,下面表示全通配

      • * *..*.*(..)
  • 方法參數

    • 可以直接寫數據類型:例如 int

    • 引用類型寫包名.類名的方式 java.lang.String

    • 可以使用通配符表示任意類型,但是必須有參數

    • 可以使用…表示有無參數均可,有參數可以是任意類型

在實際使用中,更加推薦的寫法也就是上面代碼中的那種,將包結構給出(一般都是對業務層增強),其他的使用通配符

pointcut="execution(* cn.ideal.service.impl.*.*(..))"

在給出4中通知類型後,就需要多次書寫這個切入表達式,所以我們可以使用 pointcut-ref 參數解決重複代碼的問題,其實就相當於抽象出來了,方便以後調用

ponitcut-ref:用於指定切入點的表達式的引用(調用次數過多時,更多的使用這個,減少了重複的代碼)

位置放在 config裏,aspect 外就可以了

<aop:pointcut id="pt1" 
expression="execution(* cn.ideal.service.impl.*.*(..))"></aop:pointcut>

調用時:

<aop:before method="PrintLog" pointcut-ref="pt1"></aop:before>

D:環繞通知

接着,spring框架爲我們提供的一種可以手動在代碼中控制增強代碼什麼時候執行的方式,也就是環繞通知

配置中需要這樣一句話,pt1和前面是一樣的

<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>

Logger類中這樣配置

public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint) {
    Object returValue = null;
    try {
        Object[] args = proceedingJoinPoint.getArgs();
        System.out.println("這是Logger類中的aroundPrintLog前置方法");

        returValue = proceedingJoinPoint.proceed(args);

        System.out.println("這是Logger類中的aroundPrintLog後置方法");

        return returValue;
    } catch (Throwable throwable) {
        System.out.println("這是Logger類中的aroundPrintLog異常方法");
        throw new RuntimeException();
    } finally {
        System.out.println("這是Logger類中的aroundPrintLog最終方法");
    }
}

來解釋一下:

Spring 中提供了一個接口:ProceedingJoinPoint,其中有一個方法叫做 proceed(args),這個方法就相當於明確調用切入點方法,proceed() 方法就好像以前動態代理中的 invoke,同時這個接口可以作爲環繞通知的方法參數,這樣看起來,和前面的動態代理的那種感覺還是很相似的

(3) 基於註解的方式

依賴,以及業務層方法,我們都是用和 XML 一致的嗎,不過爲了演示方便,這裏就只留下 一個 add 方法

A:配置文件

配置文件中一個是需要引入新的約束,再有就是開啓掃描以及開啓註解 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="cn.ideal"></context:component-scan>

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

</beans>

B:添加註解

首先是業務層中把 Service 注進來

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    public void addAccount() {
        System.out.println("這是增加方法");
    }
}

接着就是最終要的位置Logger類中,首先將這個類通過 @Component(“logger”) 整體注入

然後使用 @Aspect 表明這是一個切面類

下面我分別使用了四種通知類型,以及環繞通知類型,在註解中這裏是需要注意的

第一次我首先測試的是四種通知類型:將環繞通知先註釋掉,把前面四個放開註釋

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

    @Pointcut("execution(* cn.ideal.service.impl.*.*(..))")
    private void pt1(){}


//    @Before("pt1()")
    public void printLog1(){
        System.out.println("Logger類中的printLog方法執行了-前置");
    }

//    @AfterReturning("pt1()")
    public void printLog2(){
        System.out.println("Logger類中的printLog方法執行了-後置");
    }

//    @AfterThrowing("pt1()")
    public void printLog3(){
        System.out.println("Logger類中的printLog方法執行了-異常");
    }

//    @After("pt1()")
    public void printLog4(){
        System.out.println("Logger類中的printLog方法執行了-最終");
    }


    @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint) {
        Object returValue = null;
        try {
            Object[] args = proceedingJoinPoint.getArgs();
            System.out.println("這是Logger類中的aroundPrintLog前置方法");

            returValue = proceedingJoinPoint.proceed(args);

            System.out.println("這是Logger類中的aroundPrintLog後置方法");

            return returValue;
        } catch (Throwable throwable) {
            System.out.println("這是Logger類中的aroundPrintLog異常方法");
            throw new RuntimeException();
        } finally {
            System.out.println("這是Logger類中的aroundPrintLog最終方法");
        }
    }

}

四種通知類型測試結果:

可以看到,一個特別詭異的事情出現了,後置通知和最終通知的位置出現了問題,同樣異常情況下也會出現這樣的問題,確實這是這裏的一個問題,所以我們註解中一般使用 環繞通知的方式

環繞通知測試結果:

(4) 純註解方式

純註解還是比較簡單的 加好 @EnableAspectJAutoProxy 就可以了

@Configuration
@ComponentScan(basePackages="cn.ideal")
@EnableAspectJAutoProxy//主要是這個註解
public class SpringConfiguration {
}

到這裏,兩種XML以及註解兩種方式的基本使用就都說完了,下面我們會講一講如何完全基於 Spring 實現事務的控制

(三) 完全基於 Spring 的事務控制

上面Spring中 AOP 知識的入門,但是實際上,Spring 作爲一個強大的框架,爲我們業務層中事務處理,已經進行了考慮,它爲我們提供了一組關於事務控制的接口,基於 AOP 的基礎之上,就可以高效的完成事務的控制,下面我們就通過一個案例,來對這部分內容進行介紹,這一部分,我們選用的的例如 持久層 單元測試等中的內容均使用 Spring,特別注意:持久層我們使用的是 Spring 的 JdbcTemplate ,不熟悉的朋友可以去簡單瞭解一下,在這個案例中,重點還是學習事務的控制,這裏不會造成太大的影響的

(1) 準備代碼

注:準備完代碼第一個要演示的是基於 XML 的形式,所以我們準備的時候都沒有使用註解,後面介紹註解方式的時候,會進行修改

A:導入依賴座標

<packaging>jar</packaging>

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

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

B:創建賬戶表以及實體

創建 Account 表

-- ----------------------------
-- Table structure for account
-- ----------------------------
CREATE TABLE `account`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32),
  `balance` float,
  PRIMARY KEY (`id`)
)

創建 Account 類

沒什麼好說的,對應着我們的表創出實體

public class Account implements Serializable {
    private  Integer id;
    private String name;
    private Float balance;
    ......補充 get set toString 方法

C:創建 Service 和 Dao

爲了減少篇幅,就給了實現類,接口就不貼了,這很簡單

業務層

package cn.ideal.service.impl;

import cn.ideal.dao.AccountDao;
import cn.ideal.domain.Account;
import cn.ideal.service.AccountService;

public class AccountServiceImpl implements AccountService {

    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);

    }

    public void transfer(String sourceName, String targetName, Float money) {
        System.out.println("轉賬方法執行");
        //根據名稱分別查詢到轉入轉出的賬戶
        Account source = accountDao.findAccountByName(sourceName);
        Account target = accountDao.findAccountByName(targetName);

        //轉入轉出賬戶加減
        source.setBalance(source.getBalance() - money);
        target.setBalance(target.getBalance() + money);
        //更新轉入轉出賬戶
        accountDao.updateAccount(source);

        int num = 100/0;

        accountDao.updateAccount(target);
    }
}

持久層

public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {

    public Account findAccountById(Integer accountId) {
        List<Account> accounts = super.getJdbcTemplate().query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId);
        return accounts.isEmpty()?null:accounts.get(0);
    }


    public Account findAccountByName(String accountName) {
        List<Account> accounts = super.getJdbcTemplate().query("select * from account where name = ?",new BeanPropertyRowMapper<Account>(Account.class),accountName);
        if(accounts.isEmpty()){
            return null;
        }
        if(accounts.size()>1){
            throw new RuntimeException("結果集不唯一");
        }
        return accounts.get(0);
    }


    public void updateAccount(Account account) {
        super.getJdbcTemplate().update("update account set name=?,balance=? where id=?",account.getName(),account.getBalance(),account.getId());
    }
}

D:創建 bean.xml 配置文件

提一句:如果沒有用過 JdbcTemplate,可能會好奇下面的 DriverManagerDataSource 是什麼,這個是 Spring 內置的數據源

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

    <!-- 配置業務層-->
    <bean id="accountService" class="cn.ideal.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!-- 配置賬戶的持久層-->
    <bean id="accountDao" class="cn.ideal.dao.impl.AccountDaoImpl">
        <property name="dataSource" ref="dataSource"></property>
    </bean>


    <!-- 配置數據源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/ideal_spring"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root99"></property>
    </bean>
</beans>

E:測試

/**
 * 使用Junit單元測試:測試我們的配置
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {

    @Autowired
    private AccountService as;

    @Test
    public void testTransfer() {
        as.transfer("張三", "李四", 500f);
    }

(2) 基於 XML 的方式

首先要做的就是修改配置文件,這裏需要引入的就是 aop 和 tx 這兩個名稱空間

配置 業務層 持久層 以及數據源 沒什麼好說的,直接複製過來,下面就是我們真正的重要配置

A:配置事務管理器

真正管理事務的對象 Spring 已經提供給我們了

使用Spring JDBC或iBatis 進行持久化數據時可以使用
org.springframework.jdbc.datasource.DataSourceTransactionManager

使用 Hibernate 進行持久化數據時可以使用org.springframework.orm.hibernate5.HibernateTransactionManager

在其中將數據源引入

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource"></property>
</bean>

B:配置事務通知

進行事務通知以及屬性配置時就需要引入事務的約束,tx 以及 aop 的名稱空間和約束

在這裏,就可以將事務管理器引入

<!-- 配置事務的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">

</tx:advice>

C:配置事務屬性

<tx:advice></tx:advice> 中就可以配置事務的屬性了,這裏有一些屬性需要熟悉一下,關於事務的隔離級別可以暫時看一看就可以了,只針對這個例程的話,我們並沒有太多的涉及,事務是一個大問題,需要深入的瞭解,我們在這裏更重點講的是如何配置使用它

  • name:指定你需要增加某種事務的方法名,可以使用通配符,例如 * 代表所有 find* 代表名稱開頭爲 find 的方法,第二種優先級要更高一些

  • isolation:用於指定事務的隔離級別,表示使用數據庫的默認隔離級別,默認值是DEFAULT

    • 未提交讀取(Read Uncommitted)

      • Spring標識:ISOLATION_READ_UNCOMMITTED

      • 代表允許髒讀取,但不允許更新丟失。也就是說,如果一個事務已經開始寫數據,則另外一個事務則不允許同時進行寫操作,但允許其他事務讀此行數據

    • 已提交讀取(Read Committed)

      • Spring標識:ISOLATION_READ_COMMITTED

      • 只能讀取已經提交的數據,解決了髒讀的問題。讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交的寫事務將會禁止其他事務訪問該行

    • 可重複讀取(Repeatable Read)

      • Spring標識:ISOLATION_REPEATABLE_READ
      • 是否讀取其他事務提交修改後的數據,解決了不可重複讀以及髒讀問題,但是有時可能出現幻讀數據。讀取數據的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務
    • 序列化(Serializable)

      • Spring標識:ISOLATION_SERIALIZABLE。
      • 提供嚴格的事務隔離。它要求事務序列化執行,解決幻影讀問題,事務只能一個接着一個地執行,不能併發執行。
  • propagation:用於指定事務的傳播屬性,默認值是 REQUIRED,代表一定會有事務,一般被用於增刪改,查詢方法可以選擇使用 SUPPORTS

  • read-only:用於指定事務是否只讀。默認值是false示讀寫,一般查詢方法才設置爲true

  • timeout:用於指定事務的超時時間,默認值是-1,表示永不超時,如果指定了數值,以秒爲單位,一般不會用這個屬性

  • rollback-for:用於指定一個異常,當產生該異常時,事務回滾,產生其他異常時,事務不回滾。沒有默認值。表示任何異常都回滾

  • no-rollback-for:用於指定一個異常,當產生該異常時,事務不回滾,產生其他異常時事務回滾。沒有默認值。表示任何異常都回滾

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!-- 配置事務的屬性 -->
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED" read-only="false"/>
        <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
    </tx:attributes>
</tx:advice>

D:配置 AOP 切入點表達式

<!-- 配置aop-->
<aop:config>
     !-- 配置切入點表達式-->
    <aop:pointcut id="pt1" expression="execution(* cn.ideal.service.impl.*.*(..))"></aop:pointcut>
</aop:config>

E:建立切入點表達式和事務通知的對應關係

<aop:config></aop:config> 中進行此步驟

<!-- 配置aop-->
<aop:config>
     !-- 配置切入點表達式-->
    <aop:pointcut id="pt1" expression="execution(* cn.ideal.service.impl.*.*(..))"></aop:pointcut>
    <!--建立切入點表達式和事務通知的對應關係 -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>

E:全部配置代碼

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

    <!-- 配置業務層-->
    <bean id="accountService" class="cn.ideal.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!-- 配置賬戶的持久層-->
    <bean id="accountDao" class="cn.ideal.dao.impl.AccountDaoImpl">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 配置數據源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/ideal_spring"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root99"></property>
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
	<!-- 配置事務的通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 配置事務的屬性 -->
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
        </tx:attributes>
    </tx:advice>

    <!-- 配置aop-->
    <aop:config>
        <!-- 配置切入點表達式-->
        <aop:pointcut id="pt1" expression="execution(* cn.ideal.service.impl.*.*(..))"></aop:pointcut>
        <!--建立切入點表達式和事務通知的對應關係 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
    </aop:config>
    
</beans>

(3) 基於註解的方式

還是基本的代碼,但是需要對持久層進行一個小小的修改,前面爲了配置中簡單一些,我們直接使用了繼承 JdbcDaoSupport 的方式,但是它只能用於 XML 的方式, 註解是不可以這樣用的,所以,我們還是需要用傳統的一種方式,也就是在 Dao 中定義 JdcbTemplate

A:修改 bean.xml 配置文件

註解的常規操作,開啓註解,我們這裏把數據源和JdbcTemplate也配置好

<?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:tx="http://www.springframework.org/schema/tx"
       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/tx
        http://www.springframework.org/schema/tx/spring-tx.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="cn.ideal"></context:component-scan>

    <!-- 配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 配置數據源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/ideal_spring"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root99"></property>
    </bean>

</beans>

B:業務層和持久層添加基本註解

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

    @Autowired
    private AccountDao accountDao;
    
    //下面是一樣的
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    //下面基本是一樣的
    //只需要將原來的 super.getJdbcTemplate().xxx 改爲直接用  jdbcTemplate 執行
}

C:在bean.xml中配置事務管理器

<!-- 配置事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>

D:在bean.xml中開啓對註解事務的支持

<!-- 開啓spring對註解事務的支持-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

E:業務層添加 @Transactional 註解

這個註解可以出現在接口上,類上和方法上

  • 出現接口上,表示該接口的所有實現類都有事務支持

  • 出現在類上,表示類中所有方法有事務支持

  • 出現在方法上,表示方法有事務支持

例如下例中,我們類中指定了事務的爲只讀型,但是下面的轉賬還涉及到了寫操作,所以又在方法上增加了一個 readOnly 值爲 false 的註解

@Service("accountService")
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public class AccountServiceImpl implements AccountService {
	.... 省略
	@Transactional(readOnly=false,propagation=Propagation.REQUIRED)
    public void transfer(String sourceName, String targetName, Float money) {
    	...... 省略
    }
}

F:測試代碼

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {

    @Autowired
    private AccountService as;

    @Test
    public void testTransfer() {
        as.transfer("張三", "李四", 500f);
    }
}

(4) 基於純註解方式

下面使用的就是純註解的方式,bean.xml 就可以刪除掉了,這種方式不是很難

A: 配置類註解

@Configuration
  • 指定當前類是 spring 的一個配置類,相當於 XML中的 bean.xml 文件

獲取容器時需要使用下列形式

private ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);

如果使用了 spring 的單元測試

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

B: 指定掃描包註解

@ComponentScan

@Configuration 相當於已經幫我們把 bean.xml 文件創立好了,按照我們往常的步驟,應該指定掃描的包了,這也就是我們這個註解的作用

  • 指定 spring 在初始化容器時要掃描的包,在 XML 中相當於:

  • <!--開啓掃描-->
    <context:component-scan base-package="cn.ideal"></context:component-scan>
    
  • 其中 basePackages 用於指定掃描的包,和這個註解中value屬性的作用是一致的

C: 配置 properties 文件

@PropertySource

以前在創建數據源的時候,都是直接把配置信息寫死了,如果想要使用 properties 進行內容的配置,在這時候就需要,使用 @PropertySource 這個註解

  • 用於加載 .properties 文件中的配置
  • value [] 指定 properties 文件位置,在類路徑下,就需要加上 classpath

SpringConfiguration 類(相當於 bean.xml)

/**
 * Spring 配置類
 */
@Configuration
@ComponentScan("cn.ideal")
@Import({JdbcConfig.class,TransactionConfig.class})
@PropertySource("jdbcConfig.properties")
@EnableTransactionManagement
public class SpringConfiguration {

}

D: 創建對象

@Bean

寫好了配置類,以及指定了掃描的包,下面該做的就是配置 jdbcTemplate 以及數據源,再有就是創建事務管理器對象,在 XML 中我們會通過書寫 bean 標籤來配置,而 Spring 爲我們提供了 @Bean 這個註解來替代原來的標籤

  • 將註解寫在方法上(只能是方法),也就是代表用這個方法創建一個對象,然後放到 Spring 的容器中去
  • 通過 name 屬性 給這個方法指定名稱,也就是我們 XML 中 bean 的 id
  • 這種方式就將配置文件中的數據讀取進來了

JdbcConfig (JDBC配置類)

/**
 * 和連接數據庫相關的配置類
 */
public class JdbcConfig {

    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    /**
     * 創建JdbcTemplate
     * @param dataSource
     * @return
     */
    @Bean(name="jdbcTemplate")
    public JdbcTemplate createJdbcTemplate(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }

    /**
     * 創建數據源對象
     * @return
     */
    @Bean(name="dataSource")
    public DataSource createDataSource(){
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        return ds;
    }
}

jdbcConfig.properties

將配置文件單獨配置出來

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ideal_spring
jdbc.username=root
jdbc.password=root99

TransactionConfig

/**
 * 和事務相關的配置類
 */
public class TransactionConfig {
    /**
     * 用於創建事務管理器對象
     * @param dataSource
     * @return
     */
    @Bean(name="transactionManager")
    public PlatformTransactionManager createTransactionManager(DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}

總結:

① 這篇文章就寫到這裏了,學習任何一門技術,只有知其然,才能明白其所有然,很多人在某個技術領域已經沉浸多年,自然有了特殊的思考與理解,憑藉着強大的經驗,自然也能快速上手,但如果處於門外狀態,或者對這一方面接觸的不多,就更需要了解一門技術的前因後果,不過什麼源碼分析,各種設計模式,這也都是後話,我們的第一要義就是要用它做事,要讓他跑起來,自認爲我不是什麼過於聰明的人,直接去學習一堆配置,一堆註解,一堆專有名詞,太空洞了,很難理解。

② 我們往往都陷入了一種,爲學而學的狀態,可能大家都會SSM我也學,大家都說 SpringBoot 簡單舒服,我也去學,當然很多時候因爲一些工作或者學習的需要,沒有辦法,但是仍覺得,私下再次看一門技術的時候,可以藉助一些文章或者資料,亦或者找點視頻資源,去看看這一門究竟帶來了什麼,其過人之處,必然是解決了我們以前遇到的,或者沒考慮到的問題,這樣一種循序漸進的學習方式,可以幫助我們對一些技術有一個整體的概念,以及瞭解其之間的聯繫。

③ 這一篇文章,我參考了 《Spring 實戰》、某馬的視頻、以及百度谷歌上的一些參考內容,從一個非常簡單的 增刪改查的案例出發,通過分析其事務問題,一步一步從動態代理,到 AOP進行了多次的改進,其中涉及到一些例如 動態代理或者JdcbTemplate的知識,或許有的朋友不熟悉,我也用了一些篇幅說明,寫這樣一篇長文章,確實很費功夫,如果想要了解 Spring AOP 相關知識的朋友,可以看一看,也可以當做一個簡單的參考,用來手生的時候作爲工具書參考

非常希望能給大家帶來幫助,再次感謝大家的支持,謝謝!

Tips:同時有需要的朋友可以去看我的前一篇文章

【萬字長文】Spring框架 層層遞進輕鬆入門 (IOC和DI)

結尾

如果文章中有什麼不足,歡迎大家留言交流,感謝朋友們的支持!

如果能幫到你的話,那就來關注我吧!如果您更喜歡微信文章的閱讀方式,可以關注我的公衆號

在這裏的我們素不相識,卻都在爲了自己的夢而努力 ❤

一個堅持推送原創開發技術文章的公衆號:理想二旬不止

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