• 2

  • 479

  • 收藏

Spring 核心概念——AOP 理解及运用

3星期前

什么是 AOP

AOP (Aspect Orient Programming) 是面向切面编程,它是一种编程思想,是面向对象编程(OOP)的一种补充。

面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。也即在 OOP 中模块化的单元是类,而在 AOP 中模块化的单元则是切面。

面向切面示意图

AOP 框架是 Spring 的一个重要组成部分。但是 Spring IoC 容器并不依赖于 AOP,这意味着你有权利选择是否使用 AOP,AOP 做为 Spring IoC 容器的一个补充,使它成为一个强大的中间件解决方案。


AOP 的作用——横向切割(抽取)

上面也说了,AOP 是对 OOP 的一种补充。

什么时候会出现 AOP 的需求?

按照软件重构的思想,如果多个类中出现重复的代码,就应该考虑定义一个共同的抽象类,将这些共同的代码提取到抽象类中,比如 Teacher,Student 都有 username,那么就可以把 username 及相关的 get、set 方法抽取到 SysUser 中,这种情况,我们称为纵向抽取。 但是如果想给所有的类方法添加性能检测、事务控制,该怎么抽取?现在,使用 OOP 显然已经满足不了需求,而 AOP 则就是希望将这些分散在各个业务逻辑代码中的相同代码,通过横向切割的方式抽取到一个独立的模块中,让业务逻辑类依然保存最初的单纯,以使开发人员只专注于业务流程的编写。

我们以数据库的操作为例来说明,一般在项目中执行一条 SQL 命令需要如下步骤:

  1. 获取连接对象
  2. 执行 SQL (核心业务代码)
  3. 如果有异常,回滚事务,无异常则提交事务
  4. 关闭连接

上述步骤中第 2 步才是核心业务代码,其他都是非核心业务代码,但是我们又必须得写。面向切面编程就是为了解决这样的问题,它将这些非核心业务代码进行抽离,使开发者只需要关注核心业务代码即可。其流程图如下:

AOP约定SQL流程

如下是 Spring + Mybatis 项目中一段代码,其功能是购买某商品,更新商品数量并保存订单,如果有异常则事务回滚。

