作为 Java 后端开发者,你一定用过 Spring 的事务管理、日志记录、权限校验这些功能。但你有没有想过:为什么只需要加一个@Transactional注解,就能自动实现事务?为什么不需要在每个业务方法中都写重复的日志代码?

这背后的核心就是AOP(面向切面编程)。AOP 是 Spring 的三大核心之一(另外两个是 IoC 和事务),它通过横向抽取重复代码,实现了业务逻辑与非业务逻辑的解耦,是 Spring 中最优雅也最容易被误解的特性。

面试时,AOP 更是 100% 的必考题,面试官会从原理到实践层层深挖:

  • AOP 的核心概念是什么?切面、连接点、通知分别指什么?
  • JDK 动态代理和 CGLIB 动态代理有什么区别?
  • 为什么同一个类中方法调用 AOP 不生效?怎么解决?
  • AOP 不生效的常见场景有哪些?
  • 如何控制多个切面的执行顺序?
  • AOP 会影响性能吗?

这篇文章,我们就从核心概念→底层原理→执行流程→实战应用→面试坑点五个维度,彻底搞懂 Spring AOP。不仅会讲清楚理论,更会提供可直接落地的代码示例和避坑指南,让你看完既能轻松应对面试,又能解决实际项目中的 AOP 问题。

一、先搞懂:什么是 AOP?

1. AOP 解决了什么问题?

在传统的面向对象编程(OOP)中,我们通过类的继承和组合来实现代码复用。但对于一些横切关注点(Cross-cutting Concerns),比如日志记录、性能监控、事务管理、权限校验等,OOP 就显得力不从心了。

这些横切关注点有两个特点:

  1. 分散在多个业务方法中:几乎每个业务方法都需要记录日志、校验权限
  2. 与核心业务逻辑无关:日志、事务这些功能不影响核心业务逻辑,但又必不可少

如果用 OOP 的方式实现,我们需要在每个业务方法中都重复编写这些代码,导致代码冗余、耦合度高、难以维护。

而 AOP 的出现完美解决了这个问题。它将这些横切关注点从业务逻辑中抽取出来,封装成独立的切面,然后在运行时动态地将切面代码织入到业务方法中。这样,业务方法只需要关注核心业务逻辑,不需要关心日志、事务这些通用功能。

2. 一个例子看懂 AOP

我们用最常见的日志记录场景来对比 OOP 和 AOP 的区别:

OOP 方式(代码冗余)
@Service
public class UserService {

    public void createUser(User user) {
        // 日志记录
        System.out.println("开始执行createUser方法,参数:" + user);
        long start = System.currentTimeMillis();
        
        try {
            // 核心业务逻辑
            userMapper.insert(user);
            System.out.println("createUser方法执行成功");
        } catch (Exception e) {
            System.out.println("createUser方法执行失败:" + e.getMessage());
            throw e;
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("createUser方法执行耗时:" + (end - start) + "ms");
        }
    }

    public void updateUser(User user) {
        // 同样的日志代码,重复编写
        System.out.println("开始执行updateUser方法,参数:" + user);
        long start = System.currentTimeMillis();
        
        try {
            userMapper.update(user);
            System.out.println("updateUser方法执行成功");
        } catch (Exception e) {
            System.out.println("updateUser方法执行失败:" + e.getMessage());
            throw e;
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("updateUser方法执行耗时:" + (end - start) + "ms");
        }
    }
}
AOP 方式(代码解耦)
// 日志切面,只需要编写一次
@Aspect
@Component
public class LogAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        System.out.println("开始执行" + methodName + "方法,参数:" + Arrays.toString(args));
        long start = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed(); // 执行原方法
            System.out.println(methodName + "方法执行成功");
            return result;
        } catch (Exception e) {
            System.out.println(methodName + "方法执行失败:" + e.getMessage());
            throw e;
        } finally {
            long end = System.currentTimeMillis();
            System.out.println(methodName + "方法执行耗时:" + (end - start) + "ms");
        }
    }
}

// 业务方法,只关注核心逻辑
@Service
public class UserService {

    public void createUser(User user) {
        userMapper.insert(user);
    }

    public void updateUser(User user) {
        userMapper.update(user);
    }
}

