Spring AOP:企业级实战教学

在企业级Java开发中,Spring框架 是核心基础设施,而AOP(面向切面编程) 是Spring的两大核心特性之一(另一个是IOC/DI)。

OOP(面向对象编程)通过继承、封装、多态 处理纵向的业务逻辑,但对于横向通用逻辑(如日志记录、权限校验、事务管理、性能监控、全局异常处理),OOP会导致代码冗余、耦合度高。


基础概念

Spring AOP基于AspectJ切面规范实现,先掌握核心术语,是理解AOP的关键:

术语 英文 通俗解释 企业级类比
连接点 JoinPoint 程序中可以被拦截 的执行点(Spring中仅支持方法 所有员工的打卡机(所有可打卡的位置)
切点 Pointcut 真正需要拦截的连接点(筛选后的方法) 公司指定必须打卡的部门/岗位
通知 Advice 拦截后执行的增强逻辑(前置/后置/异常等) 打卡时的语音提示、考勤记录逻辑
切面 Aspect 【切点 + 通知】的结合体(Java类) 考勤管理模块(定义了谁打卡、打卡做什么)
目标对象 Target 被AOP增强的原始业务对象 打卡的员工
代理对象 Proxy Spring运行时生成的、包含增强逻辑的对象 员工+打卡逻辑的结合体
织入 Weaving 将切面逻辑嵌入目标方法的过程 把考勤逻辑绑定到员工打卡动作

核心总结

  1. Spring AOP 仅支持方法级别的拦截(不支持字段、构造器拦截);
  2. 底层基于动态代理实现:JDK动态代理(接口)、CGLIB代理(类);
  3. 运行时织入(区别于AspectJ的编译期织入),轻量无侵入。

开发环境准备

Spring Boot 对AOP提供自动配置,无需手动编写配置类,仅需引入依赖即可。

1. Maven依赖

xml 复制代码
<!-- Spring AOP 核心依赖(Spring Boot自动装配) -->
<!-- 无需写版本,Spring会自动调用与SpringBoot对应的版本 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 核心注解

Spring AOP 注解驱动(企业主流方式)核心注解:

  • @Aspect:标记当前类为切面类
  • @Pointcut:定义切点表达式(筛选拦截的方法);
  • 5种通知注解:@Before/@After/@AfterReturning/@AfterThrowing/@Around
  • @Component:将切面类交给Spring容器管理。

切点表达式

切点表达式用于精准筛选需要拦截的方法 。Spring支持多种切点表达式,execution 是企业最常用

1. execution 表达式(万能)

语法

复制代码
execution(修饰符 返回值类型 包名.类名.方法名(参数类型) 异常类型)
  • 修饰符:可省略(public/private/protected,Spring仅支持public);
  • *:通配符,匹配任意字符;
  • ..:匹配任意参数、任意包路径;
常用样例
java 复制代码
// 1. 拦截 com.example.service 包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")

// 2. 拦截 UserServiceImpl 类的 addUser 方法(参数为String+Integer)
@Pointcut("execution(* com.example.service.UserServiceImpl.addUser(String, Integer))")

// 3. 拦截 所有返回值为String的方法
@Pointcut("execution(String *(..))")

// 4. 拦截 所有public方法
@Pointcut("execution(public * *(..))")

2. 其他常用切点表达式

表达式 作用 样例
within 拦截指定类/包下的所有方法 within(com.example.service.*)
@annotation 拦截添加了自定义注解的方法 @annotation(com.example.anno.Log)
args 拦截参数类型匹配的方法 args(Long, String)
bean 拦截指定名称的Spring Bean bean(userServiceImpl)

五种通知类型

通知是AOP的增强逻辑,Spring提供5种标准通知,覆盖所有业务增强场景。

前置准备:业务接口+实现类

先编写一个基础业务类,用于AOP增强测试:

java 复制代码
// 1. 业务接口
public interface UserService {
    String addUser(String username, Integer age);
    void deleteUser(Long id);
}

// 2. 业务实现类
@Service
public class UserServiceImpl implements UserService {

    @Override
    public String addUser(String username, Integer age) {
        System.out.println("【业务逻辑】执行添加用户:" + username);
        // 模拟异常:if(age < 0) throw new RuntimeException("年龄不能为负数");
        return "用户添加成功";
    }

    @Override
    public void deleteUser(Long id) {
        System.out.println("【业务逻辑】执行删除用户:" + id);
    }
}

类型1:前置通知 @Before

作用 :目标方法执行之前 执行增强逻辑。
场景:参数校验、权限预检查、日志打印。

java 复制代码
@Aspect
@Component
public class LogAspect {

    /**
     * 切点:拦截 UserServiceImpl 下的所有方法
     * execution(返回值 包名.类名.方法名(参数))
     */
    @Pointcut("execution(* com.example.service.UserServiceImpl.*(..))")
    public void pointcut() {}

    // 前置通知
    @Before("pointcut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("===== 前置通知 =====");
        System.out.println("目标方法名:" + joinPoint.getSignature().getName());
        System.out.println("目标方法参数:" + Arrays.toString(joinPoint.getArgs()));
    }
}

类型2:后置通知 @After

作用 :目标方法执行之后 执行(无论方法正常返回/抛出异常)。
场景:资源释放、日志收尾。

java 复制代码
// 后置通知
@After("pointcut()")
public void afterAdvice() {
    System.out.println("===== 后置通知 =====");
}

类型3:返回通知 @AfterReturning

作用 :目标方法正常返回结果后 执行,可获取返回值。
场景:记录返回结果、数据二次封装。

java 复制代码
// 返回通知:returning = "result" 绑定返回值参数名
@AfterReturning(pointcut = "pointcut()", returning = "result")
public void afterReturningAdvice(Object result) {
    System.out.println("===== 返回通知 =====");
    System.out.println("方法返回值:" + result);
}

类型4:异常通知 @AfterThrowing

作用 :目标方法抛出异常后 执行,可获取异常信息。
场景:全局异常捕获、异常告警、日志记录。

java 复制代码
// 异常通知:throwing = "ex" 绑定异常参数名
@AfterThrowing(pointcut = "pointcut()", throwing = "ex")
public void afterThrowingAdvice(Exception ex) {
    System.out.println("===== 异常通知 =====");
    System.out.println("异常信息:" + ex.getMessage());
}

类型5:环绕通知 @Around(最实用)

作用 :包裹目标方法的执行,可控制目标方法是否执行、修改参数/返回值、捕获异常
场景:性能监控、事务管理、限流、缓存。

注意:环绕通知必须接收 ProceedingJoinPoint 参数(JoinPoint的子类),必须调用 proceed() 执行目标方法。

java 复制代码
// 环绕通知
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
    System.out.println("===== 环绕通知-方法执行前 =====");

    // 执行目标方法(必须调用,否则业务方法不执行)
    Object result = pjp.proceed();

    System.out.println("===== 环绕通知-方法执行后 =====");
    System.out.println("方法执行耗时:" + (System.currentTimeMillis() - start) + "ms");

    // 可修改返回值
    // return "修改后的返回值";
    return result;
}

