Spring基於AOP的事務管理
事務是一系列動作,這一系列動作綜合在一起組成一個完整的工作單元,如果有任何一個動作執行失敗,那麼事務就將回到最開始的狀態,仿佛一切都沒發生過。例如,老生常談的轉賬問題,從轉出用戶的總存款中扣除轉賬金額和增加轉出用戶的賬戶金額是一個完整的工作單元,如果只完成扣除或者增加都會導致錯誤,造成損失,而事務管理技術可以避免類似情況的發生,保證數據的完整性和一致性。同樣在企業級應用程序開發過程中,事務管理技術也是必不可少的。
事務有四個特性:ACID
Spring事務是基於面向切面編程(Aspect Oriented Programming,AOP)實現的(文中會簡單講解AOP)。Spring的事務屬性分別為傳播行為、隔離級別、回滾規則、只讀和事務超時屬性,所有這些屬性提供了事務應用方法和描述策略。如下我們介紹Spring事務管理的三個核心接口。
關於事務管理器PlatformTransactionManager的詳細介紹見:http://www.linuxidc.com/Linux/2017-01/139108.htm。
面向切面編程(Aspect Oriented Programing,AOP)采用橫向抽取機制,是面向對象編程(Object Oriented Programing,OOP)的補充和完善。OOP引入封裝、繼承、多態等概念來建立一種對象層次結構,OOP允許開發者定義縱向的關系,但並不適合定義橫向的關系,例如日志功能、權限管理、異常處理等,該類功能往往橫向地散布在核心代碼當中,這種散布在各處的無關代碼被稱為橫切。AOP恰是一種橫切技術,解剖開封裝對象的內部,將那些影響了多個類的公共行為封裝到一個可重用模塊,並將其命名為Aspect(切面),所謂切面,簡單的說就是那些與業務無關,卻為業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重復代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性。
AOP術語
接下來我們就利用Spring的事務管理實現如上例子中所述的轉賬案例,利用mysql數據庫創建名稱為User的數據庫,在User數據庫中創建兩張表:用戶信息表(t_user)、用戶存款表(account),然後實現用戶A向用戶B轉賬,更新數據庫表信息,如果轉賬失敗,則數據庫信息自動返回轉賬前的狀態。
在Eclipse下創建Java工程,其中必要的jar包以及工程中的類如下所示,jar包的下載地址為:Spring_AOP.zip。
項目中的類介紹如下:
用戶類(User):包含用戶基本信息(id,name,password),以及基本信息的get/set方法。
public class User {
private int userID; //用戶ID
private String userName; //用戶名
private String password; //用戶密碼
public int getUserID() {
return userID;
}
public void setUserID(int userID) {
this.userID = userID;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString(){
return "user ID:" + this.getUserID() + " userName:" + this.getUserName() + " user password:" + this.getPassword();
}
}
View Code
創建用戶的工廠(UserFactory):創建用戶的工廠,創建具體用戶對象。
public class UserFactory {
public User createUser(String name, int id, String password){
User user = new User();
user.setUserName(name);
user.setUserID(id);
user.setPassword(password);
return user;
}
}
用戶數據訪問接口(UserDao):定義對用戶表(t_user)的基本操作(增、刪、改、查)。
public interface UserDao {
public int addUser(User user);
public int updateUser(User user);
public int deleteUser(User user);
public User findUserByID(int id);
public List<User> findAllUser();
}
用戶數據訪問實現類(UserDaoImpl):實現接口(UserDao)中定義的方法。
public class UserDaoImpl implements UserDao{
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbc){
this.jdbcTemplate = jdbc;
}
@Override
public int addUser(User user) {
// TODO Auto-generated method stub
String sql = "insert into t_user(userid,username,password)values(?,?,?)";
Object[] obj = new Object[]{
user.getUserID(),
user.getUserName(),
user.getPassword()
};
return this.execute(sql, obj);
}
@Override
public int updateUser(User user) {
// TODO Auto-generated method stub
String sql = "update t_user set username=?,password=? where userid=?";
Object[] obj = new Object[]{
user.getUserName(),
user.getPassword(),
user.getUserID()
};
return this.execute(sql, obj);
}
@Override
public int deleteUser(User user) {
// TODO Auto-generated method stub
String sql = "delete from t_user where userid=?";
Object[] obj = new Object[]{
user.getUserID()
};
return this.execute(sql, obj);
}
private int execute(String sql, Object[] obj){
return this.jdbcTemplate.update(sql, obj);
}
@Override
public User findUserByID(int id) {
// TODO Auto-generated method stub
String sql = "select * from t_user where userid=?";
RowMapper<User> rowMapper = new BeanPropertyRowMapper(User.class);
return this.jdbcTemplate.queryForObject(sql, rowMapper, id);
}
@Override
public List<User> findAllUser() {
// TODO Auto-generated method stub
String sql = "select * from t_user";
RowMapper<User> rowMapper = new BeanPropertyRowMapper(User.class);
return this.jdbcTemplate.query(sql, rowMapper);
}
}
View Code
存款訪問接口(AccountDao):定義對存款表(account)的基本操作。
public interface AccountDao {
public void addAccount(int id, double account);
public void inAccount(int id, double account);
public void outAccount(int id, double account);
}
存款訪問實現類(AccountDaoImpl):實現接口(AccountDao)定義的方法。
public class AccountDaoImpl implements AccountDao{
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbc){
this.jdbcTemplate = jdbc;
}
@Override
public void addAccount(int id, double account) {
// TODO Auto-generated method stub
String sql = "insert into account values(" + id + "," + account + ")";
this.jdbcTemplate.execute(sql);
}
@Override
public void inAccount(int id, double account) {
// TODO Auto-generated method stub
String sql = "update account set account=account+? where userid=?";
this.jdbcTemplate.update(sql, account,id);
}
@Override
public void outAccount(int id, double account) {
// TODO Auto-generated method stub
String sql = "update account set account=account-? where userid=?";
this.jdbcTemplate.update(sql, account,id);
}
}
View Code
存款服務層方法接口(AccountService):定義暴露對外的,提供給用戶訪問的接口。
public interface AccountService {
/*
* 轉賬,實現從outUser轉出account金額的錢到inUser
*/
public void transfer(User outUser, User inUser, double account);
}
存款服務層方法實現類(AccountServiceImpl):實現接口(AccountService)中定義的方法。
public class AccountServiceImpl implements AccountService{
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(User outUser, User inUser, double account){
// TODO Auto-generated method stub
this.accountDao.outAccount(outUser.getUserID(), account);
//模擬程序異常,無法執行inAccount方法
int i = 1 / 0;
this.accountDao.inAccount(inUser.getUserID(), account);
}
}
View Code
創建數據庫表的類(CreateTables)
public class CreateTables {
//通過JdbcTemplate對象創建表
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbc){
jdbcTemplate = jdbc;
}
public void createTable(String sql){
jdbcTemplate.execute(sql);
}
}
View Code
客戶端類(Client)如下:
public class Client {
public static void main(String[] args) {
//定義配置文件路徑
String path = "com/jdbc/JdbcTemplateBeans.xml";
//加載配置文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(path);
//獲取CreateTables實例
CreateTables tables = (CreateTables) applicationContext.getBean("createTables");
//創建t_user表
String create_user = "create table t_user(userid int primary key auto_increment, username varchar(20), password varchar(32))";
tables.createTable(create_user);
//創建工資表,工資表的userid關聯t_user表的userid
String create_account = "create table account(userid int primary key auto_increment, account double, foreign key(userid) references t_user(userid) on delete cascade on update cascade)";
tables.createTable(create_account);
//創建用戶
User user1 = new UserFactory().createUser("張三", 1, "zhangsan");
User user2 = new UserFactory().createUser("李四", 2, "lisi");
User user3 = new UserFactory().createUser("王五", 3, "wangwu");
User user4 = new UserFactory().createUser("趙六", 4, "zhaoliu");
//獲取用戶數據訪問對象
UserDao userDao = (UserDao) applicationContext.getBean("userDao");
System.out.println(userDao.addUser(user1));
System.out.println(userDao.addUser(user2));
System.out.println(userDao.addUser(user3));
System.out.println(userDao.addUser(user4));
//獲取存款數據訪問對象
AccountDao account = (AccountDao) applicationContext.getBean("accountDao");
account.addAccount(1, 100);
account.addAccount(2, 290.5);
account.addAccount(3, 30.5);
account.addAccount(4, 50);
AccountService accountService = (AccountService) applicationContext.getBean("accountService");
accountService.transfer(user1, user3, 10);
}
}
View Code
最後的也是我們實現Spring AOP最關鍵的配置文件JdbcTemplateBeans.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"
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.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.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-4.3.xsd">
<!-- 配置數據源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 數據庫驅動 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 連接數據庫的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/User"/>
<!-- 連接數據庫的用戶名 -->
<property name="user" value="root"/>
<!-- 連接數據的密碼 -->
<property name="password" value="123"/>
</bean>
<!-- 配置JDBC模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默認必須使用數據源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="createTables" class="com.jdbc.CreateTables">
<!-- 通過setter方法實現JdbcTemplate對象的注入 -->
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="userDao" class="com.jdbc.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="accountDao" class="com.jdbc.AccountDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="accountService" class="com.jdbc.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>
<!-- 事務管理器,依賴於數據源 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 編寫通知:對事務進行增強,需要對切入點和具體執行事務細節 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<!-- <tx:method> 給切入點添加事務詳情
name:方法名稱, *表示任意方法, do* 表示以do開頭的方法
propagation:設置傳播行為
isolation:隔離級別
read-only:是否只讀 -->
<tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" read-only="false"/>
</tx:attributes>
</tx:advice>
<!-- aop編寫,讓Spring自動對目標進行代理,需要使用AspectJ的表達式 -->
<aop:config>
<!-- 切入點 -->
<aop:pointcut expression="execution(* com.jdbc.AccountServiceImpl.*(..))" id="txPointCut"/>
<!-- 切面:將切入點和通知整合 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>
啟動mysql數據庫創建名稱為User的數據庫,然後運行該Java工程,輸出如下所示:
然後去mysql的User數據庫中查看剛才生成的表如下:
從控制台輸出中,我們得知在代碼(int i = 1 / 0)處發生了異常,而在異常發生之前,轉出方存款已經發生變化,而通過查看account表發現金額還是輸入的狀態,User1的金額並沒有減少,從而實現了在系統出現異常情況下,事務的回滾。本來想寫到這裡就結束的,但是總感覺沒有把AOP說的特別透徹,於是想通過在轉賬前後增加日志的方式對AOP做進一步的講解。
在原來項目的基礎上,增加一個日志打印類(LogHandler),該類代碼如下:
public class LogHandler {
//切入點執行之前需要執行的方法
public void LogBefore(){
System.out.println("轉賬開始時間:" + System.currentTimeMillis());
}
//切入點執行結束執行該方法
public void LogAfter(){
System.out.println("轉賬結束時間:" + System.currentTimeMillis());
}
}
當然啦,還需要在XML配置文件中增加配置信息:
<!-- 配置日志打印類 -->
<bean id="logHandler" class="com.jdbc.LogHandler"/>
<aop:config>
<!-- order屬性表示橫切關注點的順序,當有多個時,序號依次增加 -->
<aop:aspect id="log" ref="logHandler" order="1">
<!-- 切入點為AccountServiceImpl類下的transfer方法 -->
<aop:pointcut id="logTime" expression="execution(* com.jdbc.AccountServiceImpl.transfer(..))"/>
<aop:before method="LogBefore" pointcut-ref="logTime"/>
<aop:after method="LogAfter" pointcut-ref="logTime"/>
</aop:aspect>
</aop:config>
然後將AccountServiceImpl類中transfer方法中異常語句(int i = 1 / 0)注釋掉,將Client類中的創建表、添加表項的代碼也注釋掉,再次執行主函數,則顯示日志輸出,查看轉賬前後數據庫表狀態。表如下:
如上就是對Spring AOP事務管理一個簡單的介紹,希望能對讀者產生一點幫助。