在 Java 开发中,OOP(面向对象编程)解决了代码模块化的问题,但面对「日志记录、权限校验、事务管理」这类跨模块的通用功能,OOP 会陷入 “重复代码” 的困境 —— 比如每个业务方法都写日志、每个接口都加权限判断,代码冗余且难以维护。

AOP(面向切面编程)正是为解决这个问题而生:它能将通用功能从业务逻辑中抽离,动态织入到目标方法中,实现「业务逻辑」和「通用功能」的解耦。本文带你从 0 到 1 掌握 AOP,所有代码可直接复用。

一、核心概念

1.AOP核心价值

通俗来说:AOP 就是 “在不修改原有业务代码的前提下,给方法增加额外功能”。

  • 比如:给所有接口方法自动加日志、给敏感接口加权限校验、给耗时方法加性能监控;

  • 核心优势:无侵入式扩展、通用逻辑统一维护、减少重复代码。

2.AOP核心概念

AOP 术语

通俗解释

装修比喻

切面(Aspect)

封装的通用功能(如日志、权限),是 AOP 的核心载体

装修中的 “防水工程”“刷漆工程”

连接点(JoinPoint)

程序执行过程中可被拦截的点(如方法调用、异常抛出),Spring AOP 仅支持方法级

房子的所有墙面(可操作的位置)

切入点(Pointcut)

筛选后的连接点(只对特定方法织入切面)

只给 “客厅墙面” 刷漆(指定操作位置)

通知(Advice)

切面的执行时机和逻辑(如方法执行前 / 后 / 异常时执行)

刷漆的时机(开工前保护、刷漆、验收后清理)

目标对象(Target)

被切面织入的业务对象(包含核心业务逻辑)

房子本身(核心是 “住”,装修是附加)

代理对象(Proxy)

织入切面后生成的新对象,业务代码实际调用的是代理对象

装修后的成品房

织入(Weaving)

将切面逻辑融入目标对象的过程(编译期 / 类加载期 / 运行期)

工人刷漆、做防水的施工过程

二、AOP实现方法

Spring AOP底层依赖两种代理方式,开发中无需手动实现,但必须了解区别:

代理方式

实现原理

适用场景

核心限制

JDK 动态代理

基于接口生成代理类

目标对象实现了接口

无法代理未实现接口的类

CGLIB 代理

基于继承生成子类代理

目标对象无接口(如普通类)

无法代理 final 类 / 方法

Spring Boot 默认规则:

  • 目标对象有接口 → 用 JDK 动态代理;

  • 目标对象无接口 → 用 CGLIB 代理;

  • 可通过配置强制使用 CGLIB: spring.aop.proxy-target-class=true

三、经典案例

以下代码基于 Spring Boot 3.2.x,无需额外引入依赖(spring-boot-starter-web 已包含 AOP 核心包)

环境准备

创建Spring Boot项目,仅需引入 web 依赖:

<!-- pom.xml核心依赖 -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

案例1:日志记录切面(最常用)

实现功能:自动记录接口的请求参数、响应结果、执行耗时,无需在每个接口写日志代码。

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
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;
 
/**
 * 日志切面:记录接口请求日志
 */
@Slf4j
@Aspect // 标记为切面类
@Component // 交给Spring容器管理
public class LogAspect {
 
    // 1. 定义切入点:拦截com.example.demo.controller包下的所有方法
    @Pointcut("execution(* com.example.demo.controller..*(..))")
    public void logPointcut() {}
 
    // 2. 定义环绕通知:方法执行前后都能处理(最灵活的通知类型)
    @Around("logPointcut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // ====== 方法执行前:记录请求信息 ======
        long startTime = System.currentTimeMillis();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        
        // 打印请求信息
        log.info("===== 请求开始 =====");
        log.info("请求URL: {}", request.getRequestURL());
        log.info("请求方法: {}", request.getMethod());
        log.info("目标方法: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        log.info("请求参数: {}", Arrays.toString(joinPoint.getArgs()));
 
        // 执行目标方法(核心业务逻辑)
        Object result = joinPoint.proceed();
 
        // ====== 方法执行后:记录响应信息 ======
        long endTime = System.currentTimeMillis();
        log.info("响应结果: {}", result);
        log.info("执行耗时: {}ms", endTime - startTime);
        log.info("===== 请求结束 =====\n");
 
        return result;
    }
}

案例2:权限校验切面

实现功能:给指定接口加权限校验,只有携带指定Token的请求才能访问。

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
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 javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
/**
 * 权限校验切面:拦截敏感接口,校验Token
 */
@Slf4j
@Aspect
@Component
public class AuthAspect {
 
    // 定义切入点:拦截所有标注@RequireAuth的方法(自定义注解)
    @Pointcut("@annotation(com.example.demo.annotation.RequireAuth)")
    public void authPointcut() {}
 