可以看到,AOP 方式将日志代码从业务方法中完全抽取出来,业务方法变得非常简洁。如果需要修改日志逻辑,只需要修改LogAspect一个地方即可,不需要修改所有业务方法。

二、AOP 核心概念(面试必背)

AOP 有几个非常重要的核心概念,很多人学了很久 AOP 还是搞不清这些概念的区别。我用最通俗的语言和表格来解释它们:

概念 英文 定义 通俗理解
切面 Aspect 横切关注点的模块化,包含通知和切点 封装了通用功能的类,比如日志切面、事务切面
连接点 Join Point 程序执行过程中的某个点,比如方法调用、异常抛出 所有可以被切面增强的方法
切点 Pointcut 匹配连接点的表达式,用于指定哪些连接点需要被增强 筛选条件,决定哪些方法需要被切面处理
通知 Advice 切面在特定连接点上执行的动作,也就是切面的具体代码 切面要执行的具体逻辑,比如前置通知、后置通知
目标对象 Target 被一个或多个切面增强的对象 原始的业务对象
代理对象 Proxy AOP 框架生成的对象,包含了目标对象的所有方法和切面逻辑 Spring 生成的代理对象,我们实际调用的是这个对象
织入 Weaving 将切面应用到目标对象并创建代理对象的过程 把切面代码插入到业务方法中的过程

五种通知类型

Spring AOP 提供了五种通知类型,分别在连接点的不同时机执行:

通知类型 注解 执行时机
前置通知 @Before 在目标方法执行之前执行
后置通知 @After 在目标方法执行之后执行(无论是否抛出异常)
返回通知 @AfterReturning 在目标方法正常返回之后执行
异常通知 @AfterThrowing 在目标方法抛出异常之后执行
环绕通知 @Around 在目标方法执行前后都执行,可以控制目标方法是否执行

注意:环绕通知是功能最强大的通知,它可以完全控制目标方法的执行,但也是最容易出错的通知。如果忘记调用joinPoint.proceed(),会导致原方法不执行。

三、AOP 底层原理:动态代理

Spring AOP 的底层是基于动态代理实现的。动态代理是指在运行时动态生成代理对象,不需要手动编写代理类。Spring 提供了两种动态代理实现:JDK 动态代理CGLIB 动态代理

1. JDK 动态代理

JDK 动态代理是 Java 原生提供的动态代理机制,它基于接口实现。

实现原理
  1. 定义一个业务接口
  2. 编写目标对象实现这个接口
  3. 编写一个调用处理器实现InvocationHandler接口,在invoke方法中编写切面逻辑
  4. 调用Proxy.newProxyInstance()方法生成代理对象
代码示例
// 业务接口
public interface UserService {
    void createUser(User user);
}

// 目标对象
public class UserServiceImpl implements UserService {
    @Override
    public void createUser(User user) {
        System.out.println("创建用户:" + user);
    }
}

// 调用处理器
public class LogInvocationHandler implements InvocationHandler {

    private final Object target;

    public LogInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 前置通知
        System.out.println("开始执行" + method.getName() + "方法");
        
        // 执行目标方法
        Object result = method.invoke(target, args);
        
        // 后置通知
        System.out.println(method.getName() + "方法执行完成");
        
        return result;
    }
}

// 生成代理对象
public class ProxyTest {
    public static void main(String[] args) {
        UserService target = new UserServiceImpl();
        UserService proxy = (UserService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new LogInvocationHandler(target)
        );
        
        proxy.createUser(new User()); // 调用代理对象的方法
    }
}
特点
  • 基于接口实现,目标对象必须实现至少一个接口
  • 生成的代理类实现了目标对象的所有接口
  • 只能代理接口中定义的方法

2. CGLIB 动态代理

CGLIB(Code Generation Library)是一个第三方代码生成库,它基于继承实现动态代理。

实现原理
  1. 编写目标对象,不需要实现接口
  2. 编写一个方法拦截器实现MethodInterceptor接口,在intercept方法中编写切面逻辑
  3. 调用Enhancer.create()方法生成代理对象,代理对象是目标对象的子类
代码示例
// 目标对象,不需要实现接口
public class UserService {
    public void createUser(User user) {
        System.out.println("创建用户:" + user);
    }
}

