首页/新闻资讯/正文详情

Spring面试必问:依赖注入DI原理、三种注入方式与循环依赖解析

发布时间:2026/9/24 20:28:15 来源:云帆数科 栏目:资讯中心
Spring面试必问:依赖注入DI原理、三种注入方式与循环依赖解析
前阵子帮团队做模拟面试出了一道“送分题”Spring中的DI是什么结果十个人里只有三个人能讲到点子上。DIDependency Injection依赖注入这个词初学者背定义人人都会但真到面试现场面试官追问几个“为什么”就露馅了。这篇文章不打算只给标准答案我想从面试官的视角、源码的视角、还有实际项目的视角把这道经典面试题彻底拆开。不管你是准备跳槽的候选人还是工作两三年想补一补基础的后端开发这篇都值得花十分钟看完。1. 面试官问DI到底在考什么1.1 一道“送分题”为什么成了“送命题”我见过太多候选人在这道题上翻车不是因为不知道DI的全称而是因为回答得太“标准”。一上来就背“DI就是依赖注入是IoC的一种实现方式分构造器注入、Setter注入和字段注入。”然后呢然后就没有然后了。面试官等了三秒钟发现你没有继续说下去的意思心里就打了个问号。这道题之所以被高频面试题收录不是因为它在考概念而是在考你“懂不懂设计”。Spring框架最核心的竞争力从来不是那些花哨的注解而是它把“对象间的依赖关系”这件事提到了设计层面。DI只是这个设计的外壳真正的内核是当一个对象不再自己掌控依赖的创建而是把控制权交给外部容器时系统的耦合度会发生怎样翻天覆地的变化。所以面试官听到你背完定义之后一般会立刻追问一句“那为什么需要DI没有它行不行”这一问就把背答案的人和真正做过项目的人区分开了。1.2 面试官想听的两个层次我把面试官对这个问题的期待拆成两层你在回答时最好也按这个层次走。第一层是概念层。能说清楚DI的全称、它和IoC的关系、依赖是谁、注入是谁、反转了什么。这一层做到位你拿的是基础分。第二层是设计层。你得能说出DI解决的核心痛点——耦合。举个例子没有DI的时候你在Service里new一个Mapper这个Service就和“怎么创建Mapper”绑定死了将来要换一个Mock实现、要换一个数据库访问方式都得改Service的代码。有了DI之后Service只需要声明“我需要一个XX类型的依赖”至于这个依赖是真实实现还是Mock是本地实例还是远程代理都由容器来安排。这就是所谓的“依赖倒置”在工程上的落地。如果你能在回答里主动提一句“DI让对象的依赖关系在运行期由容器统一管理配合面向接口编程能让系统更易于扩展和测试”那面试官基本就会认定你不只是背过题而是真用过。2. DI的本质从“new出来”到“被注入”2.1 没有DI的日子代码是怎么被耦合死的为了把DI讲透我习惯先给候选人看一段反面教材。假设你现在要写一个订单服务第一个版本你可能这么写public class OrderService { private UserService userService new UserService(); private PaymentService paymentService new PaymentService(); private InventoryService inventoryService new InventoryService(); public void placeOrder(OrderDTO order) { userService.checkUser(order.getUserId()); paymentService.pay(order.getAmount()); inventoryService.deduct(order.getProductId(), order.getQuantity()); } }这段代码看起来挺正常甚至跑起来也没毛病。但问题藏在看不见的地方编译期绑定OrderService在编译时就知道UserService、PaymentService、InventoryService这些具体类叫什么将来类路径一变编译直接挂。测试困难你想单测OrderService但UserService里有真实数据库操作、PaymentService里调了第三方支付接口你没法轻易替换成假实现。个性化创建被写死如果某个依赖的构造函数需要读取配置、需要传参数这些逻辑全要写死在OrderService里类越长越臃肿。全局修改成本高假设你后来要用一个新的PaymentService实现你得到OrderService里一行行改new的地方而不是在外部去替换。最常见的类比是你家里的电器都自己接电表每家每户都自己拉电线、装变压器结果一栋楼乱成一团。DI的思路是——你们别自己折腾了我容器统一建一个配电房谁需要多少电跟我说一声我从配电房里给你送过去。2.2 Spring容器接管之后发生了什么Spring把这种“统一配电房”的角色叫做IoC容器官方文档里管它叫Bean Container我们习惯叫Spring容器。它干的事有三件创建和管理Bean根据配置或扫描规则把需要受管的类实例化放进容器里统一管理生命周期。分析依赖关系容器在创建Bean时会检查这个Bean声明了哪些依赖构造器参数、Setter、字段上的注解等。完成依赖注入在合适的时机把依赖送进Bean里。这个“合适的时机”就是Bean的属性填充阶段。我画个简单的流程Spring启动 → 扫描类 → 实例化Bean → 发现OrderService依赖UserService → 又从容器里找UserService实例 → 注入给OrderService → 返回一个完全准备好的OrderService给调用方。所以DI或者说IoC本质就是把“对象的创建权”从代码里剥离出去统一上交容器。你不再说“我要什么我去造”而是说“我要什么容器给我”。2.3 一个最小示例看懂DI的运作我用一段最精简的代码来演示Spring中的DI长什么样。先定义一个依赖Service public class UserService { public String getUserName(Long id) { return 用户- id; } }再定义一个需要依赖它的服务Service public class OrderService { private final UserService userService; Autowired public OrderService(UserService userService) { this.userService userService; } }看到区别没有OrderService里没有任何new UserService()它只是通过构造函数告诉Spring“我需要一个UserService类型的参数。”Spring收到这个信号后会自动从容器里找到UserService实例通过构造函数把它传进来。当你在项目中通过如下方式获取OrderService时OrderService orderService context.getBean(OrderService.class);你拿到的已经是一个依赖被完整注入好的对象。这就是DI最朴素、最本质的样子。3. 三种注入方式的实战权衡别只会背名称3.1 字段注入写起来最爽维护起来最痛接下来聊面试里非常容易追问的部分Spring支持哪几种注入方式怎么选市面上绝大多数入门教程给的示例都是字段注入Service public class OrderService { Autowired private UserService userService; Autowired private PaymentService paymentService; }代码确实最短一个注解打在字段上完事。日常开发里我见过很多人图省事全用字段注入甚至团队规范也这么写。但我不推荐在正式项目里大规模使用字段注入原因很实在依赖不透明看一个类的字段列表你不能确定哪些字段是必须的、哪些是可有可无的所有依赖都藏在对象内部。无法声明不可变性字段不能加final所以依赖可以被后续代码重新赋值破坏了对象状态的一致性。脱离Spring容器就无法使用你想在单元测试里直接new一个OrderService结果发现依赖没法传进去只能被迫用反射或者Mockito的InjectMocks去塞字段测试代码变得很别扭。容易掩盖循环依赖字段注入允许在对象创建过程中先放一个“半成品”很多循环依赖靠字段注入能侥幸绕过反而让设计问题藏得更久。面试时我通常建议这么表态字段注入适合写快速原型、写测试桩、或者团队明确约定无法改字段可见性的场景但不是企业级应用的首选。3.2 Setter注入适合可选依赖Setter注入长这样Service public class OrderService { private UserService userService; Autowired public void setUserService(UserService userService) { this.userService userService; } }它比字段注入好的一点是依赖可以在对象创建后动态修改方便做“延迟注入”或者“可选注入”。比如某个依赖不是强制需要的只有当配置项开启时才注入那用Setter就很自然。但Setter注入有个天生缺陷——依赖可能是null。因为Setter调用是后置的如果有人直接new了OrderService而不调用setter这个对象就处于一个“缺一条腿”的状态。你没法在构造函数里加校验也没法把字段声明成final来保证它非空。所以在实际项目里Setter注入的我用得非常少通常只用在两类场景一类是确实可选的依赖另一类是需要在运行期替换依赖的配置类或回调组件。3.3 构造器注入Spring官方推荐的底气在哪如果你翻开Spring官方文档推荐部分你会发现官方明确推荐“constructor injection”。这个推荐不是拍脑袋它有很强的工程理由依赖不可变配合final关键字依赖一旦确定就不能被修改对象生命周期内状态稳定。依赖不可能为null对象创建时必须把依赖全部传入少了直接编译失败杜绝了NPE隐患。可测试性好单元测试里直接 new OrderService(mockUserService, mockPaymentService) 就能构造被测对象不需要反射不需要容器。提前暴露循环依赖问题构造器注入要求创建对象时依赖必须齐备所以很多循环依赖场景会直接抛异常促使你尽早解决设计问题而不是让它在运行半年后炸掉。我改造一下前面的例子Service public class OrderService { private final UserService userService; private final PaymentService paymentService; Autowired public OrderService(UserService userService, PaymentService paymentService) { this.userService userService; this.paymentService paymentService; } }订单服务的依赖一目了然而且任何一个依赖缺失Spring启动时就报错不会拖到线上跑挂才暴露。3.4 一个典型取舍案例我去年接手过一个老项目里面有个ReportService字段注入了一堆依赖大概有七八个。其中一个依赖是RedisTemplate项目里只用了一个分支会用到。我重构的时候碰到一个真实的测试难题想单测ReportService的某个方法但字段注入的依赖必须靠Mockito的InjectMocks来注入而且因为构造器里没有参数IDEA静态检查还一直提示“Field injection is not recommended”。最后我改成构造器注入但面临另一个问题七八个参数塞在构造器里读起来也很痛苦。这个问题的正确解法不是退回字段注入而是审视这个类是不是“上帝类”——依赖这么多说明职责过重。后来我把它拆成了三个服务每个服务依赖两三个协作对象构造器都清爽了测试也好写多了。这个案例我想说明一个观点构造器注入参数很多的时候不是注入方式选错了而是你的类设计有问题。遇到依赖超过四五个的情况先做职责拆分而不是急着改注入方式。4. 面试高频追问循环依赖与注解辨析4.1 三级缓存解决循环依赖的完整链路讲完三种注入方式面试官大概率会抛一个“加分题”Spring是怎么解决循环依赖的这里说的循环依赖就是A依赖B、B又依赖A。很多人在这一步卡住因为平时光顾着用框架没想过框架在背后做了多精巧的设计。我尽量用通俗但不失准确的方式来描述Spring的三级缓存机制。Spring在创建单例Bean的时候维护了三个Map缓存名称存储内容作用singletonObjects创建完成的单例Bean一级缓存正常获取Bean的地方earlySingletonObjects提前暴露的Bean半成品二级缓存用于处理已实例化但未完成依赖填充的BeansingletonFactoriesObjectFactory工厂三级缓存用于生成Bean的早期引用流程我用一个经典场景来说明。假设容器要创建AA依赖BB依赖ASpring先实例化A调用A的构造函数得到一个A对象注意此时A的属性还没注入是个“半成品”。在A填充属性之前Spring把A的ObjectFactory放进三级缓存singletonFactories。A开始填充属性发现需要一个B于是去容器找B。容器发现B还不存在转而创建B实例化B把B的ObjectFactory放进三级缓存。B开始填充属性发现需要一个A于是去容器找A。B先查一级缓存没有查二级缓存没有查三级缓存发现了A的ObjectFactory。B调用这个ObjectFactory的getObject()方法得到一个“提前暴露”的A引用。这个A引用被存进二级缓存earlySingletonObjects同时删除三级缓存里的ObjectFactory。B顺利拿到A完成自己的属性填充、初始化B创建完成进一级缓存。回到A的创建流程A从一级缓存拿到B完成属性填充、初始化A也创建完成进一级缓存。关键点在于三级缓存里存的不是对象而是一个ObjectFactory。为什么要这么绕一下因为Spring需要在某些场景下对Bean生成代理对象比如AOP。如果缓存里直接放一个普通实例那代理就没法做了。通过ObjectFactory延迟到“真正发生循环依赖”时再去生成早期引用Spring就能在那一刻决定要不要生成代理对象。这也是三级缓存设计得比二级缓存更优雅的原因。4.2 为什么构造器注入解决不了循环依赖面试官问到这一般会追加一句那为什么构造器注入的循环依赖解决不了你要抓住核心区别三级缓存能解决循环依赖的前提是Bean在被其他Bean引用时实例化已经完成只是属性还没填充完。也就是说对象壳子已经存在可以先借给别的Bean用再回头补齐属性。但构造器注入发生在实例化阶段。A的构造函数需要BSpring不得不先创建BB的构造函数需要ASpring又得先创建A。可是A还没实例化完连放进三级缓存的ObjectFactory都还没有B去缓存里当然找不到A。两个Bean互相“卡死”Spring只好抛BeanCurrentlyInCreationException。所以结论很简单循环依赖不是靠注入方式解决的三是靠“提前暴露半成品对象”这个机制解决的。构造器注入之所以解决不了是因为它压根没有提前暴露的机会。4.3 Autowired与Resource面试必备的对比表DI的注解使用里另一个高频追问就是Autowired和Resource的区别。我直接给一张对比表面试时照着这个思路说基本就稳了对比项AutowiredResource来源Spring框架自带JSR-250规范属于Java EE标准默认注入方式按类型ByType按名称ByName名称找不到再按类型指定Bean名称搭配Qualifier自带name属性多实现处理需配合Qualifier或Primary指定直接通过指定名字规避歧义适用范围只能在Spring容器中使用理论上可用于任何支持JSR-250的容器很多老项目里用Resource居多因为它不用额外引Spring的注解也天然支持按名称注入。新项目里用Autowired多一些因为Spring生态圈对它支持最好配合Qualifier、Primary都很方便。我个人的习惯是如果团队不排斥Spring注解就用Autowired加Qualifier语义清晰如果遇到那种只按名字取Bean的场景用Resource反而更省事。4.4 面试回答的节奏与话术最后送一套我整理的回答节奏大家可以直接背下来再自己润色先一句话点题“DI是Dependency Injection的缩写也就是依赖注入它是IoC的一个核心实现方式。它把对象依赖的创建和查找责任从对象内部转移到外部容器让对象只声明‘我需要什么’不用关心‘怎么来’。”然后补一个例子“比如Service里不再new一个Mapper而是通过构造器参数声明依赖由Spring容器在运行期把Bean实例注入进来。”最后主动升华“这样做的主要收益是降低耦合、提升可测试性配合面向接口编程还能实现很灵活的运行时替换。我个人推荐用构造器注入因为它能保证依赖不可变、不为空也更方便单元测试。另外如果遇到循环依赖Spring可以用三级缓存处理基于字段或Setter的循环引用但构造器注入的循环依赖无法解决会直接报创建异常。”这样说下来既覆盖了定义、原理、实践又展示了自己踩过坑之后的总结面试官很难不给你加分。5. 从面试题到项目实践DI用得好代码才有救5.1 别让“上帝类”拖垮你的设计文章写到这我想把视角从面试拉回真实项目。很多团队把DI当成一种“配置方式”来用只会在类上打Service、Autowired一遇到设计问题就手足无措。实际上DI用得好不好直接反映在类设计的质量上。最常见的反面模式就是我前面提到的“上帝类”。一个Service类里有十几个字段依赖了十几个不同的Repository、Client、Utils。表面上看它都用构造器注入挺规范但本质上它把所有业务逻辑都焊死在一个类里了。这种类别说测试难写连读懂代码都是一种折磨。我的建议是看到构造器参数超过四五个就停下来想两步第一步这些依赖是不是都属于同一个职责维度第二步能不能把其中一部分依赖聚合成一个新的角色比如你有OrderRepository、OrderItemRepository、OrderHistoryRepository、OrderNotificationClient四个依赖完全可以把它们放进一个OrderDomainService或者OrderRepositoryFacade里让外层只依赖一个门面。DI给你的是“依赖可以单独替换”的能力不是让你把所有东西都塞进一个对象里。5.2 接口加DI替换实现的艺术既然DI把依赖从具体类变成了可替换的引用那它和接口设计就是天作之合。我举一个真实场景公司原来的支付模块对接的是微信支付后来要接入支付宝。如果代码里所有Service都直接依赖一个WechatPayService类每次扩展都要动到一堆调用方。但如果你定义了一个PaymentGateway接口public interface PaymentGateway { PaymentResult pay(Long orderId, BigDecimal amount); }然后用两个实现类去实现它在Spring里通过Qualifier或Primary来选择默认实现。这样一来新增支付渠道只需要写一个实现类不影响任何调用方。这就是“依赖接口而非实现”在DI加持下的威力。当然这个设计不是万能的。接口设计得不好滥用抽象反而增加代码阅读成本。我的实践经验是只有那些“未来大概率会出现多种实现”的协作对象才值得抽象成接口比如支付、短信、缓存、消息队列这些强外部依赖纯粹的内部工具类、内部计算逻辑直接注入具体类反而更清晰。5.3 Configuration与Bean的正确打开方式聊DI的实战不能不提配置类。很多初学者只知道在类上用Service但遇到第三方库或者一些无法加注解的类时就得靠Configuration加Bean手动声明Configuration public class AppConfig { Bean public RestTemplate restTemplate() { return new RestTemplateBuilder() .setConnectTimeout(Duration.ofSeconds(5)) .build(); } }这段代码声明了一个RestTemplate对象交给容器管理其他类需要时直接注入就行。这个机制的强大之处在于它让那些来自第三方Jar包的类也能融入DI体系。JavaConfig已经是现代Spring应用最常用的装配方式比起老早的XML配置它类型安全、重构友好、IDE提示全。另外说一个细节Bean注解的方法方式命名默认Bean名称就是方法名。如果你想显式指定名字可以在Bean(xx)里写。这个看似无用但在多个同类型Bean存在时能救命。5.4 真正该记在心里的几条实战经验文章最后我把这些年和DI“交手”的经验浓缩成几条建议都是踩过坑换来的看到就是赚到第一新写的业务代码坚决用构造器注入字段只加final。如果遇到测试不好构造的类先从设计上找问题而不是退回字段注入图省事。第二依赖超过四五个别急着吐槽“构造器注入太啰嗦”先想想这个类是不是该拆了。第三第三方类、需要读配置初始化、需要定制生命周期的对象用Bean放入容器别到处手写new然后自己管理。第四尽量做到面向接口编程尤其是外部依赖类的替换边界。将来换实现的时候你会发现当初那点抽象成本非常值。第五注意单一职责原则。DI容器可以管理依赖但管不了你的类设计类本身职责混乱注入得再规范也救不了代码的可维护性。说到底DI也好IoC也好真正想通的感受就像从一个事事亲力亲为的人变成了一个善于委托的团队管理者。所有对象各司其职依赖关系清清楚楚系统自然就活了。

