Spring AOP:面向切面编程的优雅解耦之道

Spring AOP:面向切面编程的优雅解耦之道

在企业开发中,日志记录、事务管理、权限校验等如果直接写在业务代码中,会导致逻辑混杂、重复冗余、难以维护。

Spring AOP(面向切面编程) 正是为了解决这一问题而生。它通过"在不修改源码的前提下,动态织入通用逻辑"的方式,实现关注点分离,让核心业务更纯粹。

今天,我们就从思想本质、生活类比、实战用法到最佳实践,带你彻底掌握 Spring AOP

一、生活类比:智能家居中的"自动化场景"

想象你家里装了一套智能家居系统

  • 你每天晚上 10 点躺到床上,并不需要手动去关灯、拉窗帘、调低空调温度;

  • 而是提前设置了一个 "睡眠模式"自动化场景

    • 当你对语音助手说"我要睡觉了",
    • 系统自动执行一系列操作:关主灯、拉上窗帘、空调调至 26℃、关闭电视......

✅ 这些操作(关灯、调温等)并不是你"主动写进每个夜晚流程"的,而是通过一个独立的规则(切面) ,在特定触发点(你说"睡觉")动态插入执行。

二、什么是 AOP?

AOP 是一种编程范式,用于将与核心业务无关但又广泛使用的功能(如日志、事务)模块化为"切面(Aspect)",并在运行时动态织入目标方法。

概念 说明
Aspect(切面) 横切关注点的模块化,如日志切面、事务切面
Join Point(连接点) 程序执行过程中的某个点,如方法调用、异常抛出(Spring AOP 仅支持方法级)
Pointcut(切入点) 匹配哪些连接点要被拦截(通过表达式定义)
Advice(通知) 切面在特定连接点上执行的动作(前置、后置、环绕等)
Weaving(织入) 将切面应用到目标对象的过程(Spring 采用运行时动态代理)

✅ 本质:通过代理模式 + 表达式匹配,在方法调用前后插入通用逻辑

三、Spring AOP 实战五步走

第一步:pom 中新增依赖

xml 复制代码
<!-- Spring AOP 支持 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.2.12</version>
</dependency>

<!-- 若使用注解驱动 AOP,还需确保 spring-context 已引入 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.2.12</version>
</dependency>

💡 注意:spring-aspects 依赖了 AspectJ 的注解和运行时库,但 Spring AOP 本身不使用编译时织入,而是基于 JDK 动态代理或 CGLIB。

第二步:启用 AOP(配置类)

kotlin 复制代码
package org.example.spring.aop.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy // 启用 @AspectJ 注解风格的 AOP
@ComponentScan(basePackages = "org.example.spring.aop")
public class AopConfig {
    // 空配置类,仅用于开启 AOP
}

@EnableAspectJAutoProxy 是关键!它告诉 Spring 自动识别 @Aspect 并创建代理。

第三步:定义业务服务

typescript 复制代码
package org.example.spring.aop.service;

import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public void placeOrder(String orderId) {
        System.out.println("Processing order: " + orderId);
    }

    public String getOrderStatus(String orderId) {
        return "ORDER_CONFIRMED";
    }
}

第四步:编写切面(日志示例)

typescript 复制代码
package org.example.spring.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // 定义切入点:所有 service 包下的 public 方法
    @Pointcut("execution(public * org.example.spring.aop.service..*.*(..))")
    public void serviceMethods() {}

    @Before("serviceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("[AOP] Before method: " + methodName + ", args: " + java.util.Arrays.toString(args));
    }

    @AfterReturning(pointcut = "serviceMethods()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[AOP] After method: " + methodName + ", returned: " + result);
    }
}

第五步:启动容器并测试

arduino 复制代码
package org.example.spring.aop;

import org.example.spring.aop.config.AopConfig;
import org.example.spring.aop.service.OrderService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        var context = new AnnotationConfigApplicationContext(AopConfig.class);
        OrderService orderService = context.getBean(OrderService.class);

        orderService.placeOrder("ORD-12345");
        System.out.println("---");
        orderService.getOrderStatus("ORD-12345");

        context.close();
    }
}

输出:

yaml 复制代码
[AOP] Before method: placeOrder, args: [ORD-12345]
Processing order: ORD-12345
[AOP] After method: placeOrder, returned: null
---
[AOP] Before method: getOrderStatus, args: [ORD-12345]
[AOP] After method: getOrderStatus, returned: ORDER_CONFIRMED

四、一张表总结 Spring AOP 核心要点

要素 注解 / 配置 说明
启用 AOP @EnableAspectJAutoProxy 必须在配置类上声明
定义切面 @Aspect + @Component 切面本身也是 Spring Bean
定义切入点 @Pointcut("execution(...)") 使用 AspectJ 表达式
通知类型 @Before, @After, @AfterReturning, @AfterThrowing, @Around 环绕通知最强大
代理机制 JDK 动态代理(接口) / CGLIB(类) Spring 自动选择

⚠️ 注意:只有通过 Spring 容器获取的 Bean 才会被代理! 直接 new 的对象不会触发 AOP。

五、思考题

以下说法是否正确?

"在同一个类中,方法 A 调用本类的方法 B(带 @Transactional ),AOP 会生效。"
💡 答案:不会生效!

原因:

  • Spring AOP 基于代理对象,只有外部调用才会经过代理;
  • 类内部的自调用(this.methodB())绕过了代理,直接调用原始对象方法;
  • 解决方案:通过 AopContext.currentProxy() 获取当前代理,或重构为两个 Bean 互相调用。

📌 关注我 ,每天5分钟,带你从 Java 小白变身编程高手!

👉 点赞 + 关注,让更多小伙伴一起进步!

相关推荐
萝卜青今天也要开心1 小时前
2025年下半年系统架构设计师考后分享
java·数据库·redis·笔记·学习·系统架构
Unstoppable221 小时前
八股训练营第 39 天 | Bean 的作用域?Bean 的生命周期?Spring 循环依赖是怎么解决的?Spring 中用到了那些设计模式?
java·spring·设计模式
程序员根根1 小时前
JavaSE 进阶:多线程核心知识点(线程创建 vs 线程安全 + 线程池优化 + 实战案例
java
阿伟*rui1 小时前
互联网大厂Java面试:音视频场景技术攻防与系统设计深度解析
java·redis·websocket·面试·音视频·高并发·后端架构
qq_348231851 小时前
Spring AI核心知识点
java·人工智能·spring
关于不上作者榜就原神启动那件事2 小时前
【java后端开发问题合集】
java·开发语言
徐_三岁2 小时前
Python 入门学习
java·python·学习
500842 小时前
鸿蒙 Flutter 接入鸿蒙系统能力:通知(本地 / 推送)与后台任务
java·flutter·华为·性能优化·架构
脸大是真的好~2 小时前
尚硅谷-Kafka02-主题创建-生产数据
java