ARTICLE DETAIL

资讯详情

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

Spring IoC与DI完全指南:深入理解容器原理与Bean生命周期

Spring IoC与DI完全指南:深入理解容器原理与Bean生命周期 1. 先从设计思想说起为什么Spring要搞出IoC和DI1.1 安全感缺失的地方2004年之前Java程序员最头痛的问题如果你是老Java程序员一定对下面这种代码无比熟悉public class OrderService { private UserDao userDao; private PaymentService paymentService; public OrderService() { // 直接在构造函数里new this.userDao new UserDao(); this.paymentService new PaymentService(); } public void createOrder(String userId, double amount) { userDao.checkUser(userId); double fee paymentService.calculateFee(amount); // ... 业务逻辑 } }这段代码看着没什么问题对吗但等你的项目到了第20个Service、第50个依赖对象时问题就全暴露了。首先OrderService和UserDao、PaymentService是强耦合关系。哪天你想把UserDao换成UserDaoRedisImpl就得改OrderService的构造代码。其次测试困难——你想给OrderService做单元测试结果它连带着把UserDao和PaymentService全实例化了你根本没法轻易地传入Mock对象。最后你写的每个Service都要自己管new、管生命周期、管销毁这完全是在重复造轮子而且每个人都按自己习惯造代码风格五花八门。1.2 控制反转到底反转了什么控制反转这四个字我见过很多人背了五年都没真正想明白。把它翻译成人话就是对象创建和依赖管理的控制权从开发者手里反转给了Spring容器。传统写法是你写代码new对象你自己决定在哪儿创建、什么时候创建、创建成什么样。IoC之后这些事全部交给Spring容器你在代码里只声明我需要一个UserDao容器就给你一个。你没创建任何东西只是告诉容器给我吧——这就是反转控制权从主动创建变成了被动接收。有人会问这不就是把new换个地方吗不是它解决的是整个架构层面的问题。举个例子你的Service层需要访问数据库。如果用的是MySQL你得手动创建MySQL连接后来项目切PostgreSQL你要改所有Service。有了IoC容器后你只需要在配置或注解里声明数据源的类型容器在启动时帮你装配业务代码一行不用动。1.3 依赖注入控制反转的具体落地姿势控制反转是设计思想依赖注入是实现方式。Spring通过依赖注入来完成IoC当一个对象需要另一个对象时不是自己创建而是由容器注入进来。注入方式已经在骨架代码里声明的三种——构造器注入、Setter注入、字段注入后面我会逐个用代码演示。用一个楼梯间类比传统方式相当于你自己掏钥匙开每一扇门从一楼走到顶楼每一层的门你都得管。IoC之后呢你只管按电梯楼层按钮电梯系统自动把你送到指定楼层中间的门它替你开。代码里的Service就是按电梯的人Spring容器就是那部电梯。1.4 一个工厂也干得了这事为何要引入容器可能有人觉得对象交给工厂管理不就行了是工厂模式确实能解耦但工厂模式解决的问题和容器完全不同。工厂通常只解决创建逻辑的集中而Spring容器是一个完整的对象生命周期管理平台它管对象的创建、属性填充、初始化方法调用、销毁方法调用还管作用域、代理、AOP、事务等一整套生态系统。还有一个关键点工厂是硬编码的你写UserDao.getInstance()工厂就返回一个UserDao实例它不知道你的依赖图。而Spring容器启动时会扫描所有Bean定义BeanDefinition构建一张完整的依赖关系图然后按图装配。这张依赖图就是容器最大的价值它能自动分析出创建OrderService必须先创建UserDao但UserDao又依赖DataSourceDataSource依赖连接池于是按序实例化。工厂模式做不到这一点。2. 把Spring容器底层机制拆开讲一段代码是如何变成Bean的2.1 玩命绕场热身BeanDefinition、BeanFactory与ApplicationContext在Spring里有两个核心接口很多人一直分不清面试也常问BeanFactoryIoC容器的顶层接口提供了最基础的能力——getBean()、containsBean()、isSingleton()。它是个基础设施不面向普通开发者。ApplicationContext是BeanFactory的子接口做了大量增强——支持国际化、事件发布、资源加载等。你平时用的AnnotationConfigApplicationContext、ClassPathXmlApplicationContext都属于它。这里有个容易混淆的概念ApplicationContext是接口AnnotaionConfigApplicationContext是实现类它内部组合了一个DefaultListableBeanFactory——这个才是真正的容器本体。所以你在绝大多数场景下操作的其实是DefaultListableBeanFactory只是ApplicationContext给你包了一层更方便的壳。另一个核心概念是BeanDefinition。Spring把一个Bean应该长什么样用元数据来描述比如类是哪个、是单例还是原型、懒加载还是非懒加载、初始化方法是什么。这些元数据汇总成一个BeanDefinition对象容器拿到后才能按图索骥去实例化。我拿盖房子类比BeanDefinition是施工图纸BeanFactory是施工队ApplicationContext是装修公司包设计、包工、还包验收你住进Bean实例时剩下的全是拎包入住。2.2 Bean的一生从定义到使用再到销毁一个Bean在Spring容器里完整走一遍大概要经历以下阶段解析配置XML、注解或Java Config生成BeanDefinition调用BeanFactoryPostProcessor这一步可以修改BeanDefinition比如PropertySourcesPlaceholderConfigurer就是在这里把${jdbc.url}占位符替换成真实值按BeanDefinition实例化对象反射本质上就是new属性填充也就是执行你写的那些注入调用BeanPostProcessor#postProcessBeforeInitialization执行afterPropertiesSet()或init-method调用BeanPostProcessor#postProcessAfterInitialization这一步是AOP代理生成的关键时机Bean就绪可以被使用容器关闭时执行destroy-method或DisposableBean#destroy()特别值得注意第5到第7步BeanPostProcessor是整个Spring扩展能力最强大的节点。AOP的代理对象、Autowired的解析、Async的代理全都是通过注册各种BeanPostProcessor在背后悄悄完成的。如果你把Bean生命周期面试题答到这层细节至少能加一个档次的分——大多数人只会说实例化、属性赋值、初始化、销毁四部曲你能说出BeanPostProcessor分两步、AOP代理在最后一步生成这已经能拉开差距。2.3 三级缓存与循环依赖Spring如何解开死结这是Spring最出名的一个底层问题也是面试高频题。既然标题说完全理解我就把它彻底讲透。假设有两个类Service public class AService { Autowired private BService bService; } Service public class BService { Autowired private AService aService; }A依赖BB依赖A。这是鸡生蛋、蛋生鸡的问题。如果一个菜鸟来设计直接写new AService()然后需要B时new BService()B又需要A又new AService()无限递归栈溢出。Spring的解法是三级缓存。它是三个Map定义在DefaultSingletonBeanRegistry里// 一级缓存存放已经完整创建好的单例Bean MapString, Object singletonObjects; // 二级缓存存放早期暴露的Bean已实例化、但属性还没填充完 MapString, Object earlySingletonObjects; // 三级缓存存放ObjectFactory用来生成早期Bean MapString, ObjectFactory? singletonFactories;处理A和B互相依赖的过程大致是这样Spring开始创建AService实例化出A的原始对象还没填属性把这个对象包进一个ObjectFactory丢进三级缓存singletonFactories。开始给A填充属性发现需要BService于是去创建BService。BService实例化出原始对象也丢进三级缓存。开始给B填充属性发现需要AService。此时一级缓存没有A二级缓存也没有A但从三级缓存找到了A的ObjectFactory调用它的getObject()拿到A的早期引用放进二级缓存同时把三级缓存里的A删掉。B拿到A的引用属性填充完成执行初始化完整创建成功放进一级缓存。回到A的创建流程此时把BService注入到A里A的属性也填完了执行初始化完整创建成功放进一级缓存。为什么需要三级而不是二级因为三级缓存里有ObjectFactory它是在创建完原始对象后、属性填充前就生成的。AOP代理的生成往往需要在这时候介入——如果A需要被代理二级缓存里的引用应该是代理对象而不是原始对象。如果你只有二级缓存你没法提前知道到底要不要生成代理因为代理增强的时机在BeanPostProcessor最后一步。三级缓存的ObjectFactory就是留一个后门等真正需要暴露引用时才决定暴露原始对象还是代理对象。这里顺带说一个很重要的点Spring只解决单例模式下、非构造器注入的循环依赖。构造器注入是没法解决的因为构造器在实例化阶段就需要参数连原始对象都造不出来谈不上提前暴露。你如果遇到BeanCurrentlyInCreationException第一反应就该是有人用构造器注入了循环依赖或作用域不是单例。2.4 默认单例也藏着门道Bean的作用域Spring的Bean默认是单例的也就是singleton作用域。这意味着容器里只有一个实例所有注入的地方拿到的是同一个对象。这个特性省内存、保证性能但也埋了一个坑单例Bean如果注入了原型Bean每次拿到的还是同一个原型Bean。为什么因为原型Bean是在注入那一刻被创建的注入完成后单例Bean持有的只是一个普通引用。你后面再从容器里拿原型Bean拿到的是新实例但单例Bean里的引用不会更新。解决办法是使用ObjectProviderT或者Lookup方法让每次调用时都从容器重新获取原型实例。Component public class SingletonService { Autowired private ObjectProviderPrototypeService prototypeServiceProvider; public PrototypeService getPrototypeService() { return prototypeServiceProvider.getObject(); } }大多数入门教程不会讲这个坑但实际项目里如果你把一个原型Bean注入到单例Bean里写出来的代码在某些场景下会出现每次都复用同一个对象的诡异问题。我在第一家公司就吃过这亏调了一天后来打印了Bean的hashCode才定位到原因。作用域速查表作用域说明常用场景singleton每个容器一个实例默认值无状态服务、DAO、配置类prototype每次获取都新建实例有状态业务对象模型对象request每个HTTP请求一个实例Web层一次请求内共享session每个HTTP会话一个实例会话级数据application每个ServletContext一个实例全局共享数据websocket每个WebSocket会话一个实例实时通信场景3. 手把手实操启动一个带IoC和DI的Spring项目3.1 准备工作不靠IDE模板徒手搭一个最小工程很多人用Spring Boot的脚手架一键生成项目反而对最底层的Spring容器没什么体感。我建议你手动建一个工程把依赖关系看个明白。用Maven建一个普通Java项目pom.xml里引入最基础的东西dependencies !-- 核心容器模块IoC/DI全靠它 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.39/version /dependency !-- 注解驱动的依赖Component Autowired等 -- dependency groupIdorg.springframework/groupId artifactIdspring-beans/artifactId version5.3.39/version /dependency !-- 常用工具类不是必需但建议加上 -- dependency groupIdorg.springframework/groupId artifactIdspring-core/artifactId version5.3.39/version /dependency /dependencies然后写一个最简单的启动入口public class Main { public static void main(String[] args) { // 创建一个基于注解扫描的容器 AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(com.example); // 直接从容器拿Bean注意这里没有new OrderService orderService context.getBean(OrderService.class); orderService.createOrder(U10001, 199.00); context.close(); } }就这么一小段代码背后发生的事情是容器扫描com.example包找到所有标注了Component或派生注解的类解析它们的依赖关系创建实例注入依赖全部完成后getBean才能直接拿到一个完整可用的OrderService。3.2 三种注入方式优缺点和适用场景先准备一个基础场景。假设我们有OrderService依赖UserService和OrderDao三个类都用Service或Repository标注Repository public class OrderDao { public void saveOrder(String orderId) { System.out.println(订单已保存: orderId); } } Service public class UserService { public boolean isUserValid(String userId) { return userId ! null !userId.isEmpty(); } }第一种字段注入Field Injection。Service public class OrderService { Autowired private UserService userService; Autowired private OrderDao orderDao; public void createOrder(String userId, double amount) { if (!userService.isUserValid(userId)) { throw new IllegalArgumentException(非法用户); } String orderId ORD System.currentTimeMillis(); orderDao.saveOrder(orderId); } }这是最简洁、也最容易上手的写法。三个字短、平、快。Spring官方其实不推荐这种方式因为字段注入有几个问题无法声明final意味着字段可以中途被替换无法轻易构造一个没有依赖的纯净实例IDE侧边栏一眼看不到类的依赖有哪些。但实际项目里依然大量存在因为它省事、代码量小。第二种构造器注入Constructor Injection。Service public class OrderService { private final UserService userService; private final OrderDao orderDao; public OrderService(UserService userService, OrderDao orderDao) { this.userService userService; this.orderDao orderDao; } }Spring 4.3以后如果类只有一个构造器可以省略Autowired注解Spring会自动用这个构造器注入。构造器注入的好处是依赖不可变final、创建即完整、测试时直接new一个实例传入Mock依赖即可。这个才是Spring官方推荐的姿势。新项目我建议一律用构造器注入。第三种Setter注入Setter Injection。Service public class OrderService { private UserService userService; Autowired public void setUserService(UserService userService) { this.userService userService; } }Setter注入允许对象创建后再替换依赖适合那些“依赖可选”或“允许运行时替换”的场景。缺点是无法保证依赖一定被注入可能会出现NPE。老式XML配置里常常见到现在Java Config项目里用得越来越少。三张姿势对比维度字段注入构造器注入Setter注入可读性简洁依赖全在构造器非常清晰依赖分散final支持不支持支持不支持测试难注入Mock易注入Mock可用想测试可后期替换Spring推荐否是不推荐作为一个强制手段3.3 配置革命从XML注解到JavaConfigSpring IoC的配置方式是这条框架最直观的演进史。最老的项目里所有Bean都写在applicationContext.xml里?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd bean idorderDao classcom.example.dao.OrderDao/ bean iduserService classcom.example.service.UserService/ bean idorderService classcom.example.service.OrderService constructor-arg refuserService/ constructor-arg reforderDao/ /bean /beansXML的最大问题是写起来冗长、无法在编译期校验类型。Spring 2.5开始引入Component、Autowired等注解配置量明显减少。到Spring 3.0的JavaConfig出现后配置彻底变成了代码Configuration ComponentScan(com.example) public class AppConfig { }一个Configuration加一个ComponentScan整个容器的扫描装配逻辑就齐了。为什么JavaConfig能赢因为它本身是Java代码可以享受IDE的编译提示、重构支持、debug能力不需要在XML和Java之间来回切换上下文。再往后的Spring Boot更是把自动配置做成了艺术——SpringBootApplication一个注解就包含了Configuration、ComponentScan和EnableAutoConfiguration容器自动装配那些你常用的模块。3.4 手动注册BeanDefinition面试官都很少问的野路子除了Component和BeanSpring还允许你完全用编程方式注册Bean。这个知识点很少有人提但对理解IoC的本质很有帮助DefaultListableBeanFactory factory new DefaultListableBeanFactory(); BeanDefinitionBuilder builder BeanDefinitionBuilder .genericBeanDefinition(OrderService.class) .addConstructorArgValue(U10001) .addPropertyValue(name, 测试订单服务); factory.registerBeanDefinition(orderService, builder.getBeanDefinition()); OrderService orderService factory.getBean(orderService, OrderService.class);看清楚这段代码在做什么没有任何注解、没有任何XML你手动构造了一个BeanDefinition手动注册到工厂然后从工厂拿Bean。这就是IoC容器最骨感的模样——所谓的注解扫描其实就是把找到标注了Component的类、创建BeanDefinition、注册进容器这三件事做了自动化而已。如果你能在一张白纸上写出这段代码说明你理解的不是Spring怎么用而是Spring是什么。4. 高级扩展IoC容器如何影响你的代码结构4.1 为什么说IoC是AOP的基础AOP面向切面编程是Spring的另一半江山而AOP离不开IoC。因为AOP要对Bean生成代理对象但代理由谁生成又由谁管理生命周期答案是容器。一个标准流程是这样的Bean通过IoC容器创建经过BeanPostProcessor这个扩展点AOP框架在这里检测到Bean有没有匹配的切面如果有就生成一个Proxy对象替换掉原来的Bean然后放入容器。业务代码里注入的其实是代理对象但你完全没有感知——表面看是IoC容器在管理Bean背后AOP已经悄悄替换了Bean。这也是为什么Spring的设计核心是容器扩展点。你不需要在业务代码里写任何代理逻辑切面通过配置声明式地挂上去IoC容器负责把所有零件组装好。4.2 关于Bean的自动装配你有多少种玩法除了Autowired按类型注入Spring还提供了几种装配策略理解它们能帮你写出更灵活的代码按类型byTypeAutowired的默认行为容器里只有一个类型时直接注入有多个同类型Bean时会根据Primary或字段名来决策。按名称byNameQualifier(userService)或Resource(name userService)可以指定名字。Primary声明当有多个同类型Bean时优先选谁。集合注入比如ListUserService会把所有UserService类型的Bean全部注入。这个用处很大比如做策略模式时把各种策略实现类一次性撸进来。我实际项目里最常用的组合是构造器注入Qualifier。一个接口多个实现类是常态比如支付接口有AlipayService、WechatPayService、UnionPayService你可以在运行时按条件选择策略也可以用MapString, PaymentService把所有实现类按名字放进去。Service public class PaymentRouter { private final MapString, PaymentService paymentServiceMap; public PaymentRouter(MapString, PaymentService paymentServiceMap) { this.paymentServiceMap paymentServiceMap; } public void pay(String channel, double amount) { PaymentService service paymentServiceMap.get(channel); if (service null) { throw new IllegalArgumentException(不支持的支付渠道: channel); } service.pay(amount); } }这个例子把IoC容器活生生变成一个注册中心——所有支付服务全都注册进Map渠道路由选择纯粹变成Map取值。如果新增一个支付渠道只需要加一个实现类路由代码一分不用改。这就是IoC给你带来的扩展性体验。4.3 懒加载与依赖工厂跑起来前要想清楚的两件事Spring的Bean默认在容器启动阶段就全部创建完毕非懒加载。好处是启动时就能发现配置错误、依赖缺失等问题代价是启动时间变长。如果你有大量重型Bean可以设置LazyLazy Service public class HeavyService { }懒加载的本质是第一次被使用时才创建。但注意懒加载会延迟报错时机如果Bean有问题可能服务跑了很多天才暴露。生产环境建议只在确实必要的时候使用懒加载。ObjectProviderT值得在这个阶段一并介绍。它相当于一个延迟的依赖解析器Service public class NotificationService { private final ObjectProviderSmsSender smsSenderProvider; public NotificationService(ObjectProviderSmsSender smsSenderProvider) { this.smsSenderProvider smsSenderProvider; } public void send(String message) { SmsSender sender smsSenderProvider.getIfAvailable(); if (sender null) { throw new IllegalStateException(没有可用的短信发送器); } sender.send(message); } }getIfAvailable()可以在容器里没有对应Bean时返回null避免启动时直接报错。这比直接注入SmsSender灵活多了在编写框架性质代码时非常顺手。5. 实战记录一个典型Spring项目的装配全过程5.1 场景设计模拟一个订单服务从0到1我准备了一个更接近真实项目的例子把前面讲的概念全串起来。先建一个电商订单模块结构大概是OrderController——接收请求注纯演示不依赖Spring MVC只用容器OrderService——业务逻辑OrderDao——数据访问PaymentService——支付策略接口以及两个实现目录结构com.example ├── AppConfig.java ├── Main.java ├── controller │ └── OrderController.java ├── service │ ├── OrderService.java │ ├── PaymentService.java │ ├── AlipayService.java │ └── WechatPayService.java └── dao └── OrderDao.java核心代码Configuration ComponentScan(com.example) public class AppConfig { }public interface PaymentService { void pay(double amount); } Service public class AlipayService implements PaymentService { Override public void pay(double amount) { System.out.println(支付宝支付: amount); } } Service public class WechatPayService implements PaymentService { Override public void pay(double amount) { System.out.println(微信支付: amount); } }Repository public class OrderDao { public void save(String orderId) { System.out.println(订单落库: orderId); } }Service public class OrderService { private final OrderDao orderDao; private final MapString, PaymentService paymentServiceMap; public OrderService(OrderDao orderDao, MapString, PaymentService paymentServiceMap) { this.orderDao orderDao; this.paymentServiceMap paymentServiceMap; } public void createOrder(String userId, String channel, double amount) { String orderId ORD System.currentTimeMillis(); orderDao.save(orderId); PaymentService paymentService paymentServiceMap.get(channel); if (paymentService null) { throw new IllegalArgumentException(渠道不存在: channel); } paymentService.pay(amount); } }Controller public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService orderService; } public void createOrder(String userId, String channel, double amount) { orderService.createOrder(userId, channel, amount); } }最后是启动入口public class Main { public static void main(String[] args) { try (AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class)) { OrderController controller context.getBean(OrderController.class); controller.createOrder(U10001, alipay, 99.00); controller.createOrder(U10002, wechat, 199.00); } } }运行结果订单落库: ORD1718... 支付宝支付: 99.0 订单落库: ORD1719... 微信支付: 199.0你注意一下这段代码里没有一个new关键字。OrderController、OrderService、OrderDao、两个支付服务全是被容器创建并注入的。MapString, PaymentService的注入尤其值得品味——Spring会自动把两个支付类的名字作为key放进Map。这就是IoC容器的自动化带给你最直观的体验。5.2 装配路径复盘容器在启动时到底做了哪些决策拿上面这个例子容器启动时的决策过程大致是这样的解析AppConfig发现ComponentScan(com.example)开始扫描包。扫描结果OrderController、OrderService、OrderDao、AlipayService、WechatPayService五个类被识别为Bean。构建BeanDefinition几个类都是单例、非懒加载。按依赖关系排序OrderService构造器需要OrderDao和MapString, PaymentService所以容器先创建OrderDao和两个支付服务。实例化并注入先创建OrderDao再创建两个支付服务然后为OrderService创建实例从容器中找OrderDao和两个支付服务注入进去。继续往上创建OrderController注入OrderService。所有Bean全部就绪放入一级缓存singletonObjects。这就是一个容器启动即装配的过程。如果你把Spring Boot项目跑起来看日志那些Tomcat started on port 8080之前就是这整套流程在做预热。5.3 为什么我推荐你放弃字段注入在实操部分我还有一个强烈的个人建议新代码一律用构造器注入。理由不只是官方推荐而是我在生产环境里真真切切踩过的坑。第一字段注入会让Bean依赖变的“隐形”。一个类有10个Autowired字段你扫一眼代码是看不出它需要谁先创建的IDE的调用关系也断了。构造器注入把依赖全部放在构造器里一目了然。第二字段注入没法做final。某些业务场景需要不可变的依赖比如一个在并发环境下要被多个线程共享的Service它的依赖如果中途换掉了很容易出诡异问题。第三测试的友好度。构造器注入的类你在单元测试里直接new OrderService(mockOrderDao, mockPaymentMap)就完了。字段注入则必须借助ReflectionTestUtils或者启动Spring容器才能测太费劲。当然字段注入也有它的优势——代码量少。写小型Demo、个人项目时怎么方便怎么来我完全没意见。但涉及团队协作、生产代码我建议统一用构造器注入减少讨论成本也是价值。6. 常见异常与翻车现场你一定会遇到的问题6.1 NoSuchBeanDefinitionException找不到Bean的常见四类原因这个异常是IoC新手最常碰到的。报错信息跑不掉这几句话No qualifying bean of type xxx available或NoSuchBeanDefinitionException。原因通常是第一类类上忘了加注解。最常见没有Component/Service/Repository/Controller容器压根不知道这个类的存在。解决加上对应注解。第二类没扫描到。你类上明明有注解但扫不到。通常是ComponentScan的包路径和你的类所在包不一致。比如ComponentScan(com.example.aaa)你的类在com.example.bbb里扫了个寂寞。解决把包路径改成上一级com.example或直接并列多个路径。第三类类型不匹配。接口注入时容器里有多个实现类但没有Primary或QualifierSpring不知道怎么选。或者你声明的类型是接口扫描到的却是某个实现类的子类类型对不上。解决添加Qualifier或Primary或者注入ListInterface/MapString, Interface全部接收。第四类Bean的scope问题。你在SessionScope或RequestScope的Bean里注入了一个singleton范围里才存在的Bean或者反之scope生命周期对不上。解决检查作用域声明。排查思路看一眼报错信息中提到的类先确认类有没有被扫描到可以在容器启动后打印所有Bean名再确认有没有多个实现类。搞一个ApplicationRunner用context.getBeanDefinitionNames()把所有注册的Bean打出来一眼就能看出来少了谁。6.2 BeanCurrentlyInCreationException循环依赖的解药与毒药如果你遇到BeanCurrentlyInCreationException基本是循环依赖问题而且是Spring解决不了的那种。主要有这么几种构造器注入的循环依赖前面讲过Spring三级缓存救不了因为构造器在实例化阶段卡死。原型作用域的循环依赖三级缓存只解决了单例原型Bean创建时不会提前暴露所以绕不过。Async / Transactional等代理类的循环依赖有些代理模式会把Bean提前代理破坏三级缓存的流程导致循环依赖失败。解决方案优先级重构代码把互相依赖的逻辑抽到一个新类里打破环。改为Setter注入或字段注入让Spring能提前暴露原始对象。使用Lazy在循环依赖的注入点上加Lazy让Spring注入一个代理对象延迟真正的依赖解析。我见过很多团队为了省事一遇到循环依赖就加Lazy。短期能用长期会隐藏设计问题。建议当成临时方案最终目标还是消解依赖环。6.3 单例Bean持有PrototypeBean为什么你的“新对象”不新了这个问题很多三年经验以下的开发也没遇到过但遇到了非常迷惑。场景一个SingletonBean注入了PrototypeBean然后每次调用SingletonBean.someMethod()都期待PrototypeBean是新实例。结果打印的PrototypeBean的hashCode永远一样这还怎么玩原因前面提过注入发生在容器创建单例Bean的时候那次创建后PrototypeBean实例就被固定住了。容器后续再创建新的PrototypeBean也不会换掉这个引用。三种解法ObjectProviderT每次getObject()重新拿一个。Lookup方法注入在单例Bean里声明一个Lookup方法容器会动态生成一个子类每次调用都走容器获取。ApplicationContextAware不推荐因为它把代码和容器耦合了能让单元测试变得很难写。我实测下来ObjectProvider是最干净的方案。那段代码前面展示过了拿到手就能用。6.4 Autowired与Resource到底选谁这个问题面试被问烂了但很多人说不清楚。最核心的区别Autowired是Spring提供的默认按类型byType注入类型不唯一再按名字byName匹配。Resource是JSR-250标准默认按名字注入名字匹配不到再按类型。还有一个细节几乎没人提Autowired支持required false属性容器里找不到对应Bean时不会报错Resource没有这个设计。另外同类型有多个Bean时Autowired会让你必须指定Qualifier或者一个都不注入Resource则可以直接指定name来精确定位。我的选择建议新项目统一用Autowired毕竟是Spring原生配合构造器注入更深得人心维护老项目时看项目里的既有规范别混着用。6.5 抛几个面试必问的变种题既然标题说完全理解我把面试中围绕IoC和DI的变种问题也整理一下当作自查清单问题一Spring容器在启动时做了什么答扫描配置类或XML生成BeanDefinition执行BeanFactoryPostProcessor实例化单例Bean填充属性DI执行各种BeanPostProcessor初始化前后的逻辑放入单例缓存。懒加载的Bean在第一次调用时才执行流程。问题二为什么默认是单例的答单例省内存一个类只实例化一次同时没有了并发创建的性能开销。对象本身无状态时单例天然线程安全。前提是Bean里不能有可变的共享字段。问题三IoC和DI是同一个概念吗答不是。IoC是思想DI是思想的一种实现方式。依赖注入的目的就是实现控制反转。问题四BeanPostProcessor是干什么的能不能举个例子答它是IoC容器的后置处理器在Bean初始化前后各有一个回调。AutowiredAnnotationBeanPostProcessor就是通过它来解析Autowired注入的AbstractAutoProxyCreator通过它创建AOP代理对象。问题五一级缓存和三级缓存存的东西有什么不同答一级缓存放完整Bean是全局唯一的单例池。三级缓存放的是ObjectFactory是一个惰性工厂它的存在是为了支持循环依赖场景下的提前暴露。完整之后就会从二级、三级缓存中移除只保留一级缓存。7. 写在最后的实际操作体会我在教了非常多新手之后发现大部分人学IoC和DI卡在的不是代码不会写而是为什么非要从new换成容器管理。这个坎过了后面全都是水到渠成的事。分享一个我带新人时的教学方法你也可以自己在项目里试试先写一个完全没有Spring的纯Java版本把对象关系用new硬编码串联出一个小功能然后小步引入Spring容器把new逐个替换成依赖注入。你会发现改动完成后整个项目的类之间不再有创建依赖代码的扩展入口全集中到了容器装配层。这个从硬编码到容器的过程就是理解IoC最好的路径。另外不要迷信任何一个最佳实践。字段注入确实官方不推荐但在写快速原型时它最顺手构造器注入虽好但参数过多的类反而暴露了设计问题——你首先要做的是拆分大类而不是争论注入方式。工具是人用的理解背后原理后你会知道什么时候该坚持什么时候该妥协。Spring的IoC和DI是一个很大的话题但核心就那么点东西容器通过BeanDefinition知道你有哪些类通过依赖注入组装它们通过生命周期管理保证它们在正确的时机以正确的状态出现。把这句口诀记在心里所有的源码、报错、面试题都可以往这个框架上靠。最后再补一个小技巧当你面对一个陌生的Spring项目、搞不清Bean之间的依赖关系时在启动入口加一行代码把容器里所有的Bean依赖关系打出来看try (AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class)) { String[] beanNames context.getBeanDefinitionNames(); for (String beanName : beanNames) { System.out.println(Bean: beanName - context.getBean(beanName).getClass().getName()); } }这行输出基本能帮你还原整个项目的装配结构定位到谁少了注解、谁类型冲突这类问题比对着报错信息瞎猜快得多。我每次接手老项目的第一件事就是干这个。
返回列表