本案要解决的是一个简单的银行账户转账的问题
采用事务控制保证账户总金额的一致性,通过控制事务的提交和回滚避免转账过程中出现运行异常时带来的影响。
pom.xml配置
需要的jar包:
- spring ioc容器:spring-context
- 数据库相关:mysql、dbutils、c3p0
- AOP相关:aspectjweaver(处理切面表达式)
- 用于单元测试:(spring和junit整合包)spring-test、(单元测试包)junit 4.12及以上
- 注解:javax.annotation-api(使用@Resource时需要的jar包)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mj</groupId>
<artifactId>day04_03anno_tx_aop</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<!--spring整合junit jar包
需配合junit4.12以上的版本-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.6.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>8.0.19</version>
</dependency>
<!--c3p0 开源的JDBC数据库连接池-->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
</project>
数据库表格信息对应的java类
package com.mj.domain;
import java.io.Serializable;
public class Account implements Serializable{
//变量名称与数据库表中字段一致
private Integer id;
private String name;
private Integer money;
/* 补充驼峰命名规则:若数据库中含字段名形式为:xx_xx,
则在java中对应形式为xxXx(_后面字母大写)
开启驼峰命名:在主配置文件中设置参数mapUnderscoreToCamelCase的值为true
*/
public Account() {
}
public Account(Integer id, String name, Integer money) {
this.id = id;
this.name = name;
this.money = money;
}
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setMoney(Integer money) {
this.money = money;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public Integer getMoney() {
return money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
DAO实现类
package com.mj.dao;
import com.mj.domain.Account;
import com.mj.util.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.List;
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao{
@Resource(name = "runner")
private QueryRunner runner;
@Autowired
private ConnectionUtils connectionUtils;
@Override
public Account getAccountByName(String name) {
try {
List<Account> accounts=runner.query(connectionUtils.getThreadConnect(),"select * from Account where name=?",new BeanListHandler<Account>(Account.class),name);
if ( accounts ==null || accounts.size()==0){
return null;
}else if ( accounts.size()>1 ){
throw new RuntimeException("查询账户不唯一,数据有问题");
}else{
return accounts.get(0);
}
}catch (Exception e){
throw new RuntimeException(e);
}
}
@Override
public void updateAccount(Account account) {
try {
runner.update(connectionUtils.getThreadConnect(),"update Account set money=? where id=?",account.getMoney(),account.getId());
}catch (Exception e){
e.printStackTrace();
}
}
}
Service层代码
package com.mj.service;
import com.mj.dao.IAccountDao;
import com.mj.domain.Account;
import com.mj.util.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("accountService")
public class AccountService {
@Autowired
private IAccountDao accountDao;
public Account getAccountByName(String name){
return accountDao.getAccountByName(name);
};
public void updateAccount(Account account){
accountDao.updateAccount(account);
};
public void transfer(String sourceName, String targetName, Integer tMoney) {
Account sourceA = accountDao.getAccountByName(sourceName);
Account targetA = accountDao.getAccountByName(targetName);
sourceA.setMoney(sourceA.getMoney() - tMoney);
targetA.setMoney(targetA.getMoney() + tMoney);
accountDao.updateAccount(sourceA);
accountDao.updateAccount(targetA);
}
}
连接池控制类
将当前线程与数据库连接进行绑定,保证当前线程下获取到的数据库连接为同一个。
package com.mj.util;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
@Component
public class ConnectionUtils {
private ThreadLocal<Connection> tc=new ThreadLocal<>();
@Resource(name = "dataSource")
private DataSource dataSource;
public Connection getThreadConnect(){
try{
Connection con=tc.get();
if ( con==null ){
con=dataSource.getConnection();
tc.set(con);
}
return con;
}catch(Exception e){
throw new RuntimeException(e);
}
}
public void removeConnection(){
tc.remove();//清除连接池中所有连接
}
}
事务控制类
管理事务的开启、提交、回滚与释放工作
同时也是通知类:管理所有提取出来的公共代码,其作用是AOP中拦截到切入点后对切入方法的增强。
package com.mj.util;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
impor/t org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
@Component("transactionManager")
@Aspect//配置切面
@EnableAspectJAutoProxy//动态代理
public class TransactionManager {
@Autowired
private ConnectionUtils con;
//配置切入表达式:通知类和切入点(服务层类对象中被拦截的方法)的连接点
@Pointcut("execution(* com.mj.service.*.*(..))")
public void pt(){}
//前置通知-事务的开启
@Before("pt()")
public void beginTransaction(){
try {
con.getThreadConnect().setAutoCommit(false);
}catch (Exception e){
throw new RuntimeException(e);
}
}
//后置通知-事务的提交
@AfterReturning("pt()")
public void commitTransaction(){
try {
con.getThreadConnect().commit();
}catch (Exception e){
throw new RuntimeException(e);
}
}
//异常通知
@AfterThrowing("pt()")
public void rollbackTransaction(){
try {
con.getThreadConnect().rollback();
}catch (Exception e){
throw new RuntimeException(e);
}
}
//最终通知
@After("pt()")
public void releaseTransaction(){
try {
con.getThreadConnect().close();//把连接还回连接池中
con.removeConnection();
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
以上四种通知的设置在基于xml的案例中是可以正常循行的,但是当使用注解版时,四种通知的执行顺序会更预想的不一样,最终通知往往会先于后置通知执行,于是会出现在执行事务提交时连接已经被释放的运行异常。所以此时选择使用环绕通知才时正确的选择,环绕通知的执行顺序是我们手动确定的,不会顺序颠倒
改进后的通知类:
package com.mj.util;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
@Component("transactionManager")
@Aspect//配置切面
@EnableAspectJAutoProxy//动态代理
public class TransactionManager {
@Autowired
private ConnectionUtils con;
@Pointcut("execution(* com.mj.service.*.*(..))")
public void pt(){}
public void beginTransaction(){
try {
con.getThreadConnect().setAutoCommit(false);
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void commitTransaction(){
try {
con.getThreadConnect().commit();
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void rollbackTransaction(){
try {
con.getThreadConnect().rollback();
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void releaseTransaction(){
try {
con.getThreadConnect().close();//把连接还回连接池中
con.removeConnection();
}catch (Exception e){
throw new RuntimeException(e);
}
}
@Around("pt()")//环绕通知
public Object aroundAdvice(ProceedingJoinPoint pjp){
Object returnValues=null;
try {
this.beginTransaction();
Object[] args=pjp.getArgs();
returnValues=pjp.proceed(args);
this.commitTransaction();
return returnValues;
}catch (Throwable e){
this.rollbackTransaction();
throw new RuntimeException(e);
}finally {
this.releaseTransaction();
}
}
}
配置类:替换xml文件
指定spring在创建容器时要扫描的包
配置sql执行对象和数据库
package configuration;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.context.annotation.*;
/*
* @Configuration注解可以指明当前类为配置类,可以用来替代xml文件
* 注意:当该类作为AnnotationConfigApplicationContext对象的参数传入时可以省去该注解
例:ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfiguration.class);
* @ComponentScan:指定spring在创建容器时要扫描的包
* 属性:basePackages与value等价,用于指定要扫描的包的路径
*
* @Bean:将当前方法的返回值作为Bean对象存入spring容器中
* 属性:value与name等价,用于指定bean的id,不设置则默认为方法名
*
* @Import:用于导致其他配置类
* 属性:用于指定其他配置类的字节码 xxx.class
*/
@Configuration
@ComponentScan(basePackages = "com.mj")
public class SpringConfiguration
@Bean("runner")
@Scope("prototype")
public QueryRunner createQueryRunner(){
//当有多个数据库连接时,可以在参数中用注解@Qualify指定数据库源的id
QueryRunner runner=new QueryRunner();
return runner;
}
@Bean("dataSource")
public ComboPooledDataSource createDataSource(){
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass("com.mysql.cj.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/mytest?serverTimezone=GMT");
ds.setUser("root");
ds.setPassword("1234567890");
return ds;
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
测试代码
import com.mj.service.AccountService;
import configuration.SpringConfiguration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
//以上两个注解的使用可以使得不必在单元测试中执行方法前都要先用ApplicationContext获取容器
public class AccountServiceProxyTest {
@Resource(name = "accountService")
public AccountService as;
@Test
public void transfer() {
//从鬼鬼的账户中向里里的账户存入一笔钱
as.transfer("鬼鬼","里里",100);
}
}