一个案例教你理解Spring面向切面编程(Spring Aop)

本文深入探讨了Spring AOP(面向切面编程)的概念与应用,通过账户转账案例展示了如何使用AOP处理事务,避免业务代码臃肿。文章详细讲解了基于XML配置和注解的AOP实现,包括切点、通知类型、动态代理和环绕通知的使用。

Spring Aop 案例引入

Aspect oriented programming 面向切面编程

AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。

在这里插入图片描述

从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。

前期案例:账户转账

账户类、dao、service具体代码和前面类似

service类的转账方法

/**
     * 转账
     * @param account1 转出账户
     * @param account2 转入账户
     * @param money 转账金额
     * @throws SQLException
     */
public void transfer(Account account1,Account account2,double money) throws SQLException {
    account1.setMoney(account1.getMoney()-money);
    account2.setMoney(account2.getMoney()+money);
    update(account1);
    update(account2);

}

模拟转账异常

/**
     * 转账
     * @param account1 转出账户
     * @param account2 转入账户
     * @param money 转账金额
     * @throws SQLException
     */
public void transfer(Account account1,Account account2,double money) throws SQLException {
    account1.setMoney(account1.getMoney()-money);
    account2.setMoney(account2.getMoney()+money);
    update(account1);
    int i=1/0; //这里抛出异常,下面代码不再执行,足见职务的重要性
    update(account2);
}

存在问题

  1. 没有开启事务
  2. 就算开启事务,由于我们每次获取的数据库连接都不同,转账方法里面可能会用到好几个连接,每条语句执行完之后提交各自的事务,无法完成整体的转账事务

需要使用ThreadLocal对象把Connection和当前线程绑定,从而使一个线程中只有一个能控制事务的Connection对象

事务都应该在业务层

案例改进

  • 使用ThreadLocal对象绑定线程和Connection
  • 使用事务管理类管理事务

在这里插入图片描述

ConnectionUtils.java

@Component("connectionUtils")
public class ConnectionUtils {
    private ThreadLocal<Connection> threadLocal=new ThreadLocal<>();

    @Resource(name = "dataSource")
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 获取当前线程上的连接
     * @return
     * @throws SQLException
     */
    public Connection getThreadConnection() throws SQLException {
        Connection connection =threadLocal.get();

        if (connection==null){
            connection=dataSource.getConnection();
            threadLocal.set(connection);
        }
        return connection;

    }

    /**
     * 把当前线程和Connection解绑
     */
    public void remove(){
        threadLocal.remove();
    }
}

TransactionManager.java

/**
 * @ClassName TransactionManager 事务管理工具类,开启,提交,回滚,关闭连接
 */
@Component("transactionManager")
public class TransactionManager {

    @Resource(name = "connectionUtils")
    private ConnectionUtils connectionUtils;

