Spring 05:Spring AOP

面向切面的编程(AOP)是一种编程思想,旨在将跨越多个类型的关注点进行模块化,来实现对业务中共同关注点功能的解耦。OOP中模块化的关键单位是类,而AOP中模块化的单位是切面。

一、核心概念

在学习Spring AOP使用方法之前,我们需要先了解AOP(面向切面编程)的一些关键概念。

AOP概念

  1. 切面(Aspect)。一个跨越多个类的关注点的模块化。在Spring AOP中,切面是通过使用常规类(基于 schema 的方法)或使用 @Aspect 注解的常规类(@AspectJ 风格)实现的。常见的切面,例如:事务管理、日志记录、权限校验等。

  2. 连接点(Join Point)。程序执行过程中的一个点,例如一个方法的执行或一个异常的处理。在Spring AOP中,一个连接点总是代表一个方法的执行。

  3. 通知(Advice)。一个切面在一个特定的连接点采取的行动。不同类型的advice包括 “around”、“before” 和 “after” 的advice(Advice 类型将在后面讨论)。许多AOP框架,包括Spring,都将advice建模为一个拦截器,并在连接点(Join point)周围维护一个拦截器链。

  4. 切点(PointCut)。一个匹配连接点的谓词(predicate)。advice与一个切点表达式相关联,并在切点匹配的任何连接点上运行(例如,执行一个具有特定名称的方法)。由切点表达式匹配的连接点概念是AOP的核心,Spring默认使用AspectJ的切点表达式语言。

  5. 引入(Introduction)。代表一个类型声明额外的方法或字段。Spring AOP允许你为任何 advice 的对象引入新的接口(以及相应的实现)。例如,你可以使用引入来使一个bean实现 IsModified 接口,以简化缓存。(介绍在AspectJ社区中被称为类型间声明)。

  6. 目标对象(Target Object)。被一个或多个切面所 advice 的对象。也被称为 “advised object”。由于Spring AOP是通过使用运行时代理来实现的,这个对象总是一个被代理的对象。

  7. AOP代理(AOP Proxy)。一个由AOP框架创建的对象,以实现切面契约(advice 方法执行等)。在Spring框架中,AOP代理是一个JDK动态代理或CGLIB代理。

  8. 织入(Weaving)。将aspect与其他应用程序类型或对象连接起来,以创建一个 advice 对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。Spring AOP和其他纯Java AOP框架一样,在运行时进行织入。

二、Spring中@AspectJ配置流程

1. 启用@AspectJ

AspectJ是一个非常强大的AOP框架。Spring AOP参照前者的风格,引入了很多AspectJ中的概念与注解,提供了弱化一些的AOP支持。在Spring中,我们既可以使用基于XML配置的Schema模式,也可以使用AspectJ风格的注解化的@AspectJ模式来配置。本文不讨论基于XML的配置方式。

要想在Spring中启用AOP,我们需要在配置类中引入@EnableAspectJAutoProxy注解。

@Configuration  
@ComponentScan("xxx")  
@EnableAspectJAutoProxy  
public class AppConfig {  
  // ...
}

Spring AOP 工作原理简介

Spring AOP是基于代理实现的。其首先扫描我们的声明的切面类,以及切面类中定义的切点。在创建一个Bean前,其首先判断该类中的成员方法是否满足切点的定义。

  • 如果符合,则生成一个该类的动态代理,以对切点方法进行增强。此后该Bean切点方法的调用,将都会执行代理对象的增强版本,也就是引入对该切点的通知。例如,给某些类的方法都加上调用日志。

  • 如果不符合,则正常创建该Bean。调用也是正常调用。

Spring AOP默认使用Java动态代理,但是如果一个业务对象没有实现一个接口,此时就会使用CGLIB生成增强后的代理对象。

2. 声明切面类

Spring AOP中,切面类本身是一个Bean,此外需要@Aspect将其声明为一个切面类。例如,我们将一个应用的日志模块作为一个切面抽象出来,那么我们就可以定义下面这样一个切面类:

@Component
@Aspect
class LogAspect {
	@Autowired
	private Logger logger; // 自动注入一个日志记录器
}

