深入理解Nest的REQUEST范围和TRANSIENT范围

295 阅读5分钟

单例模式

单例模式是指在整个程序执行期间,程序内的类都会实例化,且与应用程序生命周期直接相关,例如有个服务类名为CatsService,在程序开启之时就会被创建实例(以依赖注入形式为例),自创建至程序结束,有且仅有一个CatsService 的实例

假设有如下代码:

// cats.modules.ts
@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

当程序启动时,由于依赖注入Nest会自动解析和实例化CatsService,也就是代码中的this.catsServicethis为控制器本身)。实例对象在内存中具有唯一地址,该地址用于标识当前的实例对象。而单例模式的意义,就在于不管请求多少次,所发生的行为都是在当前的实例对象身上的。与之相对应的,是非单例模式的每次请求都会在内存中新建一个实例对象,所造成的问题之一就是对内存造成了负担

所以如果发生了Post请求,创建了一只猫,那么通过Get请求,可以获取猫的数组

注意:这里忽略了Catservice代码实现

Nest中,默认为单例模式,且官方推荐使用单例模式

而如果是非单例模式会怎么样呢?即标题所提到的REQUEST范围和TRANSIENT范围

三种不同的范围

REQUEST范围

REQUEST范围是指在每次请求时创建新的实例,并且在请求处理完成后,对实例进行垃圾回收,在请求期间实例的状态是共享的。REQUEST范围的另外一个关键的点是请求的生命周期,在一个请求的生命周期中,实例的状态是共享的

实现的方式有两种,第一是在@Injectable()添加{ scope: Scope.REQUEST },代码如下:

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class CatsService {}

第二种是在@Module中标记,代码如下:

{
  provide: 'CACHE_MANAGER',
  useClass: CacheManager,
  scope: Scope.TRANSIENT,
}

一个请求的生命周期非常简单,就是从客户端发送请求,接着服务器接受请求并处理和返回结果

如何理解状态共享?假设存在控制器,同时对一个服务类 注入了两次,那么在一个请求上下文中,这两个服务类创建出来的实例是同一个,也就是说在请求期间实例的状态是共享的,代码如下:

// app.service.ts
import { Injectable,Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST }) // REQUEST范围
export class RequestService {
  private instanceId = Math.random().toString(36).substring(7); // 生成一个唯一ID

  getInstanceId() {
    return this.instanceId;
  }
}
// abb.service.ts 另外一个服务
import { Injectable } from '@nestjs/common';
import { RequestService } from './app.service'; // 引用了app.servcie.ts里的服务

@Injectable()
export class AdditionalService {
  constructor(private readonly requestService: RequestService) {} // app.servcie.ts里的服务

  getAdditionalId(): string {
    return this.requestService.getInstanceId();
  }
}
// app.controller.ts
import { Controller, Get,Request } from '@nestjs/common';
import { RequestService } from './app.service';
import {AdditionalService} from './add.service'

@Controller('id')
export class IdController {
  constructor(
    private readonly requestService: RequestService,
    private readonly additionalService: AdditionalService,
  ) {}

  @Get()
  getUniqueIds(@Request() req): any {
    const idFromController = this.requestService.getInstanceId();
    const idFromAdditionalService = this.additionalService.getAdditionalId();

    return {
      idFromController,
      idFromAdditionalService,
    };
  }
}

输出情况如下: 输出案例 也就说明this.requestServicethis.additionalService都是指向同一个实例,状态都是共享的(值都指向同一个地址)

另一个例子说明每次请求的实例不一样,代码如下:

@Controller('request')
export class RequestController {
  constructor(private readonly requestService1: RequestService) {}

  @Get()
  getExample() {
    return { // 每次返回的Id将是不同的
      instanceId1: this.requestService1.getInstanceId(), // 获取实例Id
    };
  }
}

两次请求结果如下: 第一次请求 第二次请求 由此可见,两次请求都是创建了新的实例

控制器的REQUEST范围

可以在控制器中添加REQUEST范围,代码如下:

@Controller({
  path: 'cats',
  scope: Scope.REQUEST,
})
export class CatsController {}

假设在CatsController中包含CatsServiceDogsService,那么都会添加上REQUEST范围

REQUEST范围的冒泡特性

假设存在CatsController <- CatsService <- CatsRepository依赖结构的代码:

// cats.repository.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsRepository {
  private cats: string[] = [];

  addCat(name: string) {
    this.cats.push(name);
  }

  getCats() {
    return this.cats;
  }
}

// cats.service.ts
import { Injectable, Scope } from '@nestjs/common';
import { CatsRepository } from './cats.repository';

@Injectable({ scope: Scope.REQUEST }) // 设置了范围
export class CatsService {
  constructor(private readonly catsRepository: CatsRepository) {} // 注入了CatsRepository

  addCat(name: string) {
    this.catsRepository.addCat(name);
  }

  getCats() {
    return this.catsRepository.getCats();
  }
}

// cats.controller.ts
import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {} // 注入了CatsService

  @Get()
  async getCats(@Req() req: Request, @Res() res: Response) {
    this.catsService.addCat(`Cat-${Math.random().toString(36).substr(2, 5)}`);
    const cats = this.catsService.getCats();
    res.json({ cats });
  }
}

在代码中,CatsService添加了REQUEST范围,由于CatsController 注入了CatsService,所以CatsController 也变为REQUEST范围,而CatsRepository不受影响

TRANSIENT不遵循此规则

场景

待完善...

TRANSIENT范围

TRANSIENT范围与REQUEST范围的区别在于,其在每次注入的时候创建新实例,而REQUEST是在请求时创建新实例(在同一个HTTP请求中,无论服务被注入多少次,都将使用相同的实例)

还是以上面关于获取实例Id的代码为例,仅仅修改范围参数,按其特性,两次注入所创建的实例不属于同一个,代码如下:

import { Injectable,Scope } from '@nestjs/common';

@Injectable({ scope: Scope.TRANSIENT })
export class RequestService {
  private instanceId = Math.random().toString(36).substring(7);

  getInstanceId() {
    return this.instanceId;
  }
}

输出的结果如下: 输出案例 可看到返回的值已经不一样了

例外

在同一个请求上下文中,构造函数中多次注入同一服务,不管任何范围,状态都是共享的,代码如下:

import { Controller, Get } from '@nestjs/common';
import { RequestService } from './app.service';

@Controller('request')
export class RequestController {
  constructor(
    private readonly requestService1: RequestService, 
    private readonly requestService2: RequestService) {} // 注入两次

  @Get()
  getExample() {
    return { // 返回的Id将会是同一个
      instanceId1: this.requestService1.getInstanceId(),
      instanceId2: this.requestService2.getInstanceId(),
    };
  }
}

场景

待完善...

总结

关键在于理解注入创建实例同一请求上下文请求生命周期的概念