1. 简介
1.1 定义
Spring提供了两种AOP(Aspect Oriented Program 面向切面编程)的实现,基于注解式配置和基于XML配置;AOP编程主要有两种:
- Spring AOP :基于运行时的动态代理实现的功能增强;编译快,运行慢;
- AspectJ :基于编译时的静态代理实现的功能增强;编译慢,运行快;
当切面较少时,二者性能相差并不大,如果切面太多的话,AspectJ要快得多;
1.2 目的
功能区分:核心业务与周边功能;
核心业务:最为核心的业务逻辑,关键数据的增删改查;
周边功能:辅助性质的业务逻辑,比如性能统计、日志记录、事务管理等;
AOP的目的:将周边功能封装起来,减少重复代码,降低模块耦合度,增强可扩展性;在不改变原始设计的前提下进行功能的增强;
1.3 概念
- 连接点(join point):核心业务逻辑的方法;
- 切入点(Pointcut):被选中的连接点,被增强的业务方法;
- 通知(Advice):伴随切入点执行的操作,内部由周围功能的代码逻辑组成;
- 前置通知(Before Advice):在目标方法被调用前调用通知功能;
- 后置通知(After Advice):在目标方法被调用后调用通知功能;
- 返回通知(After-returning):在目标方法成功执行(未抛异常,有返回值)后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):在方法执行前和执行后都调用一次通知;
- 切面(Aspect):切面是特殊的类,由通知和切入点组成,作用是定义切点所执行的通知;
- 织入(Weaving):根据切面创建出代理类的过程;
- 切入点表达式:指明被增强的方法的位置;
1.4 原理
Spring Aop 是通过动态代理的方式实现的,同时采用了jdk动态代理和cglib动态代理这两种代理模式:
- jdk动态代理:采用反射实现,只能对实现接口的类生成代理,加载速度快、执行效率低;
- cglib动态代理:采用asm实现,直接操作字节生成代理类,加载速度慢,执行效率高;
AspectJ 是通过静态代理的方式实现的:
- 在程序编译阶段生成代理类,并加载进内存中;
- 相对于SpringAop 而言,AspectJ生成的静态代理类在编译阶段会耗费较长的时间,但是在运行阶段会更快;
- AspectJ的运用场景相对于SpringAop是有局限性的;
1.5 场景
AOP主要用于,在无代码入侵的场景下实现一些扩展功能,比如:
- 日志打印;
- 消息发送;
- 自定义注解的捕获及处理;
2. 源码解读
在aop编程中,我们常用的 @Aspect、@Pointcut、@Before之类的注解,其实并不在spring-aop包内,而是在aspectjweaver包内,这些注解的包位置为:org.aspectj.lang.annotation ;但我们并不需要专门引入这个包,在maven中引入spring-boot-starter-aop 这个包即可,因为它整合Spring AOP 和 AspectJ。
2.1 AspectJ框架
AspectJ是一个基于Java语言的AOP框架,Spring2.0 之后,Spring AOP引入了对AspectJ的支持,也就是现在SpringAop中,基于注解形式的实现方式。AspectJ支持两种实现AOP的方式:XML声明和注解。
2.1.1 简单例子
一个简单切面编程例子如下:
定义一个业务服务接口:
/**
* 业务服务接口
*/
public interface BusinessService {
String functionA(String arg);
String functionB(String arg);
String functionC(String arg);
String functionD(String arg);
String functionE(String arg);
}
顶一个上述业务服务接口的实现类:
@Component
public class BusinessServiceImpl implements BusinessService {
@Override
public String functionA(String arg) {
System.out.println("run function A");
return "A";
}
@Override
public String functionB(String arg) {
System.out.println("run function B");
return "B";
}
@Override
public String functionC(String arg) {
throw new RuntimeException();
}
@Override
public String functionD(String arg) {
System.out.println("run function B");
return "D";
}
@Override
public String functionE(String arg) {
System.out.println("run function B");
return "E";
}
}
定义个切面:
@Aspect
@Component
public class MyAspect {
// 切点定义
@Pointcut("execution(* example.yang.yu.app.service.impl.BusinessServiceImpl.*(..))")
private void pointCut() {}
// 前置通知
@Before("pointCut()")
public void before() {
System.out.println("执行:Before 通知");
}
@After("pointCut()")
public void after() {
System.out.println("执行:After 通知");
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("执行:Around 通知开始");
try {
Object proceed = proceedingJoinPoint.proceed();
System.out.println("执行:Around 通知结束");
return proceed;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
@AfterReturning("pointCut()")
public void afterReturning() {
System.out.println("执行:afterReturning 通知");
}
@AfterThrowing("pointCut()")
public void afterThrowing() {
System.out.println("执行:afterThrowing 通知");
}
}
需要注意的是:
-
@Around 注解修饰的环绕通知是特殊的:
- 必须接收一个ProceedingJoinPoint类型参数,表示被代理的目标方法;
- 必须返回一个Object对象;
- 若存在其他通知,则其他通知会伴随着ProceedingJoinPointer的proceed() 方法执行,环绕通知代码里若不执行该方法,则其他通知也不会被执行;
-
@AfterReturning注解的通知和@AfterThrowing注解的通知互斥,同时只能有一个被触发;
-
@Aspect 注解自身并不提供容器注入支持,但切面生效的前提就是被注入容器中,因此需要添加例如 @Component 之类的注解,使切面被容器收集;
2.1.2 切点表达式
切点表达式既可以直接内置在通知注解中,也可以写在独立注解 @PointCut 中,效果相同,使用如下:
@Aspect
@Component
public class MyAspect {
// 1. 切点表达式内置于通知注解
@Before("excution(public void indi.example.target.BusinessServiceImpl.run())")
public void doBefore() {
}
// 2. 切点表达式独立于 @PointCut 注解
@PointCut("excution(public void indi.example.target.BusinessServiceImpl.run())")
private void pt(){}
@After("pt()")
public void doAfter() {
}
}
需要注意的是,访问控制为public的@PointCut注解的方法,是一个公共的切点,也就是说,可以在其他切面调用,也就是切点的复用。
表达式格式:
excution([修饰符] [返回值类型] 包名.类名.方法名(参数列表))
/**
* 访问修饰符可以省略
* 返回值类型、类名、包名可以用 * 表示通配符
* 参数列表可以用两个英文点号 .. 表示通配符
* 包名和类名之间的一个点当前包下的类,两个代表当前包及其子包下的类
*/
execution(void indi.example.target.BusinessServiceImpl.run())
execution(* indi.example.*.BusinessServiceImpl.*())
// 匹配任意个、任意类型的参数
execution(* indi.example.*.BusinessServiceImpl.*(..))
// 匹配一个、任意类型的参数
execution(* indi.example.*.BusinessServiceImpl.*(*))
execution(* indi.example.*.BusinessServiceImpl.*(*, String args))
// 包名和类名之间的两个点代表当前包及其子包下的类
execution(* indi.example..*.BusinessServiceImpl.*(*, String args))
切点表达式规则可以总结为:
- 返回值类型、类名、包名可以用 * 表示通配符;
- 参数列表可以用两个英文点号 … 表示通配符;
- 包名和类名之间的一个点代表当前包下的类,两个代表当前包及其子包下的类;
- 访问修饰符可以省略;
2.2 SpringAop框架
不同于AspectJ 框架,spring-aop 框架采用的是动态代理方式,实现的切面编程,底层采用CGLIB和JDK动态代理实现。由于AspectJ中用于定义AOP的API非常好,直观易用,所以Spring引入了AspectJ中的部分注解,帮助自身在获取Advisor阶段生产Advisor,后面的代理生成与代理增强与AspectJ无关。
2.2.2 核心接口
-
Advisor: 一个接口,代表被拦截方法需要增强的逻辑,即切面,Advisor有许多子接口:
- PointcutAdvisor: 切点模式的切面,最常用的Advisor;
-
MethodBeforeAdvice: 前置通知;
-
AfterReturningAdvice : 后置通知;
-
MethodInterceptor :环绕通知;
-
ThrowsAdvice : 异常通知;