Spring入门(十):Spring AOP使用讲解
1. 什么是AOP?
AOP是Aspect Oriented Programming的缩写,意思是:面向切面编程,它是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
可以认为AOP是对OOP(Object Oriented Programming 面向对象编程)的补充,主要使用在日志记录,性能统计,安全控制等场景,使用AOP可以使得业务逻辑各部分之间的耦合度降低,只专注于各自的业务逻辑实现,从而提高程序的可读性及维护性。
比如,我们需要记录项目中所有对外接口的入参和出参,以便出现问题时定位原因,在每一个对外接口的代码中添加代码记录入参和出参当然也可以达到目的,但是这种硬编码的方式非常不友好,也不够灵活,而且记录日志本身和接口要实现的核心功能没有任何关系。
此时,我们可以将记录日志的功能定义到1个切面中,然后通过声明的方式定义要在何时何地使用这个切面,而不用修改任何1个外部接口。
在讲解具体的实现方式之前,我们先了解几个AOP中的术语。
1.1 通知(Advice)
在AOP术语中,切面要完成的工作被称为通知,通知定义了切面是什么以及何时使用。
Spring切面有5种类型的通知,分别是:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,此时不关心方法的输出结果是什么
- 返回通知(After-returning):在目标方法成功执行之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
1.2 连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、修改某个字段时。
1.3 切点(Pointcut)
切点是为了缩小切面所通知的连接点的范围,即切面在何处执行。我们通常使用明确的类和方法名称,或者利用正则表达式定义所匹配的类和方法名称来指定切点。
1.4 切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容:它是什么,在何时和何处完成其功能。
1.5 引入(Introduction)
引入允许我们在不修改现有类的基础上,向现有类添加新方法或属性。
1.6 织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。
切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里,有以下几个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
2. Spring 对AOP的支持
2.1 动态代理
Spring AOP构建在动态代理之上,也就是说,Spring运行时会为目标对象动态创建代理对象。
代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。
当代理类拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。
2.2 织入切面时机
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring 管理的bean中,也就是说,直到应用需要被代理的bean时,Spring才会创建代理对象。
因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP切面。
2.3 连接点限制
Spring只支持方法级别的连接点,如果需要字段级别或者构造器级别的连接点,可以利用AspectJ来补充Spring AOP的功能。
3. Spring AOP使用
假设我们有个现场表演的接口Performance和它的实现类SleepNoMore:
package chapter04.concert;
/**
* 现场表演,如舞台剧,电影,音乐会
*/
public interface Performance {
void perform();
}
package chapter04.concert;
import org.springframework.stereotype.Component;
/**
* 戏剧:《不眠之夜Sleep No More》
*/
@Component
public class SleepNoMore implements Performance {
@Override
public void perform() {
System.out.println("戏剧《不眠之夜Sleep No More》");
}
}
既然是演出,就需要观众,假设我们的需求是:在看演出之前,观众先入座并将手机调整至静音,在观看演出之后观众鼓掌,如果演出失败观众退票,我们当然可以把这些逻辑写在上面的perform()方法中,但不推荐这么做,因为这些逻辑理论上和演出的核心无关,就算观众不将手机调整至静音或者看完演出不鼓掌,都不影响演出的进行。
针对这个需求,我们可以使用AOP来实现。
3.1 定义切面
首先,定义一个观众的切面如下:
package chapter04.concert;
import org.aspectj.lang.annotation.Aspect;
/**
* 观众
* 使用@Aspect注解定义为切面
*/
@Aspect
public class Audience {
}
注意事项:
@Aspect
注解表明Audience类是一个切面。
3.2 定义前置通知
在Audience切面中定义前置通知如下所示:
/**
* 表演之前,观众就座
*/
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("Taking seats");
}
/**
* 表演之前,将手机调至静音
*/
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
这里的重点代码是@Before("execution(* chapter04.concert.Performance.perform(..))")
,它定义了1个前置通知,其中execution(* chapter04.concert.Performance.perform(..))
被称为AspectJ切点表达式,每一部分的讲解如下:
- @Before:该注解用来定义前置通知,通知方法会在目标方法调用之前执行
- execution:在方法执行时触发
- *:表明我们不关心方法返回值的类型,即可以是任意类型
- chapter04.concert.Performance.perform:使用全限定类名和方法名指定要添加前置通知的方法
- (..):方法的参数列表使用(..),表明我们不关心方法的入参是什么,即可以是任意类型
3.3 定义后置通知
在Audience切面中定义后置通知如下所示:
/**
* 表演结束,不管表演成功或者失败
*/
@After("execution(* chapter04.concert.Performance.perform(..))")
public void finish() {
System.out.println("perform finish");
}
注意事项:@After注解用来定义后置通知,通知方法会在目标方法返回或者抛出异常后调用
3.4 定义返回通知
在Audience切面中定义返回通知如下所示:
/**
* 表演之后,鼓掌
*/
@AfterReturning("execution(* chapter04.concert.Performance.perform(..))")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
注意事项:@AfterReturning注解用来定义返回通知,通知方法会在目标方法返回后调用
3.5 定义异常通知
在Audience切面中定义异常通知如下所示:
/**
* 表演失败之后,观众要求退款
*/
@AfterThrowing("execution(* chapter04.concert.Performance.perform(..))")
public void demandRefund() {
System.out.println("Demanding a refund");
}
注意事项:@AfterThrowing注解用来定义异常通知,通知方法会在目标方法抛出异常后调用
3.6 定义可复用的切点表达式
细心的你可能会发现,我们上面定义的5个切点中,切点表达式都是一样的,这显然是不好的,好在我们可以使用@Pointcut
注解来定义可重复使用的切点表达式:
/**
* 可复用的切点
*/
@Pointcut("execution(* chapter04.concert.Performance.perform(..))")
public void perform() {
}
然后之前定义的5个切点都可以引用这个切点表达式:
/**
* 表演之前,观众就座
*/
@Before("perform()")
public void takeSeats() {
System.out.println("Taking seats");
}
/**
* 表演之前,将手机调至静音
*/
@Before("perform()")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
/**
* 表演结束,不管表演成功或者失败
*/
@After("perform()")
public void finish() {
System.out.println("perform finish");
}
/**
* 表演之后,鼓掌
*/
@AfterReturning("perform()")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
/**
* 表演失败之后,观众要求退款
*/
@AfterThrowing("perform()")
public void demandRefund() {
System.out.println("Demanding a refund");
}
3.7 单元测试
新建配置类ConcertConfig如下所示:
package chapter04.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;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
@Bean
public Audience audience() {
return new Audience();
}
}
注意事项:和以往不同的是,我们使用了
@EnableAspectJAutoProxy
注解,该注解用来启用自动代理功能。
新建Main类,在其main()方法中添加如下测试代码:
package chapter04.concert;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConcertConfig.class);
Performance performance = context.getBean(Performance.class);
performance.perform();
context.close();
}
}
运行代码,输出结果如下所示:
Silencing cell phones
Taking seats
戏剧《不眠之夜Sleep No More》
perform finish
CLAP CLAP CLAP!!!
稍微修改下SleepNoMore类的perform()方法,让它抛出一个异常:
@Override
public void perform() {
int number = 3 / 0;
System.out.println("戏剧《不眠之夜Sleep No More》");
}
再次运行代码,输出结果如下所示:
Silencing cell phones
Taking seats
perform finish
Demanding a refund
Exception in thread “main” java.lang.ArithmeticException: / by zero
由此也可以说明,不管目标方法是否执行成功,@After注解都会执行,但@AfterReturning注解只会在目标方法执行成功时执行。
值得注意的是,使用@Aspect
注解的切面类必须是一个bean(不管以何种方式声明),否则切面不会生效,因为AspectJ自动代理只会为使用@Aspect
注解的bean创建代理类。
也就是说,如果我们将ConcertConfig配置类中的以下代码删除或者注释掉:
@Bean
public Audience audience() {
return new Audience();
}
运行结果将变为:
戏剧《不眠之夜Sleep No More》
3.8 创建环绕通知
我们可以使用@Around
注解创建环绕通知,该注解能够让你在调用目标方法前后,自定义自己的逻辑。
因此,我们之前定义的5个切点,现在可以定义在一个切点中,为不影响之前的切面,我们新建切面AroundAudience,如下所示:
package chapter04.concert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class AroundAudience {
/**
* 可重用的切点
*/
@Pointcut("execution(* chapter04.concert.Performance.perform(..))")
public void perform() {
}
@Around("perform()")
public void watchPerform(ProceedingJoinPoint joinPoint) {
try {
System.out.println("Taking seats");
System.out.println("Silencing cell phones");
joinPoint.proceed();
System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable throwable) {
System.out.println("Demanding a refund");
} finally {
System.out.println("perform finish");
}
}
}
这里要注意的是,该方法有个ProceedingJoinPoint类型的参数,在方法中可以通过调用它的proceed()方法来调用目标方法。
然后修改下ConcertConfig类的代码:
package chapter04.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;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
/*@Bean
public Audience audience() {
return new Audience();
}*/
@Bean
public AroundAudience aroundAudience() {
return new AroundAudience();
}
}
运行结果如下所示:
Taking seats
Silencing cell phones
戏剧《不眠之夜Sleep No More》
CLAP CLAP CLAP!!!
perform finish
4. 源码及参考
源码地址:https://github.com/zwwhnly/spring-action.git,欢迎下载。
Craig Walls 《Spring实战(第4版)》
5. 最后
打个小广告,欢迎扫码关注微信公众号:「申城异乡人」,定期分享Java技术干货,让我们一起进步。