    /**
     * 开启事务
     */
    public void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    public void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    public void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放连接
     */
    public void release(){
        try {
            connectionUtils.getThreadConnection().close();
            connectionUtils.remove();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

AccountDao.java

Dao类应注意:

不能再直接从DataSource中获得连接,即QueryRunner对象中不能注入数据源

而应该使用从ThreadLocal对象中获取的Connection

执行sql语句时,在参数里面加上Connection对象

runner.query(connectionUtils.getThreadConnection(),sql, new BeanListHandler<Account>(Account.class));

package com.console.dao;

import com.console.domain.Account;
import com.console.utils.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.springframework.stereotype.Repository;

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

/**
 * @author caozj
 * @version 1.0
 * @className AccountDao
 * @date 2020/7/29 15:10
 */
@Repository("accountDao")
public class AccountDao {
    @Resource(name = "queryRunner")
    private QueryRunner runner;


    @Resource(name = "connectionUtils")
    private ConnectionUtils connectionUtils;


    /**
     * 获取所有账户对象
     * @return
     * @throws Exception
     */
    public List<Account> allAccount() throws Exception {
        String sql = "select * from account";
        return runner.query(connectionUtils.getThreadConnection(),sql, new BeanListHandler<Account>(Account.class));

    }
	......
}

AccountService.java

在Service中使用事务管理,流程如下:

try{
    //开启事务
    transactionManager.beginTransaction();
    //执行操作
    accountDao.deleteById(id);
    //提交事务
    transactionManager.commit();

}catch (Exception e){
    e.printStackTrace();
    //回滚事务
    transactionManager.rollback();
}finally {
    //释放连接
    transactionManager.release();
}
/**
 * @className AccountService 账户服务类
 */
@Service("accountService")
public class AccountService {


    @Resource(name = "accountDao")
    private AccountDao accountDao;

    @Resource(name = "transactionManager")
    private TransactionManager transactionManager;


    /**
     * 转账
     * @param account1 转出账户
     * @param account2 转入账户
     * @param money 转账金额
     * @throws SQLException
     */
    public void transfer(Account account1,Account account2,double money) throws SQLException {


        try{
            //开启事务
            transactionManager.beginTransaction();
            //执行操作
            account1.setMoney(account1.getMoney()-money);
            account2.setMoney(account2.getMoney()+money);
            accountDao.update(account1);
            int i=1/0;
            accountDao.update(account2);
            //提交事务
            transactionManager.commit();

        }catch (Exception e){
            e.printStackTrace();
            //回滚事务
            transactionManager.rollback();
            throw  e;
        }finally {
            //释放连接
            transactionManager.release();
        }

    }
}

至此,使用事务改造过的案例已经完成。

开启事务之后案例中存在的问题

改进后的案例有了事务支持,但是每个方法都对事务进行开启提交或者回滚,整个Service类显得臃肿

改进:使用动态代理,将事务的支持在代理类中实现

基于接口的动态代理实现

被代理类必须实现特定接口

使用Proxy.newProxyInstance()创建代理对象

使用动态代理对案例改进

在这里插入图片描述

AccountServiceWithouttransaction类为使用没有添加事务之前的AccountService版本

BeanFactory.java

/**
 * @ClassName BeanFactory 创建Service代理对象的工厂
 */
@Component("beanFactory")
public class BeanFactory {

    @Resource(name = "accountService")
    private IAccountService accountService;

    @Resource(name = "transactionManager")
    TransactionManager transactionManager;

    /**
     * 获取accountService的代理对象
     * @return
     */
    public IAccountService getAccountService(){
        IAccountService obj=(IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 添加事务的支持
                     * @param o
                     * @param method
                     * @param objects
                     * @return
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                        Object returnValue=null;
                        try{
                            transactionManager.beginTransaction();
                            
                            //这里实际执行的是原Service类中的方法
                            returnValue=method.invoke(accountService,objects);
                            transactionManager.commit();
                            return  returnValue;
                        }catch (Exception e){
                            transactionManager.rollback();
                            throw  e;
                        }finally {
                            transactionManager.release();
                        }
                    }
                });
        return obj;
    }
}

使用动态代理类之后,以后使用AccountService的时候使用代理对象即可实现事务支持。

再次理解Aop概念

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

基于xml配置的Aop实现案例

案例介绍:在业务类的方法执行之前切入日志记录的操作

pom.xml中的依赖配置

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.3.RELEASE</version>
    </dependency>

    <!-- spring aop相关-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.7</version>
    </dependency>

    <!--使用Resources注解注入依赖-->
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.1</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.1.RELEASE</version>
    </dependency>
</dependencies>

IAccountService业务接口

/**
 * 这里一定要有接口,因为SprigAop使用基于接口的动态代理实现Aop
 */
public interface IAccountService {

    /**
     * 模拟更新
     */
    public void update();

    /**
     * 模拟获取用户数
     * @return
     */
    public int getCount();


    /**
     * 模拟使用id查找
     * @param i
     */
    public void queryId(int i);
}

AccountService模拟业务实现类

/**
 * @ClassName AccountService 模拟服务
 */

@Service("accountService")
public class AccountService implements IAccountService{
    @Override
    public void update(){
        System.out.println("update account");
    }

