ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring IOC核心机制:从DI到三级缓存,一次拆解Bean生命周期

Spring IOC核心机制:从DI到三级缓存,一次拆解Bean生命周期 Spring作为Java后端开发事实上的标准IOCInversion of Control控制反转一直是它最核心的设计思想。很多写了几年代码的朋友天天用Service、Autowired但被问起IOC到底解决什么问题、Bean是怎么new出来的、三级缓存是干嘛的往往说不清楚。尤其面试场合几乎绕不开IOC和AOP的原理。这篇我想把所有相关的核心机制结合Spring Boot里的实际表现好好拆开讲一遍争取让刚入门的朋友能建立整体认知让有经验的朋友能查漏补缺。1. 先搞清楚IOC是什么控制反转到底反转了什么1.1 从传统new对象到Spring容器管理在没有Spring的年代代码里最常见的写法就是到处new。比如一个订单服务需要调用库存服务直接在构造方法里new StockService()订单服务就需要自己去管库存服务的创建过程。耦合紧接着就来了如果库存服务换了实现类或者构造方法里多了参数所有new它的地方全都得改一遍。IOC的核心逻辑说白了就是把“创建对象、组装依赖”这件事从业务代码里抽出来交给一个容器统一处理。这个容器管着你需要的一系列对象在Spring里叫Bean它会在合适的时机创建对象并且把对象依赖的其他对象自动塞进去。业务代码不再需要new了只是声明“我需要一个库存服务”剩下的事情交给框架。这就是“控制反转”对象创建和装配的控制权从程序员手里反转到了容器手里。1.2 控制反转和依赖注入是一回事吗很多资料里IOC和DIDependency Injection依赖注入混着讲容易把人绕晕。实际上依赖注入是IOC的一种具体实现方式也是Spring采用的方案。IOC一种设计理念关注的是控制权交给谁。DI具体做法容器通过构造器、Setter方法或者字段注入把依赖传给对象。打个生活化的比方。传统做法像你每次吃饭前自己买菜、洗菜、炒菜吃什么、怎么做完全自己控制。引入IOC之后你变成了订餐的人只需要说“我要一份鱼香肉丝”食堂容器负责把菜做好送到你面前。你不用关心今天的菜是谁种的、厨师怎么切肉只关心自己能不能吃到这顿饭。Spring里最常见的注入方式就是字段上的Autowired虽然日常写起来最省事但严格来说构造器注入更值得推荐。构造器注入的好处在于一创建对象依赖就完整不容易出现对象还没装配完就被拿去用的情况也方便后续做单元测试。字段注入的主要缺点是不容易看出来依赖顺序测试的时候也麻烦些。1.3 为什么Spring选择IOC而不是工厂模式硬扛有人会问我自己写个工厂类也能解耦为什么非要用Spring容器实际上工厂模式依然是面向对象层面的解耦它解决的是“两个类之间不直接发生依赖”的问题。但是工厂模式本身也需要维护每新增一个产品就要改工厂的代码。Spring容器比工厂更彻底的地方在于它不只负责创建对象还负责整个Bean的生命周期管理、代理增强、事件机制、配置解析以及跟Spring Boot自动配置体系无缝衔接。举个实际场景。你在Spring Boot项目里引入一个DataSource自己不做任何配置Spring Boot的自动配置就能通过ConditionalOnMissingBean之类的条件帮你装配一个默认的数据源。这种能力在纯工厂模式里基本无法想象。换句话说Spring容器的定位已经超出了“对象工厂”它是整个应用运行时的骨架。2. Bean的生命周期与三级缓存面试重点必须吃透2.1 一个Bean从定义到销毁经历了什么理解IOC只停留在“容器替我new对象”是远远不够的Spring容器对一个Bean的管理是一个完整的过程。我把关键环节按顺序列一下阶段说明典型参与机制定义解析读取配置类、XML或注解生成BeanDefinitionBean、ComponentScan合并定义处理父子BeanDefinition继承覆盖属性ChildBeanDefinition实例化前允许在真正new之前自定义逻辑InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation实例化调用构造器创建实例构造器注入、工厂方法属性填充给实例注入属性、依赖Autowired、Resource、Value初始化前BeanPostProcessor的postProcessBeforeInitializationPostConstruct底层也依赖这个机制初始化执行InitializingBean#afterPropertiesSet和自定义init-methodBean(initMethod ...)初始化后BeanPostProcessor的postProcessAfterInitializationAOP代理正是在这里生成使用阶段Bean就绪交给业务代码使用正常的业务调用销毁执行DisposableBean#destroy和自定义destroy-methodPreDestroy、Bean(destroyMethod ...)从这个过程里能看出Spring把Bean的“一生”切得非常细几乎每个关键节点都留了钩子。这也是为什么Spring生态能有那么多扩展能力的原因。业务代码平时接触最多的PostConstruct和PreDestroy本质上就是容器在特定阶段帮忙回调的机制。2.2 三级缓存到底解决了什么问题三级缓存这个词基本是Spring面试绕不开的题。它对应Spring源码里的DefaultSingletonBeanRegistry中三个Map缓存层级存储内容存在目的一级缓存singletonObjects存放已经初始化完成的完整单例Bean获取最终成品 Bean二级缓存earlySingletonObjects存放提前曝光的半成品对象尚未完成属性填充保存半成品隔离不同阶段的同一对象三级缓存singletonFactories存放能产生半成品的工厂ObjectFactory延迟创建代理对象解决循环依赖时可能出现的代理问题三级缓存的核心目标说得直白些是为了支持单例Bean之间的Setter循环依赖。什么叫循环依赖呢A依赖BB又依赖A如果没有特殊处理创建A时发现需要B创建B时又发现需要A两边等对方就成了死循环。Spring用一个简单的机制打破了这个循环A实例化之后不急着完成所有初始化先把刚才new出来的A包装成一个半成品工厂放到三级缓存里然后用这个半成品去填充B的依赖。B创建时能看到A的半成品把A填进自己的属性里B初始化完成后A再继续走完自己剩下的初始化最后两个Bean都完整就绪。2.3 二级缓存和三级缓存为什么必须分两层不少人在看到三级缓存之后会有疑问既然二级缓存里已经有半成品了为什么还要多搞一个三级缓存原因是AOP代理。Spring里很多Bean实际要替换成代理对象代理的创建时机一般是初始化之后也就是postProcessAfterInitialization阶段。如果提前到实例化后就创建代理会破坏正常的初始化流程判断。三级缓存里放的不是对象本身而是一个ObjectFactory这个工厂可以在恰当的时候决定是返回原始对象还是生成代理对象。这样循环依赖发生的时候能从三级缓存取出一个“还没有完全决定形态”的半成品等真正需要的时候才确认到底要不要代理。如果只有二级缓存提前把原始对象存进去等后续发现这个Bean需要代理就得额外处理“二级缓存里的原始对象和最终代理对象不是同一个”的情况很容易导致注入的是原始对象而容器里最终保存的是代理对象出现类型不一致的诡异问题。Spring用三级缓存将“实例化”和“代理决策”延迟分离既解决了循环依赖又不破坏代理逻辑这个设计可以说是整个IOC容器里最精妙的部分之一。2.4 这些循环依赖Spring也解决不了三级缓存的适用范围有限不少人被面试官问到这一点时容易答错。Spring的方法级循环依赖构造器注入循环依赖是无解的因为构造器调用是实例化阶段的一部分A还没实例化完B就没法拿到A的对象此时三级缓存里还没东西可用。原型作用域的Bean循环依赖同样无法解决因为Spring不会为prototype作用域实例缓存半成品。此外使用了Async这类代理增强的循环依赖场景也容易出现异常提示原因在于异步代理的提前创建与三级缓存的设计有冲突。实际工作中最稳妥的办法是重构依赖结构尽量不要让两个类相互直接依赖。就算Spring基于Setter循环依赖能兜底循环依赖本身也是设计上的坏味道。3. 别只会用注解Bean作用域与容器机制也得懂3.1 单例和多例以及更多作用域Spring里最常用的两个作用域是singleton和prototype。默认情况是singleton也就是同一个容器里同一个Bean只保留一个实例。以Spring Boot里常见的Service服务类为例绝大多数情况下用单例就够了而且单例也最省内存。但需要特别注意的是单例Bean里如果持有可变状态就要小心并发问题尽量把状态设计成无状态的。prototype意味着每次获取Bean都会得到一个新实例。适合保存临时状态、不希望被共享的场景。但对应地prototype的Bean生命周期管理也变轻了Spring在创建之后就不会跟踪它的销毁阶段需要使用者自己负责清理。除了这两种Web场景还有request、session、application这几个作用域分别对应一次HTTP请求、一个会话、整个ServletContext生命周期。实际开发中用得不多但理解它们能帮你判断一个Bean在什么范围内是共享的可以避免出现“我的变量怎么跨请求了”之类的困惑。作用域选错往往是Spring应用里状态混乱的根源。3.2 BeanFactory和ApplicationContext是什么关系源码初看时容易被这两个概念搞懵。BeanFactory是Spring容器的顶层接口定义了最基本的getBean、containsBean这些方法。ApplicationContext在它之上扩展了国际化、事件发布、资源加载、环境配置等很多能力。日常开发里我们用的基本都是ApplicationContext因为功能完整适合真实应用。Spring Boot里注入一个ApplicationContext也很常见比如在工具类中通过它获取某个Bean。做法通常是用Autowired注入甚至是通过ApplicationContextAware回调在Bean初始化时拿到容器引用。这里有个小提醒拿到容器引用不是什么大问题但如果业务代码里大量用applicationContext.getBean()去获取对象往往说明依赖注入没有做彻底。正确姿势是把依赖关系声明成属性而不是到处去容器里找。3.3 Spring Boot自动配置背后还是IOC很多人学Spring Boot时忽略了它跟IOC的紧密关系。Spring Boot的自动配置本质就是一堆被条件化控制的Configuration类和Bean方法。容器的职责从XML时代的手工注册变成了基于Classpath里的依赖、配置文件和条件注解自动决定装配什么。这也意味着你理解了IOC再看ConditionalOnClass、ConditionalOnMissingBean这些自动配置注解思路会清晰很多。Spring Boot提供了感知IOC的绝佳入口启动日志里能看到好多BeanDefinition注册信息用得到debugtrue配置或者spring-boot-actuator的beans端点都能查到容器里到底有哪些Bean。遇到“我的方法没执行”这类由Bean装配失败引起的问题直接从Bean定义和自动配置角度排查比单纯搜异常消息会更有效率。4. 手写一个极简IOC容器加深理解4.1 设计和思路有时候听原理总觉得明白了但手一写就露馅。我建议初学者可以自己实现一个迷你IOC容器不需要完整复刻Spring关键是把“容器维护Bean定义、实例化对象、注入依赖”这几件事说清楚。我设计了一个非常精简的版本支持按类名注册和查找Bean依赖注入通过字段上的自定义注解完成。基本的流程是这样维护一个MapString, Object beanMap保存实例化完成的单例对象。通过一个register(Class? clazz)方法扫描类上的自定义SimpleComponent注解。使用反射创建对象实例。遍历字段找到标了SimpleAutowired的字段再递归获取依赖对象完成注入。所有对象创建完成后把Bean放回容器。4.2 核心代码实现Retention(RetentionPolicy.RUNTIME) Target(ElementType.TYPE) public interface SimpleComponent { } Retention(RetentionPolicy.RUNTIME) Target(ElementType.FIELD) public interface SimpleAutowired { }然后是容器的核心逻辑public class SimpleIocContainer { private final MapString, Object beanMap new ConcurrentHashMap(); public void register(Class? clazz) throws Exception { if (!clazz.isAnnotationPresent(SimpleComponent.class)) { return; } String beanName Character.toLowerCase(clazz.getSimpleName().charAt(0)) clazz.getSimpleName().substring(1); Object instance clazz.getDeclaredConstructor().newInstance(); beanMap.put(beanName, instance); } public void autowire() throws Exception { for (Object bean : beanMap.values()) { populateProperties(bean); } } private void populateProperties(Object bean) throws Exception { for (Field field : bean.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(SimpleAutowired.class)) { String fieldBeanName Character.toLowerCase(field.getType().getSimpleName().charAt(0)) field.getType().getSimpleName().substring(1); Object dependency beanMap.get(fieldBeanName); if (dependency null) { throw new RuntimeException(找不到可注入的依赖: field.getName()); } field.setAccessible(true); field.set(bean, dependency); } } } public T T getBean(String beanName) { return (T) beanMap.get(beanName); } }用起来大概是这个效果SimpleComponent public class UserService { SimpleAutowired private UserDao userDao; public void save() { userDao.insert(); } } SimpleComponent public class UserDao { public void insert() { System.out.println(插入一条用户数据); } }启动时手动注册并装配public class DemoApplication { public static void main(String[] args) throws Exception { SimpleIocContainer container new SimpleIocContainer(); container.register(UserService.class); container.register(UserDao.class); container.autowire(); UserService userService container.getBean(userService); userService.save(); } }整个过程非常粗糙没考虑循环依赖、作用域、代理这些高级特性但能帮你直观感受IOC的核心是什么。你会发现所谓的容器不过就是一个Map加反射加一点注册逻辑。Spring干的事情比这复杂得多但骨架思路是相通的。4.3 手写之后再看Spring源码就轻松多了有过手写经验后再翻开DefaultListableBeanFactory和AbstractAutowireCapableBeanFactory的源码很多方法名虽然长但你能看出它们对应的是哪个环节。doCreateBean方法里从createBeanInstance到populateBean再到initializeBean跟我们手写的流程是一一对应的。三级缓存里的getSingleton和getEarlyBeanReference其实就是换了一种更精巧的方式处理Bean之间的依赖。我还记得第一次看Spring源码时被各种类名和回调方法吓到后来自己动手写了一个两百行的迷你容器再回头读才豁然开朗。建议想深入源码的朋友都可以先做这一步效率会高很多。5. 高频面试问题和实际排查经验5.1 高频知识点速查结合我经历的和见到的面试题IOC相关的内容翻来覆去就问这几点高频问题参考答案要点IOC是什么好处是什么控制反转解耦、集中管理、易于扩展、方便测试循环依赖怎么解决三级缓存一级存完成品二级存半成品三级存ObjectFactory为什么二级缓存不够需要延迟决定是否创建代理对象避免代理与实际对象不一致BeanPostProcessor的作用在Bean初始化前后做处理是AOP代理的关键入口BeanFactory和ApplicationContext区别ApplicationContext是BeanFactory的子接口功能更全作用域有哪些主要是singleton和prototypeWeb场景有request、session构造器注入为什么推荐依赖完整、方便测试、避免字段注入随意性如果面试官让你画图说明循环依赖流程重点画“实例化A - 放入三级缓存 - 填充B - B获取A半成品 - B完成 - A继续完成”这条线比背定义有说服力得多。5.2 我踩过的坑注入没生效、代理失效和懒加载第一个高频问题字段加了Autowired但运行时报空指针。原因往往不是Spring坏了而是这个类本身没有被Spring管理。比如直接new了一个Service或者这个Service没有加Service注解。排查思路很简单看类上有没有被容器扫描到或者到Actuator的beans端点里查有没有这个Bean。第二个坑作用域配错造成的状态污染。有人把某个带缓存状态的组件设置成singleton结果多个用户请求共享同一个可变成员变量数据互相串了。解决办法要么把状态抽走要么改用更适合的作用域或者干脆设计成无状态类。第三个坑懒加载Lazy使用不当时可能导致依赖注入的代理由一个延迟代理对象代替有人调试时发现调用方法没有执行到真实实现其实是注解加错了位置。还有AOP相关的问题比如自己写的Service里this.save()调用同类另一个Transactional方法事务不生效。这不是IOC本身的问题而是代理机制导致的。Spring实现事务等能力时给Bean生成了代理外部调用走代理内部调用直接访问原始对象于是增强逻辑失效。理解了初始化阶段之后生成代理这点基本就能明白为什么内部调用不走代理了。5.3 面对源码别硬刚带着问题读如果想深入Spring源码我的建议是别从头到尾硬读。找一个具体问题作为抓手比如“循环依赖是怎么解决的”然后跟着AbstractAutowireCapableBeanFactory的执行路径走一遍记录关键方法名和分支。这样效率高而且理解深刻。第一遍不懂的类名记下来回去手写模拟流程再回来看源码。调试时也可以利用IDEA的断点在DefaultSingletonBeanRegistry.getSingleton里观察三级缓存里三个Map的变化比空想理解快得多。有一次我就是通过断点看到earlySingletonObjects里存的是半成品才明白二级缓存存在的必要性。写在最后的个人体会IOC这个设计真正厉害的地方不在于让程序员少写几行new而在于它为整个框架的扩展提供了土壤。没有容器统一管理Bean生命周期就没有后面的AOP、事务管理、Spring Boot自动配置这些生态能力。我的建议是不管是写业务还是准备面试都值得花时间把它作为理解和审视整个Spring的钥匙。先会用再懂原理最后能自己复现一个迷你容器这个进阶路径走下来你面对复杂问题时的底气和普通CRUD程序员是完全不一样的。
返回列表