從零開始認識並操縱Spring Aop事務
目錄
宣告:從零開始,並不代表你對java Spring一點都不懂的程度哈,本例項只是過一個概貌,詳細內容會分多篇文章拆解
業務介紹
要想要設計一個和事務操作相關的業務,我們以最經典的轉賬的例子來描述
轉賬,即兩個使用者之間錢的關係變動,但要求總和不變
版本宣告
本案例使用的Web 版本2.5,Spring 版本為4.3
開發工具為eclipse
操作步驟
導包
下載的包包含Spring包以及第三方工具整合Spring的所有包,但是我們不需要全部匯入,只需根據專案需求匯入即可
Spring 核心包 + apache logging包
- Spring-core
- Spring-bean
- Spring-express
Spring-context
- apache.commons.logging
apache.log4j
Spring 測試包
- Spring-test
Spring Aop 事務包
- Spring-aop
- Spring-aspect
- com.springsource.org.aopalliance
- com.springsource.org.aspectj.weaver
- Spring-jdbc
- Spring-tx
其他包
- c3p0連線池
- JDBC取得包
以上一共15個包
注意的是:每一類Spring 包分為 RELEASE 和 javadoc和source包,匯入只需用RELEASE包
準備資料庫
這裡準備的資料庫就簡單一個表,主要包含使用者名稱和賬戶即可
而我實際運用的表會多幾個欄位,但這並不影響
編寫javaBean
編寫User 的Bean類,注意的是屬性名和表字段名一樣即可,順便加個toString和構造方法,一定要有空參構造哦。
書寫Dao實現介面
為了完整起見,我們把Dao類的增刪改查也順便實現以下
public interface AccountDao {
void save(User u);
void delete(Integer id);
void update(User u);
User getById(Integer id);
int getTotalCount();
List<User> getAll();
void increaseAccount(Integer id, Double money);
void decreaseAccount(Integer id, Double money);
}
實現Dao類,這裡我們不整合其他ORM框架,採用的是原生的JDBC + Spring技術
在寫實現類的時候,需要繼承一個JdbcDaoSupport來為我們生成JdbcTemplate模板
這裡就順帶體驗一下使用JdbcDaoSupport增刪改查的操作,如果已經知道的可以略過
public class UserDaoImpl extends JdbcDaoSupport implements UserDao {
@Override
public void save(User u) {
String sql = "insert into sys_user values(null, ?,?,?,?,?)";
super.getJdbcTemplate().update(sql, u.getUser_code(), u.getUser_name(),
u.getUser_password(), u.getUser_state(), u.getAccount());
}
@Override
public void delete(Integer id) {
String sql = "delete from sys_user where user_id=?";
super.getJdbcTemplate().update(sql, id);
}
@Override
public void update(User u) {
String sql = "update sys_user set user_name=? where user_id=?";
super.getJdbcTemplate().update(sql, u.getUser_name(), u.getUser_id());
}
@Override
public User getById(Integer id) {
String sql = "select * from sys_user where user_id = ?";
return super.getJdbcTemplate().queryForObject(sql, new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int index) throws SQLException {
User u = new User(rs.getInt("user_id"), rs.getString("user_code"),
rs.getString("user_name"), rs.getString("user_password"),
rs.getString("user_state").toCharArray()[0], rs.getDouble("account"));
return u;
}
}, id);
}
@Override
public int getTotalCount() {
String sql = "select count(*) from sys_user";
Integer count = super.getJdbcTemplate().queryForObject(sql, Integer.class);
return count;
}
@Override
public List<User> getAll() {
String sql = "select * from sys_user";
return super.getJdbcTemplate().query(sql, new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int index) throws SQLException {
User u = new User(rs.getInt("user_id"), rs.getString("user_code"),
rs.getString("user_name"), rs.getString("user_password"),
rs.getString("user_state").toCharArray()[0], rs.getDouble("account"));
return u;
}
});
}
@Override
public void increaseAccount(Integer id, Double money) {
String sql = "update sys_user set account=account+? where user_id=?";
super.getJdbcTemplate().update(sql, money, id);
}
@Override
public void decreaseAccount(Integer id, Double money) {
String sql = "update sys_user set account=account-? where user_id=?";
super.getJdbcTemplate().update(sql, money, id);
}
}
Spring 配置
在Spring 中,只要把業務寫好了,剩下的就全靠配置了
資料庫連線配置
這裡我們採用通用的一種,使用properties檔案配置資料庫引數
在 src 目錄下的db.properties中
jdbc.jdbcUrl=jdbc:mysql:///test
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.user=root
jdbc.password=******
建立Spring配置檔案
原則上Spring配置檔案可以放置在任何位置,起任意名字,但是一般都會被統一化
統一是src 目錄下的 applicationContext.xml檔案
匯入約束
在這裡匯入約束的操作可以直接使用eclipse完成,Spring 約束檔案都在Spring包中的schema目錄下,
裡邊有很多,這裡需要匯入的只需要
- beans (物件注入)
- context (讀取properties配置)
- aop (aop配置)
- tx (Spring事務)
裡邊找到最高版本的xsd約束檔案即可
使用eclipse 匯入約束的方法:
windows -> preferences 裡搜尋xml Catalog
裡邊找到剛才介紹的其中一個約束檔案,比如這裡使用context
注意將key type 修改為 Schema location,並在後面追加檔名
接下來是需要在配置檔案標籤中引入這種新版的xsd配置,這裡使用eclipse操作
開啟applicationContext.xml檔案,輸入beans標籤,進入設計模式
對beans新增名稱空間
首先先載入xsi
然後再載入剛剛在xml Catalog匯入的xsd檔案
如果剛才加入的是beans就先找spring-beans
這裡讓beans 使用無字首,但是所有的約束當中只能存在一個xsi約束無字首
比如下一個context就需要帶字首了
對於每一個約束,都要進行以上操作哦,也就是說,重複4次
其實最終的效果就是生成這樣的配置
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" 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-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd ">
</beans>
配置資料庫連線池
使用連線池進行資料庫連線,連線引數讀取properties檔案
配置如下:
<!-- 讀取db.properties配置 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 將連線池放入spring容器 -->
<bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
注入連線池到UserDao
接下來我們要分析依賴關係,在UserDao中繼承了JDBCDaoSupport類
這個類中可以獲得JDBC模板,這個模板的建立需要依賴連線池
因此首先要先建立連線池物件(上面已經完成),再將連線池注入到UserDao類中去
<!-- 將UserDao放入到Spring容器中 -->
<bean name="UserDao" class="edu.scnu.dao.UserDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
使用Junit和Spring整合測試
到了這個階段,有必要進行一波連線測試了,
這裡測試我們顯然需要使用JUnit,但是我們發現寫每個測試方法都要加入這麼一句Spring的Context物件建立的宣告
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) ac.getBean("userDao");
Spring 給了我們貼心的簡化測試類書寫的操作,就是使用註解
- RunWith
- ContextConfiguration
- 這些都在Spring-test包下
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestJdbc {
@Resource(name="userDao")
private UserDao ud;
@Test
public void testSave() {
User u = new User();
u.setUser_name("測試使用者");
u.setUser_state('0');
u.setAccount(1000d);
ud.save(u);
}
@Test
public void testUpdate() {
User u = new User();
u.setUser_name("測試使用者2");
ud.update(u);
}
@Test
public void testDelete() {
ud.delete(7);
}
@Test
public void testFind() {
System.out.println(ud.getById(7));
System.out.println(ud.getAll());
}
}
一波測試後確實發現很多問題... 這裡都修改完成了。
AOP 事務操作的實現方式
一些相關概念回顧
資料庫事務
Spring Aop
編碼式
書寫轉賬的業務介面和實現類
public class UserServiceImpl implements UserService {
private UserDao ud;
@Override
public void transfer(Integer from, Integer to, Double money) {
ud.decreaseAccount(from, money);
ud.increaseAccount(to, money);
}
public void setUd(UserDao ud) {
this.ud = ud;
}
}
將核心事務管理器配置到spring容器
<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
配置TransactionTemplate模板
<bean name="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"></property>
</bean>
將事務模板注入Service
<bean name="userService" class="edu.scnu.service.UserServiceImpl">
<property name="ud" ref="userDao"></property>
</bean>
在Service中使用事務模板
public class UserServiceImpl implements UserService {
private UserDao ud;
private TransactionTemplate txtemplate;
@Override
// 注意在老版本的java中,內部函式訪問不了外部引數的時候,需要把外部引數型別加上final修飾
public void transfer(Integer from, Integer to, Double money) {
txtemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus arg0) {
ud.decreaseAccount(from, money);
ud.increaseAccount(to, money);
}
});
}
public void setUd(UserDao ud) {
this.ud = ud;
}
}
配置式
使用程式設計式的方法,其實本質上還是要寫事務的程式碼到每個業務身上,只不過它內部封裝的一下,讓我們不用寫這麼多,但說白了還是要寫
那麼通過配置的方式,我們可以徹底在業務層上移除事務處理的程式碼
那這個怎麼來配置呢?在Spring Aop中,對於這類問題的抽象,分為了三個核心概念:通知和切點以及他兩構成的切面
我們需要配的就是通知和織入配置這兩個部分了:
配置事務通知
首先配置通知,這個通知其實是用於完成事務操作,如果要自己寫的話,我們肯定會選用環繞通知,異常包裹,但是貼心的Spring給我們簡化了這步操作,我們只需要配置事務管理器注入即可。
配置事務的一些屬性,分別註明了被通知的方法名,隔離級別,傳播級別以及只讀限制,這些內容會在後續補充哦!
<bean name="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="save*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
<tx:method name="update*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
<tx:method name="delete*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
<tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
<tx:method name="find*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
<tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>
配置織入————切面
首先需要的是切點,切點要求用spring提供的表示式來匹配實際需要被通知的類方法名
其次是切面,切面可以說就是切點和通知的交際了!
<!-- 配置織入 -->
<aop:config>
<!-- 配置切點 -->
<aop:pointcut expression="execution(* edu.scnu.service.UserServiceImpl.*(..))" id="txPc"/>
<!-- 配置切面: 通知+切點 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPc"/>
</aop:config>
<!-- 將UserDao放入到Spring容器中 -->
<bean name="userService" class="edu.scnu.service.UserServiceImpl">
<property name="ud" ref="userDao"></property>
</bean>
到這裡,配置就算完成了!
簡化的Service
通過通知和切點的配置後,Service方法不再需要顯示顯明事務操作了,直接專注於業務操作即可,也就是回到遠古時代,那個時候你不知道事務,不知道什麼異常,你還是可以完成安全無誤的程式碼
@Override
public void transfer(Integer from, Integer to, Double money) {
ud.decreaseAccount(from, money);
ud.increaseAccount(to, money);
}
測試方法
對於測試方法,需要建立UserService物件呼叫transfer方法即可,這裡UserService物件我們也是採用@Resource註解引入,這項配置在剛才 將UserDao放入到Spring容器中
已經完成過
@Resource(name="userService")
private UserService us;
@Test
public void testTransfer() {
us.transfer(3, 1, 100d);
}
對於異常測試,我們只需要嘗試在業務方法,轉賬過程中間插入異常測試即可
@Override
public void transfer(Integer from, Integer to, Double money) {
ud.decreaseAccount(from, money);
int i = 1 / 0; // 引入異常測試
ud.increaseAccount(to, money);
}
註解式
註解配置可以說是上述xml配置的一種簡化版,因為上面的方法好是好,只不過配置檔案的內容畢竟太多,而且很多時候還需要對照著來看
因此JDK1.6註解的引入,大大簡化了大多數框架的xml配置
開啟使用註解管理aop事務
<!-- 開啟使用註解管理aop事務 -->
<tx:annotation-driven/>
在需要使用事務的Service方法中新增如下註解
@Override
@Transactional(isolation=Isolation.REPEATABLE_READ, propagation=Propagation.REQUIRED,readOnly=false)
public void transfer(Integer from, Integer to, Double money) {
ud.decreaseAccount(from, money);
ud.increaseAccount(to, money);
}
注意,在eclipse中,修改了配置檔案,最好要clean一下project否則容易報配置檔案失效是產生的錯誤
java.lang.Error: Unresolved compilation problems:
也可以將該註解配置到類宣告之前,讓所有方法都有該事務通知
@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=false)
public class UserServiceImpl implements UserService {
private UserDao ud;
@Override
public void transfer(Integer from, Integer to, Double money) {
ud.decreaseAccount(from, money);
ud.increaseAccount(to, money);
}
@Override
public void save(User u) {
ud.save(u);
}
@Override
public User find(Integer id) {
return ud.getById(id);
}
// ...
}
當然,如果有某些方法需要特殊配置,那麼只需要專門針對這個方法添加註解即可
@Override
@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=true)
public User find(Integer id) {
return ud.getById(id);
}
@Override
@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=true)
public List<User> getAll() {
return ud.getAll();
}
好了,整個流程到這裡就算完成了,整個流程的詳細專案程式碼可以踩我的github結合觀光:https://github.com/Autom-liu/SpringAopLearn,整個流程涉及到諸多知識細節需要慢慢琢磨,這裡只給大家展示整個概貌,具體細節會在後續的文章中慢慢解釋,期待大家的支援和star哦~~~