// 方法拦截器
public class LogMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 前置通知
        System.out.println("开始执行" + method.getName() + "方法");
        
        // 执行目标方法
        Object result = proxy.invokeSuper(obj, args);
        
        // 后置通知
        System.out.println(method.getName() + "方法执行完成");
        
        return result;
    }
}

// 生成代理对象
public class CglibTest {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserService.class);
        enhancer.setCallback(new LogMethodInterceptor());
        
        UserService proxy = (UserService) enhancer.create();
        proxy.createUser(new User()); // 调用代理对象的方法
    }
}
特点
  • 基于继承实现,目标对象不需要实现接口
  • 生成的代理类是目标对象的子类
  • 可以代理目标对象的所有非 final 方法

3. JDK 动态代理 vs CGLIB 动态代理

这是面试最常考的问题之一,我整理了一张详细的对比表:

特性 JDK 动态代理 CGLIB 动态代理
实现方式 基于接口 基于继承
目标对象要求 必须实现至少一个接口 不需要实现接口,不能是 final 类
可代理方法 只能代理接口中定义的方法 可以代理所有非 final 方法
生成代理的速度 慢(需要生成字节码)
执行速度 慢(反射调用) 快(直接调用父类方法)
Spring 默认版本 Spring 3.x 及以前 Spring 4.x 及以后(Spring Boot 2.x 默认)

4. Spring 如何选择动态代理?

Spring 会根据目标对象是否实现接口来自动选择动态代理方式:

  1. 如果目标对象实现了接口,默认使用 JDK 动态代理
  2. 如果目标对象没有实现接口,默认使用 CGLIB 动态代理
  3. 可以通过配置强制使用 CGLIB 动态代理:
    spring:
      aop:
        proxy-target-class: true # 强制使用CGLIB动态代理

四、Spring AOP 完整执行流程

理解了动态代理的原理,我们来看一下 Spring AOP 的完整执行流程,从容器启动到方法调用的全过程:

阶段 1:容器启动

  1. Spring 容器启动,扫描所有的 Bean
  2. 扫描所有带有@Aspect注解的类,将其识别为切面
  3. 解析每个切面中的切点表达式和通知
  4. 为每个匹配切点的 Bean 生成代理对象
  5. 将代理对象注册到 Spring 容器中,替代原始的目标对象

阶段 2:方法调用

  1. 客户端调用 Bean 的方法,实际上调用的是代理对象的方法
  2. 代理对象的方法被调用时,会触发拦截器链
  3. 按照顺序执行所有匹配的前置通知
  4. 执行目标对象的方法
  5. 按照顺序执行所有匹配的返回通知(如果正常返回)
  6. 按照顺序执行所有匹配的异常通知(如果抛出异常)
  7. 按照顺序执行所有匹配的后置通知
  8. 返回方法执行结果

执行流程图

五、@AspectJ 注解实战

Spring AOP 支持使用 @AspectJ 注解来定义切面,这是目前最常用也是最简洁的方式。

1. 引入依赖

首先需要在 pom.xml 中引入 Spring AOP 的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 定义切面

一个完整的切面定义包含三个部分:切面类、切点、通知。

@Aspect // 标记这是一个切面
@Component // 将切面交给Spring管理
public class LogAspect {

    // 定义切点,匹配com.example.service包下的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void servicePointcut() {}

    // 前置通知
    @Before("servicePointcut()")
    public void before(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("【前置通知】执行" + methodName + "方法,参数:" + Arrays.toString(args));
    }

    // 后置通知
    @After("servicePointcut()")
    public void after(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("【后置通知】" + methodName + "方法执行完成");
    }

    // 返回通知
    @AfterReturning(pointcut = "servicePointcut()", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("【返回通知】" + methodName + "方法返回结果:" + result);
    }

    // 异常通知
    @AfterThrowing(pointcut = "servicePointcut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("【异常通知】" + methodName + "方法抛出异常:" + e.getMessage());
    }

    // 环绕通知
    @Around("servicePointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        long start = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed(); // 执行原方法
            long end = System.currentTimeMillis();
            System.out.println("【环绕通知】" + methodName + "方法执行耗时:" + (end - start) + "ms");
            return result;
        } catch (Throwable e) {
            System.out.println("【环绕通知】" + methodName + "方法执行异常");
            throw e;
        }
    }
}

3. 切点表达式详解

切点表达式是 AOP 中最灵活也最容易出错的部分,最常用的是execution表达式,语法如下:

