Spring AOP进阶-实战:AOP实现事务控制

从 0 开始深入学习 Spring 专栏目录总览

学完了前面的基础和一部分进阶知识之后,这一章咱使用 AOP 来搞定一个简单的事务控制。希望小伙伴通过这个简单的实战,对 AOP 有一个更深入的认识。

本章源码均在 com.linkedbear.spring.transaction 包下。

1. 代码准备

咱先把基本的环境搭建一下哈,分为以下几个步骤。

1.1 导入MySQL的依赖

前面在 IOC 的工程中咱已经导入过一次 MySQL 的依赖了,不多赘述,直接导入即可:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
1.2 初始化数据库

数据库就不搞那么复杂了,来一个简单的员工表得了:

CREATE TABLE `tbl_employee` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(12) NOT NULL,
  `age` int(3) DEFAULT NULL,
  `dept_id` int(11) NOT NULL,
  `salary` decimal(10,2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然后,插入两条数据:

INSERT INTO tbl_employee(id, name, age, dept_id, salary) VALUES (1, 'zhangsan', 18, 1, 1000.00);
INSERT INTO tbl_employee(id, name, age, dept_id, salary) VALUES (2, 'lisi', 20, 2, 1000.00);

很明显我们要搞转账的例子吧!这样数据库就算初始化好了。

1.3 编写Dao和Service

简单的准备一下 Dao 和 Service 吧,Dao 有一个加钱、一个减钱的方法即可:

@Repository
public class FinanceDao {

    public void addMoney(Long id, int money) {
        try {
            Connection connection = JdbcUtils.getConnection();
            PreparedStatement preparedStatement = connection
                    .prepareStatement("update tbl_employee set salary = salary + ? where id = ?");
            preparedStatement.setInt(1, money);
            preparedStatement.setLong(2, id);
            preparedStatement.executeUpdate();
            preparedStatement.close();
            connection.close();
        } catch (SQLException e) {
        	throw new RuntimeException(e);
        }
    }

    public void subtractMoney(Long id, int money) {
        try {
            Connection connection = JdbcUtils.getConnection();
            PreparedStatement preparedStatement = connection
                    .prepareStatement("update tbl_employee set salary = salary - ? where id = ?");
            preparedStatement.setInt(1, money);
            preparedStatement.setLong(2, id);
            preparedStatement.executeUpdate();
            preparedStatement.close();
            connection.close();
        } catch (SQLException e) {
        	throw new RuntimeException(e);
        }
    }
}

Service 只需要一个方法:转账。

@Service
public class FinanceService {
    
    @Autowired
    FinanceDao financeDao;
    
    public void transfer(Long source, Long target, int money) {
        financeDao.subtractMoney(source, money);
        financeDao.addMoney(target, money);
    }
}
1.4 JdbcUtils的制作

上面的 Dao 中有一个 JdbcUtils ,咱先简单的写一下(连接池咱就不弄了哈):

public class JdbcUtils {
    
    public static final String JDBC_URL = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8";
    
    public static Connection getConnection() {
        Connection connection;
        try {
            connection = DriverManager.getConnection(JDBC_URL, "root", "123456");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }
}
1.5 配置类

剩下的,就是配置类了,这里咱提前把 AOP 打开了,还有记得包扫描一下:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.linkedbear.spring.transaction")
public class TransactionAspectConfiguration {
    
}
1.6 测试运行

先编写一下测试代码试一下是不是好使:

public class TransactionApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                TransactionAspectConfiguration.class);
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.transfer(1L, 2L, 100);
    }
}

运行 main 方法后,数据库中的金额发生了变化,证明代码的编写一切正常:

Spring AOP进阶-实战:AOP实现事务控制

好,接下来咱开始搞一下麻烦。

2. 事务的引入

修改一下 FinanceService 的 transfer 方法,让它报一个运行时异常:

public void transfer(Long source, Long target, int money) {
    financeDao.subtractMoney(source, money);
    int i = 1 / 0;
    financeDao.addMoney(target, money);
}

这样再运行,subtractMoney 方法执行成功,addMoney 方法被异常阻止中断运行了,张三哭了,银行乐了。

这个运行结果当然不是我们想要的,整个转账的动作应该是一个原子操作才对。那我们就可以引入切面来实现事务控制。

2.1 编写事务切面类

前面已经练习过的小伙伴,现在写起来肯定是飞快了吧:

@Component
@Aspect
public class TransactionAspect {
    