    // 前置通知:方法执行前校验权限
    @Before("authPointcut()")
    public void doAuthCheck() throws IOException {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        HttpServletResponse response = attributes.getResponse();
        
        // 获取请求头中的Token
        String token = request.getHeader("Authorization");
        
        // 权限校验逻辑(实际开发中可对接Redis/JWT)
        if (token == null || !"admin_token_123".equals(token)) {
            log.warn("权限校验失败:Token无效或缺失");
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":403,\"msg\":\"无访问权限\"}");
            response.setStatus(403);
            throw new RuntimeException("权限校验失败");
        }
        log.info("权限校验通过");
    }
}

配套自定义注解(标记需要权限的方法):

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

案例3:异常处理切面

实现功能:统一捕获业务方法的异常,格式化返回结果,避免接口直接抛出堆栈信息。

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
 
/**
 * 异常处理切面:统一捕获并处理异常
 */
@Slf4j
@Aspect
@Component
public class ExceptionAspect {
 
    // 切入点:拦截所有业务层方法
    @Pointcut("execution(* com.example.demo.service..*(..))")
    public void exceptionPointcut() {}
 
    // 异常通知:方法抛出异常时执行
    @AfterThrowing(pointcut = "exceptionPointcut()", throwing = "e")
    public void handleException(Throwable e) {
        // 分类处理异常
        if (e instanceof IllegalArgumentException) {
            log.error("业务参数异常:{}", e.getMessage(), e);
            // 可封装为统一返回对象
        } else if (e instanceof RuntimeException) {
            log.error("业务运行异常:{}", e.getMessage(), e);
        } else {
            log.error("系统未知异常:{}", e.getMessage(), e);
        }
    }
}

四、AOP高频使用场景

场景

实现方式

核心价值

日志记录

环绕通知 / 前置 + 后置通知

统一记录请求 / 响应 / 耗时,无重复代码

权限校验

前置通知 + 自定义注解

接口权限统一管控,易扩展

事务管理

Spring 内置 @Transactional(基于 AOP)

无需手动开启 / 提交事务

性能监控

环绕通知(记录方法执行时间)

定位慢方法,优化性能

异常处理

异常通知(AfterThrowing)

统一异常格式,避免暴露堆栈

五、避坑指南

1. 切入点表达式写错(最常见)

  • 错误示例: execution(* com.example.demo.controller.*(..)) (仅匹配 controller 下的直接方法,子包不生效);

  • 正确写法: execution(* com.example.demo.controller..*(..)).. 表示当前包及子包);

  • 避坑技巧:用 @annotation (注解)或 within (包)替代复杂的 execution 表达式,更易维护。

2. 代理失效(内部调用不触发切面)

场景 :Service 类中方法 A 调用本类方法 B,方法 B 的切面不生效;

原因 :内部调用是直接调用目标对象,而非代理对象;

解决方案

  1. 将方法 A 和 B 拆到不同类;

  2. 自注入代理对象( @Resource private SelfService selfProxy; ),通过代理对象调用方法 B;

  3. 用 AopContext.currentProxy () 获取当前代理对象。

3. 通知类型用错

  • 想要修改返回值 → 必须用 环绕通知(Around) (只有 Around 能控制目标方法执行、修改返回值);

  • 仅前置校验 → 用 前置通知(Before)

  • 仅记录结果 → 用 后置通知(AfterReturning)

  • 异常处理 → 用 异常通知(AfterThrowing)

4. 多个切面的执行顺序混乱

场景 :日志切面和权限切面同时作用于一个方法,想要先校验权限再记录日志;

解决方案 :给切面加 @Order 注解,数值越小越先执行:

@Order(1) // 权限切面先执行
@Aspect
@Component
public class AuthAspect { ... }
 
@Order(2) // 日志切面后执行
@Aspect
@Component
public class LogAspect { ... }

5.CGLIB代理无法代理 final 方法 / 类

  • 若目标方法是 final ,CGLIB 无法生成子类代理,切面不生效;

  • 避坑技巧:业务方法不要随意加 final ,除非确定不需要代理。

6. 过度使用 AOP 导致性能损耗

  • AOP 基于动态代理,高频调用的方法(如循环内调用)使用 AOP 会增加性能开销;

  • 避坑技巧:核心高频方法尽量不用 AOP,通用功能优先考虑其他方案(如工具类)。

六、AOP使用时机

场景

推荐方案

原因

核心业务逻辑(如订单创建)

OOP

核心逻辑需清晰、可调试

跨模块通用功能(日志 / 权限)

AOP

解耦、减少重复代码

临时功能(如线上调试)

AOP

无侵入式,无需修改业务代码

高频调用的简单功能

工具类

避免 AOP 的代理性能损耗

总结

  1. AOP 核心价值是 解耦通用逻辑和业务逻辑 ,核心是 “无侵入式扩展”,避免重复代码;

  2. Spring AOP 底层是 JDK 动态代理(有接口)和 CGLIB 代理(无接口),开发中重点关注 切入点表达式通知类型

  3. 避坑关键:避免内部调用导致代理失效、正确使用通知类型、控制 AOP 的使用范围(不滥用);

  4. 90% 的业务场景中,AOP 最适合做日志、权限、事务、异常处理,是 Java 后端开发的必备技能。

掌握 AOP 的核心逻辑后,你可以告别 “复制粘贴通用代码” 的低效开发模式,让业务代码更纯粹、更易维护。