【一文吃透 Spring Boot 面向切面编程(AOP):实例\+实现\+注意事项】

面向切面编程(AOP,Aspect-Oriented Programming)是 Spring 框架的核心特性之一,它通过"横切"思想,将日志、权限校验、事务管理等通用功能与业务逻辑解耦,大幅提升代码复用性和可维护性。本文将从「实例演示→实现方式→核心细节→避坑注意事项」,一步步带你掌握 AOP,内容适合新手入门,也可作为实战参考,直接用于项目开发。

一、先搞懂:AOP 核心概念(新手必看)

在写代码前,先明确 AOP 的核心术语,避免后续 confusion,用通俗的话解释,不堆砌专业名词:

  • 切面(Aspect):就是我们要实现的通用功能(比如日志、权限),是一个独立的类,包含通知和切入点。

  • 通知(Advice):切面的具体逻辑(比如日志要打印什么内容),分 5 种类型(后文实例会逐个演示)。

  • 切入点(Pointcut):指定切面作用在哪些方法上(比如所有 controller 方法、带特定注解的方法)。

  • 连接点(JoinPoint):所有可能被切面拦截的方法(比如项目中所有的接口方法),切入点是连接点的子集。

  • 目标对象(Target):被切面拦截的对象(比如我们的业务ServiceImpl)。

一句话总结:用切面(Aspect)定义通用功能(通知),并指定作用在哪些方法(切入点)上,实现与业务逻辑的解耦

二、实战实例:用 AOP 实现「接口日志记录」

我们以最常用的「接口请求日志记录」为实例,实现以下功能:

  1. 记录请求的 URL、请求方法(GET/POST)、请求参数、请求者 IP;

  2. 记录接口执行耗时;

  3. 记录接口返回结果(成功/失败);

  4. 接口抛出异常时,记录异常信息。

技术栈:Spring Boot 2.7.x + Spring AOP(Spring 自带,无需额外引入依赖)

步骤 1:创建 Spring Boot 项目,引入依赖

创建 Spring Boot 项目后,无需额外引入 AOP 依赖(Spring Boot 已自动集成 spring-boot-starter-aop),pom.xml 核心依赖如下(可直接复制):

xml 复制代码
<!-- Spring Boot 核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring AOP 依赖(Spring Boot 自动集成,可省略,但建议显式引入,清晰) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- 日志依赖(方便格式化输出,可选) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

步骤 2:编写 AOP 切面类(核心代码)

创建切面类 LogAspect,通过注解定义切面、通知和切入点,代码中包含详细注释,新手可直接复制使用:

java 复制代码
package com.example.aop.demo.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * 接口日志切面类
 * 注解说明:
 * @Aspect:标识此类是一个切面类
 * @Component:将切面类交给 Spring 管理(必须加,否则切面不生效)
 */
@Slf4j
@Aspect
@Component
public class LogAspect {

    /**
     * 切入点:指定切面作用的范围
     * 表达式说明:
     * execution(* com.example.aop.demo.controller..*(..))
     * 1. *:返回值任意
     * 2. com.example.aop.demo.controller:包名(可替换为你的 controller 包路径)
     * 3. ..:当前包及子包下的所有类
     * 4. *(..):所有方法(任意参数、任意方法名)
     */
    @Pointcut("execution(* com.example.aop.demo.controller..*(..))")
    public void logPointCut() {
        // 切入点方法,无需实现任何逻辑,仅用于定义切入点表达式
    }

    /**
     * 1. 前置通知(@Before):在目标方法执行前执行
     * 作用:记录请求的基础信息(URL、IP、请求方法、参数)
     */
    @Before("logPointCut()")
    public void doBefore(JoinPoint joinPoint) {
        // 获取请求上下文
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 打印请求信息
        log.info("=== 接口请求开始 ===");
        log.info("请求URL:{}", request.getRequestURI());
        log.info("请求方法:{}", request.getMethod());
        log.info("请求IP:{}", request.getRemoteAddr());
        log.info("目标方法:{}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.info("请求参数:{}", Arrays.toString(joinPoint.getArgs()));
    }

    /**
     * 2. 环绕通知(@Around):在目标方法执行前后都执行,可控制目标方法的执行
     * 作用:计算接口执行耗时(最常用场景)
     * 注意:环绕通知必须有返回值,且参数是 ProceedingJoinPoint(可执行目标方法)
     */
    @Around("logPointCut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 记录方法开始执行时间
        long startTime = System.currentTimeMillis();

        // 执行目标方法(放行,让业务方法执行)
        Object result = proceedingJoinPoint.proceed();

        // 计算执行耗时
        long endTime = System.currentTimeMillis();
        log.info("接口执行耗时:{} ms", endTime - startTime);

        // 返回目标方法的执行结果(必须返回,否则接口会返回 null)
        return result;
    }

    /**
     * 3. 后置通知(@After):无论目标方法是否抛出异常,都会执行
     * 作用:标记请求结束,做一些收尾工作
     */
    @After("logPointCut()")
    public void doAfter() {
        log.info("=== 接口请求结束 ===\n");
    }

    /**
     * 4. 后置返回通知(@AfterReturning):目标方法正常执行完成后执行
     * 作用:记录接口返回结果
     * returning = "result":指定返回值的参数名,必须和方法参数一致
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "result")
    public void doAfterReturning(Object result) {
        log.info("接口返回结果:{}", result);
    }

    /**
     * 5. 后置异常通知(@AfterThrowing):目标方法抛出异常时执行
     * 作用:记录异常信息,方便排查问题
     * throwing = "e":指定异常参数名,必须和方法参数一致
     */
    @AfterThrowing(pointcut = "logPointCut()", throwing = "e")
    public void doAfterThrowing(Throwable e) {
        log.error("接口执行异常:", e); // 打印异常堆栈信息
    }
}}

