前言
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术语
切点:指需要增强的方法
横切关注点:指方法的四个调用时机,开始前、返回后、异常后、后置
通知:额外添加的方法
切面:切点+通知
四个横切关注点:
- 前置,方法开始时
- 后置,方法最终执行,无论是否异常,相当于finally块
- 异常,方法抛出异常时,相当于catch
- 返回,方法正常执行完成,相当于return之前
事务是如何实现的
在JAVA中,我们通过DataSource去getConnection,拿出来的连接都是autoCommit,也就是说每次通过Connection去操作数据库,自动帮我们开启了一个事务,并且每次都自动提交。
我们知道事务的原子性要求事务里的操作要么一次全部执行,要么一次全部不执行,如果按照初始连接的自动提交去执行一次Service类中的方法,有可能发生前面的操作成功,后面的操作失败,导致事务的原子性被破坏的情况。
为了防止破坏原子性的事情发生,我们需要保证事务只在我们需要的时候提交,也要在发生异常的时候去回滚,而不是每次都去提交,所以我们需要保证以下几点:
- 事务不能自动提交
- 所有的操作使用的连接都是同一个
- 在抛出异常的时候需要回滚
- 正常操作的时候要及时提交
我们不妨想象事务是这样的一个代码块:
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实现事务的思路其实很简单:
- 在方法开始调用的时候,获取连接,把连接的自动提交设置为false
- 在方法正常执行完毕的时候,AOP得帮我们提交事务
- 在方法出现异常的时候,AOP得帮我们回滚事务
- 不管是否出现异常,最后都需要把连接放回连接池,并把自动提交还原回true
具体实现思路
有了思路之后就很清晰了,根据对应的需求,转换成技术的实现:
- 在所有Service方法开始的时候,也就是做一个前置通知
- 在方法执行完毕的时候,做一个AfterReturning通知帮我们提交
- 在方法结束时,帮我们关闭连接,做一个After通知
- 在方法抛出异常时,做一个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是一个强大且实用的工具,希望可以继续挖掘它的用处。