一、引入场景:为什么要使用 Spring AOP?
在项目开发中,我们经常会遇到这样一个需求:统计每个业务方法的执行耗时。
比如部门管理模块中有这些方法:
java
List<Dept> list();
void delete(Integer id);
void save(Dept dept);
Dept getById(Integer id);
void update(Dept dept);
如果我们想统计每个方法的运行时间,最直接的写法可能是在每个方法中手动加入开始时间、结束时间和日志输出:
java
long begin = System.currentTimeMillis();
// 执行业务逻辑
long end = System.currentTimeMillis();
log.info("方法耗时:{}ms", end - begin);
这种写法虽然能实现功能,但是问题也很明显:
- 每个方法都要写一遍,代码重复度很高。
- 业务代码里混入了耗时统计逻辑,代码侵入性比较强。
- 如果以后日志格式要改,所有方法都要改,维护起来很麻烦。
- 项目方法越多,开发效率越低。
这时候就可以使用 Spring AOP。
AOP 的全称是 Aspect Oriented Programming,也就是面向切面编程。它的核心思想是:把多个业务方法中重复出现的通用逻辑抽取出来,统一放到一个切面类中处理。
使用 Spring AOP 的好处主要有:
- 减少重复代码。
- 对原有业务代码无侵入。
- 提高开发效率。
- 后期维护更加方便。
像日志记录、耗时统计、权限校验、事务处理、操作日志等功能,都非常适合使用 AOP 来完成。
二、编写一个简单的 AOP 例子
在 Spring Boot 项目中使用 AOP,首先需要引入依赖。
1. 引入 AOP 起步依赖
在 pom.xml 中加入:
xml
<!-- AOP 起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
这个依赖非常关键。如果没有引入它,@Aspect、@Around 等 AOP 注解就无法正常使用。
2. 编写耗时统计切面类
java
package com.itheima.aop;
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 RecordTimeAspect {
@Around("execution(* com.itheima.service.impl.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object result = pjp.proceed();
long end = System.currentTimeMillis();
log.info("方法 {} 执行耗时:{} ms", pjp.getSignature(), end - begin);
return result;
}
}
这里有几个核心注解和对象:
@Component:把当前类交给 Spring 容器管理。@Aspect:声明当前类是一个切面类。@Around:声明当前方法是一个环绕通知。ProceedingJoinPoint:可以调用目标方法,并获取目标方法的信息。pjp.proceed():执行原始目标方法。
这样写完以后,com.itheima.service.impl 包下所有类的所有方法执行时,都会自动被这个切面拦截,然后统计执行耗时。
业务方法本身不需要加入任何耗时代码,这就是 AOP 的价值。
三、AOP 的核心特性
3.1 连接点
连接点,英文叫 JoinPoint。
简单来说,连接点就是程序执行过程中可以被 AOP 控制的位置。
在 Spring AOP 中,连接点主要指的是 方法执行。
比如当前项目中的 DeptServiceImpl:
java
@Service
public class DeptServiceImpl implements DeptService {
@Override
public List<Dept> list() {
return deptMapper.list();
}
@Override
public void delete(Integer id) {
deptMapper.delete(id);
}
}
这里的 list()、delete()、save()、getById()、update() 方法,都可以理解为连接点。
如果我们想在这些方法执行前、执行后、出现异常时插入一些逻辑,就可以通过 AOP 来完成。
在通知方法中,也可以通过 JoinPoint 获取目标方法信息:
java
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
public void before(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("目标对象:{}", target);
log.info("目标方法:{}", methodName);
log.info("方法参数:{}", Arrays.toString(args));
}
常用方法有:
| 方法 | 作用 |
|---|---|
getTarget() |
获取目标对象 |
getSignature() |
获取目标方法签名 |
getArgs() |
获取目标方法参数 |
3.2 通知
通知,英文叫 Advice。
通知就是切面中具体要执行的代码。比如记录日志、统计耗时、权限校验,这些都可以写在通知方法中。
1. @Around 环绕通知
@Around 是最重要、也是功能最强的通知。
它可以在目标方法执行前后都加入逻辑,并且可以控制目标方法是否执行。
java
@Around("execution(* com.itheima.service.impl.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("目标方法执行前...");
Object result = pjp.proceed();
log.info("目标方法执行后...");
return result;
}
这里最关键的是:
java
Object result = pjp.proceed();
这行代码表示执行原始目标方法。
如果没有调用 pjp.proceed(),目标方法就不会执行。所以环绕通知不仅能增强方法,还能控制方法执行流程。
它常见的使用场景有:
- 统计方法耗时。
- 统一记录操作日志。
- 权限校验。
- 参数校验。
- 对返回结果做统一处理。
如果目标方法有返回值,环绕通知也要把结果返回出去:
java
return result;
否则外部调用方可能拿不到原本的返回结果。
2. @Before 前置通知
@Before 会在目标方法执行之前执行。
java
@Before("execution(* com.itheima.service.impl.*.*(..))")
public void before() {
log.info("before...");
}
它适合做一些方法执行前的操作,比如:
- 打印请求参数。
- 权限校验。
- 前置日志记录。
但是它不能控制目标方法是否执行,也不能获取目标方法的返回值。
3. @AfterReturning 返回后通知
@AfterReturning 会在目标方法正常返回之后执行。
java
@AfterReturning("execution(* com.itheima.service.impl.*.*(..))")
public void afterReturning() {
log.info("afterReturning...");
}
注意,它只有在目标方法没有抛出异常时才会执行。
适合做:
- 正常执行完成后的日志记录。
- 统计成功操作。
- 获取正常返回后的处理逻辑。
4. @AfterThrowing 异常后通知
@AfterThrowing 会在目标方法抛出异常之后执行。
java
@AfterThrowing("execution(* com.itheima.service.impl.*.*(..))")
public void afterThrowing() {
log.info("afterThrowing...");
}
它适合做:
- 异常日志记录。
- 异常告警。
- 失败操作统计。
3.3 切入点表达式
切入点表达式用来指定:哪些方法需要被 AOP 增强。
常用的切入点表达式主要有两种:
execution@annotation
1. execution 表达式
execution 是最常用的切入点表达式,它根据方法签名来匹配目标方法。
基本语法如下:
java
execution(访问修饰符? 返回值 包名.类名.方法名(方法参数) throws 异常?)
比如:
java
@Before("execution(* com.itheima.service.impl.*.*(..))")
public void before() {
log.info("before...");
}
这个表达式表示:
匹配 com.itheima.service.impl 包下所有类的所有方法,参数任意,返回值任意。
execution 的语法规则如下:
- 方法的访问修饰符可以省略。
- 返回值可以使用
*号代替,表示任意返回值类型。 - 包名可以使用
*号代替,表示任意包,一层包使用一个*。 - 使用
..配置包名,表示此包以及此包下的所有子包。 - 类名可以使用
*号代替,表示任意类。 - 方法名可以使用
*号代替,表示任意方法。 - 可以使用
*配置参数,表示一个任意类型的参数。 - 可以使用
..配置参数,表示任意个任意类型的参数。
几个常见例子:
java
// 匹配 DeptServiceImpl 的 delete 方法,参数为 Integer
execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
// 匹配 service.impl 包下所有类的所有方法
execution(* com.itheima.service.impl.*.*(..))
// 匹配 com.itheima 包及其子包下 service.impl 中所有方法
execution(* com.itheima..service.impl.*.*(..))
// 匹配方法名以 del 开头的方法
execution(* com.itheima.service.impl.*.del*(..))
// 匹配只有一个任意类型参数的方法
execution(* com.itheima.service.impl.*.*(*))
// 匹配任意参数个数的方法
execution(* com.itheima.service.impl.*.*(..))
在实际开发中,建议尽量写清楚包名和类名范围,不要匹配范围过大。否则可能会把一些不需要增强的方法也拦截进去。
2. @annotation 表达式
@annotation 是根据注解来匹配方法。
它的好处是更加灵活,想让哪个方法被增强,就在哪个方法上加注解。
当前项目中有一个自定义注解:
java
package com.itheima.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
}
这个注解的含义是:
@Target(ElementType.METHOD):只能标注在方法上。@Retention(RetentionPolicy.RUNTIME):运行时仍然生效,AOP 才能读取到它。
然后在业务方法上使用:
java
@LogOperation
@Override
public List<Dept> list() {
return deptMapper.list();
}
@LogOperation
@Override
public void delete(Integer id) {
deptMapper.delete(id);
}
最后在切面中通过 @annotation 匹配:
java
@Before("@annotation(com.itheima.anno.LogOperation)")
public void before() {
log.info("myaspect5...before");
}
这样只有加了 @LogOperation 注解的方法,才会被当前通知增强。
3. execution 和 @annotation 的区别
| 对比项 | execution |
@annotation |
|---|---|---|
| 匹配方式 | 根据方法签名匹配 | 根据方法上的注解匹配 |
| 控制粒度 | 按包、类、方法规则统一匹配 | 哪个方法加注解,哪个方法生效 |
| 使用场景 | 批量增强某一层代码 | 精准增强指定方法 |
| 优点 | 配置一次,批量生效 | 灵活、直观、可读性强 |
| 缺点 | 表达式写错容易范围过大或过小 | 需要手动加注解 |
简单总结:
如果想对整个 service 层统一增强,适合用 execution。
如果只想对某几个方法增强,适合用 @annotation 自定义注解。
3.4 切面
切面,英文叫 Aspect。
切面就是把切入点和通知组合到一起的类。
比如:
java
@Slf4j
@Component
@Aspect
public class MyAspect1 {
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
public void pt() {}
@Before("pt()")
public void before() {
log.info("before...");
}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("around before...");
Object result = pjp.proceed();
log.info("around after...");
return result;
}
}
这里:
MyAspect1是切面类。@Pointcut定义切入点。@Before、@Around定义通知。pt()这个方法只是用来承载切入点表达式,本身不需要写业务逻辑。
把切入点单独抽取出来有一个好处:多个通知可以复用同一个切入点。
比如:
java
@Before("pt()")
public void before() {}
@AfterReturning("pt()")
public void afterReturning() {}
@AfterThrowing("pt()")
public void afterThrowing() {}
这样代码会更加清晰,后期如果切入点规则变了,只需要修改 @Pointcut 中的表达式即可。
3.5 目标对象
目标对象,英文叫 Target。
目标对象就是被 AOP 增强的原始对象。
在当前项目中,DeptServiceImpl 就是目标对象:
java
@Service
public class DeptServiceImpl implements DeptService {
// 业务方法
}
当外部调用 DeptServiceImpl 中的方法时,Spring AOP 会通过代理对象在目标方法前后加入增强逻辑。
也就是说,实际执行流程可以理解为:
text
Controller 调用 Service
↓
进入 AOP 代理对象
↓
执行通知逻辑
↓
执行目标对象中的原始方法
↓
继续执行通知逻辑
↓
返回结果
这里要注意一点:Spring AOP 主要是基于 Spring 容器中的 Bean 来完成代理的。所以目标对象一般要交给 Spring 管理,比如加上:
java
@Service
@Component
@RestController
3.6 通知顺序:默认排序和 @Order 自定义排序
在实际开发中,可能会出现多个切面同时作用到同一个目标方法的情况。
java
@Aspect
@Component
@Order(1)
public class MyAspect1 {
}
java
@Aspect
@Component
@Order(2)
public class MyAspect2 {
}
java
@Aspect
@Component
@Order(3)
public class MyAspect3 {
}
java
@Aspect
@Component
@Order(4)
public class MyAspect4 {
}
如果这些切面都匹配到了同一个目标方法,那么 Spring 就需要决定:到底谁先执行,谁后执行。
1. 默认排序
如果多个切面都没有加 @Order,Spring 也会执行它们,但是执行顺序不建议依赖。
也就是说,默认情况下虽然程序能运行,但是我们不能把业务逻辑建立在"某个切面一定先执行"这个假设上。
比如下面这种写法:
java
@Aspect
@Component
public class MyAspect1 {
}
java
@Aspect
@Component
public class MyAspect2 {
}
这两个切面的执行顺序没有明确指定。项目简单时可能看不出问题,但是一旦切面多了,比如同时有日志、权限、事务、耗时统计,顺序就很重要。
所以如果切面之间存在先后关系,建议使用 @Order 明确指定。
2. 使用 @Order 自定义排序
@Order 可以用来控制切面的执行优先级。
规则很简单:
text
@Order 中的数字越小,优先级越高。
比如:
java
@Order(1)
public class MyAspect1 {
}
java
@Order(2)
public class MyAspect2 {
}
此时 MyAspect1 的优先级高于 MyAspect2。
对于环绕通知来说,可以理解成一层一层包裹目标方法:
text
MyAspect1 环绕前
MyAspect2 环绕前
目标方法执行
MyAspect2 环绕后
MyAspect1 环绕后
也就是说:
- 进入目标方法之前,优先级高的切面先执行。
- 退出目标方法之后,优先级高的切面后结束。
这就像外层包装一样,@Order(1) 在最外层,@Order(2) 在里面一层。
3. 示例理解
假设有两个切面:
java
@Aspect
@Component
@Order(1)
@Slf4j
public class MyAspect1 {
@Around("execution(* com.itheima.service.impl.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("MyAspect1 around before");
Object result = pjp.proceed();
log.info("MyAspect1 around after");
return result;
}
}
java
@Aspect
@Component
@Order(2)
@Slf4j
public class MyAspect2 {
@Around("execution(* com.itheima.service.impl.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("MyAspect2 around before");
Object result = pjp.proceed();
log.info("MyAspect2 around after");
return result;
}
}
执行目标方法时,日志顺序大概是:
text
MyAspect1 around before
MyAspect2 around before
目标方法执行
MyAspect2 around after
MyAspect1 around after
所以可以总结为一句话:
text
@Order 数字越小,进入越早,退出越晚。
4. 使用建议
在实际开发中,如果只有一个切面,不写 @Order 也没有问题。
但是如果项目中有多个切面,比如:
- 权限校验切面
- 参数校验切面
- 操作日志切面
- 方法耗时统计切面
- 事务相关切面
这时候最好明确指定顺序,避免执行顺序不清晰。
常见安排可以是:
text
权限校验 -> 参数校验 -> 业务执行 -> 日志记录 / 耗时统计
对应到 @Order,可以让越需要提前执行的切面,数字越小:
java
@Order(1) // 权限校验
@Order(2) // 参数校验
@Order(3) // 日志记录
@Order(4) // 耗时统计
这样代码的执行顺序就更加清楚,后期维护时也不容易出问题。
四、AOP 执行流程总结
以 @Around 统计耗时为例,执行流程大概是:

