📌 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")java// ❌ Service Locator:组件耦合了容器 API public class OrderService { private UserRepository userRepo = ServiceLocator.get("userRepository"); } // ✅ 依赖注入:组件完全无感知 public class OrderService { private final UserRepository userRepo; public OrderService(UserRepository userRepo) { // 容器推送依赖 this.userRepo = userRepo; } }关键认知 :DI 是 IOC 原则的一种实现模式,Service Locator 是另一种实现模式。Spring 同时支持两者(
@Autowired是 DI,getBean()是 Service Locator),但推荐优先使用 DI。citation:1
2. DI 的三种实现方式------源码级差异
Spring 对三种注入方式的处理逻辑完全不同,体现在不同的处理类和注入时机:citation:3citation:4
-
2.1 构造器注入(Constructor Injection)------官方推荐 Spring 从 4.0 起明确推荐构造器注入:citation:4
java@Service public class OrderService { private final UserRepository userRepository; private final PaymentService paymentService; // Spring Boot 2.x+ 单构造器可省略 @Autowired public OrderService(UserRepository userRepository, PaymentService paymentService) { this.userRepository = userRepository; this.paymentService = paymentService; } }Spring 源码处理 :
ConstructorResolver.autowireConstructor()java// AbstractAutowireCapableBeanFactory.createBeanInstance() protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) { Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); // 解析构造器参数类型,递归解析依赖 return autowireConstructor(beanName, mbd, ctors, args); }注入时机 :实例化阶段 (
createBeanInstance),对象创建时就完成依赖注入。核心优势:
优势 说明 不可变性 final字段,对象创建后不可变,线程安全非空保证 无依赖则无法创建对象,杜绝 NPE 依赖可见 构造器参数列表即依赖清单 测试友好 直接 new Service(mockDep)循环依赖早暴露 启动时抛出异常,而非运行时 -
2.2 Setter 注入(Setter Injection)------可选依赖 适用于依赖可选、运行期可替换的场景:
java@Service public class UserService { private DataSource dataSource; @Autowired(required = false) public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }Spring 源码处理 :
AutowiredMethodElement.inject()java// AutowiredAnnotationBeanPostProcessor.AutowiredMethodElement protected void inject(Object bean, String beanName, PropertyValues pvs) { Method method = (Method) this.member; Object[] arguments = resolveMethodArguments(method); // 解析参数依赖 ReflectionUtils.makeAccessible(method); method.invoke(bean, arguments); // 调用 Setter 方法注入 }注入时机 :属性填充阶段 (
populateBean),对象创建后通过反射调用 Setter 方法注入。 -
2.3 字段注入(Field Injection)------不推荐 IDEA 会提示警告:
java@Service public class UserService { @Autowired // ⚠️ Field injection is not recommended private UserRepository userRepository; }Spring 源码处理 :
AutowiredFieldElement.inject()java// AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement protected void inject(Object bean, String beanName, PropertyValues pvs) { Field field = (Field) this.member; Object value = beanFactory.resolveDependency( new DependencyDescriptor(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 替换 MySQLUserRepository |
4. 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 限定
java@Service public class PaymentRouter { @Autowired @Qualifier("aliPayService") // 指定注入 aliPayService private PaymentService paymentService; } -
5.2 集合注入------策略模式的天然支持
java@Service public class PaymentRouter { // 自动收集所有 PaymentService 的实现类 @Autowired private List<PaymentService> paymentServices; public PaymentService route(String channel) { return paymentServices.stream() .filter(p -> p.supports(channel)) .findFirst() .orElseThrow(); } } -
5.3 @Value 注入------配置外部化
java@Service public class OrderService { @Value("${order.timeout:30000}") private int timeout; @Value("${order.max-retry:3}") private int maxRetry; } -
5.4 @Lazy 延迟注入
java@Service public class ServiceA { private final ServiceB serviceB; public ServiceA(@Lazy ServiceB serviceB) { // 延迟注入,解决循环依赖 this.serviceB = serviceB; } }
6. 生产环境避坑指南
-
6.1 构造器注入参数过多 = 设计异味 如果构造器参数超过 4-5 个,说明类可能承担了过多职责,应使用 Facade 模式或拆分:
java// ❌ 参数过多,违反单一职责 public class OrderService { public OrderService(UserRepo userRepo, ProductRepo productRepo, InventoryRepo inventoryRepo, PaymentRepo paymentRepo, LogRepo logRepo, NotificationRepo notificationRepo) { } } // ✅ 拆分为多个小类 public class OrderCreationService { } public class OrderPaymentService { } public class OrderNotificationService { } -
6.2 循环依赖的正确处理 遇到循环依赖首先反思设计是否合理。如果确实需要:
- 构造器注入:使用
@Lazy延迟注入; - 字段注入:Spring 自动通过三级缓存解决,但建议重构消除。
- 构造器注入:使用
-
6.3 避免
@Autowired注入null如果 Bean 未被 Spring 管理(如new创建的对象),@Autowired不会生效:java// ❌ userRepository 为 null,因为 OrderService 不是 Spring 管理的 Bean OrderService service = new OrderService(); service.createOrder(); // NPE! // ✅ 必须从容器中获取 OrderService service = applicationContext.getBean(OrderService.class); -
6.4
@Resource与@Autowired的选择维度 @Autowired@Resource来源 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 的构造器依赖 B,B 的构造器依赖 A,Spring 会直接抛出BeanCurrentlyInCreationException。 - Setter/字段注入 :循环依赖在运行时 暴露,但 Spring 通过三级缓存 自动解决。因为字段注入在
populateBean()阶段完成,此时对象已经实例化(通过无参构造器),可以先放入三级缓存(singletonFactories),供循环依赖的对方获取早期引用。
工程建议 :遇到构造器循环依赖,首先反思设计是否违反了单一职责原则。如果确实需要,使用@Lazy延迟注入其中一个依赖,而不是改用字段注入绕过问题。"
- 构造器注入 :循环依赖在启动时 直接暴露。因为构造器注入在
-
追问 5:"@Autowired 的注入过程是怎样的?从源码角度说说。"
高分回答:
"
@Autowired的注入过程因注入方式不同而有所差异,以字段注入为例:- 解析阶段 :
AutowiredAnnotationBeanPostProcessor在postProcessMergedBeanDefinition()中扫描类的所有字段,找到标记@Autowired的字段,创建AutowiredFieldElement对象缓存; - 注入阶段 :在
populateBean()中调用AutowiredFieldElement.inject():- 通过
DependencyDescriptor封装字段信息(类型、是否必需); - 调用
beanFactory.resolveDependency()解析依赖:先按类型查找所有候选 Bean,如果多个则按@Qualifier或字段名过滤; - 如果找不到且
required=true,抛出NoSuchBeanDefinitionException; - 通过反射
field.set(bean, value)直接设置字段值。
- 通过
- 构造器注入的差异 :在
createBeanInstance()中由ConstructorResolver处理,解析构造器参数类型,递归调用resolveDependency()获取每个参数,然后反射调用Constructor.newInstance(args)创建对象。
关键认知:@Autowired的处理者是AutowiredAnnotationBeanPostProcessor(一个BeanPostProcessor),它在populateBean()阶段介入,这是 ApplicationContext 自动注册的 BPP 之一。"
- 解析阶段 :
-
追问 6:"如果不用 Spring,怎么实现依赖注入?手写一个极简版。"
高分回答:
"不依赖 Spring 实现 DI 的核心思路是:扫描注解 → 解析依赖 → 递归创建 → 反射注入。
javapublic class SimpleDIContainer { private Map<Class<?>, Object> instances = new HashMap<>(); public void register(Class<?> clazz) { // 1. 解析构造器参数 Constructor<?> ctor = clazz.getDeclaredConstructors()[0]; Class<?>[] paramTypes = ctor.getParameterTypes(); // 2. 递归解析依赖 Object[] args = new Object[paramTypes.length]; for (int i = 0; i < paramTypes.length; i++) { args[i] = getOrCreate(paramTypes[i]); // 递归创建依赖 } // 3. 反射创建对象 Object instance = ctor.newInstance(args); instances.put(clazz, instance); } private Object getOrCreate(Class<?> type) { if (instances.containsKey(type)) return instances.get(type); return register(type); // 递归创建 } }这个极简版展示了 DI 的核心:递归解析构造器参数、缓存单例实例。实际框架还需处理:循环依赖(三级缓存)、作用域、生命周期回调、AOP 代理等。"
8. 方案选型速查表
| 场景 | 推荐注入方式 | 核心理由 |
|---|---|---|
| 强制依赖(核心业务类) | 构造器注入 | 不可变、非空保证、测试友好 |
| 可选依赖(配置、插件) | Setter 注入 | 灵活性高,运行期可替换 |
| 同类型多实例 | 构造器注入 + @Qualifier |
明确指定实现类 |
| 策略模式(多实现) | 构造器注入 + List<Interface> |
自动收集所有实现 |
| 循环依赖(构造器) | @Lazy + 构造器注入 |
保持构造器优势,延迟打破循环 |
| 配置属性注入 | @Value + 字段/构造器 |
配置外部化 |
| 框架无关性要求 | @Inject(JSR-330) |
不绑定 Spring |
💡 面试官想要的满分总结:
依赖注入不是"Spring 自动注入对象"这么简单,而是一种支撑现代软件架构的设计模式。
核心定义:DI 是 IOC 原则的一种实现,特征是**'推送'而非'拉取'**------依赖由外部容器在组件创建时主动注入,组件无需知道容器的存在。这与 Service Locator(组件主动查找)有本质区别。
Spring 支持三种注入方式:构造器注入(
ConstructorResolver处理,实例化时注入,官方推荐)、Setter 注入(AutowiredMethodElement处理,属性填充时注入,可选依赖场景)、字段注入(AutowiredFieldElement处理,反射直接注入,不推荐)。构造器注入的优势在于不可变性、非空保证、依赖可见、测试友好------这些不是"风格偏好",而是工程质量的保障。DI 支撑了 SOLID 原则中的依赖倒置(DIP)和开闭原则(OCP),是解耦、可测试、可扩展的架构基石。理解 DI 必须理解它与 Service Locator 的区别、三种注入方式的源码差异、以及循环依赖的处理机制------这才是大厂面试官想听到的深度。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