AOP
AOP 基础
概述
AOP 英文全称为 Aspect Oriented Programming,中文译为面向切面编程或面向方面编程,本质是面向特定方法编程,可在不改动原始方法的基础上对其进行功能增强或改变。
场景:项目中部分功能运行较慢,定位执行耗时较长的业务方法,需要统计每一个业务方法的执行耗时,若逐个修改业务方法添加计时逻辑过于繁琐,而 AOP 可解决此问题。
AOP 实现逻辑:通过定义模板方法,在其中编写公共逻辑(如记录开始和结束时间),中间运行原始业务方法,项目运行时会自动执行模板方法而非直接执行原始方法,类似动态代理技术。动态代理是面向切面编程最主流的实现,而 SpringAOP 是 Spring 框架的高级技术,旨在管理 Bean 对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
快速入门
统计各个业务层方法执行耗时
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
入门程序代码:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class TimeAspect {
@Around("execution(* com.example.demo.service.*.*(..))") // 切入点表达式
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录开始时间
long start = System.currentTimeMillis();
// 调用原始方法
Object result = joinPoint.proceed();
// 记录结束时间
long end = System.currentTimeMillis();
// 计算耗时
long time = end - start;
log.info("方法耗时:{}毫秒", time);
return result;
}
}
@Aspect
:Spring AOP 的注解,标识该类是一个切面类(Aspect),用于定义横切逻辑(如日志、性能统计等)
@Around
:AOP 中的环绕通知注解,表示该方法会包裹目标方法的执行 —— 既可以在目标方法执行前做操作,也可以在执行后做操作
注解内的表达式execution(* com.example.demo.service.*.*(..))
是切入点表达式,用于指定哪些方法会被该切面拦截:
*
:第一个*
表示匹配任意返回值类型的方法com.example.demo.service.*
:表示匹配com.example.demo.service
包下的所有类*
:表示匹配类中的所有方法(..)
:表示匹配任意参数(任意数量、任意类型)的方法
综上,该表达式会拦截com.example.demo.service
包下所有类的所有方法
ProceedingJoinPoint joinPoint
:环绕通知特有的参数,用于访问目标方法的信息(如方法名、参数等),并通过joinPoint.proceed()
手动调用目标方法
逻辑流程:
- 执行目标方法前:通过
System.currentTimeMillis()
记录当前时间(开始时间)。 - 调用
joinPoint.proceed()
:执行被拦截的目标方法,并获取其返回值(result
)。 - 执行目标方法后:再次记录时间(结束时间),计算两者差值(即方法执行耗时),并通过
log.info
打印耗时日志。 - 返回目标方法的结果:保证业务逻辑不受切面影响(调用方仍能拿到原方法的返回值)。
执行查询所有部门操作的结果如下:
AOP 的应用场景:包括记录操作日志(记录操作者、时间、参数、返回值等)、完成项目权限控制、实现事务管理(Spring 事务管理底层基于 AOP)等。
AOP 的优势:具有代码无侵入(不修改原始业务方法)、减少重复代码、提高开发效率、维护方便(只需修改 AOP 中的方法)等优势。
AOP 可以理解为 “用代理技术实现的、带有精准目标匹配能力的公共逻辑提取与自动增强机制”。提取公共类是其对逻辑的组织方式,代理技术是其实现无侵入增强的手段,而 “面向特定方法编程”(通过切入点精准匹配)和 “自动织入” 才是其核心价值。
核心概念
连接点:JoinPoint
,可以被 AOP 控制的方法(暗含方法执行时的相关信息)
通知:advice
,指重复的逻辑,也就是共性功能(最终体现为 AOP 中的一个方法)
切入点:PointCut
,匹配连接点的条件,通知仅会在切入点方法执行时被应用(用切入点表达式来表达)
切面:Aspect
,描述通知与切入点的对应关系(通知+切入类)
目标对象:Target
,通知所应用的对象
AOP 执行流程:
- 目标对象(DeptServiceImpl):标记
@Service
的业务实现类,含实际要执行的业务方法(如list()
),是代理增强的 “原始对象”。 - 切面(TimeAspect):标记
@Aspect
,通过@Around
定义切点(匹配service
层方法),实现 “方法耗时统计” 的横切逻辑,会包裹目标方法执行。 - 代理对象(DeptServiceProxy):由 Spring 动态生成(或手动模拟),实现与目标对象相同接口(
DeptService
),内部会先执行切面逻辑,再调用目标对象方法,起到 “增强 + 转发” 作用。 - 流程逻辑:
- 启动时,Spring 识别切面与目标对象,为目标对象创建代理;
- Controller 注入的是代理对象,调用
deptService.list()
时,先进入代理逻辑(执行切面的耗时统计),再转发调用DeptServiceImpl
的真实方法; - 最终实现 “不修改业务类,却能附加通用逻辑(如监控、日志)” 的 AOP 思想。
AOP 进阶
通知类型
@Around
:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before
:前置通知,此注解标注的通知方法在目标方法前被执行
@After
:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning
:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing
:异常后通知,此注解标注的通知方法发生异常后执行
以下是测试代码:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class TestAspect {
@Before("execution(* com.example.demo.service.*.*(..))")
public void before() {
log.info("before");
}
@Around("execution(* com.example.demo.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("around before");
Object ret = joinPoint.proceed();
log.info("around after");
return ret;
}
@After("execution(* com.example.demo.service.*.*(..))")
public void after() {
log.info("after");
}
@AfterReturning("execution(* com.example.demo.service.*.*(..))")
public void afterReturning() {
log.info("afterReturning");
}
@AfterThrowing("execution(* com.example.demo.service.*.*(..))")
public void afterThrowing() {
log.info("afterThrowing");
}
}
运行结果如下:
高亮部分为成功执行的通知,会发现除了 @AfterThrowing
通知以外的其他通知都成功执行了,但如果在原始方法中添加一个 int i = 1/0;
的异常,结果将变为以下
结果显示 @AfterThrowing
、@Before
、@After
和 @Around
的前置部分成功执行,但是由于原始方法中存在异常,@AfterReturning
和 @Around
的后置部分并未执行
由于多个通知的切入点表达式可能重复,可将其抽取。声明一个返回值为 void 的无参空方法,在方法上添加 @Pointcut
注解并指定切入点表达式,其他地方通过类似方法调用的形式引用该表达式:
@Slf4j
@Component
@Aspect
public class TestAspect {
@Pointcut("execution(* com.example.demo.service.*.*(..))")
public void pt() {}
@Before("pt()")
public void before() {
log.info("before");
}
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("around before");
Object ret = joinPoint.proceed();
log.info("around after");
return ret;
}
@After("pt()")
public void after() {
log.info("after");
}
@AfterReturning("pt()")
public void afterReturning() {
log.info("afterReturning");
}
@AfterThrowing("pt()")
public void afterThrowing() {
log.info("afterThrowing");
}
}
若方法为 private,仅能在当前切面类中引用;若要在其他切面类中引用,需将方法设为 public。
注意事项:
@Around
环绕通知需要自己调用ProceedingJoinPoint.proceed()
来让原始方法执行,其他通知不需要考虑目标方法执行@Around
环绕通知方法的返回值,必须指定为 Object 类型,来接收原始方法的返回值
通知顺序
通过前面的程序运行结果可以得知,在同一切面类中:
- 若原始方法中没有出现异常,通知执行的顺序为:
@Around
的前置部分 → \to →@Before
→ \to →@AfterReturning
→ \to →@After
→ \to →@Around
的后置部分 - 若原始方法中出现异常,通知执行的顺序为:
@Around
的前置部分 → \to →@Before
→ \to →@AfterThrowing
→ \to →@After
接下来研究的是多个切面类中通知的执行顺序。
准备三个切面类 TestAspect1、TestAspect2、TestAspect3,每个类都有前置通知(@Before)和后置通知(@After),且切入点表达式相同:
// TestAspect1
public class TestAspect1 {
@Before("execution(* com.example.demo.service.*.*(..))")
public void before() {
log.info("before");
}
@After("execution(* com.example.demo.service.*.*(..))")
public void after() {
log.info("after");
}
}
// TestAspect2
public class TestAspect2 {
@Before("execution(* com.example.demo.service.*.*(..))")
public void before() {
log.info("before");
}
@After("execution(* com.example.demo.service.*.*(..))")
public void after() {
log.info("after");
}
}
// TestAspect3
public class TestAspect3 {
@Before("execution(* com.example.demo.service.*.*(..))")
public void before() {
log.info("before");
}
@After("execution(* com.example.demo.service.*.*(..))")
public void after() {
log.info("after");
}
}
程序运行结果如下:
前置通知执行顺序为 1、2、3,后置通知执行顺序为 3、2、1,这与切面类的类名字母排序有关,目标方法运行前的通知,类名排名越靠前越先执行;目标方法运行后的通知,类名排名越靠前越后执行。
可以在切面类上添加 @Order 注解,通过指定数字控制顺序。目标方法运行前的通知,数字越小越先执行;目标方法运行后的通知,数字越小越后执行。
// TestAspect1
@Order(2)
public class TestAspect1 {
@Before("execution(* com.example.demo.service.*.*(..))")
public void before() {
log.info("before");
}
@After("execution(* com.example.demo.service.*.*(..))")
public void after() {
log.info("after");
}
}
// TestAspect2
@Order(3)
public class TestAspect2 {
@Before("execution(* com.example.demo.service.*.*(..))")
public void before() {
log.info("before");
}
@After("execution(* com.example.demo.service.*.*(..))")
public void after() {
log.info("after");
}
}
// TestAspect3
@Order(1)
public class TestAspect3 {
@Before("execution(* com.example.demo.service.*.*(..))")
public void before() {
log.info("before");
}
@After("execution(* com.example.demo.service.*.*(..))")
public void after() {
log.info("after");
}
}
程序运行结果如下:
切入点表达式
切入点表达式用于决定项目中哪些目标方法应用定义的通知。
常见形式:
- execution:根据方法签名匹配
- annotation:根据注解匹配
execution
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution (访问修饰符? 返回值 包名.类名.? 方法名(方法参数) throws 异常?)
其中带 ?
的表示可省略的部分:
- 访问修饰符:可省略(public、protected、private)
- 包名.类名:可省略
- throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
execution(public * com.example.demo.service.impl.DeptServiceImpl.list())
也可以省略为:
execution(* list())
一般包名和类名不建议省略
可以使用通配符描述切入点:
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分execution(* com.*.*.service.*.list*())
..
:多个连续的任意符号,可以通配任意层级的包或任意类型、任意个数的参数* com.example..service..*(..))
根据业务需要,可以使用 与(&&)
、或(||)
、非(!)
来组合比较复杂的切入点表达式
书写建议:
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update 开头。
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用
..
,使用*
匹配单个包。
annotation
@annotation 切入点表达式,用于匹配标识有特定注解的方法
@annotation(注解全类名)
首先自定义一个注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}
在方法上添加这个自定义注解:
@MyLog
@Override
public List<Dept> list() {return deptMapper.list();}
在切面类中使用这个注解:
@Before("@annotation(com.example.demo.aop.MyLog)")
public void before() {
log.info("before");
}
程序运行结果如下:
两种切入点表达式的总结区别:execution 根据方法描述信息匹配,是常用方式;annotation 基于注解匹配,在方法名不规则或特殊需求时更灵活,虽需自定义注解但操作灵活。
连接点
连接点可简单理解为可以被 AOP 控制的方法,在 Spring AOP 中特指方法的执行,Spring 通过 JoinPoint 对其抽象,可通过该对象获取目标方法执行时的相关信息,如目标对象的类名、目标方法的方法名、参数信息等,并可在通知中通过 JoinPoint 获取这些信息。
@Around
通知需使用 ProceedJoinPoint 获取连接点信息,其他四种通知类型需使用 JoinPoint 获取,且 JoinPoint 是 ProceedJoinPoint 的父类型。
@Around
通知通过 ProceedJoinPoint 获取信息:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class TestAspect4 {
@Pointcut("execution(* com.example.demo.service.*.*(..))")
public void pt() {}
@Before("pt()")
public void before() {
log.info("before");
}
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("around before");
// 1.获取目标对象的类名
String className = joinPoint.getTarget().getClass().getName();
log.info("目标对象的类名:{}",className);
// 2.获取目标方法的方法名
String methodName = joinPoint.getSignature().getName();
log.info("目标方法的方法名:{}",methodName);
// 3.获取目标方法运行时传入的参数
Object[] args = joinPoint.getArgs();
log.info("目标方法运行时传入的参数:{}",args);
// 4.放行目标方法执行
Object ret = joinPoint.proceed();
// 5.获取目标方法运行的返回值
log.info("目标方法运行的返回值:{}",ret);
log.info("around after");
return ret;
}
}
程序运行结果如下:
运行测试方法后,控制台输出了获取到的相关信息;前置通知无法获取返回值,因为其在原始方法运行前执行;环绕通知中若未将 result 返回,会导致原始方法执行结果丢失,且可在 AOP 中篡改目标方法执行结果。