所以,AOP 并不是替代业务代码,而是在业务代码外面包了一层增强逻辑。
五、常见注意事项
- 使用 AOP 前一定要引入
spring-boot-starter-aop依赖。 - 切面类要加
@Aspect和@Component。 - 环绕通知中必须调用
pjp.proceed(),否则目标方法不会执行。 - 有返回值的方法,环绕通知要把返回结果
return出去。 execution表达式不要写得过大,避免误拦截。- 使用
@annotation时,自定义注解要设置RetentionPolicy.RUNTIME。 - 目标对象必须交给 Spring 容器管理,否则 AOP 不会生效。
- 切入点表达式中的括号要成对出现,少一个或多一个都会导致启动失败。
六、总结
这篇文章主要通过"统计方法执行耗时"这个场景,引出了 Spring AOP 的使用。
AOP 最核心的价值就是:把日志、耗时统计、权限校验这类重复逻辑从业务代码中抽取出来,统一放到切面中维护。
本文重点梳理了几个核心概念:
- 连接点:可以被 AOP 控制的方法执行位置。
- 通知:增强逻辑,比如
@Around、@Before、@AfterReturning、@AfterThrowing。 - 切入点表达式:决定哪些方法需要被增强。
- 切面:通知和切入点的组合。
- 目标对象:被增强的原始业务对象。
实际开发中,如果是统一增强某一层代码,可以优先考虑 execution;如果是精确控制某些方法,可以使用自定义注解配合 @annotation。
掌握了这些概念之后,再看操作日志、权限校验、事务处理等功能时,就会发现它们背后的思想其实都是类似的:把通用逻辑抽出来,让业务代码保持干净。