    @Override
    public int getCount(){
        System.out.println("getCount()");
        int i=(int)Math.floor(Math.random());
        return i;
    }

    @Override
    public void queryId(int i){
        System.out.println("query id "+i);
    }
}

Logger类,模拟动态代理中代理类拓展的业务

package com.console.utils;
/**
 * @ClassName logger 记录日志的工具类
 */
@Component("logger")
public class Logger {

    public void printLog(){
        System.out.println("Logger<printLogger>:打印日志 "+
                new Date().toString());
    }

    public void afterReturning(){
        System.out.println("afterReturning "+new Date().toString());
    }

    public void afterThrow(){
        System.out.println("afterThrow "+new Date().toString());
    }

    public void after(){
        System.out.println("after "+new Date().toString());
    }
}

配置文件Aop.xml

<?xml version="1.0" encoding="UTF-8"?>
<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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!--注解扫描-->
    <context:component-scan base-package="com.console"></context:component-scan>
    
    <!--Aop配置-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置切入点和通知类型
			method为切面执行的方法
			pointcut为切入点,即需要被代理的方法
			-->
            <aop:before method="printLog"
                        pointcut="execution(public void com.console.service.AccountService.update())"></aop:before>
        </aop:aspect>
    </aop:config>

</beans>

测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:springAop.xml")
public class AccountServiceTest {

    @Resource(name = "accountService")
    private IAccountService accountService;
	//这里注入的accountService对象一定要用接口类型来声明,不能用实现类来声明,负责会出错误
    
    @Before
    public void setUp() throws Exception {
    }

    @Test
    public void update() {
        accountService.update();
    }

    @Test
    public void getCount() {
        accountService.getCount();
    }

    @Test
    public void queryId() {
        accountService.queryId(10);
    }
}

XML配置Aop详解

<!--配置Aop,配置文件其他部分省略-->
<aop:config>
    <!--配置切面-->
    <aop:aspect id="logAdvice" ref="logger">
        <!--配置切入点和通知类型
       method为切面执行的方法
       pointcut为切入点,即需要被代理的方法
       -->
        <aop:before method="printLog"
                    pointcut="execution(public void com.console.service.AccountService.update())"></aop:before>
    </aop:aspect>
</aop:config>

pointcut可以使用通配符指定

  • 访问修饰符可以不用写
  • public void com.console.service.AccountService.update()void com.console.service.AccountService.update()相同
  • *表示所有
    • * com.console.service.AccountService.update()
    • * *.*.*.AccountService.update() 包名层级要写完
    • 使用*..表示当前包及其子包
    • * *..AccountService.update()
    • * *..*.update() 所有包下所有类的update()无参数方法
    • * *..*.*(int) 所有包下所有类的参数为int的方法
    • * *..*.*(..) 所有包下所有类的所有方法的所有参数列表的方法(包括无参数方法)

pointcut-ref

如果pointcut需要多次使用,可以使用 aop:pointcut 标签声明,再使用pointcut-ref引入

<aop:config>
    <aop:aspect>
   		<aop:pointcut id="accountServicePointcut" expression="execution(* com.console.service.AccountService.*(..))"/>
        <aop:after method="after" pointcut-ref="accountServicePointcut"></aop:after>
    </aop:aspect>
</aop:config>

注意:aop:pointcut 可以写在 aop:aspect标签内部,也可以写在外部,如果写在外部,必须写在aop:aspect标签前面

通知的类型

  • before 前置通知 :切入点之前执行
  • afterReturning 后置通知:返回结果后执行
  • afterThrowing 异常通知:抛出异常通知
  • after 最终通知: 所有完成后通知,类似于try-catch-finally语句块中的代码
<aop:pointcut id="accountServicePointcut" expression="execution(* com.console.service.AccountService.*(..))"/>

<aop:aspect id="logAdvice" ref="logger">
    <aop:before method="printLog"
                pointcut-ref="accountServicePointcut"></aop:before>
    <aop:after-returning method="afterReturning"
                         pointcut-ref="accountServicePointcut"></aop:after-returning>

