在这里插入图片描述

Spring IoC和AOP实战:两个经典案例带你避开框架陷阱

Spring框架的核心是IoC(控制反转)和AOP(面向切面编程)。它们为Java应用带来了极大的灵活性和可扩展性,但使用不当也会引发各种“诡异”的问题。本文将通过两个真实案例,深入剖析Spring IoC和AOP的常见陷阱,并给出最佳实践。我们将借助Mermaid图直观展示核心概念和流程,助你彻底掌握这些关键技术。

一、IoC与AOP核心概念速览

IoC:容器管理对象,解耦并带来无限可能

IoC将对象创建和依赖关系的控制权从代码中转移到容器。这不仅实现了松耦合,还为框架扩展提供了基础——任何Bean都可以被容器增强、替换或监控。

AOP:切面编程,集中处理横切关注点

AOP允许你将日志、权限、事务等横切逻辑集中定义,然后通过切点动态织入目标方法。它的核心概念包括:

  • 连接点(Join point):程序执行过程中的特定点(如方法调用)。
  • 切点(Pointcut):匹配连接点的表达式,决定增强在何处执行。
  • 增强(Advice):切面在特定连接点执行的动作(前置、后置、环绕等)。
  • 切面(Aspect):切点+增强的集合。

下图清晰展示了它们之间的关系:

切面Aspect

匹配

触发

织入

切点
匹配连接点

增强
前置/后置/环绕

连接点
如方法执行

目标方法

二、案例一:单例Bean中注入Prototype Bean为何失效?

场景复现

假设我们有一个有状态的基类SayService,它维护了一个List<String>用于累积数据。两个子类SayHelloSayBye被声明为Spring Bean(默认单例):

@Slf4j
public abstract class SayService {
    List<String> data = new ArrayList<>();  // 有状态
    public void say() {
        data.add("some data...");
        log.info("size:{}", data.size());
    }
}

@Service
public class SayHello extends SayService {
    @Override
    public void say() { super.say(); log.info("hello"); }
}

@Service
public class SayBye extends SayService {
    @Override
    public void say() { super.say(); log.info("bye"); }
}

在单例模式下,data会不断增长,最终导致OOM。开发同学发现问题后,将子类改为@Scope("prototype"),但内存泄漏依旧——因为注入它们的Controller也是单例,在启动时就已经注入了唯一的Prototype Bean实例。

问题根源

单例Bean中的成员变量在容器启动时一次性注入,Prototype范围的Bean即便声明为prototype,对单例Bean来说也只有一个实例。

解决方案

方案一:使用代理模式(推荐)

为Prototype Bean设置proxyMode,让单例Bean每次通过代理获取新的实例:

@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, 
       proxyMode = ScopedProxyMode.TARGET_CLASS)
@Service
public class SayHello extends SayService { ... }
方案二:从ApplicationContext主动获取

在单例Bean中注入ApplicationContext,每次需要时手动获取:

@Autowired
private ApplicationContext context;

public void useSayHello() {
    SayHello hello = context.getBean(SayHello.class);
    hello.say();
}

下图展示了代理模式的运作机制:

实际Prototype Bean Prototype Bean代理 单例Controller 实际Prototype Bean Prototype Bean代理 单例Controller 调用方法 每次创建新实例 返回结果 返回结果

三、案例二:监控切面导致Spring事务失效

场景复现

我们实现了一个统一监控切面MetricsAspect,通过注解@Metrics记录方法入参、出参、耗时和异常。配置如下:

  • @RestController自动启用监控(默认记录所有入参出参)。
  • 对Service中的createUser方法标记@Metrics,并设置ignoreException=true(希望自动处理异常)。

上线后发现:事务回滚失效,包含“test”的用户被成功写入数据库!日志显示异常被切面捕获后没有抛出,导致TransactionAspectSupport无法感知异常。

问题分析

  1. 切面优先级问题:Spring事务也是通过AOP实现,默认优先级最低(Ordered.LOWEST_PRECEDENCE)。如果自定义切面也是最低优先级,它们的执行顺序不确定。当自定义切面环绕增强捕获异常并吞没后,事务切面就无法接收到异常,从而不会回滚。
  2. 注解获取位置错误:原代码只从方法上获取@Metrics注解,但Controller类上标记了注解,导致Controller的监控配置未被正确读取(仍然记录了入参出参)。

切面执行顺序规则

多个切面同时作用时,执行顺序由@Order决定(值越小优先级越高):

  • 入操作@Around前半部分、@Before):优先级高的先执行。
  • 出操作@Around后半部分、@After@AfterReturning@AfterThrowing):优先级低的反倒先执行。

下图展示了两个切面(优先级10和20)的执行流程:

切面Order20

切面Order10

Before

Around前半

Around后半

After

Before

Around前半

Around后半

After

调用

目标方法

返回

解决方案

  1. 明确切面优先级:将监控切面设为最高优先级,确保其环绕增强在最外层执行,且异常最终会抛出:

    @Aspect
    @Component
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class MetricsAspect { ... }
    
  2. 正确获取注解:优先从方法获取,若没有再从类获取:

    Metrics metrics = signature.getMethod().getAnnotation(Metrics.class);
    if (metrics == null) {
        metrics = signature.getMethod().getDeclaringClass().getAnnotation(Metrics.class);
    }
    // 若仍为null,使用默认配置...
    

修改后,事务正常回滚,Controller也不再打印多余的入参出参日志。

四、总结

通过这两个案例,我们可以提炼出使用Spring IoC和AOP的几个关键原则:

  1. Bean的作用域设计:有状态的Bean应考虑使用prototype,若被单例Bean引用,需通过代理或ApplicationContext获取新实例。
  2. 切面优先级管理:当多个切面共存(尤其是与事务等基础切面一起)时,必须明确@Order,避免意外吞异常导致功能失效。
  3. 注解解析位置:AOP中通过反射获取注解时,应兼顾方法和类级别,确保配置生效。

Spring的强大源于其灵活性和扩展性,但越是灵活,越需要理解背后的机制。希望本文能帮助你避开常见陷阱,写出更健壮的Spring应用。


思考与讨论

  • @Autowired@Inject@Resource三者有何区别?你平时使用哪一种?
  • Spring如何解决循环依赖?三级缓存的原理是什么?

欢迎在评论区留言交流,如果你觉得本文有帮助,请分享给更多朋友。

Logo

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

更多推荐