【Tilas|第十篇】万字讲解SpringAOP知识点

一、引入场景:为什么要使用 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);

这种写法虽然能实现功能,但是问题也很明显:

  1. 每个方法都要写一遍,代码重复度很高。
  2. 业务代码里混入了耗时统计逻辑,代码侵入性比较强。
  3. 如果以后日志格式要改,所有方法都要改,维护起来很麻烦。
  4. 项目方法越多,开发效率越低。

这时候就可以使用 Spring AOP


AOP 的全称是 Aspect Oriented Programming,也就是面向切面编程。它的核心思想是:把多个业务方法中重复出现的通用逻辑抽取出来,统一放到一个切面类中处理。

使用 Spring AOP 的好处主要有:

  1. 减少重复代码。
  2. 对原有业务代码无侵入。
  3. 提高开发效率。
  4. 后期维护更加方便。

像日志记录、耗时统计、权限校验、事务处理、操作日志等功能,都非常适合使用 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;
    }
}

这里有几个核心注解和对象:

  1. @Component:把当前类交给 Spring 容器管理。
  2. @Aspect:声明当前类是一个切面类。
  3. @Around:声明当前方法是一个环绕通知。
  4. ProceedingJoinPoint:可以调用目标方法,并获取目标方法的信息。
  5. 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(),目标方法就不会执行。所以环绕通知不仅能增强方法,还能控制方法执行流程。

它常见的使用场景有:

  1. 统计方法耗时。
  2. 统一记录操作日志。
  3. 权限校验。
  4. 参数校验。
  5. 对返回结果做统一处理。

如果目标方法有返回值,环绕通知也要把结果返回出去:

java 复制代码
return result;

否则外部调用方可能拿不到原本的返回结果。

2. @Before 前置通知

@Before 会在目标方法执行之前执行。

java 复制代码
@Before("execution(* com.itheima.service.impl.*.*(..))")
public void before() {
    log.info("before...");
}

它适合做一些方法执行前的操作,比如:

  1. 打印请求参数。
  2. 权限校验。
  3. 前置日志记录。

但是它不能控制目标方法是否执行,也不能获取目标方法的返回值。

3. @AfterReturning 返回后通知

@AfterReturning 会在目标方法正常返回之后执行。

java 复制代码
@AfterReturning("execution(* com.itheima.service.impl.*.*(..))")
public void afterReturning() {
    log.info("afterReturning...");
}

注意,它只有在目标方法没有抛出异常时才会执行。

适合做:

  1. 正常执行完成后的日志记录。
  2. 统计成功操作。
  3. 获取正常返回后的处理逻辑。

4. @AfterThrowing 异常后通知

@AfterThrowing 会在目标方法抛出异常之后执行。

java 复制代码
@AfterThrowing("execution(* com.itheima.service.impl.*.*(..))")
public void afterThrowing() {
    log.info("afterThrowing...");
}

它适合做:

  1. 异常日志记录。
  2. 异常告警。
  3. 失败操作统计。

3.3 切入点表达式

切入点表达式用来指定:哪些方法需要被 AOP 增强。

常用的切入点表达式主要有两种:

  1. execution
  2. @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 的语法规则如下:

  1. 方法的访问修饰符可以省略。
  2. 返回值可以使用 * 号代替,表示任意返回值类型。
  3. 包名可以使用 * 号代替,表示任意包,一层包使用一个 *
  4. 使用 .. 配置包名,表示此包以及此包下的所有子包。
  5. 类名可以使用 * 号代替,表示任意类。
  6. 方法名可以使用 * 号代替,表示任意方法。
  7. 可以使用 * 配置参数,表示一个任意类型的参数。
  8. 可以使用 .. 配置参数,表示任意个任意类型的参数。

几个常见例子:

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 {
}

这个注解的含义是:

  1. @Target(ElementType.METHOD):只能标注在方法上。
  2. @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;
    }
}

