什么是面向切面编程
我们现在可以把之前分散在应用各处的行为放入可重用的模块中。我们显示地声明在何处如何应用该行为。这有效减少了代码冗余,并让我们的类关注自身的主要功能。
图 4.1展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如安全和事务管理。
定义AOP术语
描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。图 4.2 展示了这些概念是如何关联在一起的。
通知(Advice)
通知定义了切面的工作是以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点(Poincut)
切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容 —— 它是什么,在何时和何处完成其功能。
Spring对AOP的支持
Spring 提供了 4 种类型的 AOP 支持:
- 基于代理的经典 Spring AOP;
- 纯 POJO 切面;
- @AspectJ 注解驱动的切面;
- 注入式 AspectJ 切面(适用于 Spring 各版本)。
需要注意的是前三种都是 Spring AOP 实现的变体,Spring 对 AOP 的支持局限于方法拦截。如果你还需要构造器或属性拦截,就需要考虑使用第四种,采用AspectJ 来实现切面。
通过切点来选择连接点
在 Spring AOP 中,要使用 AspectJ 的切点表达式语言来定义切点。使用AspectJ 指示器来执行或限制切点表达式。
AspectJ 指示器 | 描叙 | |
---|---|---|
args() | 限制连接点匹配参数为指定类型的执行方法 | |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 | |
execution() | 用于匹配是连接点的执行方法 | |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 | |
target | 限制连接点匹配目标对象为指定类型的类 | |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类 型的注解 | |
within() | 限制连接点匹配指定的类型 | |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方 法定义在由指定的注解所标注的类里) | |
@annotation | 限定匹配带有指定注解的连接点 | |
bean() | bean ID 或 bean 名称作为参数来限制切点只匹配特定的 bean。 |
编写切点
先定义一个接口,它可以代表任何类型的现场表演,如舞台剧、电影或音乐会:
public interface Performance {
public void perform();
}
设置当 perform() 方法执行时触发通知的调用的一个切点表达式:
execution(* com.springAop.Performance.perform(..))
//还可以使用操作符连接多个指示器
execution(* com.springAop.Performance.perform(..)) && bean('woodstock')
使用注解创建切面
定义切面
将观众定义为一个切面,并将其应用到演出上:
@Aspect
public class Audience {
@Pointcut("execution(* com.springAop.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");
}
}
@Aspect 表明Audience 不仅仅是一个 POJO,还是一个切面。
@Pointcut注解声明频繁使用的切点表达式,performance该方法本身只是一个标识,供 @Pointcut 注解依附,execution()执行一个切点表达式,用来指定一个切点,表示一个目标方法。
silenceCellPhones()被定义为了通知方法。
AspectJ 提供了五个注解来定义通知:
注解 | 通知 |
---|---|
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用之前执行 |
在JavaConfig中启用AspectJ注解的自动代理:
@Configuration
@EnableAspectJAutoProxy //启用 AspectJ 注解的自动代理。会为@Aspect注解的bean创建一个代理
public class ConcertConfig {
@Bean //声明bean
public Audience audience(){
return new Audience();
}
@Bean
public Performance opera(){
return new Opera();
}
}
执行perform():
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConcertConfig.class);
Performance opera = (Performance) ctx.getBean("opera");
opera.perform();
}
}
创建环绕通知
它能够让你所编写的逻辑将被通知的目标方法完全包装起来。
@Aspect
public class Audience {
@Pointcut("execution(* com.springAop.Performance.perform(..))")
public void performance() { }
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
jp.proceed();//某个点上执行目标方法
System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable e) {
System.out.println("Demanding a refund");
}
}
}
处理通知中的参数
切点声明了要提供给通知方法的参数:
//切点方法
public interface Performance {
public void perform(int data);
}
@Aspect
public class Audience {
@Pointcut("execution(* com.springAop.Performance.perform(int)) && args(data)")//切点定义
public void performance(int data){}
@Before("performance(data)")
public void methom(int data){
}
通过注解引入新功能
我们为示例中的所有的 Performance 实现引入下面的 Encoreable 接口:
public interface Encoreable {
public void performEncore();
}
我们现在假设你能够访问 Performance 的所有实现,并对其进行修改,让它们都实现 Encoreable 接口。但是,从设计的角度来看,这并不是最好的做法,并不是所有的 Performance 都是具有 Encoreable 特性的。另外一方面,有可能无法修改所有的 Performance 实现,当使用第三方实现并且没有源码的时候更是如此。
为了实现该功能,我们要创建一个新的切面:
@Aspect
public class EncodeableIntroducer {
@DeclareParents(value = "com.springAop.Performance+", defaultImpl = DefaultEncoreable.class)
public static Encoreable encoreable;
}
@DeclareParents 注解由三部分组成:
- value 属性指定了哪种类型的 bean 要引入该接口。在本例中,也就是所有实现 Performance 的类型。(标记符后面的加号表示是 Performance 的所有子类型,而不是 Performance 本身。)
- defaultImpl 属性指定了为引入功能提供实现的类。在这里,我们指定的是 DefaultEncoreable 提供实现。
- @DeclareParents 注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是 Encoreable 接口。
和其他的切面一样,在JavaConfig中将EncoreableIntroducer 声明为一个 bean:
@Bean
public EncodeableIntroducer encodeableIntroducer(){return new EncodeableIntroducer();}
执行一下:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConcertConfig.class);
Performance opera = (Performance) ctx.getBean("opera");
Encoreable encoreable = (Encoreable) opera;
encoreable.performEncore();
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。