相关推荐

40个AI指令模板:告别模糊提问,让Prompt成为你的效率杠杆
40个AI指令模板:告别模糊提问,让Prompt成为你的效率杠杆

身边总有两类人。一类把AI当高级搜索引擎,问一句答一句,答完就断,三天之后得出结论:AI不过如此。另一类把AI用成了全能助理,写文案、写方案、调代码、做表格、定计划,一个人顶一个编外团队。差距不在账号&a… · 2026/9/24 20:28:14

【Web·基础学习】布局模式和style和css的关系
【Web·基础学习】布局模式和style和css的关系

CSS 的 display 属性决定了元素的显示类型和内部布局模式。普通流布局(Normal Flow)→ block / inline / inline-block弹性布局(Flexbox)→ flex / inline-flex网格布局(Grid)→ grid / inline-grid表格布局… · 2026/9/24 20:28:08

图吧工具箱2026最新版下载与硬件检测实战:从验机到维护的完整指南
图吧工具箱2026最新版下载与硬件检测实战:从验机到维护的完整指南

1. 图吧工具箱到底是个什么东西,为什么装机的人都在用第一次接触图吧工具箱的人,多半是在某个装机群里看到别人甩出一张截图,上面密密麻麻排列着CPU-Z、GPU-Z、AIDA64、CrystalDiskInfo、DisplayX这些检测工具,然后有人问“这啥软… · 2026/9/24 20:27:56