    <aop:after-throwing method="afterThrowing" pointcut-ref="accountServicePointcut"></aop:after-throwing>
    <aop:after method="after" pointcut-ref="accountServicePointcut"></aop:after>
</aop:aspect>

环绕通知

环绕通知是一种方式,和前面四种通知类型不同,环绕通知可以同时配置所有四种类型的通知

<aop:pointcut id="accountServicePointcut" expression="execution(* com.console.service.AccountService.*(..))"/>
<aop:aspect id="logAdvice" ref="logger">
    <!--配置环绕通知-->
    <aop:around method="around" pointcut-ref="accountServicePointcut"></aop:around>
</aop:aspect>

切面类Logger.java 中对应的环绕方法,和动态代理中第三个参数类似

public  Object around(ProceedingJoinPoint proceedingJoinPoint){
    Object rtValue=null;
    try {
        System.out.println("前置通知执行 "+new Date());
        
        //下面这句显式调用切入点的业务方法
        rtValue=proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
        System.out.println("后置通知执行 "+new Date());
        return rtValue;

    } catch (Throwable throwable) {
        throwable.printStackTrace();
        System.out.println("异常通知执行 "+new Date());
        throw new RuntimeException(throwable);
    }finally {
        System.out.println("最终通知执行 "+new Date());
    }

}

基于注解的Aop配置

Logger.java

package com.console.utils;
/**
 * @ClassName logger 记录日志的工具类
 */
@Component("logger")
@Aspect //当前类是切面类
public class Logger {

    //定义切入点
    @Pointcut( "execution(* com.console.service.AccountService.*(..))")
    private void getPointcut(){

    }

    @Before("getPointcut()")
    public void printLog(){
        System.out.println("Logger<printLogger>:打印日志 "+
                new Date().toString());
    }

    @AfterReturning("getPointcut()")
    public void afterReturning(){
        System.out.println("afterReturning "+new Date().toString());
    }

    @AfterThrowing("getPointcut()")
    public void afterThrowing(){
        System.out.println("afterThrow "+new Date().toString());
    }

    @After("getPointcut()")
    public void after(){
        System.out.println("after "+new Date().toString());
    }

//    @Around("getPointcut()")
    public  Object around(ProceedingJoinPoint proceedingJoinPoint){
        Object rtValue=null;
        try {
            System.out.println("前置通知执行 "+new Date());
            rtValue=proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
            System.out.println("后置通知执行 "+new Date());
            return rtValue;

        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("异常通知执行 "+new Date());
            throw new RuntimeException(throwable);
        }finally {
            System.out.println("最终通知执行 "+new Date());
        }

    }

}

Spring配置文件

<?xml version="1.0" encoding="UTF-8"?>
<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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.console"></context:component-scan>


<!--    开启切面自动代理-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

测试类

package com.console.service;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:springAopAnnotation.xml")
public class AccountServiceTest {

    @Resource(name = "accountService")
    private IAccountService accountService;

    @Before
    public void setUp() throws Exception {
    }

    @Test
    public void update() {
        accountService.update();
    }

    @Test
    public void getCount() {
        accountService.getCount();
    }

    @Test
    public void queryId() {
        accountService.queryId(10);
    }
}

注意

  • 使用注解的切面配置的时候,Spring调用顺序和配置文件配置的Aop有出入

    • 先调用after,即最终通知,再调用异常或者后置通知
  • 使用环绕通知可以解决这个问题

  • 不适用xml的注解

    配置文件中的<aop:aspectj-autoproxy></aop:aspectj-autoproxy>可以使用@EnableAspectJAutoProxy注解代替

@Aspect //当前类是切面类
@EnableAspectJAutoProxy
public class Logger {}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_console_

您的关注与鼓励是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值