随后,我们需要在这个切面类中定义切点,以及对应的通知方法。

3. 声明切点

切点是一个使用@PointCut注解的空函数体方法。在注解的参数列表中,我们需要给出一个AspectJ风格的切点表达式,来阐述这个切点的匹配谓词。说人话就是,什么样的方法调用符合该切点的要求。

切点表达式

切点表达式的标准语法是:

切入点指定器 (访问控制说明符 返回类型 包名 类名 (参数) 抛出异常)
  • 切点指定器PCD:因为AspectJ的AOP功能十分强大,其对连接点的定义不止局限在方法执行这个概念上,因此其切点表达式提供了除此之外的其他一些切点类型说明标识,称之为PCD。
    • execution: 用于匹配方法执行的连接点。这是在使用Spring AOP时要使用的主要切点指定器。
  • 访问控制说明符抛出异常可以省略

比如说我们要精确的将space.mora.service包下任何以Service作为类名后缀的save方法作为切点:

@Component
@Aspect
class LogAspect {
	@Autowired
	private Logger logger; // 自动注入一个日志记录器

	@PointCut("excution(* * space.mora.service.*Service(..))")
	public void logPt(){}; // 务必是空函数体
}
切点表达式的通配符

在上面的例子中,我们观察到切点表达式可以使用通配符实现连接点的一对多的快捷匹配。其具体语法如下:

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现。
excution ( public * space.mora.*.UserService.find* (*) )

语义:匹配space.mora包下的任意包中的UserService类或接口中,所有find开头的带有一个参数的方法。

  • ..:多个连续的任意符号,可以独立出现。常用于简化包名与参数的书写(ps: 这种不定数量字符的匹配,在任何技术里效率都很低)例:
excution ( public User space..UserService.findById (..) )

语义:匹配space包下的任意包中的UserService类或接口中的所有名称为findById的方法。

  • +:专用于匹配子类类型,例:
excution (* *..*Service+.*(..) )

语义:匹配任意包中以Service为后缀的类的子类或是接口的实现类中的任意方法,这些方法的参数数量任意、返回值类型任意。

定义了切点,那么接下来就是要讲通知赋予给与切点相匹配的连接点。

4. 声明通知

首先,我们需要先了解Spring中通知的几种类型。

通知类型

  1. 前置通知:在方法执行前进行的通知。
  2. 后置通知:在方法执行后进行的通知。无论方法是否成功返回,该通知方法都会运行。
  3. 环绕通知:将切点的执行包裹起来,对其执行进行细致的控制与接管,类似代理。是Spring AOP中最常使用的通知类型。
  4. 成功后通知:只有当方法成功返回后才会调用的通知。@AfterReturning
  5. 异常后:只有当方法抛出异常之后才会调用的通知。@AfterThrowing

通知的声明语法

通知是一个使用与通知类型相关的注解进行标记的方法。该方法与同一个切面类下的切点方法进行绑定。

在上面的切面类例子中,我们定义了一个名为logPt的切点。我们接着以此为例:

  1. @Before前置通知、@After后置通知
@Before("logPt()") // 绑定切点
private void logBefore() {
	logger.info("方法执行前通知");
}

@After("logPt()")
private void logAfter() {
	logger.info("方法执行前通知");
}
  1. @AfterReturning方法成功后通知、@AfterThrowing方法抛出异常后通知