步骤 3:编写测试接口,验证 AOP 效果

创建一个简单的 Controller,编写两个接口(正常接口 + 异常接口),测试 AOP 是否生效:

java 复制代码
package com.example.aop.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    // 正常接口:模拟查询用户
    @GetMapping("/user/get")
    public String getUser(@RequestParam String username) {
        return "查询成功,用户:" + username;
    }

    // 异常接口:模拟抛出异常
    @PostMapping("/user/error")
    public void testError() {
        int i = 1 / 0; // 除数为0,抛出异常
    }
}

步骤 4:启动项目,测试效果

  1. 启动 Spring Boot 项目,访问正常接口:http://localhost:8080/user/get?username=zhangsan

控制台输出(AOP 日志生效):

text 复制代码
=== 接口请求开始 ===
请求URL:/user/get
请求方法:GET
请求IP:0:0:0:0:0:0:0:1
目标方法:com.example.aop.demo.controller.TestController.getUser
请求参数:[zhangsan]
接口返回结果:查询成功,用户:zhangsan
接口执行耗时:12 ms
=== 接口请求结束 ===
  1. 访问异常接口:http://localhost:8080/user/error

控制台输出(异常日志生效):

text 复制代码
=== 接口请求开始 ===
请求URL:/user/error
请求方法:POST
请求IP:0:0:0:0:0:0:0:1
目标方法:com.example.aop.demo.controller.TestController.testError
请求参数:[]
接口执行异常:
java.lang.ArithmeticException: / by zero
    at com.example.aop.demo.controller.TestController.testError(TestController.java:18)
    ...(省略异常堆栈)
=== 接口请求结束 ===

至此,AOP 实例演示完成,完美实现了接口日志的统一记录,且没有侵入任何业务代码(TestController 中没有任何日志相关代码)。

三、AOP 三种实现方式(从简单到复杂)

上面实例用的是「注解式实现」(最常用),除此之外,Spring AOP 还有另外两种实现方式,适合不同场景,逐一说明:

方式 1:注解式实现(@Aspect + 注解通知)

就是上面实例用的方式,也是目前 Spring Boot 项目中最推荐的方式,优点:

  • 代码简洁,无需配置文件,上手快;

  • 灵活度高,切入点表达式可精准控制作用范围;

  • 支持所有 5 种通知类型。

核心要点:必须加 @Aspect@Component 注解,切入点用 @Pointcut 定义,通知用 @Before@Around 等注解。

方式 2:XML 配置式实现(传统方式)

在 Spring 早期(没有注解),主要用 XML 配置实现 AOP,现在很少用,但了解即可,适合老项目维护。

步骤:1. 编写切面类(无需加任何 AOP 注解);2. 在 spring.xml 中配置切面、切入点、通知。

示例(简化版):

xml 复制代码
<!-- 1. 配置切面类 -->
<bean id="logAspect" class="com.example.aop.demo.aspect.LogAspect"/>

<!-- 2. 配置 AOP -->
<aop:config>
    <!-- 配置切入点 -->
    <aop:pointcut id="logPointCut" expression="execution(* com.example.aop.demo.controller..*(..))"/>
    
    <!-- 配置切面,关联切入点和通知 -->
    <aop:aspect ref="logAspect">
        <aop:before pointcut-ref="logPointCut" method="doBefore"/>
        <aop:around pointcut-ref="logPointCut" method="doAround"/>
        <aop:after pointcut-ref="logPointCut" method="doAfter"/>
    </aop:aspect>
</aop:config>

缺点:XML 配置繁琐,维护成本高,不推荐新项目使用。

方式 3:实现接口式实现(最原始方式)

通过实现 Spring 提供的 AOP 接口(如 MethodBeforeAdviceAfterReturningAdvice)来实现通知,仅支持部分通知类型(无环绕通知),灵活性差,几乎不用。

示例(简化版):

java 复制代码
// 实现前置通知接口
@Component
public class LogBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        // 前置通知逻辑(和 @Before 一样)
        log.info("前置通知:方法执行前执行");
    }
}

// XML 配置切入点(省略,和方式2类似)

总结:优先使用 注解式实现,XML 方式仅用于老项目维护,接口式实现可忽略。

四、AOP 核心注意事项(避坑关键,重中之重)

很多新手用 AOP 时会遇到"切面不生效""通知执行顺序错乱"等问题,以下注意事项必须牢记,覆盖 90% 的坑:

