Spring AOP 實現之 XML 配置
1. 前言
大家好,本小節,我們學習 Spring 框架中基于代理模式實現的 AOP。關于什么是代理模式,我們在前兩個小節已經詳細介紹過概念,并演示了代理模式的使用。
可能大家也有了一些體會,我們可以使用代理模式來對我們的一些功能方法做增強。只不過有一些不如人意的地方:
- 自定義代理模式代碼編寫過于臃腫
- 侵入性比較強,代碼不夠優雅
- 控制事務的實現過于繁瑣。
疑問導出:
如何簡單輕便優雅地解決這種問題呢?當然就是我們的主角 Spring 的 AOP 啦。
對于 AOP ,我們也已經詳細解釋過它的概念,對于 Spring 框架中的 AOP 實例,就在本小節做一個簡單的實現。
2. 實例演示
2.1 工程搭建介紹
數據庫表結構:
建表 SQL 語句如下:
CREATE TABLE `account` (
`id` int(11) NOT NULL auto_increment COMMENT 'id',
`accountNum` varchar(20) default NULL COMMENT '賬號',
`money` int(8) default NULL COMMENT '余額',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
工程代碼介紹:
- 實體類: 跟數據庫表對應的 Java 類 Account ;
- 操作實體類的: Dao 和 Dao 的接口實現類 ;
- 調用持久層的業務類: Service 和 Service 的實現類 ;
- 事務管理器類: TransactionManager 提供事務的一系列操作 ;
- 測試代碼類: 初始化 Spring 調用類中的方法測試 。
思路介紹:
本測試案例同前兩個小節實現的目的完全一致,不同的在于本小節使用 Spring 的 AOP 替代代理類。先回顧下 AOP 中的核心概念:
所以:對原始業務類中的方法增強行為也就是 Spring 的 AOP 中所謂的前置通知,在對原始業務類中的方法執行之后的增強行為就是后置通知。
而一旦出現異常,那么所做的動作就是異常通知。本案例使用幾種通知,來實現事務的控制。
2.2 代碼實現
1. 創建 maven 工程:
pom 文件的 jar 包坐標如下:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</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>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.4</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>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
2. 實體類 Account
public class Account implements Serializable {
//數據id
private Integer id;
//賬號編碼
private String accountNum;
//賬號金額
private Float money;
//省略get 和set 方法
}
3. 數據庫連接工具類
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();
}
}
4. 持久層 dao 和 dao 的 實現類:
//dao的接口
public interface IAccountDao {
/**
* 更新
* @param account
*/
void updateAccount(Account account);
/**
* 根據編號查詢賬戶
*/
Account findAccountByNum(String accountNum);
}
//dao的實現類
public class AccountDaoImpl implements IAccountDao {
//dbutil的查詢工具類
private QueryRunner runner;
//連接的工具類
private ConnectionUtils connectionUtils;
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
//修改賬號
public void updateAccount(Account account) {
try{
runner.update(connectionUtils.getThreadConnection(),"update account set accountNum=?,money=? where id=?",account.getAccountNum(),account.getMoney(),account.getId());
}catch (Exception e) {
throw new RuntimeException(e);
}
}
//根據賬號查詢
public Account findAccountByNum(String accountNum) {
try{
List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where accountNum = ? ",new BeanListHandler<Account>(Account.class),accountNum);
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);
}
}
}
5. 業務類 Service 和 Service 的實現類
//業務接口
public interface IAccountService {
/**
* 轉賬
* @param sourceAccount 轉出賬戶名稱
* @param targetAccount 轉入賬戶名稱
* @param money 轉賬金額
*/
void transfer(String sourceAccount, String targetAccount, Integer money);
}
//業務實現類
public class AccountServiceImpl implements IAccountService {
//持久層對象
private IAccountDao accountDao;
//省略 set 和 get 方法
//轉賬的方法
public void transfer(String sourceAccount, String targetAccount, Integer money) {
//查詢原始賬戶
Account source = accountDao.findAccountByNum(sourceAccount);
//查詢目標賬戶
Account target = accountDao.findAccountByNum(targetAccount);
//原始賬號減錢
source.setMoney(source.getMoney()-money);
//目標賬號加錢
target.setMoney(target.getMoney()+money);
//更新原始賬號
accountDao.updateAccount(source);
//更新目標賬號
accountDao.updateAccount(target);
System.out.println("轉賬完畢");
}
}
6. 事務管理器類
package com.offcn.transaction;
/**
* @Auther: wyan
* @Date: 2020-05-26 21:20
* @Description:
*/
import com.offcn.utils.ConnectionUtils;
/**
* 和事務管理相關的工具類,它包含了,開啟事務,提交事務,回滾事務和釋放連接
*/
public class TransactionManager {
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
/**
* 開啟事務
*/
public void beginTransaction(){
try {
System.out.println("開啟事務");
connectionUtils.getThreadConnection().setAutoCommit(false);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 提交事務
*/
public void commit(){
try {
System.out.println("提交事務");
connectionUtils.getThreadConnection().commit();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 回滾事務
*/
public void rollback(){
try {
System.out.println("回滾事務");
connectionUtils.getThreadConnection().rollback();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 釋放連接
*/
public void release(){
try {
System.out.println("釋放連接");
connectionUtils.getThreadConnection().close();//還回連接池中
connectionUtils.removeConnection();
}catch (Exception e){
e.printStackTrace();
}
}
}
代碼解釋:此工具類就作為 Spring 使用 AOP 管理事務的通知類,里面的各個方法用于配置 Spring 的通知使用。為了測試效果,在每個通知方法內,我們輸出打印了測試語句。
7. 配置文件中添加 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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置Service -->
<bean id="accountService" class="com.offcn.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<!--配置Dao對象-->
<bean id="accountDao" class="com.offcn.dao.impl.AccountDaoImpl">
<property name="runner" ref="runner"></property>
<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/transmoney"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!-- 配置Connection的工具類 ConnectionUtils -->
<bean id="connectionUtils" class="com.offcn.utils.ConnectionUtils">
<!-- 注入數據源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事務管理器-->
<bean id="txManager" class="com.offcn.transaction.TransactionManager">
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!-- aop相關的節點配置 -->
<aop:config>
<aop:pointcut expression="execution ( * com.offcn.service.*.*(..))" id="pc"/>
<aop:aspect ref="txManager">
<aop:before method="beginTransaction" pointcut-ref="pc"/>
<aop:after-returning method="commit" pointcut-ref="pc"/>
<aop:after method="release" pointcut-ref="pc"/>
<aop:after-throwing method="rollback" pointcut-ref="pc"/>
</aop:aspect>
</aop:config>
</beans>
配置文件說明:
- connectionUtils: 是獲取數據庫連接的工具類;
- dataSource: 采用 c3p0 數據源,大家一定要注意數據庫的名稱與賬號名和密碼;
- queryRunner: dbutils 第三方框架提供用于執行 SQL 語句,操作數據庫的一個工具類;
- accountDao 和 accountService: 是我們自定義的業務層實現類和持久層實現類;
- aop:config: 此節點是新增加 AOP 配置,AOP 相關信息都在這;
- aop:pointcut: 此節點是切入點,表示哪些類的哪些方法在執行的時候會應用 Spring 配置的通知進行增強;
- aop:aspect: 此節點是配置切面類的節點,在 AOP 介紹的小節解釋過,它的作用主要就是整合通知和切入點。
null 前置、后置、異常、和最終??梢钥吹贸鰜?before 前置通知執行的方法是開啟事務, after-returning 成功執行的方法是提交事務,after 最終執行的方法是釋放連接,after-throwing 出現異常執行的方法是回滾。
8. 測試類代碼
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class AccountServiceTest {
@Autowired
private IAccountService accountService;
@Test
public void testTransfer(){
accountService.transfer("622200009999","622200001111",100);
}
}
測試結果:
執行代碼后結果:
可以看到,我們通過在 xml 文件中配置 Spring 的 AOP 相關配置,就可以實現對我們業務類中的方法實現了增強,無需自定義對業務類做代理實現。
3. 小結
本小節學習了 Spring 中 AOP 的使用,那么哪些要求大家掌握的呢?
-
AOP 的相關概念,什么是切面,什么是通知,什么是切入點;
-
通知的幾種類型,以及他們的執行時機;
-
如何在 Spring 配置文件中使用 xml 的方式實現 AOP。