从年度目标到12次月度复盘:一套可坚持的自我管理体系
从年度目标到12次月度复盘:一套可坚持的自我管理体系

"2024 1-12",这串看起来像编号的数字,是我去年年初在复盘文档第一行敲下的项目标题。当时只是想给一整年的记录做个归档,没想到它最后长成了一套完整的月度复盘体系,也改变了我在2025年安排日程、制定目标的方式。这篇内… · 2026/9/24 21:05:01

安卓APK签名、DEX处理与安全加固全流程解析
安卓APK签名、DEX处理与安全加固全流程解析

做安卓开发或者移动安全这块的,不管你是写应用的、做逆向的,还是负责上架分发的,这两年都绕不开一套固定动作:签名、重打包、处理DEX、加固之后再签名。圈里把这套流程叫“处理系统”或者“打包系统”,听起来挺神秘&am… · 2026/9/24 21:05:00

手机热像仪能看清多大温差?从NETD到实战场景解析
手机热像仪能看清多大温差?从NETD到实战场景解析

有人可能觉得手机热像仪就是个玩具,拍出来的画面花里胡哨,除了好看没啥用。但真正把它用在电气检修、房屋渗漏排查、PCB焊接质量检查这些场合时,你才会意识到一个问题:它到底能看清多小的温度差?宣传页上写的“热灵敏度… · 2026/9/24 21:04:48