1. 切面类必须交给 Spring 管理(必坑)

切面类上必须加 @Component(或 @Service@Controller),否则 Spring 无法扫描到切面,AOP 完全不生效。

错误示例:只加 @Aspect,不加 @Component → 切面无效。

2. 切入点表达式必须正确(必坑)

切入点表达式写错,切面会作用到错误的方法,甚至完全不生效,常见错误:

  • 包路径写错(比如 controller 包路径少写一层);

  • 表达式语法错误(比如少写\.\.\* 位置错误);

  • 目标方法是 private 修饰 → AOP 无法拦截(AOP 只能拦截 public 方法)。

推荐:写完切入点后,先测试一个简单接口,验证切面是否生效。

3. 环绕通知的注意事项(高频坑)

  • 必须调用 proceedingJoinPoint\.proceed\(\),否则目标方法不会执行(相当于拦截了方法,不放行);

  • 必须返回 proceed\(\) 的结果,否则接口会返回 null(即使目标方法有返回值);

  • 环绕通知可以修改目标方法的参数和返回值(灵活,但需谨慎)。

4. 通知执行顺序(重点)

当一个方法被多个切面拦截,或一个切面有多个通知时,执行顺序如下(牢记):

正常情况:前置通知(@Before)→ 环绕通知(proceed() 前)→ 目标方法 → 环绕通知(proceed() 后)→ 后置返回通知(@AfterReturning)→ 后置通知(@After)

异常情况:前置通知 → 环绕通知(proceed() 前)→ 目标方法(抛异常)→ 后置异常通知(@AfterThrowing)→ 后置通知(@After)

补充:多个切面的执行顺序,可通过 @Order 注解控制(值越小,执行越早)。

5. AOP 无法拦截的情况(高频坑)

  • private、final、static 修饰的方法(AOP 基于动态代理,无法代理这些方法);

  • 方法内部调用(比如同一个类中,方法 A 调用方法 B,方法 B 被切面拦截 → 不生效);

  • 目标对象没有交给 Spring 管理(比如 new 出来的对象,不是 Spring 容器中的 bean)。

解决方案:内部方法调用时,通过 Spring 容器获取自身 bean,再调用方法(避免直接 this.方法名())。

6. 性能注意事项

  • 切面逻辑尽量简单(比如日志记录、参数校验),避免复杂计算,影响接口性能;

  • 切入点范围尽量精准(比如只作用于 controller 层,不要作用于所有层),减少拦截次数。

7. 注解式切入点(补充,更灵活)

除了 execution 表达式,还可以用自定义注解作为切入点,更灵活(比如只给需要日志的接口加注解):

java 复制代码
// 1. 定义自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnnotation {
    // 可添加属性,比如日志描述
    String value() default "";
}

// 2. 切入点改为注解
@Pointcut("@annotation(com.example.aop.demo.annotation.LogAnnotation)")
public void logPointCut() {}

// 3. 在接口上添加注解
@LogAnnotation("查询用户接口")
@GetMapping("/user/get")
public String getUser(@RequestParam String username) { ... }

五、AOP 常见应用场景(拓展,提升博客价值)

除了日志记录,AOP 还有很多实用场景,帮你拓展思路:

  1. 权限校验:拦截接口,判断用户是否登录、是否有操作权限;

  2. 事务管理:Spring 声明式事务(@Transactional)底层就是 AOP;

  3. 参数校验:统一校验接口请求参数(比如非空、格式校验);

  4. 接口限流:限制接口的请求频率(比如每秒最多10次请求);

  5. 异常统一处理:全局异常捕获,返回统一的错误格式。

六、总结

AOP 的核心是「解耦」,将通用功能抽离成切面,不侵入业务代码,提升代码复用性和可维护性。本文通过「接口日志」实例,讲解了注解式实现(最常用)、XML 实现、接口式实现三种方式,同时总结了新手常踩的坑和注意事项。

对于 CSDN 博客读者来说,本文代码可直接复制到项目中使用,同时理解核心原理,后续可根据实际场景(如权限、限流)灵活修改切面逻辑。

最后,留一个小思考:如何用 AOP 实现接口限流?欢迎在评论区交流~

相关推荐
fengxin_rou2 小时前
JVM 核心笔记:对象创建、生命周期与类加载器详解
java·jvm·笔记
one_love_zfl2 小时前
java面试-JVM篇
java·jvm·面试
skiy2 小时前
Spring之DataSource配置
java·后端·spring
石榴树下的七彩鱼2 小时前
医疗票据OCR识别API实战:从医保结算单到结构化数据提取(附Python/Java示例)
java·人工智能·python·ocr·api·ocr识别·医疗票据识别
Cat_Rocky2 小时前
k8s-单Master集群部署(简练理解)
java·容器·kubernetes
C雨后彩虹2 小时前
投篮大赛问题
java·数据结构·算法·华为·面试
Hello eveybody2 小时前
介绍最大公因数和最小公约数(C++)
java·开发语言·c++
ckhcxy2 小时前
抽象类和接口
java·开发语言
Gerardisite2 小时前
私域运营新利器:RPA驱动外部群多模态互动
java·人工智能·python·微信·自动化