    @Around("???")
    public Object doWithTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}

控制事务,肯定用环绕通知比较合适,不过这个切入点表达式怎么写呢?直接拦截整个 Service 吗?

当然可以,但总感觉不是很妥:对于那些 getXXX 方法,它们根本不需要事务,那这个时候开启事务就显得很没必要。。。

正好,咱前面学过基于注解的切入点表达式,不如我们就用一个自定义注解来搞定吧!

2.2 编写事务标识注解

下面咱简单的编写一个事务注解,当方法标注了 @Transactional 注解后,即代表该方法需要事务:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) // 
public @interface Transactional {
    
}

这样,切入点表达式也就可以写了:

@Around("@annotation(com.linkedbear.spring.transaction.aspect.Transactional)")
public Object doWithTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}

接下来就是怎么开启事务、提交事务、回滚事务了。

2.3 【问题】全局事务唯一?

可是现在问题就来了:切面里怎么拿到 service 方法中正在使用的 Connection 呢?而且两个 Dao 方法中获取的 Connection 也都是全新的,这个问题怎么解决为好呢?小伙伴们可以开动脑筋想一下有没有什么之间学过的东西能让两个 Dao 的方法执行期间,只有一个 Connection ?

既然是在同一个方法中执行,那就一定是同一个线程咯?那是不是可以用一下 ThreadLocal 呀!使用 ThreadLocal ,可以实现一个线程中的对象资源共享

所以,方案也就有了,咱在 JdbcUtils 中添加一个 ThreadLocal 的成员,把当前线程使用的 Connection 放在这里即可。

下面咱改造一下代码:

public class JdbcUtils {
    
    public static final String JDBC_URL = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8";
    
    private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>();
    
