Spring-AOP

面向切面编程(AOP)可以实现横切关注点与它们所影响对象之间的解耦,对业务逻辑没有任何侵入
使用场景有很多,如:日志、异常、声明式事物、安全、缓存等,包括但不限于这些场景
常见的AOP实现有Spring-AOPAspectJSpring-AOP基于动态代理实现,主要总结一下Spring-AOP

概念

切面(Aspect)

切面是通知和切点的结合,通知和切点共同定义了切面的全面内容————是什么,在何时和何处完成其功能

通知(Advice)

切面的具体功能被称为通知,同时通知不仅定义了切面功能,还定义了切面何时使用,而根据使用时机可以分为5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。环绕通知是最强大的通知类型

Spring-AOP 中的环绕通知必须有 ProceedingJoinPoint 这个对象参数,通过它的 proceed() 方法来调用被通知的方法

切点(Poincut)

通知定义了切面的 “什么” 和 “何时” ,切点则是定义了切面的 ”何处“

切点定义了通知被应用的具体位置(在哪些连接点)

连接点(Join point)

连接点是在应用执行过程中能够应用通知的 ”所有点“

连接点可以是调用方法时、抛出异常时、甚至修改一个字段时,但因为Spring-AOP基于动态代理,所以Spring只支持方法的连接点,而AspectJJBoss的AOP框架还提供了字段和构造器接入点。

但是方法拦截则满足了大部分的需求,如果需要方法拦截之外的连接点可以利用AspectJ来补充Spring-AOP的功能

引入(Introduction)

添加方法或字段到被通知的类

织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程

织入可以在对象生命周期的多个点织入:

  • 编译期:切面在目标类编译期被织入。这种需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器,可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入就支持这种方式织入
  • 运行期:切面在应用运行的某个时刻被织入。在织入时,AOP容器会为目标对象动态地创建一个代理对象。**Spring-AOP就是以这种方式织入切面的**

定义切点

切点用来定位使用通知的地方,在Spring-AOP中使用的是AspectJ的切点表达式语言来定义切点,但是Spring仅支持AspectJ切点指示器的一个子集

切点表达式

切点指示器

Spring-AOP所支持的AspectJ的切点指示器

AspectJ 指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的bean引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类 型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方 法定义在由指定的注解所标注的类里)
@annotation 限定匹配带有指定注解的连接点

只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的

操作符

支持andornot关系来连接指示器

在POJO中使用&&||!分别代表andornot关系

在XML中直接使用andornot来连接指示器

bean指示器

除去AspectJ指示器外,Spring还引入了一个新的bean()指示器,用来在切点表达式中使用bean的ID来标识bean。bean()使用bean Idbean名称作为参数来限制切点只匹配特定的bean

注解创建切面

AspectJ 5支持使用注解来创建切面,使用少量的注解就可以把任意类转变为切面

Spring同时支持AspectJ注解驱动的切面

AOP配置注解

@Aspect

表明该类不仅仅是个POJO,还是一个切面

通知注解

通知注解对应五种通知类型,来声明通知方法

注解 通知
@After 通知方法会在目标方法返回或抛出异常后调用
@AfterReturning 通知方法会在目标方法返回后调用
@AfterThrowing 通知方法会在目标方法抛出异常后调用
@Around 通知方法会将目标方法封装起来
@Before 通知方法会在目标方法调用之前执行
@Pointcut

@Pointcut注解可以在一个@AspectJ切面内定义可重用的切点

E.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Aspect
public class Audience {

@Pointcut("execution(** concert.Performance.perform(..))")
public void performce() { }

@Before("performce()")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}

@Around("performce()")
public void takeSeats(ProceedingJoinPoint joinPoint) {
joinPoint.proceed(joinPoint.getArgs())
System.out.println("Taking seats");
}

@AfterReturning("performce()")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}

@AfterThrowing("performce()")
public void demandRefund() {
System.out.println("Demanding a refund");
}
}

通过在 performance() 方法上添加 @Pointcut 注解,这样就可以在任何的切点表达式中使用 performance()

performance() 方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供 @Pointcut 注解依附。

通过注解引入新功能

类似Groovy不直接修改对象或类的定义就能为对象或类增加新的方法,虽然Java不是动态语言,但是通过AOP引用新的接口则可以实现其功能

但是当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,一个bean的实现被拆分到了多个类中

通过@DeclareParents注解可以将新的接口引入到bean

定义新的接口:

1
2
3
4
5
package concert;

public interface Encoreable {
void performEncore();
}

创建切面引入接口:

1
2
3
4
5
6
7
8
9
10
11
12
package concert;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;

