
做了十多年Java开发我自己面试过别人也被别人面试过几乎每次绕不开的一个话题就是Spring的IoC和DI。这两个概念说起来好像很简单控制反转、依赖注入背定义谁都会可真要往深了问比如容器到底怎么管理Bean的、循环依赖靠什么解决、为什么三级缓存非要留着能讲清楚的人立马就少了一大截。Spring IoCDI是整个Spring框架的地基不管你是做Web开发、微服务还是Spring Cloud全家桶每天写的那几个注解、配置文件背后全是这套机制在运转。这篇文章我想系统地把这块梳理一遍从概念本身出发到手写一个迷你IoC容器再到生命周期、作用域、三级缓存这些面试高频点中间穿插一些我当时踩过的坑和排查思路。适合刚入门想搞懂底层的新人也适合准备面试想查漏补缺的老手。1. 为什么面试官总爱问IoC和DI先把概念说透1.1 控制反转反转的到底是什么IoC全称Inversion of Control翻译过来叫控制反转。我第一次看到这个词时绕了半天没想明白什么反转谁把谁的控制权反转给谁了后来我自己悟出来一句话原本由你亲手new出来的对象改成交给容器来创建和管理。控制权从你手里反转到了容器手里。就这么点事。举个例子。不依赖Spring的时候你创建一个Service需要这样public class OrderService { private final InventoryClient inventoryClient new InventoryClient(); private final PaymentClient paymentClient new PaymentClient(); }一个Service依赖哪个客户端依赖怎么初始化全都由你自己控制。Spring出现之后你只需要声明我要什么依赖剩下的全交给容器Service public class OrderService { private final InventoryClient inventoryClient; private final PaymentClient paymentClient; public OrderService(InventoryClient inventoryClient, PaymentClient paymentClient) { this.inventoryClient inventoryClient; this.paymentClient paymentClient; } }OrderService里的两个依赖到底怎么来的不用你管了Spring容器根据类型自动找到对应的Bean通过构造器把依赖送进来。这个送进来的动作就叫依赖注入DI它是实现控制反转的核心手段。打个生活化的比方以前你想吃饭得自己去买菜、洗菜、炒菜、刷碗每个环节都是你自己掌控的用了IoC之后你只需要进餐厅告诉服务员我要一份宫保鸡丁后厨怎么做、食材从哪来、什么时候上菜你完全不用操心。服务员把菜端到你面前的动作就是依赖注入。这个设计的好处主要有四个对象之间的耦合度大幅降低、依赖关系统一由容器管理、方便做单元测试时替换Mock对象、切换实现类只需要改配置或注解而不动业务代码。这四个好处你在做任何一个稍微复杂一点的业务系统时都能体会到。1.2 没有IoC的时代代码能有多痛苦我刚入行那阵子用的还是比较老的技术栈没有Spring或者只有极简的XML配置。那种写代码的体验现在回想起来是真叫一个折磨。一个订单服务要依赖库存服务、支付服务、优惠券服务而这些服务各自又依赖底层的HTTP客户端、配置类、数据库访问对象。写起来就成了这样public class OrderServiceImpl { private InventoryClient inventoryClient; private PaymentClient paymentClient; private CouponClient couponClient; public OrderServiceImpl() { // 每个构造函数里都要手动组装依赖 this.inventoryClient new InventoryClient(new RestTemplate(), new AppConfig()); this.paymentClient new PaymentClient(new RestTemplate(), new AppConfig(), http://payment-api); this.couponClient new CouponClient(new RestTemplate(), new AppConfig(), http://coupon-api); } }这还只是一个类。如果项目里有十几个Service每个Service都这么写你就得在几十个地方重复做类似的组装。运气差一点某个依赖的构造方法签名改了你就要全项目去排查所有new这个对象的地方一个漏改就是运行时报错。更难受的是改配置。比如支付网关地址变了你得找到所有new PaymentClient(...)的地方逐个把地址改掉漏改一个线上就出问题。而且这种问题往往只在特定环境才暴露排查起来相当费劲。你可能已经看出来了没有IoC的代码核心问题就是对象创建和业务逻辑耦合在一起。Spring IoC把创建对象、组装依赖这一大坨事情收走之后你的业务类里只剩两件事声明需要什么依赖然后只关心自己的业务。依赖从哪来、怎么来不再是业务代码该管的事。2. 手写一个极简IoC容器核心机制一目了然2.1 最小内核一个Map就能把Bean装起来很多初学者觉得Spring容器是个非常高深的东西其实拆到底容器的核心数据结构不过就是一张MapBean名称到Bean实例的映射。我建议每个人都动手写一个最小版本的容器写完之后你对容器这个概念的理解会深刻很多。第一版只需要定义注册和获取两个能力public class SimpleIocContainer { private final MapString, Object beanPool new ConcurrentHashMap(); public void register(String beanName, Object bean) { beanPool.put(beanName, bean); } public Object getBean(String beanName) { Object bean beanPool.get(beanName); if (bean null) { throw new IllegalStateException(Bean not found: beanName); } return bean; } public boolean containsBean(String beanName) { return beanPool.containsKey(beanName); } }就这么简单。你手动往里面放一些对象然后通过名称去取。运行一下你就能直观感受到容器是什么——它就是一个帮你保管对象、按名称提供对象的大箱子。当然Spring的容器远比这个复杂它还包含了BeanDefinition的解析、属性填充、生命周期回调、代理生成等一大堆东西。但底层逻辑的主干线就是一张Map。所以下次面试官问Spring容器是什么你先回答本质上是管理Bean生命周期和依赖关系的注册表再展开细节方向就是对的。2.2 自动装配让容器自己去找依赖第一版容器虽然能装对象了但还没有自动装配能力。也就是说你得手动把对象一个个register进去这比不用容器强不了太多。接下来我们要做的是给定一个类的定义容器自己创建对象并且自动把依赖注入进去。public class MiniApplicationContext { private final MapString, Object singletonPool new HashMap(); private final MapString, Class? beanDefinitions new HashMap(); public void registerBean(String beanName, Class? beanClass) { beanDefinitions.put(beanName, beanClass); } public Object getBean(String beanName) { if (singletonPool.containsKey(beanName)) { return singletonPool.get(beanName); } Class? beanClass beanDefinitions.get(beanName); Object instance createInstance(beanClass); singletonPool.put(beanName, instance); return instance; } private Object createInstance(Class? beanClass) { // 简化处理取第一个构造器按参数类型去找依赖 Constructor?[] constructors beanClass.getConstructors(); Constructor? constructor constructors[0]; Class?[] paramTypes constructor.getParameterTypes(); Object[] args new Object[paramTypes.length]; for (int i 0; i paramTypes.length; i) { args[i] findBeanByType(paramTypes[i]); } return constructor.newInstance(args); } private Object findBeanByType(Class? type) { for (Map.EntryString, Class? entry : beanDefinitions.entrySet()) { if (type.isAssignableFrom(entry.getValue())) { return getBean(entry.getKey()); } } throw new IllegalStateException(No bean of type: type.getName()); } }这里其实已经包含了Spring最核心的设计思想getBean方法就是Bean创建的入口。当你请求某个Bean的时候容器先看看缓存里有没有没有就现场创建创建过程中发现构造器需要别的依赖再递归地去创建或获取那个依赖。这种按需触发、依赖优先的机制正是Spring容器最朴素的原理。我写完之后有个很深的体会Spring搞出来的那一堆复杂能力其实都是在解决这个迷你版本的各种不完善。比如你没有处理接口和实现类的关系、没有考虑AOP代理、没有循环依赖的兜底方案、没有作用域概念。一个个缺陷补下去就慢慢长成了Spring现在的样子。2.3 容器绝不只是个对象工厂写过了极简版容器你可能会觉得Spring不外如此——但它真正有价值的部分是在这块基础上扩展出来的一整套能力。单例管理默认情况下每个Bean只在容器里存在一份实例所有地方拿到的都是同一个对象省去了反复创建的开销。延迟创建不是所有Bean在启动时都要创建很多Bean在你第一次getBean的时候才实例化这能显著缩短应用启动时间。生命周期管理从实例化、属性填充、初始化回调到销毁回调Spring都定义了清晰的扩展点配合PostConstruct、InitializingBean等让开发者能在合适的时点插入自己的逻辑。AOP代理容器创建出来的Bean不一定是原始对象可能是被切面增强过的代理对象。这一点直接关系到后面要讲的三级缓存。统一配置和自动装配结合Value、ConfigurationProperties把外部配置集中管理起来让Bean的创建还跟配置中心联动。所以你去面试的时候回答什么是Spring IoC容器与其只说控制反转、依赖注入不如更进一步容器是一个负责Bean定义解析、实例化、依赖填充、初始化、代理增强、销毁管理的完整生命周期管理器。这个说法一出来面试官就知道你是懂底层的人。3. 依赖注入的实操细节与选型建议3.1 三种注入方式我建议你这样选Spring支持三种依赖注入方式构造器注入、Setter注入、字段注入。平时写代码可能不太注意区别但在设计一个可维护性强的项目时这个选择还挺关键的。下面这张对比表可以帮你看清楚三者的差异注入方式优点缺点适合场景构造器注入对象不可变、依赖明确、方便测试依赖太多时构造器冗长必选依赖团队首选Setter注入可选依赖灵活、支持运行期重新配置可能导致对象状态不完整非必选依赖字段注入写起来最省事隐藏依赖、无法直接new测试、可能导致循环依赖仅限快速原型、模板代码我个人强烈建议正式项目里以构造器注入为主。理由很直白构造器注入能让你一眼看出这个类需要哪些依赖而且对象一旦创建出来依赖就不能再被改变不会出现某个依赖忘了注入运行到一半才发现是null的情况。测试的时候也简单直接new一个对象把Mock传进构造器就行。字段注入最大的问题在于Autowired写在字段上的时候Lombok或者反射才能在背后把依赖塞进去。你想单元测试就必须依赖Spring容器把对象创建出来不能用普通的new。这种隐式依赖会让代码变得难以测试和重构。不过有一个反例要单独说构造器注入会直接卡死循环依赖。如果A的构造器里要BB的构造器里又要A两个对象谁都创建不出来启动直接报BeanCurrentlyInCreationException。后面讲三级缓存的时候会展开解释为什么字段注入能绕开这个问题。3.2 Autowired、Resource、Value到底怎么区分这三个注解被问到的频率非常高我把它们的核心区别列出来你照着记就稳了。Autowired是Spring提供的默认按类型查找Bean。如果容器里存在多个相同类型的Bean它会优先根据字段名或参数名匹配匹配不上就报NoUniqueBeanDefinitionException。想指定具体Bean时配合Qualifier(beanName)使用。Service public class OrderService { private final PayService payService; public OrderService(Qualifier(alipayService) PayService payService) { this.payService payService; } }Resource是JDK标准JSR-250提供的注解默认按名称查找Bean。它有两个属性name和type其实就是让你显式指定按名字找还是按类型找。在多个同类型Bean的场景下用Resource(name wechatPayService)往往更直观。Value用来注入配置值不在找Bean这个范畴里但因为它也是依赖注入机制的一部分经常一起被问到。Value(${app.name}) private String appName; Value(${server.port:8080}) private Integer port;注意${...}是从application.properties或application.yml里取值冒号后面是默认值。这里有个小坑如果配置项不存在且没写默认值启动时候会直接报错尤其注意从旧项目迁移配置时容易漏。3.3 把Bean交给容器的几种姿势到Spring Boot时代我们有四类常见方式把Bean放进容器。第一种也是最常用的在类上标注Component及其衍生注解Service、Repository、Controller配合ComponentScan扫描包路径。这适合我们自己编写的类。第二种标注Configuration的配置类里用Bean方法声明适合第三方库的类或者需要精细初始化逻辑的场景。比如数据源对象是Druid提供的你不能改它的源码所以用Bean来创建并交给容器Configuration public class DataSourceConfig { Bean public DruidDataSource dataSource() { DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/test); ds.setUsername(root); ds.setPassword(123456); ds.setMaxActive(20); return ds; } }第三种Import注解批量引入配置类或者某个普通类。这个在框架集成时很常见比如引入一个自动配置类。第四种用FactoryBean或者BeanDefinitionRegistryPostProcessor这种高级扩展点注册Bean属于框架设计层面才用得上的东西一般业务开发碰不到理解了就行。这里想提醒一件事Bean方法的方法名默认就是Bean的名称。如果你在同一个配置类里定义了两个Bean方法返回类型相同一定要给方法起不同的名字或者在Bean(xxx)里显式指定名称否则会混乱。4. 从容器取Bean的常见姿势与报错排查4.1 getBean的三种写法你在哪用过哪种在实际开发里大多数依赖都是自动注入进来的但你总有需要手动从容器取Bean的时候比如在一个普通工具类里或者在一个不能使用Autowired的静态方法里。ApplicationContext提供了三种最常见的取Bean方式// 按名称取返回Object需要强转 Object bean context.getBean(orderService); OrderService orderService (OrderService) bean; // 按类型取最常用类型安全 OrderService orderService context.getBean(OrderService.class); // 名称类型组合既安全又精确推荐 OrderService orderService context.getBean(orderService, OrderService.class);第三种写法建议你习惯起来。它既能明确指定是哪个Bean又省去了强转的代码在存在多个同类型Bean时尤其有用。还有一个较新的API我特别推荐ObjectProvider。它的好处是当你想要的Bean可能不存在时不会直接抛异常Autowired private ObjectProviderMailService mailServiceProvider; public void sendEmail() { MailService mailService mailServiceProvider.getIfAvailable(); if (mailService ! null) { mailService.send(); } }这里有个实战场景你开发一个公共组件发布到多个项目里用有些项目配了邮件服务有些项目没配。如果用普通的Autowired注入MailService没配的项目启动就挂了。用ObjectProvider就优雅很多没有就返回null链路照样跑。4.2 拿不到Bean按这四个方向排查NoSuchBeanDefinitionException基本上算Spring初学者最常碰到的报错了。遇到它别慌按下面四个方向一条条查十有八九能定位。第一个方向检查类上是否有容器识别的注解。Component、Service、Repository、Controller这些注解必须有一个都没加容器当然不知道这个类的存在。第二个方向检查ComponentScan的扫描路径。Spring Boot的启动类默认扫描它所在包及其子包。如果你的Service放在了启动类平级包之外是扫不到的。多模块项目里这个问题尤其常见子模块的包名如果和启动类不在同一个根下面就得在启动类上显式加ComponentScan(basePackages ...)。第三个方向检查Bean是否是abstract或者接口没有实现类。如果注入的是一个接口类型而容器里压根没有这个接口的实现类注册同样找不到。第四个方向检查是否存在重名Bean导致覆盖。Spring容器里的Bean名称不能重复重复时后注册的会覆盖先注册的。如果你不小心定义了两个同名的Bean另一个类型就可能因为名称被覆盖而消失注入时就会找不到。排查的时候可以用个小技巧在启动过程中加断点或者写一个ApplicationRunner把这些Bean打印出来看看容器里到底注册了哪些名称Component public class BeanPrinter implements ApplicationRunner { private final ApplicationContext context; public BeanPrinter(ApplicationContext context) { this.context context; } Override public void run(ApplicationArguments args) { String[] names context.getBeanDefinitionNames(); System.out.println(Container beans: String.join(, , names)); } }这一招在排查某个Bean为什么不存在时特别管用所见即所得。5. 作用域、生命周期与三级缓存面试深度一次讲透5.1 Bean的作用域singleton和prototype的坑Spring中Bean默认作用域是singleton也就是整个容器里只创建一个实例所有地方共享。这背后有个很大的优势省内存、省创建开销。但同时它也有隐患——有状态的Bean如果用单例就会并发出问题。比如一个类里有个字段private int counter多个线程同时调用这个单例的方法counter的读写就会产生竞争。所以你在设计单例Bean时除非是无状态的Service、Repository大多如此否则就得处理线程安全问题。prototype作用域意味着每次从容器获取Bean都会创建全新实例。这种场景下适合放有状态的对象比如一次用户请求过程中的上下文对象。这里有个经典问题singleton Bean里依赖了一个prototype Bean用起来却永远是同一个实例。因为singleton Bean只在创建时注入一次prototype Bean之后就固定了。网上很多人会问为什么我的prototype没生效原因就在这里。要真正实现每次取都是新的有几个解法。最推荐的是注入ObjectProviderService public class OrderService { private final ObjectProviderTaskExecutor taskExecutorProvider; public OrderService(ObjectProviderTaskExecutor taskExecutorProvider) { this.taskExecutorProvider taskExecutorProvider; } public void execute() { TaskExecutor executor taskExecutorProvider.getObject(); executor.run(); } }每次getObject()时容器才会走一遍完整的创建流程返回一个新的prototype Bean。还有个备选方案是注入ApplicationContext由它getBean但这样就把容器依赖带进了业务代码不够干净我一般不用。面试时如果你能把singleton和prototype这个组合坑讲明白顺便说出ObjectProvider这个解法通常能给面试官留下不错的印象。5.2 一个Bean的一生从定义到销毁面试问Bean生命周期如果只知道实例化、初始化、销毁三个阶段那是远远不够的。完整一点的回答应该包含下面这条链路BeanDefinition解析与注册容器根据注解或XML把类的定义封装成BeanDefinition包括类的全限定名、作用域、是否是懒加载、依赖关系等。实例化通过构造器反射创建出原始对象。这时对象还不完整属性还是默认值。属性填充Spring把依赖通过Autowired、Resource等注入进来同时执行Value的配置注入。这一步是依赖注入真正发生的地方。Aware回调如果Bean实现了BeanNameAware、BeanFactoryAware、ApplicationContextAware等接口Spring会依次回调。BeanPostProcessor前置处理在初始化之前所有注册的BeanPostProcessor的postProcessBeforeInitialization方法会被调用。PostConstruct初始化标注了该注解的方法执行。InitializingBean接口回调执行afterPropertiesSet()方法。自定义初始化方法如果Bean(initMethod init)指定了init方法这里会执行。BeanPostProcessor后置处理执行postProcessAfterInitialization。AOP代理通常在这一步生成所以你在容器里拿到的可能已经是个代理对象。使用阶段正常业务调用。销毁阶段依次执行PreDestroy注解方法、DisposableBean.destroy()、自定义destroyMethod。整个流程记住一个口诀实例化 → 属性填充 → 初始化 → 使用 → 销毁再把中间的扩展点补充进去就算答到点上了。这里有个不少新人踩过的坑在构造器里直接调用被注入依赖的方法。因为属性填充发生在构造器之后此时依赖还是null。我在一次评审代码时看到有人写的类是这样的public class ReportService { private final StatisticsService statisticsService; public ReportService(StatisticsService statisticsService) { this.statisticsService statisticsService; this.statisticsService.init(); // 空指针 } }构造器里statisticsService只是被赋值了但statisticsService自己内部的字段都还没填充完init()方法里一旦访问了它自己的依赖立刻就是空指针。正确做法是把init()逻辑放到PostConstruct阶段去执行或者放到ApplicationReadyEvent事件里。5.3 三级缓存是怎么解决循环依赖的循环依赖是面试里几乎必问的一个点。先明确问题本身A依赖BB依赖A两个Bean都是singleton时Spring靠三级缓存在创建过程中提前暴露半成品对象从而打破死循环。先记牢这三个缓存的名称和作用缓存级别缓存名称存放内容作用一级singletonObjects完整创建好的单例Bean所有Bean最终存放的地方正常获取Bean从这里返回二级earlySingletonObjects提前暴露的早期引用半成品或代理在Bean还没完成属性填充时供其他Bean注入引用三级singletonFactoriesObjectFactory工厂对象延迟决定暴露原始对象还是代理对象再描述一下A和B互相依赖的完整过程你照着这个思路讲面试官基本挑不出毛病容器加载发现A和B都是单例Bean准备创建。开始创建A调用构造器实例化出A的原始对象此时A的属性都是默认值。把A的ObjectFactory也就是一个可以生成A早期引用的工厂放入三级缓存singletonFactories。继续填充A的属性发现A依赖B于是容器去获取B。容器开始创建B构造器实例化B把B的ObjectFactory放入三级缓存然后填充B的属性。填充B属性时发现B依赖A。找A的顺序是一级缓存没有、二级缓存没有、三级缓存有A的ObjectFactory。于是调用工厂的getObject()得到A的早期引用如果A需要AOP这里返回的是代理对象同时把这个早期引用放进二级缓存。B拿到了A的引用直接注入进去。B完成属性填充完成初始化B被放进一级缓存。回到A的创建流程。A拿到了已经创建完整的B注入属性。A完成初始化A被放进一级缓存。二级缓存和三级缓存里的A和B的临时数据在后续清理中会被移除。这个问题接下来常常会引出追问为什么用三级缓存两级不行吗这里关键点在于AOP。一个Bean实例化完成后Spring并不确定它是否真的要生成代理。如果只需要二级缓存那在暴露早期引用时就必须马上确定给原始对象还是给代理对象。但代理生成是有成本的而且很多Bean根本用不到AOP。三级缓存用ObjectFactory做了一层延迟处理把要不要代理的决定推迟到真正发生循环依赖、必须暴露引用的那一刻。如果没发生循环依赖就直接用原始对象省掉了不必要的代理创建。我再补一个容易错的点构造器注入的循环依赖是无解的。原因在于三级缓存暴露的时机是在实例化完成之后而构造器注入发生在实例化过程中。A的构造器需要BA连原始对象都还没实例化完根本来不及把工厂放入三级缓存B自然拿不到任何A的引用只能死循环。所以Spring官方也建议构造器注入的循环依赖就该直接被当成设计问题来处理要么重构依赖关系要么用Lazy延迟其中一个依赖的解析要么改成字段注入或Setter注入。这个问题如果被问到你还可以顺带说出一个自己的理解三级缓存本质是在创建未完成但引用需要被共享这两者之间做权衡。理解了这一点面试就不是背答案而是在讲设计思想。6. 高频报错速查与实战避坑经验6.1 常见异常速查表依赖注入相关的报错翻来覆去就那么几类我把高频的整理成一张速查表方便你遇到问题直接核对异常信息可能原因处理思路NoSuchBeanDefinitionException容器里没有对应Bean查注解、扫描路径、是否重复覆盖NoUniqueBeanDefinitionException同一类型存在多个Bean未指定名称加Primary或QualifierUnsatisfiedDependencyException某个依赖注入失败但原因被外层包裹看Caused by的具体异常链BeanCurrentlyInCreationException循环依赖且使用了构造器注入改成字段注入、加Lazy或重构依赖BeanNotOfRequiredTypeException指定名称的Bean类型不匹配检查Bean名称、类型以及是否代理类IllegalArgumentException: Could not resolve placeholderValue引用的配置不存在检查配置项名称或补上默认值如果你看到UnsatisfiedDependencyException记住一个习惯往下翻Caused by。真正的错误原因往往藏在最底层比如某个依赖类在构造器里抛了空指针或者某个Repository没有对应的Mapper扫描。只看第一行容易绕弯路。6.2 我在实际项目里踩过的几个坑第一个坑在静态工具方法里用Autowired注入Service结果是null。当初我在一个公共校验器里这么写IDE没报错运行起来每次调用都空指针。原因很好理解静态字段不归Spring容器管Autowired不会对静态字段生效。后来我改成注入ApplicationContext在静态方法里通过上下文获取Bean彻底解决了。这里再提醒一句这个方案虽然能跑但静态方法依赖容器本身并不优雅能用注入解决的设计就尽量不要引入静态调用。第二个坑分模块项目里服务类没有被扫描到。做微服务的时候业务拆了几个Maven子模块我在公共模块里放了一些通用Service结果启动时报NoSuchBeanDefinitionException。查了半天发现启动类在com.xxx包下而公共模块的类在com.yyy包下Spring默认只扫描启动类所在包于是没扫到。解决办法是在启动类上显式添加ComponentScan(basePackages {com.xxx, com.yyy})或者用Import把公共模块的配置类引进来。第三个坑构造器里初始化依赖导致省略前面已经说过这里再强调一次任何远程调用、初始化动作都别放在构造器里否则Spring容器的属性填充顺序会让你吃大亏。第四个坑在ApplicationContext尚未完全启动时就去getBean。比如在某个顺序靠前的BeanPostProcessor或ApplicationListener里强行获取另一个Bean很容易拿到半成品或者触发奇怪的循环。正确方式是等待ApplicationReadyEvent或者ApplicationRunner执行阶段再去操作那些额外的Bean。6.3 排查循环依赖的一个实践技巧处理循环依赖排查时我会建议先开启启动日志看Bean的创建顺序。Spring Boot在启动时会打印很多Bean注册信息配合debug: true能看到更详细的加载流程。如果你发现哪两个类在日志里循环出现那基本就是它们之间存在互相依赖了。手头没有现成项目的时候可以主动写一个小Demo来理解这个机制比如故意创建两个互相依赖的Service分别用构造器注入和字段注入各跑一次观察启动结果和日志差异。我当年就是这么做的花了不到一小时就把三级缓存的逻辑彻底搞明白了。另外大家可以用DependsOn这个注解来控制Bean创建顺序但它只能解决顺序不对的问题不等于解决循环依赖。如果依赖关系本身形成了环靠DependsOn是绕不过去的唯一的正解还是调整设计。个人带新人的时候我有个用了很多年的习惯第一个月不急着让他们写业务代码先把容器这部分亲手敲一遍——手写迷你IoC、断点跟一遍Bean创建流程、故意制造一个循环依赖看Spring怎么处理。这个方法我带过不少人了效果比讲一百遍概念都好。你什么时候能不看源码画出一个Bean的完整生命周期图再回头去看Spring的官方文档会发现以前觉得生涩的段落全都变得顺理成章。Spring IoCDI看着是两个字背后却是一整套值得慢慢拆开细看的工程智慧。