    public static Connection getConnection() {
        // ThreadLocal中有,直接取出返回
        if (connectionThreadLocal.get() != null) {
            return connectionThreadLocal.get();
        }
        // 没有,则创建新的,并放入ThreadLocal中
        Connection connection;
        try {
            connection = DriverManager.getConnection(JDBC_URL, "root", "123456");
            connectionThreadLocal.set(connection);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }
}

鉴于考虑到像 getXXX 这样的方法不需要事务,所以可以把创建新 Connection 的方法单独抽取出来:

public static Connection getConnection() {
    if (connectionThreadLocal.get() != null) {
        return connectionThreadLocal.get();
    }
    return openConnection();
}

public static Connection openConnection() {
    Connection connection;
    try {
        connection = DriverManager.getConnection(JDBC_URL, "root", "123456");
        connectionThreadLocal.set(connection);
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
    return connection;
}

最后,为了能让 ThreadLocal 中的 Connection 能正常移除,再添加一个 remove 的方法吧:

public static void remove() {
    connectionThreadLocal.remove();
}

OK ,到此为止,JdbcUtils 的方法就全部设计好了。

2.4 继续编写切面类

这次来到切面类,就可以在 Service 的方法执行之前,先获取到 Connection ,然后就是我们熟悉的那一套事务控制的套路了:

@Around("@annotation(com.linkedbear.spring.transaction.aspect.Transactional)")
public Object doWithTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
    Connection connection = JdbcUtils.getConnection();
    // 开启事务
    connection.setAutoCommit(false);
    try {
        Object retval = joinPoint.proceed();
        // 方法执行成功,提交事务
        connection.commit();
        return retval;
    } catch (Throwable e) {
        // 方法出现异常,回滚事务
        connection.rollback();
        throw e;
    } finally {
        // 最后关闭连接,释放资源
        JdbcUtils.remove();
    }
}

2.5 编码测试

在 transfer 方法上添加 @Transactional 注解,直接运行 main 方法,控制台会抛出除零异常。

@Transactional
public void transfer(Long source, Long target, int money) {
    financeDao.subtractMoney(source, money);
    int i = 1 / 0;
    financeDao.addMoney(target, money);
}

但是观察数据库,此时双方的钱均没有发生变化,证明事务已经起效果了。

Spring AOP进阶-实战:AOP实现事务控制

好了这一章的内容就这么多,内容不多,但几个关键的点希望小伙伴们能理解到位,这在后面的 Dao 编程事务部分会再次用到!

【基础也学了,实操也搞了,如果想继续学习 Dao 部分的小伙伴可以直接跳过后面的 AOP 高阶和原理了,直接去学 Dao ;对于要继续深入学习的小伙伴而言,下面的内容可能不是特别重要,仅当做扩展知识就好】

学完了前面的基础和一部分进阶知识之后,这一章咱使用 AOP 来搞定一个简单的事务控制。希望小伙伴通过这个简单的实战,对 AOP 有一个更深入的认识。

本章源码均在 com.linkedbear.spring.transaction 包下。

1. 代码准备

咱先把基本的环境搭建一下哈,分为以下几个步骤。

1.1 导入MySQL的依赖

前面在 IOC 的工程中咱已经导入过一次 MySQL 的依赖了,不多赘述,直接导入即可:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
1.2 初始化数据库

数据库就不搞那么复杂了,来一个简单的员工表得了:

CREATE TABLE `tbl_employee` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(12) NOT NULL,
  `age` int(3) DEFAULT NULL,
  `dept_id` int(11) NOT NULL,
  `salary` decimal(10,2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然后,插入两条数据:

INSERT INTO tbl_employee(id, name, age, dept_id, salary) VALUES (1, 'zhangsan', 18, 1, 1000.00);
INSERT INTO tbl_employee(id, name, age, dept_id, salary) VALUES (2, 'lisi', 20, 2, 1000.00);

很明显我们要搞转账的例子吧!这样数据库就算初始化好了。

1.3 编写Dao和Service

简单的准备一下 Dao 和 Service 吧,Dao 有一个加钱、一个减钱的方法即可:

@Repository
public class FinanceDao {

    public void addMoney(Long id, int money) {
        try {
            Connection connection = JdbcUtils.getConnection();
            PreparedStatement preparedStatement = connection
                    .prepareStatement("update tbl_employee set salary = salary + ? where id = ?");
            preparedStatement.setInt(1, money);
            preparedStatement.setLong(2, id);
            preparedStatement.executeUpdate();
            preparedStatement.close();
            connection.close();
        } catch (SQLException e) {
        	throw new RuntimeException(e);
        }
    }

    public void subtractMoney(Long id, int money) {
        try {
            Connection connection = JdbcUtils.getConnection();
            PreparedStatement preparedStatement = connection
                    .prepareStatement("update tbl_employee set salary = salary - ? where id = ?");
            preparedStatement.setInt(1, money);
            preparedStatement.setLong(2, id);
            preparedStatement.executeUpdate();
            preparedStatement.close();
            connection.close();
        } catch (SQLException e) {
        	throw new RuntimeException(e);
        }
    }
}

Service 只需要一个方法:转账。

@Service
public class FinanceService {
    
    @Autowired
    FinanceDao financeDao;
    
    public void transfer(Long source, Long target, int money) {
        financeDao.subtractMoney(source, money);
        financeDao.addMoney(target, money);
    }
}
1.4 JdbcUtils的制作

上面的 Dao 中有一个 JdbcUtils ,咱先简单的写一下(连接池咱就不弄了哈):

public class JdbcUtils {
    
    public static final String JDBC_URL = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8";
    
    public static Connection getConnection() {
        Connection connection;
        try {
            connection = DriverManager.getConnection(JDBC_URL, "root", "123456");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }
}
1.5 配置类

剩下的,就是配置类了,这里咱提前把 AOP 打开了,还有记得包扫描一下:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.linkedbear.spring.transaction")
public class TransactionAspectConfiguration {
    
}
1.6 测试运行

先编写一下测试代码试一下是不是好使:

public class TransactionApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                TransactionAspectConfiguration.class);
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.transfer(1L, 2L, 100);
    }
}

运行 main 方法后,数据库中的金额发生了变化,证明代码的编写一切正常:

Spring AOP进阶-实战:AOP实现事务控制

好,接下来咱开始搞一下麻烦。

2. 事务的引入

修改一下 FinanceService 的 transfer 方法,让它报一个运行时异常:

public void transfer(Long source, Long target, int money) {
    financeDao.subtractMoney(source, money);
    int i = 1 / 0;
    financeDao.addMoney(target, money);
}

这样再运行,subtractMoney 方法执行成功,addMoney 方法被异常阻止中断运行了,张三哭了,银行乐了。

这个运行结果当然不是我们想要的,整个转账的动作应该是一个原子操作才对。那我们就可以引入切面来实现事务控制。

2.1 编写事务切面类

前面已经练习过的小伙伴,现在写起来肯定是飞快了吧:

@Component
@Aspect
public class TransactionAspect {
    
    @Around("???")
    public Object doWithTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}