```java
@AfterReturning("logPt()") // 绑定切点
private void logBefore() {
	logger.info("方法成功返回后通知");
}

@AfterThrowing("logPt()")
private void logAfter() {
	logger.info("方法抛出异常后通知");
}
  1. @Around环绕通知
    请注意,环绕通知较为特殊!!!
    前面的通知类型,连接点方法的调用是自动的,而环绕通知很类似于代理的作用模式,需要我们手动执行目标方法。

该方法应该声明 Object 为其返回类型,并且该方法的第一个参数必须是 ProceedingJoinPoint 类型。在 advice 方法的body中,你必须在 ProceedingJoinPoint 上调用 proceed()以使底层方法运行

在没有参数的情况下调用 proceed() 将导致调用者的原始参数在底层方法被调用时被提供给它。当然,我们也可以向proceed中传入一个Object[],以代替原参数列表。

@Around("logPt()")
private void logAround(ProceedingJoinPoint pjp) throws Throwable {
	// "方法调用前做一些事"
	Object rtn = pjp.proceed(); // 手动调用连接点方法,并且获取返回值
	// "方法调用后做一些事"
	return rtn; // 将方法返回值返回给调用者
}

不同通知如果绑定到同一切点,其作用顺序是怎样的?

这个问题其实比较直观:before肯定作用于after的前面。
令人困惑的是,

  • around存在时,其和beforeafter的关系。
  • AfterAfterReturningAfterThrowing同时存在时的情况。后者也比较简单,那就是After肯定是最后执行的。

所以我们重点分析前者,概括一下,Around通知是优先级最高的通知。当上述类型的通知同时存在时,只有当Around通知中,对连接点方法进行了调用(即pjp.proceed()),其它类型的通知才会启用。并且,其它类型的通知会蕴含在proceed方法的执行过程中

// Around方法逻辑

// Proceed前逻辑

/* proceed方法中

Before通知 !!!!!!

连接点方法的真正调用

After通知(包括AfterReturning和AfterThrowing) !!!!!!
*/*

// Proceed后逻辑

不同的切面匹配到同一连接点后,其作用顺序如何控制?

通过实现org.springframework.core.Ordered接口中的int getOrder()方法,或者使用@Order注解来为切面类提供一个int类型的优先级权重。Order较低的切面类,优先权较高

如何在通知方法中获取与连接点相关的数据?

方法信息

任何 advice method 都可以声明一个 org.aspectj.lang.JoinPoint 类型的参数作为其第一个参数,并通过该JoinPoint获取方法信息。请注意,around advice 方法需要声明一个 ProceedingJoinPoint 类型的第一个参数,它是 JoinPoint 的一个子接口。

JoinPoint 接口提供了许多有用的方法。

  • Object[] getArgs(): 返回方法的参数。
  • Object getThis(): 返回Spring AOP生成的代理对象。
  • Object getTarget(): 返回被代理的目标对象。
  • Signature getSignature(): 返回当前连接点方法的函数签名。可以通过方法签名对象的成员方法获知有关连接点方法的更具体的信息,常用的如下:
    • Class getDeclaringType()连接点所在类的接口类型
    • String getDeclaringTypeName()连接点所在类的接口名
    • String getName() 方法名
执行参数

使用JoinPointgetArgs()方法可以得到一个Object[],这个数组即为连接点方法的实际执行参数。

特别地,因为@Around通知中,连接点的方法需要通过ProceedJoinPoint.proceed()来手动触发,因此我们在环绕通知中获取得到执行参数后,还可以修改后,重新提交给连接点方法。相当于对原有参数进行了拦截,并给出了新值。这是一种很有用的机制。下面举一个例子:

@Around("logPt()")
private void logAround(ProceedingJoinPoint pjp) throws Throwable {
	Object[] args = pjp.getArgs();
	for (int i = 0; i < args.length; ++i) {
		if (args[i] instanceof String) {
			args[i] = ((String) args[i]).trim();
		} // 去除掉原参数中String类型参数中的空白符
	}
	Object rtn = pjp.proceed(args);
	return rtn;
}
返回值

只有返回后通知环绕通知能够获取到连接点方法执行的返回值。

  • @AfterReturning获取返回值有几个步骤:

    1. 返回后通知方法的参数在此情况下必须提供,第一个默认为JoinPoint类型,第二个参数为Object类型,用于提供返回值。
    2. 在注解参数中,显式提供returning字符串参数,该参数的名称,与参数列表中返回值的名称必须相同。
      下面举一个例子:
    @AfterReturning(value="logPt()", returning = "rtn")
    public void logAfterReturn(JoinPoint jp, Object rtn) {
    	// 可以正常处理连接点方法的返回值 rtn
    }
    
  • @Around因为是手动调用连接点方法,因此可以直接获取返回值,并且还可以对返回值进行拦截处理,返回一个修改过的新返回值(这一点是返回后通知做不到的)。在上面的例子中已经展示过了,这里不再赘述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值