一 绪论
前面的章节讲解的主要是 DI 依赖注入。这部分是 Spring AOP 与 AspectJ 两个部分。其核心的主旨都是面向切面编程。
软件中散布于应用之中的多处功能被称为 横切关注点 ,一般来说横切关注点是与应用的业务逻辑相分离的。把这些横切关注点与业务逻辑分离是面向切面编程所要关注和处理的地方。比如 消息/安全/事务/日志等等 功能,都属于横切关注点,这些内容与业务逻辑没有关联,散布在工程的各个角落。如果是硬编码,我们需要在每一个使用到这些功能的地方添加新的代码,但是我们是在做重复的工作,因为每一个地方添加的代码大体上都是一致的,这种重复枯燥的代码充斥工程的感觉很烦。
面对上述的场景,最直接的处理方式是使用 继承,但是继承会导致应用面对统一的基类代码,对象体系在演变过程中很容易变得复杂难以处理。
二 AOP 术语
2.1 通知(Advice)
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring 切面可以应用 5 种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
- 返回通知(After-returning):在目标方法执行成功之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知包裹了被统治的方法,在被通知的方法调用之前和调用之后执行自定义的行为
2.2 连接点(Join Point)
在应用之中有很多时机可以应用通知,这些时机被称为连接点。连接点是应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时或者是修改一个字段时。切面代码可以利用这些点插入到应用的正常流程中,并加载新的行为。
2.3 切点(Pointcut)
如果说通知定义了切面的“什么”和“何时”,那么切点就是定义了“何处”。 切点的定义会匹配通知所要织入的一个或多个连接点。 我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
2.4 切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容--它是什么,在何时和何处完成其功能。
2.5 引入(Introduction)
引入允许我们想现有类添加新的方法和属性。
2.6 织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期中有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入,这种方式需要 特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器。它可以在目标类被引入应用之前增强该目标类的字节码。依然是 AspectJ 的加载时织入支持这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态的创建一个代理对象。Spring AOP 就是以这种方式织入切面的。
三 Spring 对 AOP 的支持
AOP 框架之间是有差异的,有些支持在字段修饰符级别应用通知,有些只支持与方法调用相关的连接点。
Spring 提供 4 种类型的 AOP 支持:
- 基于代理的经典 Spring AOP
- 纯 POJO 切面
- @AspectJ 注解驱动的切面
- 注入式 AspectJ 切面
前面三种都是 Spring AOP 实现的变体,Spring AOP 构建在 动态代理 基础之上,因此,Spring 对 AOP 的支持局限于方法拦截。
基于 Spring 的通知是使用 Java 编写的,上手很快。不过与之对应的 AspectJ 有自己特有的 AOP 语言。它提供更强大的和细粒度的控制,以及更丰富的 AOP 工具集。
3.1 Spring 在运行时通知对象
通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。
直到应用需要被代理的 bean 时,Spring 才创建代理对象。
Spring 只支持方法级别的连接点
3.2 通过切点来选择连接点
切点用于准确定位应该在什么地方应用切面的通知。通知和切点是切面最基本的元素。
Spring AOP 要使用 AspectJ 的切点表达式语言来定义切点。这里需要注意的是 Spring 仅支持 AspectJ 切点指示器的一个子集。
AspectJ 的切点表达式语言:
AspectJ 指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配 AOP 代理的 bean 引用为指定类型的类 |
target | 限制连接点匹配目标对象为执行类型的类 |
@target | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within | 限制连接点匹配执行注解所标注的类型 |
@within | 限制连接点匹配指定注解所标注的类型 |
@annotation | 限定匹配带有指定注解的连接点 |
当我们查看到上述的 Spring 支持的指示器时,注意只有 execution 指示器是实际执行匹配的,而其他的指示器是用来限制匹配的。这说明 execution 指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器限制所匹配的切点。
@Aspect
@Component
public class Audience {
/**
* 切点表达式中: execution 指示器表示在方法执行时触发;* 表示我们不关心方法返回值的类型;然后指定了全限定类名和方法名;对于方法参数列表,
* 使用两个点号 (..) 表明切点要选择任意的 perform 方法
*/
@Before("execution(* com.doctorwork.aop.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("audience silence cell phones.");
}
/**
* 切点表达式同上,只是这个方法是在监控的方法执行结束之后再执行。
*/
@After("execution(* com.doctorwork.aop.Performance.perform(..))")
public void applause() {
System.out.println("audience applause.");
}
/**
* 自定义切点
*/
@Pointcut("execution(* com.doctorwork.aop.impl.Broadway.perform(..))")
public void performance() {}
/**
* 环绕通知
*/
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("broadway is beautiful.");
jp.proceed();
System.out.println("We will go back..");
} catch (Throwable e) {
System.out.println("something is wrong.");
}
}
}
上述就是一个基于注解的 切面 类。切面类中的方法上都带了注解,类似 @After 之类的都是 通知;注解之后的表达式就是 切点;所谓的 连接点,在一般的 Spring 通知上面都是 方法执行前/后。
四 简单测试
4.1 基于显式的 JavaConfig 配置
要将上述代码转换为 Spring 中的一个切面类,需要进一步的配置。如果使用 JavaConfig,可以在配置类级别上使用注解
// 开启自动代理功能
@EnableAspectJAutoProxy
如下为配置类
/**
* @author gaopeng@doctorwork.com
* @description
* @EnableAspectJAutoProxy 注解用于开启 aspectJ 自动代理
* @ComponentScan 用于组件扫描的注解,默认的、无附加信息的这个注解只会扫描配置类所在的包以及子包
* @date 2018-05-30 20:33
**/
@EnableAspectJAutoProxy
@ComponentScan
public class PerformanceConfig {
}
以下为目标类接口和简单的实现
public interface Performance {
void perform();
}
==================================
/**
* @author gaopeng@doctorwork.com
* @description
* @date 2018-05-30 19:59
**/
@Primary
@Component
public class Woodstock implements Performance{
public void perform() {
System.out.println("woodstock!");
}
}
以下为测试类
/**
* @author gaopeng@doctorwork.com
* @description
* 这里使用了 @Resource 注解,这个注解与前面使用的 @Autowired 注解的主要区别是,resource 是按名称装配,另一个是默认按类型装配
* 配合此处,一个接口有多个实现类,所以此处使用 @Resource 是更合适的。这个注解还可以制定 bean id
* @Resouorce(name = "woodstock")
* @date 2018-05-30 20:32
**/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = PerformanceConfig.class)
public class AOPTest {
@Resource
private Performance woodstock;
@Test
public void aopTest() {
woodstock.perform();
}
}
以下为测试输出结果
audience silence cell phones.
woodstock!
audience applause.
4.2 基于 XML 配置的声明方式
<!--<context:component-scan base-package="com.doctorwork.aop" />-->
<!-- 启用 aspectJ 切面自动代理 -->
<aop:aspectj-autoproxy />
<!-- 切面类 bean 注入 -->
<bean id="audience" class="com.doctorwork.aop.XMLAudience" />
<!-- 被代理类 bean 注入 -->
<bean id="waterCube" class="com.doctorwork.aop.impl.WaterCube" />
<!-- 将注入的 bean 声明为一个切面 -->
<aop:config>
<!-- 引用目标 bean 并将其声明为切面类 -->
<aop:aspect ref="audience">
<aop:before method="silenceCellPhones"
pointcut="execution(* com.doctorwork.aop.Performance.perform(..))" />
<aop:after method="applause"
pointcut="execution(* com.doctorwork.aop.Performance.perform(..))" />
</aop:aspect>
</aop:config>
去掉对应切面类中的各种纷杂的注解,我们可以通过 XML 配置文件的方式完成切面声明和 bean 的注入。这种方式在企业开发中更加常见。免配置的方式整体上会比较简单简洁,但是在实际的开发过程中我们都更多的是使用 XML 配置来处理。