五种通知执行顺序

  1. 正常执行:
    环绕通知前 -> 前置通知 -> 业务方法 -> 返回通知 -> 后置通知 -> 环绕通知后
  2. 异常执行:
    环绕通知前 -> 前置通知 -> 业务方法抛异常 -> 异常通知 -> 后置通知

高级特性

1. 获取连接点信息

所有通知都可通过 JoinPoint 获取目标方法的详细信息:

java 复制代码
// 获取方法签名
Signature signature = joinPoint.getSignature();
// 方法名
String methodName = signature.getName();
// 类名
String className = joinPoint.getTarget().getClass().getName();
// 方法参数
Object[] args = joinPoint.getArgs();

2. 自定义注解+AOP(主流)

通过自定义注解标记需要增强的方法,灵活性极高。

步骤1:自定义注解
java 复制代码
@Target(ElementType.METHOD) // 作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface OperationLog {
    // 操作描述
    String value() default "";
    // 操作类型
    String type() default "查询";
}
步骤2:编写切面
java 复制代码
@Aspect
@Component
public class OperationLogAspect {

    // 拦截所有添加了 @OperationLog 注解的方法
    @Pointcut("@annotation(com.example.anno.OperationLog)")
    public void logPointcut() {}

    @Around("logPointcut()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        // 获取注解信息
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        OperationLog log = signature.getMethod().getAnnotation(OperationLog.class);

        System.out.println("【操作日志】操作:" + log.value() + ",类型:" + log.type());
        Object result = pjp.proceed();
        System.out.println("【操作日志】操作执行完成");
        return result;
    }
}
步骤3:使用注解
java 复制代码
@Override
@OperationLog(value = "添加用户", type = "新增")
public String addUser(String username, Integer age) {
    System.out.println("【业务逻辑】执行添加用户:" + username);
    return "用户添加成功";
}

3. 多切面排序

多个切面拦截同一个方法时,用 @Order(数字) 指定执行顺序:

  • 数字越小,优先级越高
  • 环绕通知:优先级高的先执行前逻辑,后执行后逻辑。
java 复制代码
@Aspect
@Component
@Order(1) // 优先级最高
public class LogAspect {}

@Aspect
@Component
@Order(2) // 优先级次之
public class PermissionAspect {}

4. 拦截请求参数/修改返回值

环绕通知可修改入参返回值,适配数据脱敏、参数格式化等场景:

java 复制代码
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    // 修改参数
    Object[] args = pjp.getArgs();
    if(args.length > 0 && args[0] instanceof String){
        args[0] = "修改后的用户名";
    }
    // 执行方法
    Object result = pjp.proceed(args);
    // 修改返回值
    return "【增强】" + result;
}