@Aspect
public class EncodeableIntroducer {

@DeclareParents(value="concert.Performce+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}

@DeclareParents 注解由三部分组成:

  • value 属性指定了哪种类型的 bean 要引入该接口。在本例中,也就是所有实现 Performance 的类型。(标记符后面的加号表示是 Performance 的所有子类型,而不是 Performance 本身。)
  • defaultImpl 属性指定了为引入功能提供实现的类。在这里,我们指定的是 DefaultEncoreable 提供实现。
  • @DeclareParents 注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是 Encoreable 接口。

XML中声明切面

AOP配置元素

在 Spring 的 aop 命名空间中,提供了多个元素用来在 XML 中声明切面

AOP 配置元素 用途
<aop:advisor> 定义 AOP 通知器
<aop:after> 定义 AOP 后置通知(不管被通知的方法是否执行成功)
<aop:after-returning> 定义 AOP 返回通知
<aop:after-throwing> 定义 AOP 异常通知
<aop:around> 定义 AOP 环绕通知
<aop:aspect> 定义一个切面
<aop:aspectj-autoproxy> 启用 @AspectJ 注解驱动的切面
<aop:before> 定义一个 AOP 前置通知
<aop:config> 顶层的 AOP 配置元素。大多数的元素必须包含在元素内
<aop:declare-parents> 以透明的方式为被通知的对象引入额外的接口
<aop:pointcut> 定义一个切点

E.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="performance"
expressions="execution(** concert.Performance.perform(..))" />

<aop:before
pointcut-ref="performance"
method="silenceCellPhones" />

<aop:around
pointcut-ref="performance"
method="takeSeats" />

<aop:after-returning
pointcut="execution(** concert.Performance.perform(..))"
method="applause" />

<aop:after-throwing
pointcut-ref="performance"
method="demandRefund" />

</aop:aspect>
</aop:config>

通过切面引入新功能

使用 @DeclareParents 注解可以为被通知的方法引入新的方法,使用 Spring aop 命名空间中的 <aop:declare-parents> 元素,可以实现相同的功能

1
2
3
4
5
6
<aop:aspect>
<aop:delate-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable" />
</aop:aspect>
  • types-matching类型匹配,匹配为哪些bean引入接口
  • implement-interface指定新加的接口
  • default-impl用全限定类名来显式指定新加接口的实现
  • delegate-ref还支持引用了一个Spring bean作为引入接口的实现

启用AspectJ自动代理

无论是注解创建、还是XML声明切面,都需要启用自动代理,来创建切面的代理,否则切面不会生效

启用自动代理有两种方式:

  • JavaConfig中启用

    配置类的类级别上通过使用 @EnableAspectJAutoProxy 注解启用自动代理功能

  • XML中启用

    使用 Spring aop 命名空间中的 <aop:aspectj-autoproxy/> 元素

注入AspectJ切面

虽然 Spring AOP 能够满足许多应用的切面需求,但是与 AspectJ 相比,Spring AOP 是一个功能比较弱的 AOP 解决方案。AspectJ 提供了 Spring AOP 所不能支持的许多类型的切点

AspectJ 可以织入到任意的Java应用程序中,而我们可以借助 Spring 的依赖注入把 bean 装配进 AspectJ 切面中,这样更为方便

用AspectJ实现切面

创建 AspectJ 的切面需要使用扩展的Java语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.aspect.test;

public aspect AspectInject {
public AspectInject() {
}

/**
* Pointcut(切入点):定义了相应Advice要发生的地方。
* <p>
* 定义切入点的常用的两种方式:
* 1、使用正则表达式。
* 2、使用AspectJ表达式(Https://www.iteye.com/blog/jinnianshilongnian-1415606)。
*/
pointcut printInfo():execution(* xxx.xxx.xxx.xxx.printInfo(..));
/**
* 构造器切点
*/
pointcut constructEntity():execution(xxx.xxx.xxx.xxx.User.new());

/**
* Advice定义了在Pointcut(切入点)具体要做的操作。
* <p>
* 切入点在AOP中有多种类型,但在Spring中只有方法类型。
* <p>
* before advice, 前置。
* after return advice, 后置(出错不执行)。
* after throwing advice, 后置(出错才执行)。
* after(final) advice, 后置(怎么都执行)。
* around advice, 环绕(前后都执行)。
*/

before():printInfo(){
System.out.println("------printInfo()方法的前置------");
}

after():printInfo(){
System.out.println("------printInfo()的后置------");
}

before():constructEntity(){
System.out.println("------new User()时候的前置------");
}

after():constructEntity(){
System.out.println("-------new User()时候的后置------");
}
}

使用aspectOf()方法注入

Spring beanSpring 容器初始化,但是 AspectJ 切面是由 AspectJ 在运行期创建的。等到 Spring 有机会为 AspectJ 切面注入 bean 时,切面已经被实例化了。

所有的 AspectJ 切面都提供了一个静态的 aspectOf() 方法,该方法返回切面的一个单例。所以为了获得切面的实例,我们必须使用 factory-method 来调用 asepctOf() 方法

1
<bean class="com.aspect.test.AspectInject" factory-method="aspectOf"/>

参考文献

  • 《Spring 实战(第 4 版)》
分享到: