Skip to content

Spring ↔ NestJS 对照表 ​

本页收录计划中的完整对照表,按主题分类,并补充了每天章节里给出的对照。概念名链接到讲解它的章节小节; “TaskFlow 中的位置”一列给出能看到它实际运行的文件(路径相对于 examples/ 目录)。每张表下面有一个简短列表, 指出和 Spring 的类比在哪里不再成立:Java 开发者的大部分错误就出在这些地方。

Web:控制器、请求、响应 ​

概念SpringNestJSTaskFlow 中的位置
REST 控制器@RestController@Controller('projects')src/projects/projects.controller.ts
HTTP 映射@GetMapping、@PostMapping@Get()、@Post()src/projects/projects.controller.ts、src/tasks/tasks.controller.ts
请求体@RequestBody@Body()src/projects/projects.controller.ts
路径参数@PathVariable@Param('id')src/projects/projects.controller.ts(配合 ParseUUIDPipe)
查询参数@RequestParam@Query()src/projects/dto/project.dto.ts(ProjectQueryDto)、src/tasks/tasks.controller.ts(@Query('status'))
请求头@RequestHeader@Headers()没有用 @Headers():x-correlation-id 请求头在 src/common/correlation-id.ts 中读取
状态码ResponseEntity、@ResponseStatus返回对象 + @HttpCode()(避免用 @Res())src/projects/projects.controller.ts(DELETE → 204)
DTO + 校验Bean Validation(@Valid、@NotNull)class-validator + ValidationPipesrc/projects/dto/project.dto.ts、src/app.module.ts(APP_PIPE)
序列化Jacksonclass-transformer(@Exclude、@Expose)src/users/entities/user.entity.ts(@Exclude)、src/app.module.ts(ClassSerializerInterceptor)

类比失效的地方:

  • 没有 @Valid,但也没有隐式校验。 全局 ValidationPipe 会校验每个以 DTO 类为类型的参数。光有 TypeScript 类型,运行时什么都不检查;基本类型的参数(@Param('id') id: string)也不会被校验:需要专门的管道,比如 ParseUUIDPipe(校验)。
  • 查询字符串里只有字符串。 @Type(() => Number) 负责转换数字,?archived=false 则必须显式写 @Transform, 因为字符串 "false" 在 JavaScript 里是真值(分页)。
  • 序列化不是自动的。 Jackson 作用于所有响应;@Exclude 只有在 ClassSerializerInterceptor 已启用、并且返回值 是该类的实例时才生效。复制出来的对象({ ...user })丢了元数据,字段也就不再被排除 (原样返回实体的陷阱)。
  • @Res() 不是 ResponseEntity。 它让处理函数进入手动模式:拦截器和序列化都不再生效 (陷阱)。

注入与模块 ​

概念SpringNestJSTaskFlow 中的位置
启动SpringApplication.runNestFactory.createsrc/main.ts
Service bean@Service、@Component@Injectable()src/projects/projects.service.ts
注入构造函数或 @Autowired构造函数注入(TS 类型 + 元数据)src/tasks/tasks.service.ts(按类型、@InjectRepository(Task)、@Inject(CLOCK))
容器 / 上下文ApplicationContext、component scan@Module({ providers, controllers, imports, exports })src/users/users.module.ts、src/app.module.ts
Bean 作用域singleton、requestScope.DEFAULT(单例)、Scope.REQUEST、Scope.TRANSIENTTaskFlow 的所有 provider 都是单例(默认作用域)
工厂创建的 bean@Configuration + @BeanuseFactorysrc/projects/projects.module.ts(PROJECTS_STORE provider)
条件 bean@Profile、@ConditionalOnProperty根据配置选择的 provider、ConditionalModulesrc/projects/projects.module.ts、src/notifications/notifications.module.ts
限定符@Qualifier("nom")自定义 token:@Inject(PROJECTS_STORE)src/projects/projects.store.ts、src/common/common.module.ts(CLOCK)
可配置模块自动配置 + application.yml动态模块 forRoot() / forRootAsync()src/database/database.module.ts、src/auth/auth.module.ts