这里:

  1. MyAspect1 是切面类。
  2. @Pointcut 定义切入点。
  3. @Before@Around 定义通知。
  4. 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 环绕后

也就是说:

  1. 进入目标方法之前,优先级高的切面先执行。
  2. 退出目标方法之后,优先级高的切面后结束。

这就像外层包装一样,@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 也没有问题。

但是如果项目中有多个切面,比如:

  1. 权限校验切面
  2. 参数校验切面
  3. 操作日志切面
  4. 方法耗时统计切面
  5. 事务相关切面

这时候最好明确指定顺序,避免执行顺序不清晰。

常见安排可以是:

text 复制代码
权限校验 -> 参数校验 -> 业务执行 -> 日志记录 / 耗时统计

对应到 @Order,可以让越需要提前执行的切面,数字越小:

java 复制代码
@Order(1) // 权限校验
@Order(2) // 参数校验
@Order(3) // 日志记录
@Order(4) // 耗时统计

这样代码的执行顺序就更加清楚,后期维护时也不容易出问题。

四、AOP 执行流程总结

@Around 统计耗时为例,执行流程大概是:

所以,AOP 并不是替代业务代码,而是在业务代码外面包了一层增强逻辑。

五、常见注意事项

  1. 使用 AOP 前一定要引入 spring-boot-starter-aop 依赖。
  2. 切面类要加 @Aspect@Component
  3. 环绕通知中必须调用 pjp.proceed(),否则目标方法不会执行。
  4. 有返回值的方法,环绕通知要把返回结果 return 出去。
  5. execution 表达式不要写得过大,避免误拦截。
  6. 使用 @annotation 时,自定义注解要设置 RetentionPolicy.RUNTIME
  7. 目标对象必须交给 Spring 容器管理,否则 AOP 不会生效。
  8. 切入点表达式中的括号要成对出现,少一个或多一个都会导致启动失败。

六、总结

这篇文章主要通过"统计方法执行耗时"这个场景,引出了 Spring AOP 的使用。

AOP 最核心的价值就是:把日志、耗时统计、权限校验这类重复逻辑从业务代码中抽取出来,统一放到切面中维护。

本文重点梳理了几个核心概念:

  1. 连接点:可以被 AOP 控制的方法执行位置。
  2. 通知:增强逻辑,比如 @Around@Before@AfterReturning@AfterThrowing
  3. 切入点表达式:决定哪些方法需要被增强。
  4. 切面:通知和切入点的组合。
  5. 目标对象:被增强的原始业务对象。

实际开发中,如果是统一增强某一层代码,可以优先考虑 execution;如果是精确控制某些方法,可以使用自定义注解配合 @annotation

掌握了这些概念之后,再看操作日志、权限校验、事务处理等功能时,就会发现它们背后的思想其实都是类似的:把通用逻辑抽出来,让业务代码保持干净。

相关推荐
zhougl9961 小时前
Maven build配置 补
java·maven
W.W.H.1 小时前
Qt 应用防多开:极简单例方案
开发语言·qt·单例模式·共享内存
Seven971 小时前
dubbo服务调用源码
java
枫叶v.1 小时前
Scrapling 入门:一个现代 Python 网页采集框架
开发语言·python
长谷深风1111 小时前
多线程并发实战:从原理到应用【个人八股】
java·并发编程·线程安全·java多线程·synchronized·锁升级
咖啡八杯1 小时前
GoF设计模式——原型模式
java·后端·设计模式·原型模式
枫叶丹41 小时前
【HarmonyOS 6.0】Enterprise Data Guard Kit:新增获取重置锁屏密码的企业恢复密钥能力详解
开发语言·华为·harmonyos
Ting-yu1 小时前
Spring AI Alibaba零基础速成(4) ---- Prompt(提示词)
java·人工智能·prompt
Dicky-_-zhang1 小时前
MySQL主从复制与读写分离实战
java·jvm