下面我给你一个真正工程师能听懂的大白话解释,把:

  • Service Locator vs DI 的本质区别
  • DI 容器到底是怎么知道“给谁注入什么依赖”的
  • 为什么 Service Locator 被认为是“伪 IoC”
  • Spring / Koin / Dagger 是怎么做依赖匹配的

讲得非常清晰。


🧩 一、Service Locator vs DI —— 最通俗的大白话区别

🎯 一句话总结

Service Locator:对象主动去“找”依赖
DI(依赖注入):对象被动“被送来”依赖

再白一点:

Service Locator = 你自己去外卖平台点餐
DI = 外卖小哥把饭送到你手里


🧱 二、Service Locator(服务定位器)是什么?

Service Locator 的写法一般是这样:

val engine = ServiceLocator.get<Engine>()
val car = Car(engine)

特点:

  • 对象自己调用 ServiceLocator 去拿依赖
  • 依赖查找逻辑写在业务代码里
  • 业务代码知道“依赖从哪里来”

缺点:

  • 隐式依赖(看不到构造函数参数)
  • 难测试(你必须 mock ServiceLocator)
  • 耦合度高(业务代码依赖容器)

本质:

依赖查找(Dependency Lookup)


🧱 三、DI(依赖注入)是什么?

DI 的写法是:

class Car(val engine: Engine)

依赖由外部注入:

val car = Car(engine = GasEngine())

或者由框架注入:

@Autowired
lateinit var engine: Engine

特点:

  • 对象不关心依赖从哪里来
  • 依赖由容器负责创建和注入
  • 业务代码只依赖接口,不依赖容器

本质:

依赖注入(Dependency Injection)


🎯 四、核心区别(工程师视角)

项目Service LocatorDI(依赖注入)
谁负责获取依赖对象自己容器负责
依赖关系隐式显式(构造函数参数)
可测试性差(要 mock 容器)好(直接传 mock)
耦合度高(依赖容器)低(只依赖接口)
依赖方向对象 → 容器容器 → 对象
是否符合 IoC不完全完全

一句话:

Service Locator 是“主动找”,DI 是“被动给”。


🧠 五、DI 容器是如何知道“给哪个类注入哪个依赖”的?

这是你最关心的部分,我分三种主流 DI 框架讲:


1)Spring(运行时 DI)——通过反射 + 类型匹配

Spring 的流程:

✔ 第一步:扫描 Bean

Spring 启动时扫描所有:

  • @Component
  • @Service
  • @Repository
  • @Bean

并把它们注册到 BeanFactory。

✔ 第二步:分析依赖(反射)

Spring 通过反射查看类的:

  • 构造函数参数类型
  • 字段类型(@Autowired)
  • setter 方法参数类型

例如:

@Autowired
Engine engine;

Spring 会看到:

  • Car 需要一个 Engine 类型的依赖
  • 容器里有 GasEngine 和 ElectricEngine
  • 如果只有一个实现 → 注入它
  • 如果多个 → 需要 @Qualifier 指定

✔ 第三步:实例化并注入

Spring 创建 Engine → 再创建 Car → 注入 Engine。


2)Koin(运行时 DI)——通过 DSL 注册 + 类型匹配

Koin 的流程:

✔ 第一步:你注册依赖

module {
    single<Engine> { GasEngine() }
}

✔ 第二步:Koin 记录“Engine → GasEngine”

✔ 第三步:构造函数注入时匹配类型

class Car(val engine: Engine)

Koin 会:

  • 反射 Car 的构造函数
  • 看到需要 Engine
  • 找到你注册的 Engine 实现
  • 注入进去

3)Dagger/Hilt(编译时 DI)——通过注解处理器生成代码

Dagger 不用反射,它在编译期做:

✔ 第一步:扫描 @Inject 构造函数

class Car @Inject constructor(val engine: Engine)

✔ 第二步:扫描 @Provides / @Binds

@Provides fun provideEngine(): Engine = GasEngine()

✔ 第三步:生成依赖图 + 生成工厂类

编译器生成类似:

public class Car_Factory {
    public Car get() {
        return new Car(new GasEngine());
    }
}

所以 Dagger 是:

编译时就知道依赖关系,不用反射,性能极高。


🎯 六、DI 容器是如何“知道”依赖关系的?(最终总结)

DI 容器通过以下方式知道“给谁注入什么”:

  1. 扫描类的构造函数 / 字段 / setter
  2. 读取类型信息(Engine、Repository、Service 等)
  3. 在容器中查找对应类型的实现
  4. 实例化依赖
  5. 注入到目标对象

不同框架的实现方式不同:

框架注入时机依赖匹配方式
Spring运行时反射 + 类型匹配
Koin运行时DSL 注册 + 反射
Dagger/Hilt编译时注解处理器生成代码

🏁 最终总结(最精炼版本)

✔ Service Locator vs DI

  • Service Locator:对象主动找依赖(耦合高)
  • DI:容器主动注入依赖(耦合低)

✔ DI 如何知道注入什么?

  • 通过类型匹配
  • 通过构造函数参数
  • 通过注解(@Inject / @Autowired)
  • 通过容器注册信息(module、@Bean)

✔ 不同框架的差异

  • Spring/Koin:运行时反射
  • Dagger/Hilt:编译时生成代码

如果你愿意,我还能给你:

🔥 一张“Service Locator vs DI 对比图”
🔥 一段“为什么 Service Locator 被认为是反模式”
🔥 一段“如何从 Service Locator 重构到 DI 的步骤”

你想继续深入哪一块?