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

Spring IoC和AOP实战:两个经典案例带你避开框架陷阱
Spring框架的核心是IoC(控制反转)和AOP(面向切面编程)。它们为Java应用带来了极大的灵活性和可扩展性,但使用不当也会引发各种“诡异”的问题。本文将通过两个真实案例,深入剖析Spring IoC和AOP的常见陷阱,并给出最佳实践。我们将借助Mermaid图直观展示核心概念和流程,助你彻底掌握这些关键技术。
一、IoC与AOP核心概念速览
IoC:容器管理对象,解耦并带来无限可能
IoC将对象创建和依赖关系的控制权从代码中转移到容器。这不仅实现了松耦合,还为框架扩展提供了基础——任何Bean都可以被容器增强、替换或监控。
AOP:切面编程,集中处理横切关注点
AOP允许你将日志、权限、事务等横切逻辑集中定义,然后通过切点动态织入目标方法。它的核心概念包括:
- 连接点(Join point):程序执行过程中的特定点(如方法调用)。
- 切点(Pointcut):匹配连接点的表达式,决定增强在何处执行。
- 增强(Advice):切面在特定连接点执行的动作(前置、后置、环绕等)。
- 切面(Aspect):切点+增强的集合。
下图清晰展示了它们之间的关系:
二、案例一:单例Bean中注入Prototype Bean为何失效?
场景复现
假设我们有一个有状态的基类SayService,它维护了一个List<String>用于累积数据。两个子类SayHello和SayBye被声明为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();
}
下图展示了代理模式的运作机制:
三、案例二:监控切面导致Spring事务失效
场景复现
我们实现了一个统一监控切面MetricsAspect,通过注解@Metrics记录方法入参、出参、耗时和异常。配置如下:
- 对
@RestController自动启用监控(默认记录所有入参出参)。 - 对Service中的
createUser方法标记@Metrics,并设置ignoreException=true(希望自动处理异常)。
上线后发现:事务回滚失效,包含“test”的用户被成功写入数据库!日志显示异常被切面捕获后没有抛出,导致TransactionAspectSupport无法感知异常。
问题分析
- 切面优先级问题:Spring事务也是通过AOP实现,默认优先级最低(
Ordered.LOWEST_PRECEDENCE)。如果自定义切面也是最低优先级,它们的执行顺序不确定。当自定义切面环绕增强捕获异常并吞没后,事务切面就无法接收到异常,从而不会回滚。 - 注解获取位置错误:原代码只从方法上获取
@Metrics注解,但Controller类上标记了注解,导致Controller的监控配置未被正确读取(仍然记录了入参出参)。
切面执行顺序规则
多个切面同时作用时,执行顺序由@Order决定(值越小优先级越高):
- 入操作(
@Around前半部分、@Before):优先级高的先执行。 - 出操作(
@Around后半部分、@After、@AfterReturning、@AfterThrowing):优先级低的反倒先执行。
下图展示了两个切面(优先级10和20)的执行流程:
解决方案
-
明确切面优先级:将监控切面设为最高优先级,确保其环绕增强在最外层执行,且异常最终会抛出:
@Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class MetricsAspect { ... } -
正确获取注解:优先从方法获取,若没有再从类获取:
Metrics metrics = signature.getMethod().getAnnotation(Metrics.class); if (metrics == null) { metrics = signature.getMethod().getDeclaringClass().getAnnotation(Metrics.class); } // 若仍为null,使用默认配置...
修改后,事务正常回滚,Controller也不再打印多余的入参出参日志。
四、总结
通过这两个案例,我们可以提炼出使用Spring IoC和AOP的几个关键原则:
- Bean的作用域设计:有状态的Bean应考虑使用prototype,若被单例Bean引用,需通过代理或
ApplicationContext获取新实例。 - 切面优先级管理:当多个切面共存(尤其是与事务等基础切面一起)时,必须明确
@Order,避免意外吞异常导致功能失效。 - 注解解析位置:AOP中通过反射获取注解时,应兼顾方法和类级别,确保配置生效。
Spring的强大源于其灵活性和扩展性,但越是灵活,越需要理解背后的机制。希望本文能帮助你避开常见陷阱,写出更健壮的Spring应用。
思考与讨论:
@Autowired、@Inject、@Resource三者有何区别?你平时使用哪一种?- Spring如何解决循环依赖?三级缓存的原理是什么?
欢迎在评论区留言交流,如果你觉得本文有帮助,请分享给更多朋友。
更多推荐



所有评论(0)