execution(修饰符? 返回类型 包名.类名.方法名(参数) 异常?)
  • ?表示可选部分
  • *表示任意字符
  • ..表示任意参数或任意子包

常用示例

// 匹配所有public方法
@Pointcut("execution(public * *(..))")

// 匹配com.example.service包下的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")

// 匹配com.example.service包及其子包下的所有方法
@Pointcut("execution(* com.example.service..*.*(..))")

// 匹配所有以create开头的方法
@Pointcut("execution(* create*(..))")

// 匹配UserService类的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")

六、切面执行顺序

在实际项目中,我们可能会定义多个切面,比如日志切面、权限切面、事务切面。这时候就需要控制这些切面的执行顺序。

1. 同一个切面内的通知顺序

同一个切面内的不同通知按照以下顺序执行:

  1. 环绕通知的前置部分
  2. 前置通知(@Before)
  3. 目标方法执行
  4. 环绕通知的后置部分
  5. 后置通知(@After)
  6. 返回通知(@AfterReturning)/ 异常通知(@AfterThrowing)

2. 多个切面之间的执行顺序

多个切面之间的执行顺序可以通过@Order注解或Ordered接口来控制:

  • @Order注解的值越小,优先级越高,越先执行
  • 如果没有指定@Order,默认优先级最低

示例

@Aspect
@Component
@Order(1) // 优先级最高,最先执行
public class AuthAspect {
    // 权限校验逻辑
}

@Aspect
@Component
@Order(2) // 优先级次之
public class LogAspect {
    // 日志记录逻辑
}

@Aspect
@Component
@Order(3) // 优先级最低,最后执行
public class TransactionAspect {
    // 事务管理逻辑
}

执行顺序

AuthAspect前置通知 → LogAspect前置通知 → TransactionAspect前置通知
       ↓
执行目标方法
       ↓
TransactionAspect后置通知 → LogAspect后置通知 → AuthAspect后置通知

可以看到,切面的执行顺序是先进后出的,优先级高的切面先执行前置通知,后执行后置通知。

七、Spring AOP vs AspectJ

很多人搞不清 Spring AOP 和 AspectJ 的区别,以为它们是同一个东西。其实它们是完全不同的两个框架,只是 Spring AOP 借鉴了 AspectJ 的注解风格。

特性 Spring AOP AspectJ
实现方式 基于动态代理的运行时织入 基于字节码的编译时 / 类加载时织入
织入时机 运行时 编译时、类加载时、运行时
功能 有限,只能代理 Spring Bean 的方法 强大,可以代理所有对象(包括非 Spring 管理的对象)
复杂度 低,简单易用 高,需要特殊的编译器和配置
性能 一般,动态代理有一定开销 高,直接修改字节码,几乎没有性能开销
适用场景 大多数企业级应用,简单的 AOP 需求 复杂的 AOP 需求,需要代理非 Spring 管理的对象

核心结论:Spring AOP 是为了解决企业级应用中最常见的 AOP 需求而设计的,它简单易用,能够满足 90% 以上的场景。如果需要更强大的 AOP 功能,可以使用 AspectJ。

八、面试重点:AOP 不生效的 8 种常见场景

AOP 不生效是面试中最常考的问题,也是实际项目中最容易踩的坑。我整理了 8 种最常见的场景,每个场景都包含错误示例、原因分析和解决方案

场景 1:方法不是 public 的

错误示例

@Service
public class UserService {

    // private方法,AOP不生效
    @Transactional
    private void createUser(User user) {
        userMapper.insert(user);
    }
}

原因:Spring AOP 基于动态代理实现,JDK 动态代理只能代理接口中的 public 方法,CGLIB 动态代理只能代理 public 和 protected 方法。private 方法无法被代理。

解决方案:将方法改为 public。

场景 2:同一个类中方法调用(最常见)

错误示例

@Service
public class UserService {

    public void createUser(User user) {
        // 调用本类的另一个方法,AOP不生效
        this.logUser(user);
        userMapper.insert(user);
    }

    @Transactional
    public void logUser(User user) {
        logMapper.insert(user);
    }
}

原因:当在同一个类中调用方法时,调用的是原始对象的方法,而不是代理对象的方法,所以 AOP 不会生效。

解决方案:有三种解决方案,推荐使用第一种或第三种。

