• home > webfront > server > NestJS >

    DI框架的依赖解析本质:从JavaSpring到NestJS看DI实践

    Date:

    几乎所有现代框架(Spring Boot、ASP NET Core、NestJS、Laravel、Quarkus、Micronaut 等)都把 DI 作为一等公民,正是因为它是从根本上提升软件可维护性的基石。“不要在类内部创建依赖,而是让外部把依赖传进来”。

    依赖注入(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();
        }
    }

    这种写法存在如下问题:


    1. 无法替换实现(想换成支付宝支付、换成短信通知都很难)

    2. 无法单元测试(new 出来的对象无法 mock)

    3. 依赖隐藏在类内部,违背了“面向接口编程”

    依赖注入写法(松耦合):

    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核心实现包含:

    1. IoC 容器(容器 registry):存放所有绑定关系(Binding / Registration)

    2. Provider(依赖提供者):真正“创建对象”的函数/工厂

    3. 依赖解析(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)。

    注意:Spring 的 @Bean 方法返回的是对象,而 NestJS 的 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 接口实现更复杂的工厂逻辑。

    Spring 的 @Bean 方法本质上就是工厂方法,与 NestJS 的 useFactory 非常相似。


    依赖解析(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 个设计:

    1. 构造函数注入在实例化阶段就完成(createBeanInstance)

    2. 三级缓存 + 早期引用暴露完美解决单例循环依赖

    3. 所有扩展(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 依赖解析的真实顺序(极简版):

    1. 收集当前 Module 的所有 providers/controllers/guards...

    2. 按依赖顺序排序(通过分析构造函数参数类型 + @Optional() @Inject() 自定义 token)

    3. 对每个 provider:

      1. 反射拿到 constructor 参数类型数组

      2. 递归调用 container.get(token, { strict: false }) 拿到依赖实例

      3. new Class(...dependencies) 创建实例

      4. 如果是 USE_FACTORY 或 USE_VALUE,直接执行工厂函数

      5. 把创建好的实例放入 wrapper.instance

    4. 所有 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 SpringNestJS (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 结合 ApplicationContextTest.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