网络管理底层逻辑:从状态机到AUTOSAR NM,掌握报文与超时
网络管理底层逻辑:从状态机到AUTOSAR NM,掌握报文与超时

如果你问我,做网络管理这些年最深的体会是什么,我的答案大概率不是某个具体的路由配置,也不是哪条防火墙策略,而是四个字:底层逻辑。网线拔了、设备离线、应用卡顿,大多数问题表面上看是“链路断了”“设备… · 2026/9/24 21:04:48

基于Python的AI合成人脸检测系统:MobileNet与TFLite工程化实战
基于Python的AI合成人脸检测系统:MobileNet与TFLite工程化实战

简介:这份资源是面向计算机相关专业学生与开发者的毕业设计级项目,主题为基于Python的AI人脸合成图像检测系统,适合软件工程、人工智能、通信工程等方向的同学用于毕设、课设或项目立项演示,也可供教师与企业员工参考。压缩包共约… · 2026/9/24 21:04:48

Cell文献速递实操指南:从信息源搭建到精读笔记的完整流程
Cell文献速递实操指南:从信息源搭建到精读笔记的完整流程

每每周四晚上十点左右,手机上的邮件推送总会准时响起来——Cell Press的New Articles邮件到了。这个习惯我保持了快五年,从博士第一年到现在独立带课题,几乎没断过。身边总有朋友问我:现在数据库、预印本、公众号解读铺天盖地&… · 2026/9/24 21:04:48

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码