利用AOP手写Spring事务操作

前言

AOP是Spring的一大特性,面向切面编程给开发者提供了一种全新的开发思路,不侵入业务逻辑,不修改业务逻辑的代码,实现一些程序必要的辅助功能,比如说:输出日志、权限校验、事务处理等等,优雅的AOP让程序的方法不再紧紧地耦合在一起,达到了解耦的目的,想用就用,不想用就不用。

AOP介绍

AOP也就是面向切面编程,Aspect Oriented Programming,AOP的思想就是把非必要的功能抽取成一个切面类,在切面类中实现种种增强方法,再通过动态代理把原来实现的业务类代理起来,在原来业务方法的指定的时机调用我们指定的增强方法。
说人话就是,一个方法功能不够完善,我想在方法开始前干A事,在方法返回后干B事,在方法抛出异常的时候干C事,如果我把所有的方法都写进原来的业务的方法中,那么业务方法会很臃肿而且难维护,如果这些A、B、C事每个业务类都要干,那么我就要在每个业务类中都加入A、B、C事,这样繁琐的工作无疑是让人抓狂的。
所以我们就把A、B、C方法都抽取出来写在一个切面类里,并且指定,这些A、B、C方法在哪些类的哪些方法的哪些时机去调用,这样就解耦了业务方法和增强方法,我们只需要维护切面类里的方法,修改他们的调用时机,就可以达到动动小手,操控所有的目标。

AOP术语

切点:指需要增强的方法
横切关注点:指方法的四个调用时机,开始前、返回后、异常后、后置
通知:额外添加的方法
切面:切点+通知
四个横切关注点:

  1. 前置,方法开始时
  2. 后置,方法最终执行,无论是否异常,相当于finally块
  3. 异常,方法抛出异常时,相当于catch
  4. 返回,方法正常执行完成,相当于return之前

事务是如何实现的

在JAVA中,我们通过DataSource去getConnection,拿出来的连接都是autoCommit,也就是说每次通过Connection去操作数据库,自动帮我们开启了一个事务,并且每次都自动提交。
我们知道事务的原子性要求事务里的操作要么一次全部执行,要么一次全部不执行,如果按照初始连接的自动提交去执行一次Service类中的方法,有可能发生前面的操作成功,后面的操作失败,导致事务的原子性被破坏的情况。
为了防止破坏原子性的事情发生,我们需要保证事务只在我们需要的时候提交,也要在发生异常的时候去回滚,而不是每次都去提交,所以我们需要保证以下几点:

  1. 事务不能自动提交
  2. 所有的操作使用的连接都是同一个
  3. 在抛出异常的时候需要回滚
  4. 正常操作的时候要及时提交

我们不妨想象事务是这样的一个代码块:

void transaction(){
	// 前置通知
	Connection conn = getConnection();
	conn.setAutoCommit(false);
	try{
		Object val = method.invoke();
		// 返回通知
		conn.commit();
		return val;
	}catch(Exception e){
	// 异常通知
		conn.rollback();
	}finally{
	// 最终通知
		conn.setAutoCommit(true);
		conn.close();
	}
	
	
}

如何用AOP实现事务

AOP实现事务的思路其实很简单:

  1. 在方法开始调用的时候,获取连接,把连接的自动提交设置为false
  2. 在方法正常执行完毕的时候,AOP得帮我们提交事务
  3. 在方法出现异常的时候,AOP得帮我们回滚事务
  4. 不管是否出现异常,最后都需要把连接放回连接池,并把自动提交还原回true

具体实现思路

有了思路之后就很清晰了,根据对应的需求,转换成技术的实现:

  1. 在所有Service方法开始的时候,也就是做一个前置通知
  2. 在方法执行完毕的时候,做一个AfterReturning通知帮我们提交
  3. 在方法结束时,帮我们关闭连接,做一个After通知
  4. 在方法抛出异常时,做一个AfterThrowing通知,帮我们回滚

具体代码实现

项目结构

在这里插入图片描述
核心的类只有三个:UserService,UserMapper,AopTransaction

AOP实现

package com.csw.aoptransaction.aop;


import com.csw.aoptransaction.util.ConnectionUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.SQLException;