控制事务,肯定用环绕通知比较合适,不过这个切入点表达式怎么写呢?直接拦截整个 Service 吗?

当然可以,但总感觉不是很妥:对于那些 getXXX 方法,它们根本不需要事务,那这个时候开启事务就显得很没必要。。。

正好,咱前面学过基于注解的切入点表达式,不如我们就用一个自定义注解来搞定吧!

2.2 编写事务标识注解

下面咱简单的编写一个事务注解,当方法标注了 @Transactional 注解后,即代表该方法需要事务:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) // 
public @interface Transactional {
    
}

这样,切入点表达式也就可以写了:

@Around("@annotation(com.linkedbear.spring.transaction.aspect.Transactional)")
public Object doWithTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}

接下来就是怎么开启事务、提交事务、回滚事务了。

2.3 【问题】全局事务唯一?

可是现在问题就来了:切面里怎么拿到 service 方法中正在使用的 Connection 呢?而且两个 Dao 方法中获取的 Connection 也都是全新的,这个问题怎么解决为好呢?小伙伴们可以开动脑筋想一下有没有什么之间学过的东西能让两个 Dao 的方法执行期间,只有一个 Connection ?

既然是在同一个方法中执行,那就一定是同一个线程咯?那是不是可以用一下 ThreadLocal 呀!使用 ThreadLocal ,可以实现一个线程中的对象资源共享

所以,方案也就有了,咱在 JdbcUtils 中添加一个 ThreadLocal 的成员,把当前线程使用的 Connection 放在这里即可。

下面咱改造一下代码:

public class JdbcUtils {
    
    public static final String JDBC_URL = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8";
    
    private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>();
    
    public static Connection getConnection() {
        // ThreadLocal中有,直接取出返回
        if (connectionThreadLocal.get() != null) {
            return connectionThreadLocal.get();
        }
        // 没有,则创建新的,并放入ThreadLocal中
        Connection connection;
        try {
            connection = DriverManager.getConnection(JDBC_URL, "root", "123456");
            connectionThreadLocal.set(connection);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }
}

鉴于考虑到像 getXXX 这样的方法不需要事务,所以可以把创建新 Connection 的方法单独抽取出来:

public static Connection getConnection() {
    if (connectionThreadLocal.get() != null) {
        return connectionThreadLocal.get();
    }
    return openConnection();
}

public static Connection openConnection() {
    Connection connection;
    try {
        connection = DriverManager.getConnection(JDBC_URL, "root", "123456");
        connectionThreadLocal.set(connection);
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
    return connection;
}

最后,为了能让 ThreadLocal 中的 Connection 能正常移除,再添加一个 remove 的方法吧:

public static void remove() {
    connectionThreadLocal.remove();
}

OK ,到此为止,JdbcUtils 的方法就全部设计好了。

2.4 继续编写切面类

这次来到切面类,就可以在 Service 的方法执行之前,先获取到 Connection ,然后就是我们熟悉的那一套事务控制的套路了:

@Around("@annotation(com.linkedbear.spring.transaction.aspect.Transactional)")
public Object doWithTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
    Connection connection = JdbcUtils.getConnection();
    // 开启事务
    connection.setAutoCommit(false);
    try {
        Object retval = joinPoint.proceed();
        // 方法执行成功,提交事务
        connection.commit();
        return retval;
    } catch (Throwable e) {
        // 方法出现异常,回滚事务
        connection.rollback();
        throw e;
    } finally {
        // 最后关闭连接,释放资源
        JdbcUtils.remove();
    }
}

2.5 编码测试

在 transfer 方法上添加 @Transactional 注解,直接运行 main 方法,控制台会抛出除零异常。

@Transactional
public void transfer(Long source, Long target, int money) {
    financeDao.subtractMoney(source, money);
    int i = 1 / 0;
    financeDao.addMoney(target, money);
}

但是观察数据库,此时双方的钱均没有发生变化,证明事务已经起效果了。

Spring AOP进阶-实战:AOP实现事务控制

好了这一章的内容就这么多,内容不多,但几个关键的点希望小伙伴们能理解到位,这在后面的 Dao 编程事务部分会再次用到!

【基础也学了,实操也搞了,如果想继续学习 Dao 部分的小伙伴可以直接跳过后面的 AOP 高阶和原理了,直接去学 Dao ;对于要继续深入学习的小伙伴而言,下面的内容可能不是特别重要,仅当做扩展知识就好】

 

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring AOP进阶-实战:AOP实现事务控制