PAYATHON 2026

为什么 Spring 推荐构造函数注入,而 Angular 推荐 inject()?

数据小陈

结论:关键区别在依赖何时赋值,以及由谁赋值

Spring 的 @Autowired 字段注入通常在对象构造完成后由容器完成。Angular 字段中的 inject() 是普通的字段初始化表达式,在对象构造期间执行,并立即向当前 injector 请求依赖。两种写法虽然都位于字段位置,生命周期和使用约束却不同。

Angular 当前风格指南优先推荐 inject(),但构造函数参数注入仍然受支持。示例中将它标为“discouraged ❌”,容易让人误以为这种写法已经不可取或被废弃。官方给出的理由主要涉及可读性、类型推断和现代 JavaScript 字段初始化语义。Angular 风格指南

Spring 为什么优先推荐构造函数注入?

在普通的 Spring Bean 创建流程中,两种注入方式的区别是:

注入方式依赖赋值时机在构造函数体中能否使用该依赖
构造函数注入容器解析依赖,并作为构造函数参数传入可以使用参数及已赋值字段
@Autowired 字段注入构造完成后,由容器填充字段不能指望字段已经完成注入

Spring 的 @Autowired 文档说明,字段注入发生在 Bean 构造之后、配置方法调用之前。因此,不能在构造函数或字段初始化表达式中依赖尚未注入的字段。Spring @Autowired 生命周期说明

构造函数注入还有几个实际好处:

  • 必需依赖直接出现在构造函数签名中,调用者可以看出创建对象需要什么。
  • 字段可以声明为 final,避免引用在后续被重新赋值。
  • 单元测试可以直接传入替身对象,不必先启动 Spring 容器或通过反射设置私有字段。

这些也是 Spring 官方推荐构造函数注入的依据。不过,final 只限制引用重新赋值,不会让依赖对象本身自动变成不可变对象。Spring 依赖注入指南

假设 MySpringDependency 已注册为 Bean,服务可以写成:

import org.springframework.stereotype.Service;

@Service
class SpringConstructorInjectionService {
    private final MySpringDependency myDep;

    public SpringConstructorInjectionService(MySpringDependency myDep) {
        this.myDep = myDep;
    }
}

对于只声明一个构造函数的类,可以省略构造函数上的 @Autowired。@Autowired 也可以标记构造函数或方法,并非字段注入的专用注解。Spring @Autowired 使用说明

原始 Java 示例还有一个语法问题:类名是 SpringConstructorInjectionService,构造函数却写成了 MySpringService。Java 构造函数必须与类同名,上面的代码已经修正。

Angular 的 inject() 在字段中如何执行?

下面这行代码包含一个会立即执行的函数调用:

private readonly myDep = inject(MyAngularDependency);

Angular 在有效的 injection context 中创建实例时,字段初始化器会调用 inject(),从当前 injector 解析依赖,再将结果赋给字段。容器不需要等对象构造完毕后再填充这个字段。

inject() 也可以出现在构造函数体、provider 工厂及其他有效的 injection context 中,并不局限于字段。Angular inject API

假设 MyAngularDependency 已由对应 injector 提供,服务可以写成:

import { Injectable, inject } from '@angular/core';
import { MyAngularDependency } from './my-angular-dependency';

@Injectable({ providedIn: 'root' })
export class AngularFieldInjectionService {
  private readonly myDep = inject(MyAngularDependency);
}

在这种情况下,依赖解析失败通常会使实例创建直接失败;使用 { optional: true } 时则允许返回 null。这与 Spring 字段注入在构造期间尚未赋值的状态不同。

类型系统和字段初始化顺序有什么影响?

Angular 推荐 inject(),还有 TypeScript 特有的原因。

inject() 可以根据 token 推断返回类型。例如:

import { InjectionToken, inject } from '@angular/core';

export const API_URL = new InjectionToken<string>('API_URL');

// 以下字段位于由 Angular 创建的类中。
private readonly apiUrl = inject(API_URL);
private readonly optionalApiUrl = inject(API_URL, { optional: true });

启用 strictNullChecks 时,两个字段分别推断为 string 和 string | null。构造函数参数上的 @Optional() 装饰器则不会自动把参数类型改成可空类型;开发者仍需正确声明类型。Angular 的迁移文档解释了这个差异。Angular inject() 迁移说明

TypeScript 接口在运行时会被擦除,无法直接作为 DI token。接口依赖需要使用 InjectionToken<T> 等运行时标识;这项要求同时适用于 inject() 和构造函数注入。Angular provider 与 InjectionToken 指南

字段初始化顺序也会影响写法。在采用原生 class fields 语义的配置下,下面的代码可能在 myDep 完成参数属性赋值之前就读取它:

export class ExampleService {
  private readonly initialValue = this.myDep.read();

  constructor(private readonly myDep: MyAngularDependency) {}
}

这取决于 TypeScript 编译配置,并非所有 Angular 项目都会出错。使用字段初始化形式,可以直接表达先取得依赖、再使用依赖的顺序:

export class ExampleService {
  private readonly myDep = inject(MyAngularDependency);
  private readonly initialValue = this.myDep.read();
}

如果继续使用构造函数注入,也可以把依赖相关的初始化放进构造函数体,在参数属性赋值后执行。Angular 风格指南将 ES2022+ 与 useDefineForClassFields 下的初始化问题列为优先使用 inject() 的理由之一。Angular 风格指南

inject() 是否保留了构造函数注入的所有优点?

构造函数参数注入允许测试直接调用 new Service(mockDependency)。改用字段中的 inject() 后,依赖不再出现在构造函数参数列表里。如果普通测试代码在没有 injection context 的情况下直接调用 new Service(),执行字段初始化器时就会出错。

通常可以通过 TestBed 配置替身 provider,并让测试 injector 创建服务。沿用前面的服务定义,在已初始化 Angular 测试环境的测试文件中可以这样做:

import { TestBed } from '@angular/core/testing';
import { AngularFieldInjectionService } from './angular-field-injection-service';
import { MyAngularDependency } from './my-angular-dependency';

const mockDependency = {};

TestBed.configureTestingModule({
  providers: [
    AngularFieldInjectionService,
    { provide: MyAngularDependency, useValue: mockDependency },
  ],
});

const service = TestBed.inject(AngularFieldInjectionService);

这里的空对象仅用于展示 provider 替换方式;实际测试应提供服务会调用的方法。也可以使用 TestBed.runInInjectionContext() 执行需要注入上下文的同步代码。Angular TestBed API

对于希望脱离 Angular 独立实例化的普通业务类,构造函数传参仍有实际价值。

实际应该怎样选择?

在 Spring 中,必需依赖优先通过构造函数传入,并在适合时保存到 final 字段。

在 Angular 中,由框架创建的组件和服务可以遵循当前风格指南,优先使用 inject()。现有构造函数注入代码若满足项目需求,不需要仅因为示例中的红叉就立即重写。

无论选择哪种 Angular 写法,都要正确配置 provider。两种写法都通过 Angular DI 系统解析依赖,改用 inject() 本身不会自动改变依赖的作用域或实例数量。

inject() 必须在有效的 injection context 中同步执行。ngOnInit()、事件处理函数或异步回调不会因为属于 Angular 类就自动拥有该上下文。常见做法是在字段或构造阶段取得依赖,后续方法只使用已经保存的实例;确需临时建立上下文时,再使用 runInInjectionContext()。Angular injection context 指南

备注:内容仅供参考。