Spring基于AOP的事务管理 |
- 事务
事务是一系列动作,这一系列动作综合在一起组成一个完整的工作单元,如果有任何一个动作执行失败,那么事务就将回到最开始的状态,仿佛一切都没发生过。例如,老生常谈的转账问题,从转出用户的总存款中扣除转账金额和增加转出用户的账户金额是一个完整的工作单元,如果只完成扣除或者增加都会导致错误,造成损失,而事务管理技术可以避免类似情况的发生,保证数据的完整性和一致性。同样在企业级应用程序开发过程中,事务管理技术也是必不可少的。
事务有四个特性:ACID
- 原子性(Atomicity):事务是一个原子操作,有一系列动作组成。原子性保证所有动作都完成,或者不执行任何动作。
- 一致性(Consistency):一旦事务完成(不论成败),系统必须确保它所建模的业务处于一致的状态。
- 隔离性(Isolation):可能有很多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
- 持久性(Durability):一旦事务完成,无论系统发生生什么系统错误,它的结果都不会受到影响,保证能从系统崩溃中恢复过来,通常事务的结果会被写入到持久化存储器中。
Spring事务是基于面向切面编程(Aspect Oriented Programming,AOP)实现的(文中会简单讲解AOP)。Spring的事务属性分别为传播行为、隔离级别、回滚规则、只读和事务超时属性,所有这些属性提供了事务应用方法和描述策略。如下我们介绍Spring事务管理的三个核心接口。
- 核心接口
- TransactionDefinition接口是事务描述对象,提供获取事务相关信息的方法。
- PlatformTransactionManager接口是平台事务管理器,用于管理事务。
- TransactionStatus接口是事务的状态,描述了某一时间点上事务的状态信息。
关于事务管理器PlatformTransactionManager的详细介绍见:http://www.mamicode.com/info-detail-1248286.html。
- Spring AOP
面向切面编程(Aspect Oriented Programing,AOP)采用横向抽取机制,是面向对象编程(Object Oriented Programing,OOP)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能、权限管理、异常处理等,该类功能往往横向地散布在核心代码当中,这种散布在各处的无关代码被称为横切。AOP恰是一种横切技术,解剖开封装对象的内部,将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为Aspect(切面),所谓切面,简单的说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。
AOP术语
- 连接点(Joinpoint):被拦截到的点,该连接点可以是被拦截到的方法、字段或者构造器;
- 切入点(Pointcut):指要对哪些连接点进行拦截,即被拦截的连接点;
- 通知(Advice):指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知;
- 目标(Target):代理的目标对象;
- 织入(Weaving):把增强的代码应用到目标上,生成代理对象的过程;
- 切面(Aspect):切入点和通知的集合。
- 项目实践
接下来我们就利用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(); } }
创建用户的工厂(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); } }
存款访问接口(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); } }
存款服务层方法接口(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); } }
创建数据库表的类(CreateTables)
public class CreateTables { //通过JdbcTemplate对象创建表 private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbc){ jdbcTemplate = jdbc; } public void createTable(String sql){ jdbcTemplate.execute(sql); } }
客户端类(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); } }
最后的也是我们实现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事务管理一个简单的介绍,希望能对读者产生一点帮助。