方案 1:注入自己的代理对象
@Service
public class UserService {

    @Autowired
    private UserService userService; // 注入自己的代理对象

    public void createUser(User user) {
        userService.logUser(user); // 调用代理对象的方法
        userMapper.insert(user);
    }

    @Transactional
    public void logUser(User user) {
        logMapper.insert(user);
    }
}
方案 2:使用 AopContext.currentProxy ()
@Service
public class UserService {

    public void createUser(User user) {
        // 获取当前代理对象
        UserService proxy = (UserService) AopContext.currentProxy();
        proxy.logUser(user); // 调用代理对象的方法
        userMapper.insert(user);
    }

    @Transactional
    public void logUser(User user) {
        logMapper.insert(user);
    }
}

注意:需要在启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)来暴露代理对象。

方案 3:重构代码,将方法放到另一个类中
@Service
public class LogService {

    @Transactional
    public void logUser(User user) {
        logMapper.insert(user);
    }
}

@Service
public class UserService {

    @Autowired
    private LogService logService;

    public void createUser(User user) {
        logService.logUser(user); // 调用其他类的方法
        userMapper.insert(user);
    }
}

场景 3:类没有被 Spring 管理

错误示例

// 没有加@Service注解,没有被Spring管理
public class UserService {

    @Transactional
    public void createUser(User user) {
        userMapper.insert(user);
    }
}

原因:Spring AOP 只能代理 Spring 容器中的 Bean。如果类没有被 Spring 管理,就不会生成代理对象,AOP 自然不会生效。

解决方案:给类添加@Service@Component等注解,让 Spring 管理这个类。

场景 4:方法是 static 或 final 的

错误示例

@Service
public class UserService {

    // static方法,AOP不生效
    @Transactional
    public static void createUser(User user) {
        userMapper.insert(user);
    }

    // final方法,AOP不生效
    @Transactional
    public final void updateUser(User user) {
        userMapper.update(user);
    }
}

原因

  • static 方法属于类,不属于对象,无法被代理
  • final 方法不能被重写,CGLIB 无法生成子类代理

解决方案:去掉 static 或 final 修饰符。

场景 5:异常被捕获没有抛出

错误示例

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        try {
            userMapper.insert(user);
            int i = 1 / 0; // 抛出异常
        } catch (Exception e) {
            // 手动捕获异常,没有抛出,事务不会回滚
            e.printStackTrace();
        }
    }
}

原因:Spring AOP 的异常通知和事务回滚只有在异常没有被捕获并抛出时才会触发。如果手动捕获了异常但没有重新抛出,AOP 不会感知到异常。

解决方案:捕获异常后重新抛出,或者手动设置事务回滚。

场景 6:错误的切点表达式

错误示例

// 切点表达式写错了,匹配不到任何方法
@Pointcut("execution(* com.example.service.UserService.create(..))")
public void pointcut() {}

原因:切点表达式写错了,没有匹配到任何方法,所以 AOP 不会生效。

解决方案:仔细检查切点表达式,确保它能正确匹配目标方法。

场景 7:切面没有被 Spring 管理

错误示例

// 没有加@Component注解,切面没有被Spring管理
@Aspect
public class LogAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void before() {
        System.out.println("前置通知");
    }
}

原因:切面类本身也需要被 Spring 管理,否则 Spring 无法识别它是一个切面。

解决方案:给切面类添加@Component注解。

场景 8:使用了 @Async 或 @Transactional 注解导致代理冲突

错误示例

@Service
public class UserService {

    @Async
    @Transactional
    public void createUser(User user) {
        userMapper.insert(user);
    }
}

原因@Async@Transactional都会生成代理对象,如果多个注解同时使用,可能会导致代理冲突,AOP 不生效。

解决方案:将不同的注解放到不同的类中,或者调整代理的顺序。

九、AOP 的性能影响

很多人担心 AOP 会影响性能,其实这个担心是多余的。

性能测试数据

根据 Spring 官方的性能测试数据:

  • JDK 动态代理的性能开销大约是每次调用 1-10 微秒
  • CGLIB 动态代理的性能开销大约是每次调用 0.5-5 微秒

也就是说,即使每秒调用 10 万次 AOP 方法,总的性能开销也只有 0.05-1 秒,对于绝大多数应用来说完全可以忽略不计。

