Nest.js如何扫描application下的所有metadata

1,643 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

背景

最近项目需要到一个基于事件驱动的一些业务模块,就封装一下类似@nestjs/bull一样的API去进行事件注册和触发。

通过Nest Core的功能去完成全局扫描metadata

通过@nestjs/core,我们可以找到相应框架实现的功能和工具,去完成我们需要写业务逻辑前的工作

DiscoveryService —— 扫描所有app module注册到的controllers和services

首先我们需要扫描所有的controller和service,通过DiscoveryService可以实现,伪代码如下:

// explorer.service.ts
import { Injectable } from '@nestjs/common';
import { DiscoveryService} from '@nestjs/core';

@Injectable()
export class AccessorService {
	  constructor(
	    private readonly discoveryService: DiscoveryService
	  ) {}

	run() {
		const providers = this.discoveryService.getProviders();
		const controllers = this.discoveryService.getControllers();
		// ...
	}
}

那么调用这些方法返回的是什么?从类型定义上看,是一个InstanceWrapper,主要是在nestjs对controller和service注入到module的时候,对class实例后的对象封装了一层wrapper

MetadataScanner —— 扫描出有Metadata的类方法

然后再去遍历我们定义的decorator和metadata,这时候就需要MetadataScanner

// explorer.service.ts
import { Injectable } from '@nestjs/common';
import { DiscoveryService} from '@nestjs/core';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';

@Injectable()
export class AccessorService {
	  constructor(
	    private readonly discoveryService: DiscoveryService,
	    private readonly metadataScanner: MetadataScanner,
	  ) {}

	run() {
		const providers = this.discoveryService.getProviders();
		const controllers = this.discoveryService.getControllers();
		
		[...providers, ...controllers]
	      .filter(wrapper => wrapper.isDependencyTreeStatic())
	      .filter(wrapper => wrapper.instance)
	      .forEach((wrapper: InstanceWrapper) => {
	        const { instance } = wrapper;
	
	        const prototype = Object.getPrototypeOf(instance);
	        this.metadataScanner.scanFromPrototype(
	          instance,
	          prototype,
	          (methodKey: string) => {
	            console.log(instance, methodKey)
	            // do something with class method with custom decorator...
	          }
	        );
	      });
	}
}

我的理解是:

  • wrapper.isDependencyTreeStatic()方法是用于判断实例是否在注册依赖的时候初始化的
  • wrapper.instance就是wrapper实际封装起来的的class实例
  • metadataScanner提供了scanFromPrototype方法去扫描class里面方法(method)的decorator里面的metadata

获取metadata

我看的开源项目里面,一般都会建一个AccessorService去读取相应的metadata,而metadata key一般就是放个常量文件啦

// constants.ts
export const MY_PROCESSOR = Symbol('MY_PROCESSOR');
export const MY_PROCESS = Symbol('MY_PROCESS');
// accessor.service.ts
/* eslint-disable @typescript-eslint/ban-types */
import { Injectable, Type } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

import { MY_PROCESSOR, MY_PROCESS } from './constants';

@Injectable()
export class AccessorService {
  constructor(private readonly reflector: Reflector) {}

  isProcess(target: Type<any> | Function): boolean {
    if (!target) {
      return false;
    }
    return !!this.reflector.get(MY_PROCESS, target);
  }

  getProcessMetadata(target: Type<any> | Function): string | undefined {
    return this.reflector.get(MY_PROCESS, target);
  }

  isProcessor(target: Type<any> | Function): boolean {
    if (!target) {
      return false;
    }
    return !!this.reflector.get(MY_PROCESSOR, target);
  }

  getProcessorMetadata(target: Type<any> | Function): string | undefined {
    return this.reflector.get(MY_PROCESSOR, target);
  }
}

开始写业务