@Aspect
@Component
@Slf4j
public class AopTransaction{
    @Pointcut("execution(public * *..*Service.*(..))")
    public void serviceCut(){}

    @Before(value = "serviceCut()")
    public void transactionStart() throws SQLException {
        log.info("===========事务开始=========");
        Connection connection = ConnectionUtil.getConnection(Thread.currentThread());
        connection.setAutoCommit(false);
    }
    @After(value = "serviceCut()")
    public void transactionAfter() throws SQLException {
        Connection connection = ConnectionUtil.getConnection(Thread.currentThread());
        connection.setAutoCommit(true);
        connection.close();
        log.info("=====事务结束,关闭连接======");
    }
    @AfterReturning(value = "serviceCut()")
    public void transactionAfterReturning() throws SQLException {
        Connection connection = ConnectionUtil.getConnection(Thread.currentThread());
        connection.commit();
        log.info("===========提交===========");
    }
    @AfterThrowing(value = "serviceCut()",throwing = "e")
    public void transactionAfterThrowing(Exception e) throws SQLException {
        Connection connection = ConnectionUtil.getConnection(Thread.currentThread());
        connection.rollback();
        log.info("遇到错误:"+e.getMessage());
        log.info("===========回滚===========");
    }
}


为了保证每个线程拿到的都是同一个连接,我参考了一下ThreadLocal的实现,用每个线程自己去获取一个连接,在第一次获取的时候往Map里存放一个Connection,之后每次获取连接都能根据自己的线程获取自己的Connection

package com.csw.aoptransaction.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;


/**
 * @author Chen
 */
@Component
public class ConnectionUtil {
    private static HashMap<Thread, Connection> map = new HashMap<>();
    private static DataSource datasource;
    @Autowired
    public void setDatasource(DataSource datasource){
        ConnectionUtil.datasource = datasource;
    }

    public static Connection getConnection(Thread t) throws SQLException {
        Connection conn = null;
        if((conn = map.get(t)) == null){
            conn = datasource.getConnection();
            map.put(t,conn);
        }
        return conn;
    }
}

Mapper实现

package com.csw.aoptransaction.mapper.Impl;

import com.csw.aoptransaction.bean.User;
import com.csw.aoptransaction.mapper.UserMapper;
import com.csw.aoptransaction.util.ConnectionUtil;
import org.springframework.stereotype.Repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 * @author Chen
 */
@Repository
public class UserMapperImpl implements UserMapper {

    @Override
    public int insertUser(User user) throws SQLException {
        Connection connection = ConnectionUtil.getConnection(Thread.currentThread());
        PreparedStatement ps = connection.prepareStatement("insert into t_user values(?,?,?)");
        ps.setInt(1,user.getId());
        ps.setString(2,user.getUsername());
        ps.setString(3,user.getPassword());
        return ps.executeUpdate();
    }
}

Service实现

package com.csw.aoptransaction.service.Impl;

import com.csw.aoptransaction.bean.User;
import com.csw.aoptransaction.service.UserService;
import com.csw.aoptransaction.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.sql.SQLException;

/**
 * @author Chen
 */
@Service
@Slf4j
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;

    @Override
    public void insertUser(User user) throws SQLException {
        userMapper.insertUser(user);
    }

    @Override
    public void action(User... users) throws SQLException {
        for(User user : users){
            int i = userMapper.insertUser(user);
            if(i > 0){
                log.info(user.getUsername()+"插入暂时成功");
            }
        }
    }
}

测试结果

目前表结构:
在这里插入图片描述
第一次插入:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
第二次插入:
为了模拟异常回滚,故意让ID = 3重复插入,报错。
在这里插入图片描述
如果事务生效,那么结果是全部不插入,如果事务失败,结果是ID=4和ID=5的记录插入,ID=3的插入失败
在这里插入图片描述
在这里插入图片描述

总结

Aop是一个非常好用的工具,可以帮我们简化代码的结构,AOP甚至可以切入在注解上,每个标记了注解的地方都可以切入,所以这也让我理解了为什么Shiro可以通过注解来实现权限鉴定,就是利用了AOP在接口调用之前先检测是否满足权限,AOP是一个强大且实用的工具,希望可以继续挖掘它的用处。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值