类比失效的地方:

  • 没有 component scan。 没列在 providers 里的 provider 就不存在,没导出的 provider 对其他模块不可见。错误出现在 启动时(“Nest can't resolve dependencies of …”),而不是编译时(模块)。
  • 类型在运行时消失。 接口不能当注入键:需要一个 token。泛型同样被擦除:Repository<Task> 变成 Repository, 所以要用 @InjectRepository(Task)(差异)。
  • 没有作用域代理。 Spring 通过代理把 request bean 注入单例。在 Nest 里,一个 REQUEST provider 会让所有依赖它的 provider 都变成请求作用域,一直传到控制器:整条链在每个请求时都重新创建(作用域)。
  • 循环依赖要付出代价。 forwardRef() 是有的,但模块之间出现循环,首先说明拆分需要重新考虑 (陷阱)。

数据 ​

概念SpringNestJSTaskFlow 中的位置
RepositoryJpaRepository<T, ID>TypeORM 的 Repository<T>、@InjectRepository(T)src/projects/typeorm-projects.store.ts、src/tasks/tasks.service.ts
派生查询findByNameContaining(...)find({ where: { name: ILike(...) } }) 或 QueryBuildersrc/projects/typeorm-projects.store.ts(findPage)
分页Page<T>、PageablefindAndCount + skip / takesrc/projects/typeorm-projects.store.ts、src/common/dto/pagination.dto.ts
实体@Entity、@Id、@Column@Entity、@PrimaryGeneratedColumn、@Columnsrc/projects/entities/project.entity.ts
关系@OneToMany、@ManyToOne同名的 TypeORM 装饰器src/tasks/entities/task.entity.ts、src/projects/entities/project-member.entity.ts
事务@TransactionalDataSource.transaction() 或 QueryRunner(没有原生的声明式写法)src/projects/typeorm-projects.store.ts(create、archive)
迁移Flyway、LiquibaseTypeORM 迁移(CLI)src/database/migrations/、src/database/data-source.ts
自动同步表结构spring.jpa.hibernate.ddl-autosynchronize: true(仅限开发)src/database/postgres-options.ts(synchronize: false)
开发数据data.sql、CommandLineRunner种子数据脚本src/database/seed.ts

类比失效的地方:

  • @Transactional 没有原生的声明式对应物。 Spring 把事务绑定到当前线程。Node 在单个线程上处理所有请求: 所以事务是一个回调,每个需要参与事务的查询都要通过收到的 manager 执行。注入的 repository 则在事务之外执行 (事务)。有第三方库借助 AsyncLocalStorage 重新做出了装饰器; TaskFlow 只用 TypeORM 自带的 API。
  • 没有脏检查。 修改已加载的实体不会在事务结束时触发任何写入:必须调用 save() 或 update()。
  • 没有透明的懒加载。 没有请求的关系(relations、join)就是 undefined,也没有 LazyInitializationException 来提醒你(加载)。
  • 列类型要写出来。 对于 description: string | null,编译器只会输出 Object 作为类型:TypeORM 从中推断不出 任何东西,所以 TaskFlow 的实体里显式写了 type: 'text'、type: 'varchar' (元数据陷阱)。

请求处理管道 ​

概念SpringNestJSTaskFlow 中的位置
执行顺序Servlet 过滤器 → DispatcherServlet → 拦截器 → 控制器Middleware → Guards → Interceptors → Pipes → Handler → Interceptors → Exception Filterssrc/app.module.ts
异常处理@ControllerAdvice + @ExceptionHandler带 @Catch() 的 ExceptionFilter,通过 APP_FILTER 注册src/common/filters/all-exceptions.filter.ts
HTTP 异常ResponseStatusExceptionHttpException、NotFoundException 等src/projects/projects.service.ts、src/auth/auth.service.ts(ConflictException)
Servlet 过滤器Filter中间件src/common/middleware/correlation-id.middleware.ts
拦截器HandlerInterceptor / AOP @AroundNestInterceptor(RxJS)src/common/interceptors/logging.interceptor.ts
参数转换与校验Converter、@InitBinderPipe(ParseIntPipe、ParseUUIDPipe)src/tasks/tasks.controller.ts(ParseUUIDPipe、ParseEnumPipe)

类比失效的地方:

  • 拦截器只包裹处理函数。 Spring 的 @Around 可以作用于任意 bean 方法;Nest 拦截器只作用于路由。service 不会被代理。
  • 中间件不知道处理函数是谁。 和 Servlet Filter 一样,它看到的是原始请求,不知道哪个控制器会响应。要根据 路由或其元数据做决定,请用 guard 或拦截器(执行顺序)。
  • 拦截器操作的是 Observable。 处理函数之后的代码要在 next.handle() 上用 RxJS 操作符(tap、map、 catchError)来写,而不是写在 proceed() 之后。

配置与生命周期 ​

概念SpringNestJSTaskFlow 中的位置
配置application.yml、@Value、@ConfigurationProperties@nestjs/config(ConfigModule、ConfigService)src/app.module.ts、src/config/
类型化属性@ConfigurationPropertiesregisterAs + 类型化配置src/config/configuration.ts
启动时校验属性类上的 @Validatedvalidate 选项(class-validator 或 Joi)src/config/env.validation.ts
多环境spring.profiles.activeNODE_ENV + .env.* 文件src/app.module.ts(envFilePath)、.env.example
日志SLF4J + LogbackNest 的 Logger、nestjs-pinosrc/app.module.ts(LoggerModule)
生命周期@PostConstruct、@PreDestroyOnModuleInit、OnModuleDestroy(+ enableShutdownHooks())src/app.setup.ts(enableShutdownHooks())

类比失效的地方:

  • 一切都是字符串。 环境变量是字符串;转换和校验都要显式完成(validateEnv)。给属性写上类型 (PORT: number = 3000),否则隐式转换不会发生(陷阱)。
  • NODE_ENV 不是 Spring profile。 不会自动合并任何文件,也不会按 profile 激活任何 bean:TaskFlow 通过 envFilePath 选择 .env.${NODE_ENV} 文件,并且只接受 development、test 和 production。
  • 优雅停机要手动开启。 Spring Boot 会注册 JVM 关闭钩子;Nest 只有在调用 app.enableShutdownHooks() 之后, 收到信号时才会调用 OnModuleDestroy 和 OnApplicationShutdown。

安全 ​

概念SpringNestJSTaskFlow 中的位置
授权Spring Security、@PreAuthorizeGuard(CanActivate)、@UseGuards、自定义 @Rolessrc/auth/guards.ts、src/common/decorators/auth.decorators.ts
安全过滤链SecurityFilterChain全局 guard(APP_GUARD)src/app.module.ts
JWT 认证Spring Security 的 JWT resource server@nestjs/passport + passport-jwt、@nestjs/jwtsrc/auth/jwt.strategy.ts、src/auth/auth.module.ts
公开路由permitAll()由全局 guard 读取的 @Public() 装饰器src/common/decorators/auth.decorators.ts、src/auth/auth.controller.ts
当前用户SecurityContextHolder、@AuthenticationPrincipalrequest.user、@CurrentUser()src/common/decorators/auth.decorators.ts
密码哈希PasswordEncoderargon2(或 bcrypt)src/auth/auth.service.ts
响应头、CORS、限流Spring Security 的 headers()、cors();没有标准的限流方案helmet()、enableCors()、@nestjs/throttlersrc/app.setup.ts、src/auth/auth.controller.ts(@Throttle)

类比失效的地方:

  • 没有内置的安全框架。 你要自己组装 Passport 策略、guard 和装饰器。只有注册了全局 guard,才谈得上“默认安全”, TaskFlow 就是用 JwtAuthGuard 这样做的(全局 guard)。
  • guard 保护的是路由,不是 service 方法。 没有可以用在 @Service 上的 @PreAuthorize 对应物:业务访问规则 (是否为项目成员)写在 service 里(成员)。
  • 角色来自 token。 角色变更要到下次登录才生效(TaskFlow 的 token 有效期 15 分钟)。

异步、事件、定时任务 ​

概念SpringNestJSTaskFlow 中的位置
异步@Async、CompletableFuture、WebFluxasync/await、Promise、RxJS随处可见;事件循环见 src/demo/event-loop.controller.ts
事件ApplicationEventPublisher、@EventListener@nestjs/event-emitter(EventEmitter2、@OnEvent)src/tasks/tasks.service.ts(emit)、src/notifications/notifications.listener.ts
定时任务@Scheduled@nestjs/schedule(@Cron、@Interval)src/tasks/tasks.scheduler.ts
队列任务Spring Batch、JMS@nestjs/bullmq(Redis)src/notifications/notifications.queue.ts

类比失效的地方:

  • async 不会创建线程。 @Async 在另一个线程上执行方法;async 函数在唯一的 JavaScript 线程上运行,每遇到 一个 await 就让出控制权。一段繁重的计算会阻塞所有请求(事件循环)。
  • 监听器的错误不会向上传递。 使用 @OnEvent(..., { async: true }) 时,异常永远到不了发出事件的代码:要在监听器 里捕获并记录日志(异步错误)。也没有 @TransactionalEventListener: TaskFlow 在保存之后才发出事件。
  • 定时任务在每个实例上都会运行,就像没有加锁的 @Scheduled(Spring 那边用 ShedLock 加锁)。有多个副本时, 改用队列或锁(定时任务)。
  • BullMQ 是基于 Redis 的任务队列(重试、退避、延迟),更接近带重新投递的 JMS 队列,而不是 Spring Batch, 后者把处理过程拆分成步骤和批次。

测试 ​

概念SpringNestJSTaskFlow 中的位置
测试JUnit、Mockito、MockMvc、@SpringBootTestJest、Test.createTestingModule、supertestsrc/**/*.spec.ts、test/app.e2e-spec.ts
MockMockito(mock()、when())jest.fn()、jest.Mocked<T>src/tasks/tasks.service.spec.ts
Mock repository用在 JpaRepository 上的 @MockitoBean(原 @MockBean)getRepositoryToken(Entity) + useValuesrc/tasks/tasks.service.spec.ts
应用测试@SpringBootTest + MockMvcTest.createTestingModule + supertesttest/app.e2e-spec.ts
真实数据库@DataJpaTest、Testcontainers连接真实数据库的集成测试(Testcontainers 或 Compose 服务)docker-compose.yml 中的 db-test 服务
覆盖率JaCoCojest --coverage + coverageThresholdjest.config.ts

类比失效的地方:

  • 测试模块不扫描任何东西。 Test.createTestingModule 只包含列出来的 provider:比 Spring 上下文更快,也更明确。
  • 不启动 Nest 的测试要自己加载 reflect-metadata(陷阱)。
  • supertest 发出的是真实的 HTTP 请求,打到应用的服务器上:更接近 @SpringBootTest(webEnvironment = RANDOM_PORT), 而不是模拟 servlet 层的 MockMvc。
  • 通过 APP_* 声明的全局 provider 在端到端测试中同样生效,而写在 main.ts 里的 app.useGlobalPipes() 不会, 因为测试不执行 main.ts(端到端测试)。

工具、文档与生产 ​

概念SpringNestJSTaskFlow 中的位置
构建 / 依赖Maven、Gradlenpm 或 pnpm、package.jsonpackage.json、package-lock.json
Lombok@Getter、@Builder不需要(TS,公有属性)实体和 DTO 使用公有属性;构造函数里使用参数属性
项目生成器Spring InitializrNest CLI(nest new、nest g)nest-cli.json
热重载Spring Boot DevToolsnest start --watchpackage.json(npm run start:dev)
Actuator/actuator/health@nestjs/terminussrc/health/health.controller.ts
OpenAPIspringdoc-openapi@nestjs/swaggersrc/app.setup.ts(setupSwagger)、nest-cli.json(插件)
Docker 镜像Jib、spring-boot:build-image多阶段 DockerfileDockerfile

类比失效的地方:

  • 没有构建生命周期。 Maven 会依次执行各个阶段;这里每个工具都有自己的脚本(npm run build、npm test、 npm run lint)和自己的配置(tsconfig.json、jest.config.ts、nest-cli.json、.oxlintrc.json)。
  • Lombok 没有用武之地。 TypeScript 类直接暴露公有属性,不需要 getter, constructor(private readonly service: ProjectsService) {} 一行就声明并赋值了字段。
  • Swagger 需要一个编译插件。 springdoc 在运行时通过反射读取类型;那时 TypeScript 类型已经不存在了,所以要在 nest-cli.json 里声明 @nestjs/swagger 插件(Swagger)。它只读取 *.dto.ts 和 *.entity.ts 文件,也不理会 @Exclude():从不返回的字段要用 @ApiHideProperty() 从文档里隐藏 (/docs 上的 Swagger)。
  • Terminus 只做健康检查。 没有对应 Actuator 的 info、env 或 metrics 端点。