04.Spring框架的AOP(案例:在多例模式下基于Aspectj AOP的事务管理)

AOP的基本原理和概念请看JAVA–AOP

AOP的相关术语

  • AOP相关的术语
    1. Joinpoint(连接点):
        所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点(我的理解是目标类的所有方法)
    2. Pointcut(切入点):
        所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
    3. Advice(通知/增强):
        所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知(动态代理中的功能增强)
        通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
    4. Introduction(引介):
        引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方 法或 Field。
    5. Target(目标对象):
        代理的目标对象
    6. Weaving(织入):
        是指把增强应用到目标对象来创建新的代理对象的过程(增强后的对象)
        spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入
    7. Proxy(代理):
        一个类被 AOP 织入增强后,就产生一个结果代理类
    8. Aspect(切面):
        是切入点和通知(引介)的结合

Spring 中的 AOP 要明确的事

  • 开发阶段:
      开发人员只需把业务(服务)层中重复的代码抽离出来制成通知类,用xml配置文件或者注解的方式声明切入点和通知间的关系(就是被拦截方法要执行的通知方法),即切面即可。
  • 运行阶段:
      Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对 象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行
  • 代理模式的选择:
      在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式

基于Aspectj 通过XML的AOP配置事务

普通通知(和环绕通知互斥使用)

  • 具体案例(通过注解的方式配置Spring实现银行的curd操作),省略了持久层的接口、服务层的接口、全局配置类、数据库配置类、maven大部分依赖(spring-context、spring-test、c3p0、junit、dbutils、mysql、lombok)
  • 添加切点表达式依赖
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>
  • 创建银行账户实体类Account
