Spring之AOP面向切面编程详解

Spring之AOP面向切面编程详解

    • 一、AOP的核心思想与优势
    • 二、AOP的核心术语
    • [三、Spring AOP的实现方式](#三、Spring AOP的实现方式)
    • [四、Spring AOP实战:日志切面案例](#四、Spring AOP实战:日志切面案例)
      • [4.1 环境准备](#4.1 环境准备)
      • [4.2 目标业务类](#4.2 目标业务类)
      • [4.3 定义切面(Aspect)](#4.3 定义切面(Aspect))
      • [4.4 配置类与测试](#4.4 配置类与测试)
        • [4.4.1 Spring配置类](#4.4.1 Spring配置类)
        • [4.4.2 测试代码](#4.4.2 测试代码)
      • [4.5 执行结果与分析](#4.5 执行结果与分析)
    • [五、切入点表达式(Pointcut Expression)](#五、切入点表达式(Pointcut Expression))
      • [5.1 `execution`表达式语法](#5.1 execution表达式语法)
      • [5.2 常用表达式示例](#5.2 常用表达式示例)
      • [5.3 其他切入点表达式](#5.3 其他切入点表达式)
    • 六、AOP的高级应用:环绕通知与事务管理
      • [6.1 环绕通知(@Around)的高级用法](#6.1 环绕通知(@Around)的高级用法)
      • [6.2 Spring事务管理(AOP的典型应用)](#6.2 Spring事务管理(AOP的典型应用))
    • 七、常见问题与避坑指南
      • [7.1 切面不生效(通知未执行)](#7.1 切面不生效(通知未执行))
      • [7.2 自调用导致AOP失效](#7.2 自调用导致AOP失效)
      • [7.3 环绕通知未执行目标方法](#7.3 环绕通知未执行目标方法)

AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架的核心特性之一,它通过"横切"思想,将日志、事务、权限等通用功能从业务逻辑中分离,实现代码解耦与复用,掌握AOP是编写优雅Spring代码的关键。

一、AOP的核心思想与优势

1.1 什么是AOP?

AOP是一种编程范式,核心是将"横切关注点"(如日志、事务)与"核心业务逻辑"分离。传统OOP(面向对象)通过继承和组合纵向组织代码,而AOP通过"切面"横向切入多个类的通用功能。

示例:日志功能的两种实现
  • 传统方式:在每个业务方法中手动添加日志代码(冗余、耦合);
  • AOP方式:定义日志切面,通过配置指定需要添加日志的方法,无需修改业务代码。

1.2 AOP的核心优势

  1. 代码解耦:通用功能(如日志)与业务逻辑分离,业务类只关注核心逻辑;
  2. 代码复用:横切关注点只需实现一次,可应用到多个目标方法;
  3. 便于维护:修改通用功能(如日志格式)只需调整切面,无需修改所有业务类;
  4. 集中管控:如事务管理、权限校验等可通过AOP集中控制。

二、AOP的核心术语

理解AOP术语是学习的基础,核心术语如下:

术语 说明 示例
切面(Aspect) 封装横切关注点的类(如日志切面、事务切面) LogAspect
连接点(JoinPoint) 程序执行过程中的可切入点(如方法调用、异常抛出) 所有方法的执行过程
切入点(Pointcut) 被AOP选中的连接点(需通过表达式指定) execution(* com.example.service.*.*(..))(匹配service包下所有方法)
通知(Advice) 切面在切入点执行的操作(如日志打印) 前置通知(方法执行前)、后置通知(方法执行后)
目标对象(Target) 被切入的业务对象(如UserService) UserService的实例
代理对象(Proxy) AOP生成的目标对象的代理,用于执行切面逻辑 Spring通过JDK动态代理或CGLIB生成的代理对象

通知(Advice)的类型

Spring AOP提供5种通知类型,覆盖方法执行的全生命周期:

  1. 前置通知(@Before):目标方法执行前执行;
  2. 后置通知(@After):目标方法执行后执行(无论是否抛出异常);
  3. 返回通知(@AfterReturning):目标方法正常返回后执行;
  4. 异常通知(@AfterThrowing):目标方法抛出异常后执行;
  5. 环绕通知(@Around):包裹目标方法,可控制目标方法的执行(最强大)。

三、Spring AOP的实现方式

Spring AOP基于动态代理实现,支持两种代理方式:

  • JDK动态代理:默认方式,代理接口(目标对象需实现接口);
  • CGLIB代理:目标对象无接口时使用,通过继承目标类实现代理。

Spring会自动选择代理方式,开发者无需手动干预。

四、Spring AOP实战:日志切面案例

以"日志切面"为例,演示Spring AOP的完整使用流程(基于注解配置)。

4.1 环境准备

添加Spring AOP依赖(Maven):

xml 复制代码
<dependencies>
    <!-- Spring核心 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.20</version>
    </dependency>
    <!-- Spring AOP -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.3.20</version>
    </dependency>
</dependencies>

4.2 目标业务类

定义一个Service作为目标对象(被切入的类):

java 复制代码
package com.example.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
    // 目标方法1:查询用户
    public String getUserById(Integer id) {
        System.out.println("执行getUserById:查询ID为" + id + "的用户");
        return "用户" + id;
    }

    // 目标方法2:新增用户(可能抛出异常)
    public void addUser(String username) {
        if (username == null || username.isEmpty()) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        System.out.println("执行addUser:新增用户" + username);
    }
}

4.3 定义切面(Aspect)

创建日志切面类,实现日志记录功能:

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

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 1. 标记为切面(@Aspect)和Spring组件(@Component)
@Aspect
@Component
public class LogAspect {

    // 2. 定义切入点(Pointcut):匹配UserService的所有方法
    @Pointcut("execution(* com.example.service.UserService.*(..))")
    public void userServicePointcut() {} // 切入点签名(无实际逻辑)


    // 3. 前置通知:目标方法执行前打印请求参数
    @Before("userServicePointcut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName(); // 获取方法名
        Object[] args = joinPoint.getArgs(); // 获取方法参数
        System.out.println("[前置通知] " + methodName + " 方法参数:" + Arrays.toString(args));
    }


    // 4. 返回通知:目标方法正常返回后打印返回值
    @AfterReturning(
        pointcut = "userServicePointcut()",
        returning = "result" // 绑定返回值
    )
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[返回通知] " + methodName + " 方法返回值:" + result);
    }


    // 5. 异常通知:目标方法抛出异常后打印异常信息
    @AfterThrowing(
        pointcut = "userServicePointcut()",
        throwing = "ex" // 绑定异常对象
    )
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[异常通知] " + methodName + " 方法抛出异常:" + ex.getMessage());
    }


    // 6. 后置通知:目标方法执行后(无论是否异常)打印结束信息
    @After("userServicePointcut()")
    public void afterAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[后置通知] " + methodName + " 方法执行结束");
    }


    // 7. 环绕通知:包裹目标方法,可控制执行时机(最灵活)
    @Around("userServicePointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        long startTime = System.currentTimeMillis();

        try {
            // 执行目标方法(必须调用,否则目标方法不执行)
            Object result = joinPoint.proceed(); 
            long endTime = System.currentTimeMillis();
            System.out.println("[环绕通知] " + methodName + " 方法执行耗时:" + (endTime - startTime) + "ms");
            return result; // 返回目标方法结果
        } catch (Throwable e) {
            // 可处理异常
            throw e; // 抛出异常,让异常通知捕获
        }
    }
}

4.4 配置类与测试

4.4.1 Spring配置类
java 复制代码
package com.example.config;

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

@Configuration
@ComponentScan("com.example") // 扫描组件(包括切面和Service)
@EnableAspectJAutoProxy // 开启AOP注解支持
public class SpringConfig {
}
4.4.2 测试代码
java 复制代码
package com.example;

import com.example.config.SpringConfig;
import com.example.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopTest {
    public static void main(String[] args) {
        // 加载Spring配置,启动容器
        AnnotationConfigApplicationContext context = 
            new AnnotationConfigApplicationContext(SpringConfig.class);
        UserService userService = context.getBean(UserService.class);

        System.out.println("===== 测试正常方法 =====");
        userService.getUserById(1); // 调用无异常的方法

        System.out.println("\n===== 测试异常方法 =====");
        try {
            userService.addUser(null); // 调用会抛出异常的方法
        } catch (Exception e) {
            // 捕获异常(不影响程序执行)
        }

        context.close();
    }
}

4.5 执行结果与分析

复制代码
===== 测试正常方法 =====
[前置通知] getUserById 方法参数:[1]
执行getUserById:查询ID为1的用户
[返回通知] getUserById 方法返回值:用户1
[后置通知] getUserById 方法执行结束
[环绕通知] getUserById 方法执行耗时:5ms

===== 测试异常方法 =====
[前置通知] addUser 方法参数:[null]
执行addUser:新增用户null
[异常通知] addUser 方法抛出异常:用户名不能为空
[后置通知] addUser 方法执行结束

结果分析

  • 所有通知按预期执行,日志成功记录;
  • getUserById正常执行:触发前置→目标方法→返回→后置→环绕(耗时统计);
  • addUser抛出异常:触发前置→目标方法→异常→后置(无返回通知,因方法未正常返回)。

五、切入点表达式(Pointcut Expression)

切入点表达式用于指定"哪些方法需要被切入",是AOP的核心配置,Spring AOP支持多种表达式,最常用的是execution

5.1 execution表达式语法

复制代码
execution(修饰符? 返回值类型 包名.类名.方法名(参数类型) 异常类型?)
  • ?表示可选;
  • *表示任意(如任意返回值、任意方法名);
  • ..表示任意子包或任意参数。

5.2 常用表达式示例

表达式 说明
execution(* com.example.service.*.*(..)) 匹配com.example.service包下所有类的所有方法
execution(public * com.example..*Service.*(..)) 匹配com.example及其子包中所有以Service结尾的类的public方法
execution(* com.example.service.UserService.get*(Integer)) 匹配UserService中以get开头、参数为Integer的方法
execution(* com.example.service.UserService.*(String, ..)) 匹配UserService中第一个参数为String的方法

5.3 其他切入点表达式

  • @annotation:匹配标注特定注解的方法(如@Transactional);

    java 复制代码
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
  • within:匹配特定包或类的所有方法;

    java 复制代码
    @Pointcut("within(com.example.service..*)") // 匹配service包及其子包的所有类
  • args:匹配参数类型符合指定条件的方法;

    java 复制代码
    @Pointcut("args(Integer, String)") // 匹配第一个参数为Integer、第二个为String的方法

六、AOP的高级应用:环绕通知与事务管理

6.1 环绕通知(@Around)的高级用法

环绕通知是最灵活的通知类型,可控制目标方法的执行(如超时控制、重试机制)。

示例:实现方法重试(失败后重试)
java 复制代码
@Around("userServicePointcut()")
public Object retryAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    int maxRetry = 3; // 最大重试次数
    int retryCount = 0;

    while (retryCount < maxRetry) {
        try {
            return joinPoint.proceed(); // 执行目标方法
        } catch (Exception e) {
            retryCount++;
            if (retryCount >= maxRetry) {
                throw e; // 达到最大次数,抛出异常
            }
            System.out.println("方法执行失败,第" + retryCount + "次重试...");
        }
    }
    throw new RuntimeException("重试次数耗尽");
}

6.2 Spring事务管理(AOP的典型应用)

Spring的声明式事务(@Transactional)本质是AOP的应用:

  • 切面:Spring内置的事务切面;
  • 切入点 :标注@Transactional的方法;
  • 通知:事务切面在目标方法执行前开启事务,执行后提交/回滚。
java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    // 事务管理(AOP自动切入)
    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order); // 插入订单
        orderMapper.updateStock(order.getProductId()); // 更新库存
        // 若任意操作失败,AOP会自动回滚事务
    }
}

七、常见问题与避坑指南

7.1 切面不生效(通知未执行)

原因

  • 切面类未添加@Component(未被Spring扫描);
  • 未添加@EnableAspectJAutoProxy(未开启AOP支持);
  • 切入点表达式错误(未匹配到目标方法);
  • 目标类未被Spring管理(如手动new的对象,非容器中的Bean)。

解决方案

  • 确保切面类有@Aspect@Component
  • 配置类添加@EnableAspectJAutoProxy
  • 通过org.springframework.aop的DEBUG日志排查切入点匹配情况。

7.2 自调用导致AOP失效

问题:目标类内部方法调用(自调用)时,AOP通知不执行。

java 复制代码
@Service
public class UserService {
    public void methodA() {
        methodB(); // 自调用,AOP不生效
    }

    @Transactional // 事务AOP在自调用时不生效
    public void methodB() { ... }
}

原因:AOP通过代理对象生效,自调用是目标对象内部调用,未经过代理。

解决方案

  • 避免自调用,或通过容器获取代理对象调用;
  • 配置exposeProxy=true,通过AopContext.currentProxy()获取代理对象。

7.3 环绕通知未执行目标方法

问题 :环绕通知未调用proceed(),导致目标方法不执行。

java 复制代码
@Around("userServicePointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
    // 错误:未调用joinPoint.proceed()
    System.out.println("环绕通知");
    return null; // 目标方法未执行
}

解决方案 :环绕通知必须调用joinPoint.proceed(),否则目标方法会被拦截。

总结:AOP的核心要点与最佳实践

AOP通过"横切"思想解决了通用功能与业务逻辑的耦合问题,核心要点在于分离关注点

Spring AOP的最佳实践:

  1. 合理设计切面:一个切面专注一个功能(如日志切面、事务切面),避免大而全的切面;
  2. 精准切入点:切入点表达式尽量精确(如限定包、类、方法名),避免过度切入;
  3. 选择合适通知类型
  • 日志记录:前置+返回/异常通知;
  • 性能监控:环绕通知(需统计耗时);
  • 资源清理:后置通知(无论是否异常都需执行);
  1. 注意代理限制:避免自调用,确保目标对象是Spring容器管理的Bean;
  2. 结合注解使用 :通过@annotation切入点,实现灵活的注解驱动AOP(如自定义@Log注解标记需要日志的方法)。
    若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!

ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ

相关推荐
伊布拉西莫21 小时前
Spring 6.x HTTP interface 使用说明
spring·restclient
YDS8291 天前
苍穹外卖 —— Spring Cache和购物车功能开发
java·spring boot·后端·spring·mybatis
Elieal1 天前
Spring 框架核心技术全解析
java·spring·sqlserver
组合缺一1 天前
(对标 Spring)OpenSolon v3.7.0, v3.6.4, v3.5.8, v3.4.8 发布(支持 LTS)
java·后端·spring·web·solon
♡喜欢做梦1 天前
Spring IOC
java·后端·spring
葡萄城技术团队1 天前
迎接下一代 React 框架:Next.js 16 核心能力解读
javascript·spring·react.js
灰小猿1 天前
Spring前后端分离项目时间格式转换问题全局配置解决
java·前端·后端·spring·spring cloud
知其然亦知其所以然2 天前
这波AI太原生了!SpringAI让PostgreSQL秒变智能数据库!
后端·spring·postgresql
zhaomx19892 天前
Spring 事务管理 Transaction rolled back because it has been marked as rollback-only
数据库·spring
曹朋羽2 天前
Spring EL 表达式
java·spring·el表达式