企业级实战案例

案例1:统一接口日志切面

记录接口请求参数、返回值、耗时、请求路径,适配Web开发。

java 复制代码
@Aspect
@Component
public class WebLogAspect {

    @Pointcut("execution(* com.example.controller.*.*(..))")
    public void webLog() {}

    @Around("webLog()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 打印请求信息
        System.out.println("===== 请求开始 =====");
        System.out.println("请求URL:" + request.getRequestURL());
        System.out.println("请求方法:" + request.getMethod());
        System.out.println("请求参数:" + Arrays.toString(pjp.getArgs()));

        long start = System.currentTimeMillis();
        Object result = pjp.proceed();

        // 打印响应信息
        System.out.println("返回值:" + result);
        System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
        System.out.println("===== 请求结束 =====");
        return result;
    }
}

案例2:接口权限校验切面

通过AOP实现接口权限校验,无需在Controller编写冗余代码。

java 复制代码
@Aspect
@Component
public class PermissionAspect {

    @Pointcut("@annotation(com.example.anno.RequirePermission)")
    public void permissionPointcut() {}

    @Before("permissionPointcut()")
    public void checkPermission(JoinPoint joinPoint) {
        // 模拟:从Token获取用户权限
        String userPermission = "admin";
        RequirePermission annotation = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(RequirePermission.class);
        String requiredPermission = annotation.value();

        if(!requiredPermission.equals(userPermission)){
            throw new RuntimeException("权限不足,无法访问");
        }
    }
}

案例3:方法性能监控

统计慢查询、慢方法,用于系统优化。

java 复制代码
@Around("pointcut()")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
    Object result = pjp.proceed();
    long cost = System.currentTimeMillis() - start;
    // 耗时超过500ms打印告警
    if(cost > 500){
        System.out.println("【慢方法告警】" + pjp.getSignature().getName() + ",耗时:" + cost + "ms");
    }
    return result;
}

Spring AOP 底层原理

Spring AOP 基于动态代理实现,运行时生成代理对象,分为两种:

  1. JDK动态代理 :默认方式,要求目标类实现接口,通过实现接口生成代理;
  2. CGLIB代理 :目标类无接口 时使用,通过继承目标类生成代理。

Spring Boot 2.x 及以上:默认使用CGLIB代理(无需接口)。

核心区别

代理方式 要求 性能 适用场景
JDK动态代理 目标类必须实现接口 有接口的业务类
CGLIB代理 目标类不能被final修饰 较高 无接口的业务类

常见避坑指南

坑1:同类内部方法调用,AOP不生效

java 复制代码
@Service
public class UserServiceImpl {
    public void method1(){
        // 内部调用method2,AOP不生效(绕过了代理对象)
        this.method2();
    }
    public void method2(){}
}

解决方案

  1. 从Spring上下文获取当前Bean,调用方法;
  2. 开启AopContext.currentProxy()获取代理对象。

坑2:私有方法/ final方法不被拦截

Spring AOP 仅支持public方法拦截,private/final方法无法被代理增强。

坑3:环绕通知忘记调用 proceed()

目标方法不会执行,业务逻辑直接丢失。

坑4:切点表达式写错

导致AOP不生效,优先检查execution表达式的包名、类名、方法名。


总结

  1. 核心定位:AOP是横向解耦工具,用于抽取通用逻辑,无侵入增强业务方法;
  2. 核心组成:切面(@Aspect)+ 切点(@Pointcut)+ 5种通知;
  3. 最强通知:环绕通知@Around,可控制方法执行全流程;
  4. 企业场景:日志、权限、事务、性能监控、异常处理;
  5. 底层原理:动态代理(JDK/CGLIB),运行时织入。
相关推荐
lagrahhn2 小时前
IDEA一些提效的方法
java·ide·intellij-idea
yuanpan2 小时前
Python Scrapy 入门教程:从零学会抓取和解析网页数据
java·python·scrapy
Bat U2 小时前
JavaEE|多线程(五)
java·开发语言·jvm
疋瓞2 小时前
pringBoot + 若依框架开发与部署流程
java
豆豆2 小时前
高校网站用什么CMS?站群管理+国产化适配方案
java·大数据·cms·建站系统·信创国产化·高校网站·站群cms
Rust研习社2 小时前
使用 Tonic 构建高性能异步 gRPC 服务
开发语言·网络·后端·http·rust
captain3762 小时前
JDBC(Java Data Base Connectivity)
java·开发语言
longxibo2 小时前
【flowable 7.2.0 二开之三:基于 Flowable 7.2 的审批流系统解压即用】
java·tensorflow·jar
拾-光2 小时前
LTX-Video 2.3 实战:用图片生成视频,消费级显卡也能跑的开源 I2V 模型(GPT Image 2)
java·人工智能·python·深度学习·算法·机器学习·音视频