public void savePurchaseRecord(Long productId, PurchaseRecord record) {
    SqlSession sqlSession = null;
    try{
        sqlSession = SqlSessionUtil.openSqlSession();
        ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
        Product product = productMapper.getProduct(productId); 
		//判断库存是否大于购买数量
		if(product.getStock() >= record.getQuantity()) {
			//减库存,并更新数据库记录 
			product.setStock(product.getStock() - productMapper.update(product) ; 
			// 保存交易记录
			purchaseRecordMapper.save(record); 
		}
    } catch(Exception e) {
        e.printStackTrace();
        sqlSession.rollback();
    } finally {
        if (sqlSession != null)
            sqlSession.close();
    }
}
复制代码

由上述代码可以看出,我们主要关注的流程在 try 代码块里边,而为了执行相关的 SQL,不得不写 try…catch…finally 语句,这样看起来整块代码都很臃肿,所以如果我们使用切面把这些非核心业务流程的代码给抽取出来,形成独立模块,那么我们就不必再去关注并且不用再编写这么冗杂的代码了。

我们来看一下使用 Spring 中 AOP 的支持,我们可以对上述代码进行如下的改写:

@Autowired
private ProductMapper productMapper = null; 
@Autowired 
private PurchaseRecordMapper purchaseRecordMapper = null;
@Transactional 
public void savePurchaseRecord(Long productid, PurchaseRecord record) {
	Product product = productMapper.getProduct(productId); 
	//判断库存是否大于购买数量
	if(product.getStock() >= record.getQuantity()) {
		//减库存,并更新数据库记录 
		product.setStock(product.getStock() - productMapper.update(product) ; 
		// 保存交易记录
		purchaseRecordMapper.save(record); 
	}
}
复制代码

怎么样?是不是舒服多了,这段代码除了一个实现 AOP 功能注解 @Transactional , 没有任何关于打开或者关闭数据库资源的代码,更没有任何提交或者回滚数据库事务的代码, 但是它却能够完成全部功能。这段代码更简洁, 也更容易维护,主要都集中在业务处理上,而不是数据库事务和资源管控上,这就是 AOP 的魅力。


AOP 原理

正如上面代码所示,为什么我们加了注解 @Transactional 后,就可以实现数据库的连接、事务回滚等操作呢?

这里我们就要先引入一个概念:约定优于配置原则。对于 @Transactional 的具体实现细节咱们先不必关注,我们先看下它能够为我们提供什么功能吧(即约定):

  • 当方法标注为 @Transactional 时,则方法启用数据库事务功能。
  • 在默认的情况下(注意是默认情况下,可以通过配置改变),如果原有方法出现异常,则回滚事务;如果没有发生异常,那么就提交事务,这样整个事务管理 AOP 就完成了整个流程,无须开发者编写任何代码去实现。
  • 最后关闭数据库资源。

我们知道了上述的约定后,我们就可以直接拿来使用它了。但如果要深究 AOP 原理,我们其实也很好理解,首先还是从其功能开始,AOP 所实现的就是在执行某些类中的一些方法时,要对这些方法执行前后进行一定的操作,这不就是拦截器的功能嘛。

★★★ AOP 就是通过动态代理方式,给真实对象绑上一个拦截器,带来管控各个对象操作的切面环境,管理包括日志、数据库事务等操作,让我们拥有可以在反射原有对象方法之前正常返回、异常返回事后插入自己的逻辑代码的能力,有时候甚至取代原始方法。

对拦截器及原理不太熟悉的小伙伴可以看我以前的两篇文章:


AOP 相关术语

切面 (Aspect)

一些共同操作的抽取,或者说是一个关注点的模块化,这个关注点可能会横切多个对象。

比如上述代码中数据库的事务直接贯穿了整个代码层面,这就是一个切面,它能够在被代理对象的方法执行之前、之后、 产生异常或者正常返回后切入你的代码,甚至代替原来被代理对象的方法,在动态代理中可以把它理解成一个拦截器。

连接点(Join Point)

在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在 Spring AOP 中,一个连接点就表示一个方法的执行。用大白话说就是在哪个方法执行的时候需要开始拦截,以执行自己定义的增强逻辑,这个方法就是一个连接点。

切点(Pointcut)

如果说连接点是具体到某个方法执行,那么切点就可以理解为某类方法的集合,它定义的是一个范围。因为并不是所有的开发都需要启动 AOP,所以通常使用正则表达式进行限定,来指定某类方法执行时启动 AOP 功能。

通知(Advice)

通知是切面开启后,切面中的方法。它根据在代理对象真实方法调用前、后的顺序和逻辑区分。

  • 前置通知(before):在动态代理反射原有对象方法或者执行环绕通知前执行的通知功能。
  • 后置通知(after):在动态代理反射原有对象方法或者执行环绕通知后执行的通知功能。无论是否抛出异常,它都会被执行。
  • 返回通知(afterReturning):在动态代理反射原有对象方法或者执行环绕通知后执行的通知功能。
  • 异常通知(afterThrowing ): 在动态代理反射原有对象方法或者执行环绕通知产生异常后执行的通知功能。
  • 环绕通知(aroundThrowing):在动态代理中,它可以取代当前被拦截对象的方法,提供回调原有被拦截对象的方法。

织入(Weaving)

织入其实是一个过程:当程序执行到连接点时,就会生成代理对象并将切面内容放入到流程当中。

AOP流程图如下:

AOP流程图

Spring AOP 的使用

为了更好的理解上述的概念,我们还是使用一个简单的实例来演示一下 AOP 的使用与执行过程。

我们选择使用 Spring 框架,它只支持方法拦截,并且我们使用基于注解的方式进行配置开发。这个简单测试项目的功能是,对打印用户信息的方法作为 AOP 的连接点,从而实现在打印用户信息前后执行相关操作的功能。

选择连接点

正如上文所述,连接点就是要拦截哪个方法并织入对应的 AOP 通知。我们创建如下一个接口,声明打印用户信息的方法:

public interface RoleService {
    void printRole(Role role);
}
复制代码

然后我们提供一个实现类:

package com.codergeshu.aop.service.impl; // 包的位置
@Component
public class RoleServiceImpl implements RoleService {
    @Override
    public void printRole(Role role) {
        System.out.println("{id: " + role.getId() + ", "
                + "role_name : " + role.getRoleName() + ", "
                + "note : " + role.getNote() + "}");
    }
}
复制代码

这个类没什么特别的,只是这个时候如果把 printRole 作为 AOP 的连接点, 那么动态代理就会为类 RoleServicelmpl 生成代理对象, 然后拦截 printRole 方法,于是可以产生各种 AOP 通知方法。

创建切面

选择好了连接点就可以创建切面了,至于切点、通知之类的都是在切面里进行定义的。我们使用 Spring 框架中的 @Aspect 注解标注定义一个切面类,如下:

@Aspect
public class AnnotationAspect {
    // 定义切点
    // 定义通知
}
复制代码

定义切点

创建好切面类后,我们就需要定义切点。上文提过,切点可以看作是连接点的一个集合,它定义的是一个范围,且一般使用正则表达式进行定义。例如我们如果定义上述的 printRole 方法作为切点,我们可以这样编写:

@Pointcut("execution(* com.codergeshu.aop.service.impl.RoleServiceImpl.printRole(..)))");
复制代码

依次对这个表达式做出分析。

  • @Pointcut :表示定义一个切点,它会修饰在某方法上。
  • execution :代表执行方法的时候会触发。
  • :代表任意返回类型的方法。
  • com.codergeshu.aop.service.impl.RoleServiceImpl :代表类的全限定名。
  • printRole :被拦截方法名称。
  • (..) :被拦截的方法可以有任意的参数。

通过上述切点的定义,它就会按照 AOP 通知的规则把 printRole 方法方法织入到流程中了。

@Aspect
public class AnnotationAspect {
    // 定义切点
    @Pointcut("execution(* com.codergeshu.aop.service.impl.RoleServiceImpl.printRole(..)))")
    public void print() {
    }
    // 定义通知
}
复制代码

创建增强通知

通知类型我们在上文也已介绍,现在我们直接来看它在切面类中的定义吧。

@Aspect
public class AnnotationAspect {
    // 定义切点
    @Pointcut("execution(* com.codergeshu.aop.service.impl.RoleServiceImpl.printRole(..)))")
    public void print() {
    }
    // 定义通知
    @Before("print()")
    public void before() { System.out.println("before ...."); }
    
    @After("print()")
    public void after() { System.out.println("after ...."); }

    @AfterReturning("print()")
    public void afterReturning() { System.out.println("afterReturning ...."); }

    @AfterThrowing("print()")
    public void afterThrowing() { System.out.println("afterThrowing ...."); }

    // 设置环绕通知,ProceedingJoinPoint 由 Spring 框架提供
    @Around("print()")
    public Object around(ProceedingJoinPoint jp) {
        System.out.println("around before ....");
        try {
            jp.proceed();  // 调用原有方法,即连接点处的方法
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("around after ....");
        return null;
    }
}
复制代码

在代码中的环绕通知中我们可以看到有一个参数 ProceedingJoinPoint,它是框架自带的一个类,使用它可以反射连接点方法。

配置启用 Spring 框架 AOP

上面的切面已经定义完成,但是如果想要在 Spring 项目中使用 AOP,还需要配置启用。与其他的配置一样,有两种方式:基于注解或基于 XML 文件。这里使用基于注解的方式:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.codergeshu.aop")
public class AopConfig {
	@Bean
    public AnnotationAspect getRoleAspect() {
        return new AnnotationAspect();
    }
}
复制代码

@EnableAspectJAutoProxy 表示启用 AspectJ 框架的自动代理。并且我们在这个配置类中也把我们的切面类 AnnotationAspect 定义到了 IOC 容器中管理。

当然如果使用 XML 文件进行 AOP 的启用配置也很简单,只需要在 XML 文件中增加 AOP 的命名空间即可:<aop:aspectj-autoproxy/>

测试及结果

为了对上述的 AOP 流程进行测试,我们编写如下测试类:

@Test
public  void testAnnotation() {
	ApplicationContext ioc = new AnnotationConfigApplicationContext(AopConfig.class);
	Role role = new Role();
	role.setId(1L);
	role.setRoleName("CoderGeshu");
	role.setNote("I am CoderGeshu");
	// 获得动态代理对象,下挂在 RoleService 接口中
	RoleService roleService = ioc.getBean(RoleService.class);
	roleService.printRole(role);
}
复制代码

测试结果:

before ....
around before ....
{id: 1, role_name : CoderGeshu, note : I am CoderGeshu}
around after ....
after ....
afterReturning ....
复制代码

由测试结果并结合上述各个通知的介绍,可以更加深刻理解 AOP 通知的作用。


总结

本文从什么是 AOP 开始,用简单易懂的语言描述了 AOP 的作用,然后继而介绍了其运行原理与相关的术语概念,最后还使用一个小 demo 演示了在 Spring 框架中使用 AOP 的流程。

本文比较适合刚刚入门学习的同学,我也是在初步的学习之中,如有不正确之处,望大家不吝指正。

巨人的肩膀

《Java EE 互联网轻量级框架整合开发》 杨开振 等著

blog.csdn.net/qq_41981107…


作者信息

大家好,我是 CoderGeshu,一个热爱生活的程序员,如果这篇文章对您有所帮助,还请大家给点个赞哦 👍👍👍

另外,欢迎大家关注本人同名公众号:CoderGeshu,一个致力于分享编程技术知识的公众号!!

一个人可以走的很快,而一群人可以走的很远……

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

物联网

479

相关文章推荐

未登录头像

暂无评论