@Data
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Float money;
}
  • 创建持久层接口实现类AccountDaoImpl
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Autowired
    private QueryRunner runner;
    @Autowired
    private ConnectionUtils connectionUtils;
    
    @Override
    public List<Account> findAllAccount() {
        try {
            return runner.query(connectionUtils.getThreadConnection(),"select * from account",new BeanListHandler<Account>(Account.class));
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    @Override
    public Account findAccountById(Integer accountId) {
        try {
            return runner.query(connectionUtils.getThreadConnection(),"select * from account where id = ?",new BeanHandler<Account>(Account.class),accountId);
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    @Override
    public void saveAccount(Account account) {
        try{
            runner.update(connectionUtils.getThreadConnection(),"insert into account(name,money)values(?,?)",account.getName(),account.getMoney());
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void updateAccount(Account account) {
        try{
            runner.update(connectionUtils.getThreadConnection(),"update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void deleteAccount(Integer accountId) {
        try{
            runner.update(connectionUtils.getThreadConnection(),"delete from account where id=?",accountId);
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Account findAccountByName(String accountName) {
        try{
            List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where name = ? ",new BeanListHandler<Account>(Account.class),accountName);
            if(accounts == null || accounts.size() == 0){
                return null;
            }
            if(accounts.size() > 1){
                throw new RuntimeException("结果集不唯一,数据有问题");
            }
            return accounts.get(0);
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
  • 创建服务层接口实现类AccountServiceImpl(位置有限只写了2个方法)
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Autowired
    private IAccountDao accountDao;
    @Autowired
    private TransactionManager transactionManager;

    @Override
    public void saveAccount(Account account) {
        try {
            // 开启事务
            txManager.beginTransaction();
            // 执行操作
            accountDao.saveAccount(account);
            // 提交事务
            txManager.commit();
        }catch (Exception e){
            // 回滚操作
            txManager.rollback();
        }finally {
            // 释放连接
            txManager.release();
        }
    }

    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        try {
            // 开启事务
            transactionManager.beginTransaction();
            // 执行操作,根据名称查询转出账户
            Account source = accountDao.findAccountByName(sourceName);
            // 根据名称查询转入账户
            Account target = accountDao.findAccountByName(targetName);
            // 转出账户减钱
            source.setMoney(source.getMoney() - money);
            // 转入账户加钱
            target.setMoney(target.getMoney() + money);
            // 更新转出账户
            accountDao.updateAccount(source);
			// 假设转账异常
            //int i=1/0;
            // 更新转入账户
            accountDao.updateAccount(target);
            // 提交事务
            transactionManager.commit();
        } catch (Exception e) {
            // 回滚操作
            transactionManager.rollback();
            e.printStackTrace();
        } finally {
            // 释放连接
            transactionManager.release();
        }
    }
}
  • 创建连接工具类ConnectionUtils,因为在多例模式下,所有通过连接和线程绑定来控制事务(控制转账操作为一条连接)
@Component
public class ConnectionUtils {

    private ThreadLocal<Connection> t = new ThreadLocal<Connection>();
    @Autowired
    private DataSource dataSource;
    public Connection getThreadConnection(){
        try{
            //从ThreadLocal上获取连接
            Connection con = t.get();
            // 判断是否有连接
            if (con == null){
                // 从数据源中获取一个连接,并且存入ThreadLocal中
                con = dataSource.getConnection();
                t.set(con);
            }
            return con;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    // 把连接和线程解绑
    public void removeConnection(){
        t.remove();
    }
}
  • 创建事务管理类TransactionManager
@Component
public class TransactionManager {
    @Autowired
    private ConnectionUtils connectionUtils;
    // 开启事务
    public  void beginTransaction(){
        try {
            System.out.println("前置");
            connectionUtils.getThreadConnection().setAutoCommit(false);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 提交事务
    public  void commit(){
        try {
            System.out.println("后置");
            connectionUtils.getThreadConnection().commit();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 回滚事务
    public  void rollback(){
        try {
            System.out.println("异常");
            connectionUtils.getThreadConnection().rollback();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 释放连接
    public  void release(){
        try {
            System.out.println("最终");
            connectionUtils.getThreadConnection().close();//还回连接池中
            connectionUtils.removeConnection();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 配置xml文件
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
        <!--配置aop-->
	    <aop:config>
	        <!--配置切面-->
	        <aop:aspect id="manage" ref="transactionManager">
	            <aop:pointcut id="p" expression="execution(* com.zsc.service.impl.*.*(..))"/>
	            <!--前置通知,在切入点方法执行前执行-->
	            <aop:before method="beginTransaction" pointcut-ref="p"/>
	            <!--后置通知,在切入点方法执行后执行-->
	            <aop:after-returning method="commit" pointcut-ref="p"/>
	            <!--异常通知,在切入点方法执行异常时执行-->
	            <aop:after-throwing method="rollback" pointcut-ref="p"/>
	            <!--最终通知,不管方法执行是否异常都会执行-->
	            <aop:after method="release" pointcut-ref="p"/>
	        </aop:aspect>
	    </aop:config>
</beans>
  • XML配置标签解释
 spring基于xml的AOP配置步骤(把通知bean交给spring管理):
            1.使用aop:config标签表明开始AOP的配置
            2.使用aop:aspect标签表明配置切面
                属性:
                    id:是切面的唯一标识符,可以随便取,只有唯一就行
                    ref:指定通知(功能增强)类bean的id
            3.在aop:aspect标签内部使用对应标签来配置通知的的类型(简单来讲就是执行方法的时间)
                1).aop:before标签表示前置通知,在切入点方法(要功能增强的方法)执行之前执行
                2).aop:after-returning标签表示后置通知,在切入点方法(要功能增强的方法)执行之后执行
                3).aop:after-throwing标签表示异常通知,在方法执行出现异常时执行,和上面的后置通知互斥
                4).aop:after标签表示最终通知,不管方法执行是否异常都会执行
                ======================================分割线=========================================
                5).aop:around标签表示环绕通知,需要在通知类中配置通知循序
             上面标签都有的属性:
                method:用于指定通知类的方法(每个通知要执行的方法)
                pointcut:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
                pointcut-ref:指定aop:pointcut标签里的切入点表达式
  • 切入点表达式解释
                切入点表达式写法:
                    关键字:execution
                    表达式:
                        访问修饰符 返回值类型包名.包名(包名有多少层写多少层).类名.方法名(参数列表)
                    标准表达式:
                        public void com.zsc.service.impl.AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
                    访问修饰符可以省略:
                        void com.zsc.service.impl.AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
                    返回值类型可以使用通配符,表示任意返回值类型:
                        * com.zsc.service.impl.AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
                    包名可以使用通配符,有几层写几个*:
                        * *.*.*.*.AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
                    包名也可以使用..表示当前包及子包:
                        * *..AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
                    类名、方法名都可以使用通配符表示:
                        * *..*.*(String sourceName, String targetName, Float money)
                    参数列表:
                        可以直接写参数类型名称(比如int)
                        引用类型写包名.类名的方式(比如java.lang.String)
                        可以使用通配符表示任意类型,但是必须有参数
                        可以使用..表示有无参数均可,有参数可以是任意类型
                        全通配写法:
                            * *..*.*(..)
                    实际开发中通常写法:
                        通配符返回值类型 包名.包名.包名.包名.通配符类名.通配符方法名(..可有可无参数列表)
                        * com.zsc.service.impl.*.*(..)

环绕通知(和上面的普通通知互斥,但标签属性一样)

  • XML配置
	<aop:config>
        <!--配置切面-->
        <aop:aspect id="manage" ref="transactionManager">
            <aop:pointcut id="p" expression="execution(* com.zsc.service.impl.*.*(..))"/>
            <aop:around method="aroundPrint" pointcut-ref="p"/>
        </aop:aspect>
    </aop:config>
  • 在事务管理类TransactionManager添加环绕方法aroundPrint
public Object aroundPrint(ProceedingJoinPoint point){
        try {
            Object[] args = point.getArgs();
            beginTransaction();
            Object revalue = point.proceed(args);
            commit();
            return revalue;
        }catch (Throwable t){
            rollback();
            throw new RuntimeException(t);
        }finally {
            release();
        }
    }
  • 基于XML环绕通知解释
    1. 环绕通知和上面的通知要互斥使用
    2. 如果配置了xml没有写通知方法,切入点方法是不会运行的,而通知会运行
    3. 对比动态代理,因为只配置xml没有明确的切入点方法调用,所以切入点方法没有执行
    4. ProceedingJoinPoint接口有proceed()方法,此方法就相当于调用明确的切入点方法
    5. 若用注解的方式配置环绕通知,那么其他通知方法不要用注解,不然会执行2次

基于Aspectj 通过注解的AOP配置事务

普通通知

  • 在XML配置文件中用< aop:aspectj-autoproxy />标签开启基于注解的AOP配置或在配置类中用@EnableAspectJAutoProxy注解开启基于注解的AOP配置
  • 改造事务管理类TransactionManager
@Component
// 声明为切面类
@Aspect
public class TransactionManager {
    @Autowired
    private ConnectionUtils connectionUtils;
	// 切入点表达式
    @Pointcut("execution(* com.zsc.service.impl.*.transferByAOP(..))")
    private void p() {}
    // 开启事务
    @Before("p()")
    public  void beginTransaction(){
        try {
            System.out.println("前置");
            connectionUtils.getThreadConnection().setAutoCommit(false);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 提交事务
    @AfterReturning("p()")
    public  void commit(){
        try {
            System.out.println("后置");
            connectionUtils.getThreadConnection().commit();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

   // 回滚事务
    @AfterThrowing("p()")
    public  void rollback(){
        try {
            System.out.println("异常");
            connectionUtils.getThreadConnection().rollback();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 释放连接
    @After("p()")
    public  void release(){
        try {
            System.out.println("最终");
            connectionUtils.getThreadConnection().close();//还回连接池中
            connectionUtils.removeConnection();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 基于注解的普通通知说明
       在使用spring框架版本为5.0.2.RELEASE时基于注解的通知的顺序出错,但在使用5.2.9.RELEASE版本是正确,暂时还没知道哪个版本修复了这个bug

环绕通知

  • 改造事务管理类TransactionManager
@Component
// 声明为切面类
@Aspect
public class TransactionManager {
    @Autowired
    private ConnectionUtils connectionUtils;
	// 切入点表达式
    @Pointcut("execution(* com.zsc.service.impl.*.transferByAOP(..))")
    private void p() {}
    // 开启事务
    public  void beginTransaction(){
        try {
            System.out.println("前置");
            connectionUtils.getThreadConnection().setAutoCommit(false);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 提交事务
    public  void commit(){
        try {
            System.out.println("后置");
            connectionUtils.getThreadConnection().commit();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

   // 回滚事务
    public  void rollback(){
        try {
            System.out.println("异常");
            connectionUtils.getThreadConnection().rollback();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 释放连接
    public  void release(){
        try {
            System.out.println("最终");
            connectionUtils.getThreadConnection().close();//还回连接池中
            connectionUtils.removeConnection();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @Around("p()")
    public Object aroundPrint(ProceedingJoinPoint point){
        try {
            Object[] args = point.getArgs();
            beginTransaction();
            Object revalue = point.proceed(args);
            commit();
            return revalue;
        }catch (Throwable t){
            rollback();
            throw new RuntimeException(t);
        }finally {
            release();
        }
    }
}
  • 基于注解的环绕通知说明
      在使用环绕通知时,普通通知的方法的通知注解应该删掉或注释,否则通知会执行2次

我对基于Spring框架的AOP理解

  • Spring会帮我们管理通知,无需自己编写动态代理类,开发者只需通过注解或配置XML文件的方式,去规定通知和切入点方法的关系即可
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值