影响性能的因素

AOP 的性能影响主要来自以下几个方面:

  1. 通知类型:环绕通知的性能开销最大,因为它需要调用joinPoint.proceed()
  2. 通知数量:同一个方法上的通知越多,性能开销越大
  3. 通知逻辑复杂度:通知中的逻辑越复杂,性能开销越大

最佳实践

  • 尽量使用简单的通知类型,避免不必要的环绕通知
  • 不要在通知中编写复杂的业务逻辑
  • 对于性能要求极高的场景,可以考虑使用 AspectJ 的编译时织入,几乎没有性能开销

十、最佳实践与避坑指南

  1. 优先使用 @AspectJ 注解风格:比 XML 配置更简洁,更易于维护
  2. 合理使用切点表达式:尽量精确匹配目标方法,避免匹配过多不必要的方法
  3. 避免在通知中抛出异常:除非必要,否则不要在通知中抛出异常,以免影响原方法的执行
  4. 环绕通知必须调用 proceed ():如果忘记调用,会导致原方法不执行
  5. 同一个类中方法调用使用代理对象:避免 AOP 不生效
  6. 不要过度使用 AOP:AOP 虽然强大,但不要滥用,否则会导致代码难以理解和调试
  7. 控制切面的执行顺序:使用@Order注解明确指定多个切面的执行顺序

十一、高频面试题解答

  1. 问:AOP 的核心概念是什么? 答:AOP 的核心概念包括切面、连接点、切点、通知、目标对象、代理对象和织入。切面是横切关注点的模块化,连接点是程序执行的某个点,切点是匹配连接点的表达式,通知是切面在连接点上执行的动作。

  2. 问:JDK 动态代理和 CGLIB 动态代理有什么区别? 答:JDK 动态代理基于接口实现,目标对象必须实现接口,只能代理接口中的方法;CGLIB 动态代理基于继承实现,目标对象不需要实现接口,可以代理所有非 final 方法。Spring 4.x 以后默认使用 CGLIB 动态代理。

  3. 问:为什么同一个类中方法调用 AOP 不生效?怎么解决? 答:因为同一个类中方法调用时,调用的是原始对象的方法,而不是代理对象的方法。解决方案有三种:注入自己的代理对象、使用 AopContext.currentProxy () 获取代理对象、将方法重构到另一个类中。

  4. 问:AOP 不生效的常见场景有哪些? 答:常见的场景有:方法不是 public 的、同一个类中方法调用、类没有被 Spring 管理、方法是 static 或 final 的、异常被捕获没有抛出、错误的切点表达式、切面没有被 Spring 管理、代理冲突。

  5. 问:如何控制多个切面的执行顺序? 答:可以使用@Order注解或Ordered接口来控制切面的执行顺序。@Order的值越小,优先级越高,越先执行。

  6. 问:Spring AOP 和 AspectJ 有什么区别? 答:Spring AOP 基于动态代理的运行时织入,简单易用,只能代理 Spring Bean 的方法;AspectJ 基于字节码的编译时 / 类加载时织入,功能强大,可以代理所有对象,但复杂度更高。

  7. 问:AOP 会影响性能吗? 答:AOP 有一定的性能开销,但对于绝大多数应用来说完全可以忽略不计。性能影响主要来自通知的数量和逻辑复杂度,合理使用 AOP 不会对性能造成明显影响。

十二、总结

Spring AOP 是 Spring 框架最优雅的特性之一,它通过横向抽取重复代码,实现了业务逻辑与非业务逻辑的解耦,大大提高了代码的可维护性和复用性。

回顾一下全文的核心内容:

  • AOP 的核心是将横切关注点从业务逻辑中抽取出来,封装成切面
  • Spring AOP 基于动态代理实现,支持 JDK 动态代理和 CGLIB 动态代理
  • 同一个类中方法调用 AOP 不生效是最常见的坑,需要使用代理对象调用
  • AOP 不生效有 8 种常见场景,掌握这些场景可以快速定位和解决问题
  • 合理使用 AOP 不会对性能造成明显影响

理解了 Spring AOP 的原理和坑点,你就能在实际项目中正确使用 AOP,写出更优雅、更健壮的代码。同时,这些内容也是面试中的高频考点,掌握了它们,你就能轻松应对所有 AOP 相关的面试题。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