// explorer.service.ts
 // 获取class装饰器metadata
 private loadProcessor(instance: Record<string, any>) {
    if (this.metadataAccessor.isProcessor(instance.constructor)) {
      const metadata = this.metadataAccessor.getProcessorMetadata(
        instance.constructor,
      );
      // do something
      this.logger.log(`Load Processor with ${JSON.stringify(metadata)}`);
    }
  }

 // 获取class method装饰器metadata
   private loadProcess(instance: Record<string, any>, methodKey: string) {
    if (this.metadataAccessor.isProcess(instance[methodKey])) {
      const metadata = this.metadataAccessor.getProcessMetadata(
        instance[methodKey],
      );

      // do something
      this.logger.log(`Load Process with ${JSON.stringify(metadata)}`);
    }
  }
 

到这里已经完成了大部分工作,接下来还需要implements OnApplicationBootstrapOnApplicationShutdown完成在service生命周期过程的调用和回收资源,完整的service文件就变成这样

// explorer.service.ts
import {
  Injectable,
  Logger,
  OnApplicationBootstrap,
  OnApplicationShutdown,
} from '@nestjs/common';
import { DiscoveryService } from '@nestjs/core';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { AccessorService } from './accessor.service';

@Injectable()
export class ExplorerService
  implements OnApplicationBootstrap, OnApplicationShutdown
{
  private readonly logger = new Logger(ExplorerService.name);
  constructor(
    private readonly discoveryService: DiscoveryService,
    private readonly metadataScanner: MetadataScanner,
    private readonly metadataAccessor: AccessorService,
  ) {}

  onApplicationShutdown(signal?: string) {
    this.logger.log('app has been shutdown.');
  }

  onApplicationBootstrap() {
    this.explore();
  }

  explore() {
    const providers = this.discoveryService.getProviders();
    const controllers = this.discoveryService.getControllers();

    [...providers, ...controllers]
      .filter((wrapper) => wrapper.isDependencyTreeStatic())
      .filter((wrapper) => wrapper.instance)
      .forEach((wrapper: InstanceWrapper) => {
        const { instance } = wrapper;

        // 类实例对象的metadata去做事情
        this.loadProcessor(instance);

        const prototype = Object.getPrototypeOf(instance);
        this.metadataScanner.scanFromPrototype(
          instance,
          prototype,
          (methodKey: string) => {
            // 类实例对象的metadata去做事情
            this.loadProcess(instance, methodKey);
          },
        );
      });
  }

  private loadProcessor(instance: Record<string, any>) {
    if (this.metadataAccessor.isProcessor(instance.constructor)) {
      const metadata = this.metadataAccessor.getProcessorMetadata(
        instance.constructor,
      );
      // do something
      this.logger.log(`Load Processor with ${JSON.stringify(metadata)}`);
    }
  }

  private loadProcess(instance: Record<string, any>, methodKey: string) {
    if (this.metadataAccessor.isProcess(instance[methodKey])) {
      const metadata = this.metadataAccessor.getProcessMetadata(
        instance[methodKey],
      );

      // do something
      this.logger.log(`Load Process with ${JSON.stringify(metadata)}`);
    }
  }
}


最后你就可以写你自己的decorator去定义你需要的metadata,就可以完成你的业务逻辑咯。例如

// processor.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { MY_PROCESSOR, MY_PROCESS } from './constants';

export function Process(opts: { event: string }): MethodDecorator {
  return SetMetadata(MY_PROCESS , opts); // NOTE: no options for now
}

export function Processor(opts: { namespace: string }): ClassDecorator {
  return SetMetadata(MY_PROCESSOR, opts);
}
// example.service.ts
import { Injectable } from '@nestjs/common';

import { Processor, Process } from './test.decorator.ts';

@Injectable()
@Processor({namespace:'job'})
export class TaskService {
  @Process({ event: 'test' })
  async run() {
    console.log('do somthing');
  }
}

有兴趣的话可以去看看我写的template git repository,或者看看@nestjs/event-emitter@nestjs/bull的源码

reference: