使用注解来创建切面是 AspectJ 5 所引入的关键特性。
我们前一篇已经定义了 Performance 接口,它是切面中切点的目标对象。
A: 给出一个描述:如果一场演出没有观众的话,那就不能称之为演出。对不对?从演出的角度来看,观众是非常重要的,但是对演出本身的功能来将,它并不是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。
之前需要在 build.gradle 导入 aspectjrt 包
// https://mvnrepository.com/artifact/org.aspectj/aspectjrt
compile group: 'org.aspectj', name: 'aspectjrt', version: '1.9.0.BETA-6'
package concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
* 该类使用 @AspectJ 注解进行了标注。该注解表明该类不仅仅是一个 POJO,还是一个切面。
* 该类中的方法都使用注解来定义切面的具体行为。
*/
@Aspect
public class Audience {
//表演之前:将手机调至静音状态
@Before("execution(**concert.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
//表演之前:就座
@Before("execution(**concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("Taking seats");
}
//表演之后:精彩的话,观众应该会鼓掌喝彩
@AfterReturning("execution(**concert.Performance.perform(..))")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
//表演失败之后:没有达到观众预期的话,观众会要求退款
@AfterThrowing("execution(**concert.Performance.perform(..))")
public void demandRefund() {
System.out.println("Demanding a refund");
}
}
可以简化一下,使用 @Pointcut 注解。
package concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
/**
* 简化方案:@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。
*/
@Aspect
public class Audience {
//通过在该方法上添加 @Pointcut 注解,我们实际上扩展了切点表达式语言,
//这样就可以在任何的切点表达式中使用 performance()了。
//该方法内容并不重要,实际上应该是空的。只是一个标识,供 @Pointcut 注解依附。
@Pointcut("execution(**concert.Performance.perform(..))")
public void performance(){}
//表演之前:将手机调至静音状态
@Before("performance()")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
//表演之前:就座
@Before("performance()")
public void takeSeats() {
System.out.println("Taking seats");
}
//表演之后:精彩的话,观众应该会鼓掌喝彩
@AfterReturning("performance()")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
//表演失败之后:没有达到观众预期的话,观众会要求退款
@AfterThrowing("performance()")
public void demandRefund() {
System.out.println("Demanding a refund");
}
}
package concert;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* 如果你使用 JavaConfig 的话,可以在配置类的类级别上通过使用 @EnableAspectJAutoProxy 注解启用自动代理功能。
*/
@Configuration
@EnableAspectJAutoProxy //启用 AspectJ 自动代理
@ComponentScan
public class ConcertConfig {
@Bean
public Audience audience(){ // 声明 Audience bean
return new Audience();
}
}
或者 XML 中配置:使用 Spring aop 命名空间中的 aop:aspectJ-autoproxy 元素。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="concert"/>
<aop:aspectj-autoproxy/>
<bean class="concert.Audience"/>
beans>
不管使用 JavaConfig 还是 XML,AspectJ 自动代理都会为使用 @Aspect 注解的 bean 创建一个代理,这个代理会围绕着所有该切面的切点所匹配的 bean。在这种情况下,将会为 Concert bean 创建一个代理,Audience 类中的通知方法将会在 perform() 调用前后执行。
需要记住的是:Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,切面依然是基于代理的。本质上,它依然是 Spring 基于代理的切面。这就意味着,尽管使用的是 @AspectJ 注解,但我们仍然限于代理方法的调用。如果想利用 AspectJ 的所有能力,我们必须在运行时使用 AspectJ 并且不依赖于 Spring 来创建基于代理的切面。
A: 环绕通知是最为强大的通知类型。实际上就像在一个通知方法中同时编写前置通知和后置通知。
package concert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
/**
* 使用环绕通知重新实现 Audience 切面
* 这个通知所达到的效果跟之前的前置后置通知是一样的。但是,现在它们位于一个方法中。
*/
@Aspect
public class Audience {
// 定义命名的切点
@Pointcut("execution(**concert.Performance.perform(..))")
public void performance(){}
/**
* 环绕通知方法
* @param joinPoint 这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。
*/
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint joinPoint){
try {
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
// 通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用该方法。
// 如果不调用这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。
// 有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。
joinPoint.proceed();
System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable throwable) {
System.out.println("Demanding a refund");
}
}
}
对于 proceed() 方法,你也可以在通知中对它进行多次调用,这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试,
A:如下列代码所示:BlankDisc.java & CompactDisc.java 参考 Spring 如何通过 XML 装配 bean?,并加以修改 play() 方法。
package concert;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import java.util.HashMap;
import java.util.Map;
/**
* 假设你想记录每个磁道被播放的次数。由于记录磁道的播放次数与播放本身是不同的关注点,因此应该是切面要完成的任务。
* 使用参数化的通知来记录磁道播放的次数
*/
@Aspect
public class TrackCounter {
// 记录每个磁道播放次数
private Map trackCounts = new HashMap();
// 通知 play() 方法,在切点表达式中声明参数,这个参数传入到通知方法 countTrack() 中
// args(trackNumber) 表明传递给 play() 方法的 int 类型参数也会传递到通知中(countTrack()方法)去。
// 参数名称与切点方法签名中的参数相匹配。
@Pointcut("execution(* soundsystem.CompactDisc.play(int)) && args(trackNumber)")
public void trackPlayed(int trackNumber) {
}
// 在播放之前,为该磁道计数
// 这个通知方法是通过 @Before 注解和命名切点 trackPlayed(trackNumber) 定义的。
// 切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。
@Before("trackPlayed(trackNumber)")
public void countTrack(int trackNumber) {
int currentCount = getPlauCount(trackNumber);
trackCounts.put(trackNumber, currentCount + 1);
}
public int getPlauCount(int trackNumber) {
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}
package soundsystem;
import concert.TrackCounter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.util.ArrayList;
import java.util.List;
/**
* 在 Spring 配置中将 BlankDisc 和 TrackCounter 定义为 bean,并启用自动代理。
* 配置 TrackCount 记录每个磁道播放的次数
*/
@Configuration
@EnableAspectJAutoProxy // 启用 AspectJ 自动代理
public class TrackCounterConfig {
@Bean
public CompactDisc sgtPeppers(){
BlankDisc cd = new BlankDisc();
cd.setTitle("titleValue01");
cd.setArtist("ArtistValue01");
List tracks = new ArrayList();
tracks.add("trackValue01");
tracks.add("trackValue02");
tracks.add("trackValue03");
tracks.add("trackValue04");
tracks.add("trackValue05");
cd.setTracks(tracks);
return cd;
}
@Bean
public TrackCounter trackCounter(){
return new TrackCounter();
}
}
A: Java 不是动态语言,一旦类编译完成了,我们就很难再为该类添加新功能了。但是我们可以使用切面,为对象拥有的方法添加了新功能,而没有为对象添加任何新的方法。实际上,利用被称为引入的 AOP 概念,切面可以为 Spring bean 添加新的方法。
注意:当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,一个 bean 的实现被拆分到了多个类中。
代码如下:
package concert;
/**
* 为所有的 Performance 实现引入下面的 Encoreable 接口
*
* 需要将这个接口应用到 Performance 实现中。
* 两个问题:不能直接实现 Encoreable 接口,并不是所有的 Performance 都是具有 Encoreable 特性的;
* 也有可能无法修改所有的 Performance 实现(使用第三方实现并且没有源码时)。
* 解决方案:借助于 AOP 的引入功能,我可以避免以上两个问题。
*/
public interface Encoreable {
void performEncore();
}
package concert;
/**
* Encoreable 接口的实现类
*/
public class DefaultEncoreable implements Encoreable {
@Override
public void performEncore() {
}
}
package concert;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
/**
* 创建一个切面
*/
@Aspect
public class EncoreableIntroducer {
// 通过 @DeclareParents 注解,将 Encoreable 接口引入到 Performance bean 中。
// @DeclareParents 注解由三部分组成:
// ①、value 属性指定了哪种类型的 bean 要引入该接口。
// 此处 value 的值,代表所有实现 Performance 的类型。(标记符后面的 "+" 表示是 Performance 的所有子类型,而不是它本身)
// ②、defaultImpl 属性指定了为引入功能提供实现的类。这里,指定的是 DefaultEncoreable 提供实现。
// ③、@DeclareParents 注解所标注的静态属性指明了要引入的接口。这里,所引入的是 Encoreable 接口。
@DeclareParents(value = "concert.Performance+",defaultImpl = DefaultEncoreable.class)
public static Encoreable encoreable;
}
<bean class="concert.EncoreableIntroducer"/>
Spring 的自动代理机制将会获取到它的声明,当 Spring 发现了一个 bean 使用了 @Aspect 注解时,Spring 就会创建一个代理,然后将调用委托给被代理的 bean 或被引入的实现,这取决于调用的方法属于被代理的 bean 还是属于被引入的接口。
面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码。
如果没有源码的话,或者不想将 AspectJ 注解放到你的代码之中,我们可以在 Spring XML 配置文件中声明切面。
上一篇: 面向切面的 Spring —— 如何通过切点来选择连接点?
下一篇:面向切面的 Spring —— 如何在 XML 中声明切面?