AOP术语
通知:通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?
Spring 切面可以应用 5 种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning):在目标方法成功执行之后调用通 知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
连接点(Join Point):连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。
切点(Pointcut):如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。连接点说明目标对象哪些位置可以切入通知,切点则是实际进行切入的一个或多个连接点。
切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了切面的全部内容 — 它是什么,在何时和何处完成其功能。
引入(Introduction):允许我们向现有的类添加新方法或属性。
织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5 的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的
Spring AOP的四种类型
Spring 提供了 4 种类型的 AOP 支持:
- 基于代理的经典 Spring AOP;
- 纯 POJO 切面;
- @AspectJ 注解驱动的切面**(常用)**;
- 注入式 AspectJ 切面(适用于 Spring 各版本)。
Spring 通知是 Java 编写的。
Spring在运行时通知对象。通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。
Spring AOP是基于动态代理的,只支持方法级别的连接点,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象;对于没有实现接口的对象,无法使用 JDK Proxy 去进行代理,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。
AspectJ 切点指示器
在 Spring AOP 中,要使用 AspectJ 的切点表达式语言来定义切点。
Spring 仅支持 AspectJ 切点指示器(pointcut designator)的一个子集。
AspectJ 指示器 | 描 述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类 型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型 |
@annotation | 限定匹配带有指定注解的连接点 |
使用注解创建切面
示例代码结构说明:
- Performance接口定义了表演方法,该方法有两个实现类Movie和Drama。
// Performance.java
package org.tang.it.aop;
public interface Performance {
public void perform();
}
// Movie.java
package org.tang.it.aop;
@Component
@Qualifier("movie") //限定符, 用于消除自动装配bean时的歧义性
public class Movie implements Performance{
public void perform() {
System.out.println("---The movie is showing---");
}
}
// Drama.java
package org.tang.it.aop;
@Component
@Qualifier("drama") //限定符, 用于消除自动装配bean时的歧义性
public class Drama implements Performance{
public void perform() {
System.out.println("***The drama is on***");
}
}
- 定义切面,切点为Performance接口的perform方法,对Movie和Drama两个实现类使用不同的通知。
定义切面
使用到的主要注解:
- @Pointcut 注解:在一个 @AspectJ 切面内定义可重用的切点。
- @After注解:通知方法会在目标方法返回或抛出异常后调用。
- @AfterReturning注解:通知方法会在目标方法返回后调用。
- @AfterThrowing注解:通知方法会在目标方法抛出异常后调用。
- @Around注解:通知方法会将目标方法封装起来。
- @Before注解:通知方法会在目标方法调用之前执行。
切面定义示例如下:
// Audience.java
package org.tang.it.aop;
@Aspect
@Component // 声明为Spring Bean,保证该切面类能被装载注入
public class Audience {
// 使用pointcut定义切连接点, 便于复用. 表示切点左右的连接点是Performance的perform()方法
// 通过bean指示符限制该连接点只对名为movie的bean生效
@Pointcut("execution(** org.tang.it.aop.Performance.perform(..)) && bean(movie)")
public void performce() { }
@Before("performce()")
public void silenceCellPhones() {
System.out.println("---AOP:Silencing cell phones");
}
@Before("performce()")
public void takeSeats() {
System.out.println("---AOP:Taking seats");
}
@AfterReturning("performce()")
public void applause() {
System.out.println("---AOP:CLAP CLAP CLAP!!!");
}
@AfterThrowing("performce()")
public void demandRefund() {
System.out.println("---AOP:Demanding a refund");
}
}
目前Audience 还只是 Spring 容器中的一个 bean。即便使用了 AspectJ 注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。如果你使用 JavaConfig 的话,可以在配置类的类级别上通过使用 EnableAspectJAutoProxy 注解启用自动代理功能。示例如下:
@Configuration
@ComponentScan(basePackages={"org.tang.it.aop"})
@EnableAspectJAutoProxy
public class ConcertConfig {
}
至此,AspectJ 自动代理都会为使用 @Aspect 注解的 bean 创建一个代理,这个代理会围绕着所有该切面的切点所匹配的 bean。在这种情况下,将会为movie bean 创建一个代理,Audience 类中的通知方法将会在 perform() 调用前后执行。
Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,切面依然是基于代理的。
创建环绕通知
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。
// Audience.java
package org.tang.it.aop;
@Aspect
@Component // 声明为Spring Bean, 保证该切面类能被装载注入
public class Audience {
//定义切连接点, 便于复用. 通过target指示符限制该连接点只对Dram类生效
@Pointcut("execution(** org.tang.it.aop.Performance.perform(..)) && target(org.tang.it.aop.Drama)")
public void performceDrama() { }
@Around("performceDrama()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("***AOP:Silencing cell phones");
System.out.println("***AOP:Taking seats");
jp.proceed();
System.out.println("***AOP:CLAP CLAP CLAP!!!");
} catch (Throwable e) {
System.out.println("***AOP:Demanding a refund");
}
}
}
Around通知方法接受 ProceedingJoinPoint 作为参数。这个对象是必须要有的,因为要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用 ProceedingJoinPoint 的 proceed() 方法。
测试验证示例代码如下:
package org.tang.it.aop;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=ConcertConfig.class)
public class ConcertTest {
@Autowired //自动注入Bean
@Qualifier("movie") //Performance接口的两个实现类都声明为组件Bean,通过限定符明确注入哪个实现类
Performance movie;
@Autowired
@Qualifier("drama")
Performance drama;
@Test
public void testMoviePerform() {
System.out.println("---Perform process start---");
movie.perform();
System.out.println("---Perform process end---");
System.out.println();
System.out.println("***Perform process start***");
drama.perform();
System.out.println("***Perform process end***");
}
}
输出结果:
---Perform process start---
---AOP:Silencing cell phones
---AOP:Taking seats
---The movie is showing---
---AOP:CLAP CLAP CLAP!!!
---Perform process end---
***Perform process start***
***AOP:Silencing cell phones
***AOP:Taking seats
***The drama is on***
***AOP:CLAP CLAP CLAP!!!
***Perform process end***
通过注解引入新功能
在AOP术语中引入(Introduction)表示允许我们向现有的类添加新方法或属性。这里介绍如果通过Spring AOP给bean添加其他。
在 Spring 中,切面只是实现了它们所包装 bean 相同接口的代理。如果除了实现这些接口,代理也能暴露新接口的话,会怎么样呢?那样的话,切面所通知的 bean 看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓,原理如下:
当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。
示例代码:验证之前的代码,Movie类依然是Performance接口的实现类,现在我们希望在不改动接口Performance和实现类Movie的情况下,给Performance实现类的bean添加新方法。
- 新方法放在Encoreable接口和DefaultEncoreable实现类中。
// Encoreable.java
package org.tang.it.aop;
public interface Encoreable {
void performEncore();
}
// DefaultEncoreable.java
package org.tang.it.aop;
@Component
public class DefaultEncoreable implements Encoreable {
public void performEncore() {
System.out.println("---AOP: Introduced function proceed!!!");
}
}
- 定义切面。
@Aspect
@Component
public class EncodeableIntroducer {
@DeclareParents(value="org.tang.it.aop.Performance+", defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
可以看到,EncoreableIntroducer 是一个切面。但是,它与我们之前所创建的切面不同,它并没有提供前置、后置或环绕通知,而是通过 @DeclareParents 注解,将 Encoreable 接口引入到 Performance bean 中。
@DeclareParents 注解由三部分组成:
- value 属性指定了哪种类型的 bean 要引入该接口。在本例中,也就是所有实现 Performance 的类型。(标记符后面的加号表示是 Performance 的所有子类型,而不是 Performance 本身。)
- defaultImpl 属性指定了为引入功能提供实现的类。在这里,我们指定的是 DefaultEncoreable 提供实现。
- @DeclareParents 注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是 Encoreable 接口。
- 测试验证
// 配置类
package org.tang.it.aop;
@Configuration
@ComponentScan(basePackages={"org.tang.it.aop"})
@EnableAspectJAutoProxy
public class ConcertConfig {
}
// 测试代码
package org.tang.it.aop;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=ConcertConfig.class)
public class ConcertTest {
@Autowired
@Qualifier("movie")
Performance movie;
@Test
public void testMoviePerform() {
System.out.println("---Spring AOP introduction demo---");
((Encoreable)movie).performEncore(); // 强制类型转换以调用新方法
String className = movie.getClass().getName();
System.out.println(className);
Class<?>[] interfaceNames = movie.getClass().getInterfaces();
for (Class inter : interfaceNames) {
System.out.println(inter);
}
}
}
输出结果:
---Spring AOP introduction demo---
---AOP: Introduced function proceed!!!
com.sun.proxy.$Proxy23
interface org.tang.it.aop.Performance
interface org.tang.it.aop.Encoreable
interface org.springframework.aop.SpringProxy
interface org.springframework.aop.framework.Advised
可以看到movie是代理类,这个代理类同时实现了Performance接口和Encoreable接口。