PDF大白话说Java面试题 — 06_Spring篇第7题什么是依赖注入回答核心考点 什么是依赖注入是 Spring 面试的入门题但大厂面试官不会满足于容器自动注入依赖这种教科书定义。真正想考察的是DI 在 SOLID 原则中的定位依赖倒置原则的具体实现模式、三种注入方式的源码级差异ConstructorResolver/AutowiredMethodElement/AutowiredFieldElement的处理时机和线程安全影响、以及DI 与 Service Locator 模式的本质区别推送 vs 拉取。面试官真正想判断的是你是否将 DI 视为一种架构设计模式而非仅仅是 Spring 的语法特性。1. 依赖注入的精确定义——不是自动创建对象而是推送依赖1.1 Martin Fowler 的原始定义Martin Fowler 在 2004 年的经典文章《Inversion of Control Containers and the Dependency Injection pattern》中明确定义[citation:1]“依赖注入是一种将组件的依赖关系从外部注入的技术组件无需自己查找依赖也无需知道依赖的具体实现。”核心特征特征说明外部注入依赖由容器/调用方提供而非组件自己创建接口依赖组件声明依赖接口不绑定具体实现无感知组件不知道容器的存在与 Service Locator 的关键区别可替换运行时可通过不同实现替换依赖1.2 DI 与 Service Locator 的本质区别这是面试中最容易混淆的两个概念[citation:1]维度依赖注入DI服务定位器Service Locator依赖获取方式推送Push容器主动注入拉取Pull组件主动查找容器耦合❌ 组件不知道容器存在✅ 组件依赖容器 API依赖可见性高构造器/Setter 暴露低隐藏查找逻辑测试友好度直接传入 Mock无需容器需模拟 Service Locator典型代码new Service(mockDep)ServiceLocator.get(dep)// ❌ Service Locator组件耦合了容器 APIpublicclassOrderService{privateUserRepositoryuserRepoServiceLocator.get(userRepository);}// ✅ 依赖注入组件完全无感知publicclassOrderService{privatefinalUserRepositoryuserRepo;publicOrderService(UserRepositoryuserRepo){// 容器推送依赖this.userRepouserRepo;}}关键认知DI 是 IOC 原则的一种实现模式Service Locator 是另一种实现模式。Spring 同时支持两者Autowired是 DIgetBean()是 Service Locator但推荐优先使用 DI。[citation:1]2. DI 的三种实现方式——源码级差异Spring 对三种注入方式的处理逻辑完全不同体现在不同的处理类和注入时机[citation:3][citation:4]2.1 构造器注入Constructor Injection——官方推荐Spring 从 4.0 起明确推荐构造器注入[citation:4]ServicepublicclassOrderService{privatefinalUserRepositoryuserRepository;privatefinalPaymentServicepaymentService;// Spring Boot 2.x 单构造器可省略 AutowiredpublicOrderService(UserRepositoryuserRepository,PaymentServicepaymentService){this.userRepositoryuserRepository;this.paymentServicepaymentService;}}Spring 源码处理ConstructorResolver.autowireConstructor()// AbstractAutowireCapableBeanFactory.createBeanInstance()protectedBeanWrappercreateBeanInstance(StringbeanName,RootBeanDefinitionmbd,Object[]args){Constructor?[]ctorsdetermineConstructorsFromBeanPostProcessors(beanClass,beanName);// 解析构造器参数类型递归解析依赖returnautowireConstructor(beanName,mbd,ctors,args);}注入时机实例化阶段createBeanInstance对象创建时就完成依赖注入。核心优势优势说明不可变性final字段对象创建后不可变线程安全非空保证无依赖则无法创建对象杜绝 NPE依赖可见构造器参数列表即依赖清单测试友好直接new Service(mockDep)循环依赖早暴露启动时抛出异常而非运行时2.2 Setter 注入Setter Injection——可选依赖适用于依赖可选、运行期可替换的场景ServicepublicclassUserService{privateDataSourcedataSource;Autowired(requiredfalse)publicvoidsetDataSource(DataSourcedataSource){this.dataSourcedataSource;}}Spring 源码处理AutowiredMethodElement.inject()// AutowiredAnnotationBeanPostProcessor.AutowiredMethodElementprotectedvoidinject(Objectbean,StringbeanName,PropertyValuespvs){Methodmethod(Method)this.member;Object[]argumentsresolveMethodArguments(method);// 解析参数依赖ReflectionUtils.makeAccessible(method);method.invoke(bean,arguments);// 调用 Setter 方法注入}注入时机属性填充阶段populateBean对象创建后通过反射调用 Setter 方法注入。2.3 字段注入Field Injection——不推荐IDEA 会提示警告ServicepublicclassUserService{Autowired// ⚠️ Field injection is not recommendedprivateUserRepositoryuserRepository;}Spring 源码处理AutowiredFieldElement.inject()// AutowiredAnnotationBeanPostProcessor.AutowiredFieldElementprotectedvoidinject(Objectbean,StringbeanName,PropertyValuespvs){Fieldfield(Field)this.member;ObjectvaluebeanFactory.resolveDependency(newDependencyDescriptor(field,this.required),beanName);ReflectionUtils.makeAccessible(field);field.set(bean,value);// 反射直接设置字段值}注入时机属性填充阶段populateBean对象创建后通过反射直接设置字段值。2.4 三种注入方式深度对比对比维度构造器注入Setter 注入字段注入处理类ConstructorResolverAutowiredMethodElementAutowiredFieldElement注入时机实例化阶段属性填充阶段属性填充阶段不可变性✅final❌❌NPE 风险无构造器校验有可能未调用 Setter有构造器中访问为 null依赖可见性高参数列表中Setter 方法低隐藏字段循环依赖启动时暴露运行时暴露三级缓存解决运行时暴露三级缓存解决Spring 推荐度⭐⭐⭐⭐⭐⭐⭐⭐⭐IDEA 警告无无⚠️ “Field injection is not recommended”3. DI 与 SOLID 原则的关系DI 不是孤立的技术而是支撑多个 SOLID 原则的具体模式[citation:2]SOLID 原则DI 的支撑方式代码体现依赖倒置DIP高层模块依赖抽象接口低层模块实现接口OrderService依赖UserRepository接口开闭原则OCP新增功能只需添加新实现类无需修改使用方新增WeChatPayService implements PaymentService单一职责SRP每个类只负责自己的职责依赖由容器管理OrderService不处理对象创建接口隔离ISP依赖最小接口而非胖接口UserRepository只暴露必要方法里氏替换LSP子类实现可无缝替换父类/接口MockUserRepository替换MySQLUserRepository4. DI 在 Spring 中的完整执行链路理解 DI 必须理解 Spring 从配置到注入的完整链路[citation:3]Configuration 配置类 / ComponentScan 扫描 ↓ ClassPathBeanDefinitionScanner 扫描包路径 ↓ AnnotatedBeanDefinitionReader 解析 Configuration ↓ BeanDefinitionRegistry 注册 BeanDefinition元数据 ↓ refresh() → finishBeanFactoryInitialization() ↓ preInstantiateSingletons() 遍历所有非懒加载单例 ↓ getBean() → doGetBean() ↓ createBean() → doCreateBean() ↓ createBeanInstance() 实例化构造器注入在此完成 ↓ populateBean() 属性填充Setter/字段注入在此完成 ↓ initializeBean() 初始化PostConstruct、BPP 前后处理 ↓ 单例 Bean 放入 singletonObjects 缓存5. DI 的高级特性5.1 按类型注入 Qualifier 限定ServicepublicclassPaymentRouter{AutowiredQualifier(aliPayService)// 指定注入 aliPayServiceprivatePaymentServicepaymentService;}5.2 集合注入——策略模式的天然支持ServicepublicclassPaymentRouter{// 自动收集所有 PaymentService 的实现类AutowiredprivateListPaymentServicepaymentServices;publicPaymentServiceroute(Stringchannel){returnpaymentServices.stream().filter(p-p.supports(channel)).findFirst().orElseThrow();}}5.3 Value 注入——配置外部化ServicepublicclassOrderService{Value(${order.timeout:30000})privateinttimeout;Value(${order.max-retry:3})privateintmaxRetry;}5.4 Lazy 延迟注入ServicepublicclassServiceA{privatefinalServiceBserviceB;publicServiceA(LazyServiceBserviceB){// 延迟注入解决循环依赖this.serviceBserviceB;}}6. 生产环境避坑指南6.1 构造器注入参数过多 设计异味如果构造器参数超过 4-5 个说明类可能承担了过多职责应使用 Facade 模式或拆分// ❌ 参数过多违反单一职责publicclassOrderService{publicOrderService(UserRepouserRepo,ProductRepoproductRepo,InventoryRepoinventoryRepo,PaymentRepopaymentRepo,LogRepologRepo,NotificationReponotificationRepo){}}// ✅ 拆分为多个小类publicclassOrderCreationService{}publicclassOrderPaymentService{}publicclassOrderNotificationService{}6.2 循环依赖的正确处理遇到循环依赖首先反思设计是否合理。如果确实需要构造器注入使用Lazy延迟注入字段注入Spring 自动通过三级缓存解决但建议重构消除。6.3 避免Autowired注入null如果 Bean 未被 Spring 管理如new创建的对象Autowired不会生效// ❌ userRepository 为 null因为 OrderService 不是 Spring 管理的 BeanOrderServiceservicenewOrderService();service.createOrder();// NPE!// ✅ 必须从容器中获取OrderServiceserviceapplicationContext.getBean(OrderService.class);6.4Resource与Autowired的选择维度AutowiredResource来源Spring 注解JSR-250 标准匹配规则先按类型再按名称先按名称再按类型适用场景Spring 项目追求框架无关性7. 面试官追问与高分回答模板追问 1“什么是依赖注入”低分回答“依赖注入就是 Spring 容器自动把依赖对象注入到类中。”没有触及本质高分回答“依赖注入DI是一种设计模式是控制反转IOC原则的一种具体实现。它的核心特征是**‘推送’而非’拉取’**——依赖由外部容器在组件创建时主动注入组件无需自己查找依赖也无需知道依赖的具体实现。与 Service Locator服务定位器的关键区别在于DI 的组件完全不知道容器的存在通过构造器/Setter/字段接收依赖而 Service Locator 需要组件主动调用容器 API如getBean()查找依赖导致组件与容器耦合。Spring 支持三种注入方式构造器注入ConstructorResolver处理实例化时注入、Setter 注入AutowiredMethodElement处理属性填充时注入、字段注入AutowiredFieldElement处理反射直接注入。官方推荐构造器注入。”追问 2“为什么 Spring 推荐构造器注入而不是字段注入”高分回答Spring 官方从 4.0 起明确推荐构造器注入原因有五个不可变性构造器注入允许依赖声明为final对象创建后不可变天然线程安全字段注入无法使用final。非空保证构造器注入在对象创建时就要求所有依赖就绪没有依赖就无法创建对象从根本上杜绝 NPE字段注入在构造器中访问会得到 null。依赖可见构造器参数列表一目了然类的所有依赖关系清晰透明字段注入的依赖隐藏在私有字段中。测试友好单元测试可以直接new Service(mockDep)无需反射或 Spring 容器字段注入必须通过反射注入 Mock。设计约束构造器参数过多4个会直观提示类职责过重促使开发者重构字段注入没有这个’预警’机制。从源码角度看构造器注入在createBeanInstance()阶段完成Setter 和字段注入在populateBean()阶段完成。构造器注入的循环依赖在启动时直接暴露BeanCurrentlyInCreationException而字段注入的循环依赖被三级缓存掩盖。追问 3“依赖注入和依赖查找有什么区别”高分回答两者都是 IOC 原则的实现方式但核心区别在于谁主动依赖注入DI容器主动将依赖’推送’给组件。组件通过构造器参数、Setter 方法或字段声明’我需要什么’容器负责’给什么’。组件完全不知道容器的存在。依赖查找DL组件主动向容器’请求’依赖。组件需要知道容器的 API如ApplicationContext.getBean()通过名称或类型查找依赖。对比| 维度 | DI | DL || 耦合 | 组件与容器零耦合 | 组件与容器 API 耦合 || 测试 | 直接传入 Mock | 需模拟容器或启动容器 || 依赖可见性 | 高构造器/Setter 暴露 | 低隐藏查找逻辑 || Spring 支持 |Autowired、Inject|getBean()|Spring 同时支持两者但推荐 DI。只有在无法通过注入获取依赖时如静态方法中才使用 DL。追问 4“构造器注入和 Setter 注入在循环依赖处理上有什么区别”高分回答两者的核心区别在于循环依赖暴露的时机和解决方式构造器注入循环依赖在启动时直接暴露。因为构造器注入在createBeanInstance()阶段完成此时对象尚未创建完成构造器还没执行完无法放入三级缓存。如果 A 的构造器依赖 BB 的构造器依赖 ASpring 会直接抛出BeanCurrentlyInCreationException。Setter/字段注入循环依赖在运行时暴露但 Spring 通过三级缓存自动解决。因为字段注入在populateBean()阶段完成此时对象已经实例化通过无参构造器可以先放入三级缓存singletonFactories供循环依赖的对方获取早期引用。工程建议遇到构造器循环依赖首先反思设计是否违反了单一职责原则。如果确实需要使用Lazy延迟注入其中一个依赖而不是改用字段注入绕过问题。追问 5“Autowired 的注入过程是怎样的从源码角度说说。”高分回答Autowired的注入过程因注入方式不同而有所差异以字段注入为例解析阶段AutowiredAnnotationBeanPostProcessor在postProcessMergedBeanDefinition()中扫描类的所有字段找到标记Autowired的字段创建AutowiredFieldElement对象缓存注入阶段在populateBean()中调用AutowiredFieldElement.inject()通过DependencyDescriptor封装字段信息类型、是否必需调用beanFactory.resolveDependency()解析依赖先按类型查找所有候选 Bean如果多个则按Qualifier或字段名过滤如果找不到且requiredtrue抛出NoSuchBeanDefinitionException通过反射field.set(bean, value)直接设置字段值。构造器注入的差异在createBeanInstance()中由ConstructorResolver处理解析构造器参数类型递归调用resolveDependency()获取每个参数然后反射调用Constructor.newInstance(args)创建对象。关键认知Autowired的处理者是AutowiredAnnotationBeanPostProcessor一个BeanPostProcessor它在populateBean()阶段介入这是 ApplicationContext 自动注册的 BPP 之一。追问 6“如果不用 Spring怎么实现依赖注入手写一个极简版。”高分回答不依赖 Spring 实现 DI 的核心思路是扫描注解 → 解析依赖 → 递归创建 → 反射注入。publicclassSimpleDIContainer{privateMapClass?,ObjectinstancesnewHashMap();publicvoidregister(Class?clazz){// 1. 解析构造器参数Constructor?ctorclazz.getDeclaredConstructors()[0];Class?[]paramTypesctor.getParameterTypes();// 2. 递归解析依赖Object[]argsnewObject[paramTypes.length];for(inti0;iparamTypes.length;i){args[i]getOrCreate(paramTypes[i]);// 递归创建依赖}// 3. 反射创建对象Objectinstancector.newInstance(args);instances.put(clazz,instance);}privateObjectgetOrCreate(Class?type){if(instances.containsKey(type))returninstances.get(type);returnregister(type);// 递归创建}}这个极简版展示了 DI 的核心递归解析构造器参数、缓存单例实例。实际框架还需处理循环依赖三级缓存、作用域、生命周期回调、AOP 代理等。8. 方案选型速查表场景推荐注入方式核心理由强制依赖核心业务类构造器注入不可变、非空保证、测试友好可选依赖配置、插件Setter 注入灵活性高运行期可替换同类型多实例构造器注入 Qualifier明确指定实现类策略模式多实现构造器注入 ListInterface自动收集所有实现循环依赖构造器Lazy 构造器注入保持构造器优势延迟打破循环配置属性注入Value 字段/构造器配置外部化框架无关性要求InjectJSR-330不绑定 Spring面试官想要的满分总结依赖注入不是Spring 自动注入对象这么简单而是一种支撑现代软件架构的设计模式。核心定义DI 是 IOC 原则的一种实现特征是**‘推送’而非’拉取’**——依赖由外部容器在组件创建时主动注入组件无需知道容器的存在。这与 Service Locator组件主动查找有本质区别。Spring 支持三种注入方式构造器注入ConstructorResolver处理实例化时注入官方推荐、Setter 注入AutowiredMethodElement处理属性填充时注入可选依赖场景、字段注入AutowiredFieldElement处理反射直接注入不推荐。构造器注入的优势在于不可变性、非空保证、依赖可见、测试友好——这些不是风格偏好而是工程质量的保障。DI 支撑了 SOLID 原则中的依赖倒置DIP和开闭原则OCP是解耦、可测试、可扩展的架构基石。理解 DI 必须理解它与 Service Locator 的区别、三种注入方式的源码差异、以及循环依赖的处理机制——这才是大厂面试官想听到的深度。觉得对您有帮助麻烦点点关注啦您的关注是我创作的最大动力~