DI框架的依赖解析本质:从JavaSpring到NestJS看DI实践
Date:
依赖注入(Dependency Injection,简称 DI)是现代软件架构(如 DDD、洁净架构、微服务)中最核心的设计原则之一,它解决的核心问题是:如何让类与类之间的依赖关系可控、可测试、可替换,从而实现低耦合、高内聚。
依赖注入要解决什么问题?
传统写法(紧耦合):
public class OrderService {
private PaymentService paymentService = new PaymentService(); // 直接 new
private EmailService emailService = new EmailService();
public void createOrder() {
paymentService.pay();
emailService.send();
}
}这种写法存在如下问题:
无法替换实现(想换成支付宝支付、换成短信通知都很难)
无法单元测试(new 出来的对象无法 mock)
依赖隐藏在类内部,违背了“面向接口编程”
依赖注入写法(松耦合):
public class OrderService {
private final PaymentService paymentService;
private final EmailService emailService;
// 通过构造函数注入(最推荐)
public OrderService(PaymentService paymentService, EmailService emailService) {
this.paymentService = paymentService;
this.emailService = emailService;
}
}如果一个类有 new 关键字创建了其他服务(尤其是非纯工具类),大概率就是坏味道。
几乎所有现代框架(Spring Boot、ASP.NET Core、NestJS、Laravel、Quarkus、Micronaut 等)都把 DI 作为一等公民,正是因为它是从根本上提升软件可维护性的基石。
如果你在做任何中大型项目,没有依赖注入基本等于没有架构。
依赖注入的三种主要方式
| 注入方式 | 说明 | 优缺点 | 推荐程度 |
|---|---|---|---|
| 构造函数注入 | 通过构造器参数传入依赖(上面例子) | 最推荐!强制依赖、不可变、线程安全、测试友好 | ★★★★★ |
| Setter 注入 | 通过 setXxx() 方法注入 | 支持可选依赖、可重配置,但容易出现半初始化状态 | ★★★ |
| 接口注入 | 定义一个注入接口(如 Injector),实现类调用 | 侵入性强,已基本淘汰 | ★ |
主流框架(Spring、.NET Core、Java EE、Laravel、NestJS 等)几乎都推荐构造函数注入。
DI的核心实现
DI核心实现包含:
IoC 容器(容器 registry):存放所有绑定关系(Binding / Registration)
Provider(依赖提供者):真正“创建对象”的函数/工厂
依赖解析(dependency graph/ Resolver + Graph):递归解析依赖图、处理循环依赖、生命周期管理
管是 Java 的 Spring、.NET 的原生容器、NestJS(TypeScript)、Go 的 Wire、Python 的 Injector,还是 Kotlin 的 Koin,它们在“核心实现”上几乎一模一样,都可以抽象为下面这 3 个模块:
| 核心模块 | 职责 | 别名 / 常见实现类 |
|---|---|---|
| 1. IoC 容器(Registry) | 存放所有绑定关系(Binding / Registration) | Spring: BeanDefinitionMap NestJS: ModuleRef + providers Map |
| 2. Provider | 真正“创建对象”的函数/工厂 | Spring: ObjectFactory / Supplier NestJS: Provider(FactoryProvider、ClassProvider、ValueProvider…) |
| 3. 依赖解析器(Resolver + Graph) | 递归解析依赖图、处理循环依赖、生命周期管理 | Spring: 三级缓存 + DependencyGraph NestJS: InstanceLoader + CircularDependency |
IoC 容器(容器 registry)
容器内部维护一个 Map:
token → provider 实例 token → provider 工厂 token → provider class token → 值
Spring DI 核心实现 = 一个巨大的 Map<Token → Provider>
一个递归的依赖图解析器
三级缓存 + BeanPostProcessor 扩展点
NestJS DI 核心实现 = 一个按 Module 分层的 Map<Token → Provider>
同样递归的依赖图解析器
三级缓存(直接禁止循环依赖)
手动实现一个极简 DI 容器
typescript示例:
class Container {
registry = new Map();
bind(token, provider) {
this.registry.set(token, provider);
}
get(token) {
const provider = this.registry.get(token);
return new provider(); // 简化
}
}java示例:
public class SimpleContainer {
private Map<Class<?>, Supplier<?>> registry = new HashMap<>();
public <T> void register(Class<T> type, Supplier<T> supplier) {
registry.put(type, supplier);
}
public <T> T resolve(Class<T> type) {
Supplier<?> supplier = registry.get(type);
if (supplier == null) throw new RuntimeException("No binding for " + type);
return (T) supplier.get();
}
// 支持自动解析构造函数(反射实现)
public <T> T createInstance(Class<T> type) throws Exception {
Constructor<?> constructor = type.getConstructors()[0];
List<Object> params = new ArrayList<>();
for (Parameter param : constructor.getParameters()) {
params.add(resolve(param.getType()));
}
return (T) constructor.newInstance(params.toArray());
}
}优先构造函数注入,其次使用属性注入(@Autowired field)(Spring 官方也推荐)
服务定位器(Service Locator)比直接 new 好一点,但仍是反模式,隐藏依赖
总结:“不要在类内部创建依赖,而是让外部把依赖传进来”
Provider(依赖提供者)
Provider = 告诉容器“怎么创建一个依赖”。
三种常见 provider 类型(Spring 与 NestJS 在 Provider 的分类上高度相似):
| NestJS Provider 类型 | Spring Bean 对应写法 | 是否等价 | 说明 | |
|---|---|---|---|---|
| Class Provider | @Component, @Service, @Repository | ✔️ 完全等价 | 类自动实例化 | 优先使用 Class Provider:结构清晰,易于测试 |
| Factory Provider | @Bean 方法 / FactoryBean | ✔️ 完全等价 | 工厂生成 Bean | 用于配置或 Mock:尤其在测试模块中替换真实服务 |
| Value Provider | @Value, @ConfigurationProperties | ✔️ 本质等价 | 常量配置 Bean | 用于复杂逻辑:如多态选择、异步初始化、第三方库封装等 |
Class Provider(类提供者,就是普通 Bean)
这是最常见、最推荐的方式。
提供一个 类,NestJS 容器会自动实例化它(通过构造函数注入其依赖)。
默认使用 单例作用域(Singleton scope)。
自动处理依赖图解析。
// email.service.ts
@Injectable()
export class EmailService {
send() { console.log('Email sent'); }
}
class HandSomeMan {
name = 'HAO';
}
class TestHandSomeMan {
name = 'HAO';
}
// app.module.ts
@Module({
providers: [EmailService], // 等价于 { provide: EmailService, useClass: EmailService }
{
provide: TodoService,
useClass: process.env.NODE_ENV === 'production' ? HandSomeMan : TestHandSomeMan
}
})
export class AppModule {}对应spring
@Service
public class EmailService { ... }Value Provider(值提供者)
这类型的 Provider 主要是用来:
提供常数 (Constant)。
将外部函式库注入到控制反转容器。
将 class 抽换成特定的模拟版本。
那要如何使用呢?在展开式中使用 useValue 来配置。这里以 app.module.ts 为例:
// app.module.ts
const config = { apiUrl: 'https://api.example.com', timeout: 5000 };
@Module({
providers: [
{
provide: 'CONFIG', // token(标识符)
useValue: config, // 直接提供值
},
{
provide: AppService,
useValue: {
name: 'HAO'
}
}
],
})
export class AppModule {}在其他服务中使用:
@Injectable()
export class ApiService {
constructor(
@Inject('CONFIG') private config: any
private readonly appService: AppService
) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}对应spring
@Configuration
public class AppConfig {
@Bean
public Map<String, Object> config() {
Map<String, Object> map = new HashMap<>();
map.put("apiUrl", "https://api.example.com");
return map;
}
}或者使用 @Value 注入配置属性,但若要注入整个对象,通常用 @Bean 返回一个实例(类似 useValue)。
Factory Provider(@Bean 工厂方法)
通过一个 工厂函数 动态创建依赖实例。
适用于需要 条件创建、组合多个依赖、异步初始化 等复杂场景。
工厂函数可以注入其他 Provider(通过 inject 字段声明依赖)。
// 假设根据环境决定使用哪种日志服务
@Injectable()
export class ConsoleLogger {}
@Injectable()
export class CloudLogger {}
// 工厂函数
export const loggerFactory = {
provide: 'LOGGER',
useFactory: (config: any) => {
return config.env === 'prod' ? new CloudLogger() : new ConsoleLogger();
},
inject: ['CONFIG'], // 声明工厂依赖的其他 providers
};
@Module({
providers: [
{ provide: 'CONFIG', useValue: { env: process.env.NODE_ENV } },
loggerFactory,
],
})
export class AppModule {}类型的 Provider 使用工厂模式让 Provider 更加灵活,透过 注入其他依赖 来变化出不同的实例,是很重要的功能。使用** useFactory 来指定工厂模式的函数**,并透过** inject 来注入其他依赖**。
对应spring
@Bean
@ConditionalOnProperty(name = "app.env", havingValue = "prod")
public Logger cloudLogger() {
return new CloudLogger();
}
@Bean
@ConditionalOnProperty(name = "app.env", havingValue = "dev")
public Logger consoleLogger() {
return new ConsoleLogger();
}或者使用 FactoryBean 接口实现更复杂的工厂逻辑。
依赖解析(dependency graph)
依赖解析(Dependency Resolution)是整个 DI 容器的“大脑”和“心脏”,它决定了一个 DI 框架到底是“玩具”还是“工业级”。
Spring 是 DI 模式的先驱之一,其解析机制围绕 ApplicationContext(即 IoC 容器)和 Bean 定义展开。
NestJS 使用的是一个名为 IoC 容器 的内部模块,其依赖解析机制高度依赖于 TypeScript 的元数据反射能力。
依赖解析的核心任务(任何语言的 DI 框架都必须干这 5 件事)
| 步骤 | 任务说明 | 关键难点 |
|---|---|---|
| 1. 构建依赖图 | 把所有类的构造函数参数、字段、方法参数解析成一棵有向图 | 反射能力、类型推断 |
| 2. 拓扑排序 | 保证“先创建被依赖的,再创建依赖它的” | 循环依赖检测 |
| 3. 递归实例化 | 从叶子节点开始,一层一层往上创建对象 | 循环依赖处理、代理时机 |
| 4. 属性填充 / 参数注入 | 把解析出来的依赖塞到构造函数、字段、setter 中 | |
| 5. 生命周期回调 | 初始化前、后置处理器、init-method、@PostConstruct 等 | 扩展性 |
当容器需要实例化一个类 Class C 时,它首先需要知道Class C 依赖于哪些其他类或接口,容器需要:
1. 识别依赖 (Dependency Identification)
当容器需要实例化一个类 C 时,它首先需要知道 C 依赖于哪些其他类或接口。
实现方式:
构造函数扫描: 检查 C 的构造函数参数列表。
注解/装饰器扫描: 扫描类 C 上的特定注解(如 Spring 的 @Autowired,NestJS 的 @Injectable)或属性上的装饰器。
2. 查找映射 (Mapping Lookup)
对于识别出的依赖项 D,容器在自己的注册表 (Registry/Binding Map) 中查找,以确定应该使用哪个具体实现 $D'$ 来满足这个依赖。
实现方式:
查找 D 到 D'的映射(例如,接口 UserService 映射到实现类 UserServiceImpl)。
3. 实例化依赖 (Instantiation)
一旦确定了具体实现 D,容器就需要创建它的一个实例。这是遵循生命周期规则的关键步骤。
实现方式:
单例 (Singleton): 如果 D是单例,容器会检查是否有已存在的实例。如果有,直接返回;如果没有,创建并缓存起来。
原型/请求作用域 (Prototype/Scoped): 每次请求都创建一个新的实例。
4. 递归解析 (Recursive Resolution)
如果依赖项 D 本身也有依赖(依赖图 D'->E, F),容器会递归地回到步骤 1,对 D' 的依赖进行解析,直到所有依赖链都被满足。
5. 注入 (Injection)
将所有已解析和创建的依赖实例传递给初始类 C,完成注入。
依赖解析实现:
Java Spring 的依赖解析全流程(工业级最复杂实现)
核心入口方法(简化版):
// AbstractApplicationContext.refresh() finishBeanFactoryInitialization() → DefaultListableBeanFactory.preInstantiateSingletons() → getBean(name) → doGetBean() → createBean() → doCreateBean() ← 真正干活的地方
doCreateBean() 里依赖解析的真实顺序(极简版源码路径):
protected Object doCreateBean(...) {
// 1. 实例化(这里已经完成了构造函数注入!)
InstanceWrapper wrapper = createBeanInstance(beanDefinition, beanName, args);
→ ConstructorResolver.autowireConstructor() // 解析构造函数 + 递归 getBean 参数
// 2. 提前暴露半成品(三级缓存关键步骤)
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// 3. 属性填充(字段注入 + setter 注入)
populateBean(beanName, mbd, instanceWrapper);
→ AutowiredAnnotationBeanPostProcessor
→ CommonAnnotationBeanPostProcessor (@Resource)
// 4. 初始化回调(Aware → @PostConstruct → InitializingBean → init-method)
initializeBean(beanName, exposedObject, mbd);
// 5. 再次检查循环引用,替换早期引用
getEarlyBeanReference → AOP 代理在这里生成(三级缓存的意义)
}Spring 依赖解析最牛的 3 个设计:
构造函数注入在实例化阶段就完成(createBeanInstance)
三级缓存 + 早期引用暴露完美解决单例循环依赖
所有扩展(AOP、@Transactional、@Async)都在 BeanPostProcessor 阶段完成,解析阶段完全干净
NestJS 的依赖解析全流程(极简 + 现代 + 激进)
NestJS 依赖解析的核心类只有这几个(路径:packages/core/injector)
InstanceLoader.createInstancesOfDependencies() // 入口 → container.getModules().forEach(module => this.createInstancesOfModule(module)) → injector.loadProvider() // 真正干活 → injector.loadInstanceOfProvider(wrapper, module) → this.resolveParamDependencies(wrapper, module) // 解析构造函数参数 → this.resolveSingleParam(paramToken) // 递归调用 container.get(token) → injector.loadMiddleware()... → injector.loadController()...
NestJS 依赖解析的真实顺序(极简版):
收集当前 Module 的所有 providers/controllers/guards...
按依赖顺序排序(通过分析构造函数参数类型 + @Optional() @Inject() 自定义 token)
对每个 provider:
反射拿到 constructor 参数类型数组
递归调用 container.get(token, { strict: false }) 拿到依赖实例
new Class(...dependencies) 创建实例
如果是 USE_FACTORY 或 USE_VALUE,直接执行工厂函数
把创建好的实例放入 wrapper.instance
所有 provider 都创建完后,再创建 controller(因为 controller 依赖 provider)
NestJS 依赖解析的激进设计(和 Spring 完全相反的 3 点):
| 项目 | NestJS 做法 | Spring 做法 | 后果说明 |
|---|---|---|---|
| 循环依赖 | 直接抛错 Cannot resolve dependencies... | 三级缓存完美解决 | NestJS 强制你重构,必须用 forwardRef() 或移出循环 |
| 构造函数注入时机 | 实例化时才解析(和 Spring 一样) | 实例化时解析(一样) | 这一点其实一样 |
| 属性注入(@Inject) | 完全支持(反射 + 装饰器 metadata) | 也支持,但不推荐 | NestJS 更鼓励属性注入,因为 TS 写起来爽 |
| 代理(AOP)时机 | 在 provider 创建完成后,用 proxy 包裹一层 | 在 BeanPostProcessor 后置阶段生成代理 | NestJS 的 guard/interceptor/pipe 都是运行时 proxy,性能略差 |
| 启动阶段 | 一次性把所有 provider 都创建完(饥饿加载) | 默认懒加载(只有用到才创建) | NestJS 启动慢一点,但运行时快(没有三级缓存开销) |
直观对比表(一目了然)
| 维度 | Java Spring | NestJS (TypeScript) |
|---|---|---|
| 依赖图构建方式 | BeanDefinition 定义元数据 + ConstructorResolver 反射解析构造器依赖 | TypeScript 反射获取 constructor 参数 + Reflect.getMetadata 读取装饰器元数据 |
| 解析入口 | AbstractAutowireCapableBeanFactory.doCreateBean()(Bean 实例化核心入口) | InstanceLoader → Injector.loadProvider()(Provider 加载与实例化入口) |
| 循环依赖支持 | 支持(三级缓存): 1. Setter/字段注入完全支持 2. 构造函数注入不支持 | 有限支持: 1. 构造函数注入需 forwardRef 手动声明 2. 未声明则直接抛循环依赖错误 |
| 构造函数注入时机 | Bean 实例化阶段(doCreateBean → createBeanInstance)完成 | Provider 实例化阶段(inject 方法执行时)完成 |
| 属性注入实现方式 | AutowiredAnnotationBeanPostProcessor 后置处理器反射赋值 | 运行时通过 Object.defineProperty 拦截属性访问 + metadata 解析注入依赖 |
| AOP/拦截器生成时机 | BeanPostProcessor.postProcessAfterInitialization(初始化后生成代理) | Provider 创建后立即通过 Proxy 生成代理(依赖注入前完成) |
| 初始化/懒加载策略 | 默认预实例化:单例 Bean 在 refresh 阶段全部初始化; @Lazy 注解可改为懒加载(首次获取时初始化) | 默认预加载:单例 Provider 在模块初始化(启动)阶段加载; 请求/原型作用域为懒加载(首次使用时初始化) |
| 动态注册 Provider/Bean | 支持: 1. BeanFactory.registerSingleton 注册单例 2. BeanDefinitionRegistry 注册 BeanDefinition | 不支持: 容器启动后冻结,无法动态新增 Provider(仅启动阶段可配置) |
| 解析性能 | 稍慢:三级缓存 + 多轮 BeanPostProcessor 回调 + 复杂生命周期 | 更快:一次性解析元数据 + 无多级缓存 + 轻量化生命周期 |
| 元数据存储 | BeanDefinition(包含生命周期、作用域、依赖、初始化方法等全量配置) | 简单 Map 结构(仅存储 Provider 元数据、依赖令牌、作用域等基础信息) |
| 反射基础 | Java 原生反射 API(无需额外依赖) | TypeScript 反射 + reflect-metadata 第三方库(需手动引入 polyfill) |
| 缓存机制 | 三级缓存: 1. singletonObjects(成品单例) 2. earlySingletonObjects(早期暴露的代理) 3. singletonFactories(单例工厂,解决循环依赖) | 单级缓存: 1. 单例实例缓存 Map 2. 仅维护循环依赖跟踪表(无多级缓存) |
| 作用域支持 | 单例、原型、请求域、会话域、自定义作用域(如应用域) | 单例(默认)、请求域、原型(瞬态);无会话域,可自定义作用域但需手动实现 |
| 异步依赖解析 | 需适配: 1. @Async 注解 + CompletableFuture 2. 无原生异步依赖注入,需手动处理异步结果 | 原生支持: 1. useFactory 返回 Promise 2. @Injectable 支持异步初始化 |
| 多实现解析 | 1. @Primary 标记默认实现 2. @Qualifier/@Resource 指定名称 3. @Autowired(required=false) 可选注入 | 1. @Primary 标记默认实现 2. @Inject(令牌) 指定别名/标识 3. @Optional 可选注入 |
| 依赖注入核心注解 | @Autowired、@Resource、@Inject、@Value | @Injectable、@Inject、@Optional、@Primary、@Value |
| 代理方式 | JDK 动态代理(接口)/CGLIB 代理(类) | ES6 Proxy 原生代理(无接口/类限制) |
| 配置类/模块配置 | @Configuration + @Bean 定义配置类与 Bean | @Module + providers 数组声明模块与 Provider |
| 测试环境依赖模拟 | @MockBean(Spring Boot Test)、Mockito 结合 ApplicationContext | Test.createTestingModule().overrideProvider()、jest.mock 模拟依赖 |
所有 DI 框架的依赖解析本质都是递归 + 缓存 + 生命周期回调!
只是 Spring 为了兼容历史包袱搞出了三级缓存这套‘黑魔法’,而 NestJS 直接说‘我不救你,你自己写好’
Spring 的依赖解析 = “最大兼容性 + 最大复杂性”→ 愿意用三级缓存、BeanPostProcessor、懒加载等一切手段来“救你”,代价是极其复杂。
NestJS 的依赖解析 = “极简 + 现代 + 强制最佳实践”→ 直接一次性把依赖图解析完,不救循环依赖、不懒加载、不让你乱用属性注入,强迫你写出干净的树形依赖。
参考文章:
Nest-Provider 建立实例的原理与Provider的种类你都了解吗? https://juejin.cn/post/7096149001308733448
Nestjs 框架教程(第四篇:Providers) https://keelii.com/2019/07/04/nestjs-framework-tutorial-4
万字长文,讲透彻 NestJS 的设计模式 - 有梦想的程序猿的文章 - 知乎 https://zhuanlan.zhihu.com/p/690935654
转载本站文章《DI框架的依赖解析本质:从JavaSpring到NestJS看DI实践》,
请注明出处:https://www.zhoulujun.cn/html/webfront/server/nestjs/9705.html