注册

从零到一编写 IOC 容器




前言

本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScript 来实现的。为方便读者阅读,本文的组织结构依次为 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器。阅读完本文后,我希望你能有这样的感悟:元数据(metadata)和 装饰器(Decorator) 本是 ES 中两个独立的部分,但是结合它们, 竟然能实现 控制反转 这样的能力。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

辛苦整理良久,还望手动点赞鼓励~ 博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~

一、TS 装饰器

1、类装饰器

(1)类型声明

type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
  • 参数:

    target: 类的构造器。

  • 返回:

如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。例如我们可以添加一个 toString 方法给所有的类来覆盖它原有的 toString 方法,以及增加一个新的属性 school,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T) {
 // 新构造器继承原有的构造器,并且返回
 return class extends BaseClass {  
   // 新增属性 school
   public school = 'qinghua'
   // 重写方法 toString
   toString() {
     return JSON.stringify(this);
  }
};
}

@School
class Student {
 public name = 'tom';
 public age = 14;
}

console.log(new Student().toString())
// {"name":"tom","age":14,"school":"qinghua"}

但是存在一个问题:装饰器并没有类型保护,这意味着在类装饰器的构造函数中新增的属性,通过原有的类实例将报无法找到的错误,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T){
 return class extends BaseClass {
   // 新增属性 school
   public school = 'qinghua'
};
}


@School
class Student{
 getSchool() {
   return this.school; // Property 'school' does not exist on type 'Student'
}
}

new Student().school  // Property 'school' does not exist on type 'Student'

这是 一个TypeScript的已知的缺陷。 目前我们能做的可以额外提供一个类用于提供类型信息,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T){
 return class extends BaseClass {
   // 新增属性 school
   public school = 'qinghua'
};
}

// 新增一个类用于提供类型信息
class Base {
 school: string;
}

@School
class Student extends Base{
 getSchool() {
   return this.school;
}
}

new Student().school)

2、属性装饰器

(1)类型声明

type PropertyDecorator = (
target: Object,
 propertyKey: string | symbol
) => void;
复制代码
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

    2. propertyKey: 属性的名称。

  • 返回:

返回的结果将被忽略。

我们可以通过属性装饰器给属性添加对应的验证判断,如下所示

function NameObserve(target: Object, property: string): void {
 console.log('target:', target)
 console.log('property:', property)
 let _property = Symbol(property)
 Object.defineProperty(target, property, {
   set(val){
     if(val.length > 4){
       throw new Error('名称不能超过4位!')
    }
     this[_property] = val;
  },
   get: function() {
     return this[_property];
}
})
}

class Student {
 @NameObserve
 public name: string;  // target: Student {}   key: 'name'
}

const stu = new Student();
stu.name = 'jack'
console.log(stu.name); // jack
// stu.name = 'jack1'; // Error: 名称不能超过4位!

export default Student;

3、方法装饰器

(1)类型声明:

type MethodDecorator = <T>(
 target: Object,
 propertyKey: string | symbol,
 descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链;

    2. propertyKey: 属性的名称;

    3. descriptor: 属性的描述器;

  • 返回: 如果返回了值,它会被用于替代属性的描述器。

方法装饰器不同于属性装饰器的地方在于 descriptor 参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力

function logger(target: Object, property: string, 
   descriptor: PropertyDescriptor): PropertyDescriptor | void {
 const origin = descriptor.value;
 console.log(descriptor)
 descriptor.value = function(...args: number[]){
   console.log('params:', ...args)
   const result = origin.call(this, ...args);
   console.log('result:', result);
   return result;
}
}

class Person {
 @logger
 add(x: number, y: number){
   return x + y;
}
}

const person = new Person();
const result = person.add(1, 2);
console.log('查看 result:', result) // 3

4、访问器装饰器

访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的 key 不同: 方法装饰器的描述器的 key 为:

  • value

  • writable

  • enumerable

  • configurable

访问器装饰器的描述器的key为:

  • get

  • set

  • enumerable

  • configurable

例如,我们可以对访问器进行统一更改:

function descDecorator(target: Object, property: string, 
   descriptor: PropertyDescriptor): PropertyDescriptor | void {
 const originalSet = descriptor.set;
 const originalGet = descriptor.get;
 descriptor.set = function(value: any){
   return originalSet.call(this, value)
}
 descriptor.get = function(): string{
   return 'name:' + originalGet.call(this)
}
}

class Person {
 private _name = 'tom';

 @descDecorator
 set name(value: string){
   this._name = value;
}

 get name(){
   return this._name;
}
}

const person = new Person();
person.name = ('tom');
console.log('查看:', person.name) // name:'tom'

5、参数装饰器

类型声明:

type ParameterDecorator = (
 target: Object,
 propertyKey: string | symbol,
 parameterIndex: number
) => void;
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

    2. propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)。

    3. parameterIndex: 参数在方法中所处的位置的下标。

  • 返回:

返回的值将会被忽略。

单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。

function ParamDecorator(target: Object, property: string, 
   paramIndex: number): void {
 console.log(property);
 console.log(paramIndex);
}

class Person {
 private name: string;

 public setNmae(@ParamDecorator school: string, name: string){  // setNmae 0
   this.name = school + '_' + name
}
}

6、执行时机

装饰器只在解释执行时应用一次,如下所示,这里的代码会在终端中打印 apply decorator,即便我们其实并没有使用类 A。

function f(C) {
 console.log('apply decorator')
 return C
}

@f
class A {}

// output: apply decorator

7、执行顺序

不同类型的装饰器的执行顺序是明确定义的:

  • 实例成员:参数装饰器 -> 方法/访问器/属性 装饰器

  • 静态成员:参数装饰器 -> 方法/访问器/属性 装饰器

  • 构造器:参数装饰器

  • 类装饰器

示例如下所示

function f(key: string): any {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

@f("Class Decorator")
class C {
 @f("Static Property")
 static prop?: number;

 @f("Static Method")
 static method(@f("Static Method Parameter") foo:any) {}

 constructor(@f("Constructor Parameter") foo:any) {}

 @f("Instance Method")
 method(@f("Instance Method Parameter") foo:any) {}

 @f("Instance Property")
 prop?: number;
}

/* 输出顺序如下
evaluate: Instance Method
evaluate: Instance Method Parameter
call: Instance Method Parameter
call: Instance Method
evaluate: Instance Property
call: Instance Property
evaluate: Static Property
call: Static Property
evaluate: Static Method
evaluate: Static Method Parameter
call: Static Method Parameter
call: Static Method
evaluate: Class Decorator
evaluate: Constructor Parameter
call: Constructor Parameter
call: Class Decorator
*/

我们从上注意到执行实例属性 prop 晚于实例方法 method 然而执行静态属性 static prop 早于静态方法static method。 这是因为对于属性/方法/访问器装饰器而言,执行顺序取决于声明它们的顺序。 然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行。

function f(key: string): any {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

class C {
 method(
   @f("Parameter Foo") foo,
   @f("Parameter Bar") bar
) {}
}

/* 输出顺序如下
evaluate: Parameter Foo
evaluate: Parameter Bar
call: Parameter Bar
call: Parameter Foo
*/

8、多个装饰器组合

我们可以对同一目标应用多个装饰器。它们的组合顺序为:

  • 求值外层装饰器

  • 求值内层装饰器

  • 调用内层装饰器

  • 调用外层装饰器

如下示例所示

function f(key: string) {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

class C {
 @f("Outer Method")
 @f("Inner Method")
 method() {}
}

/* 输出顺序如下
evaluate: Outer Method
evaluate: Inner Method
call: Inner Method
call: Outer Method
*/

二、Reflect Metadata

1、背景

在 ES6 的规范当中,ES6 支持元编程,核心是因为提供了对 Proxy 和 Reflect 对象的支持。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。然而 ES6 的 Reflect 规范里面还缺失一个规范,那就是 Reflect Metadata。这会造成什么样的情境呢? 由于 JS/TS 现有的 装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上... 这就限制了 JS 中元编程的能力。 此时 Relfect Metadata 就派上用场了,可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来(当然你也可以通过反射来添加这些信息)。 综合一下, JS 中对 Reflect Metadata 的诉求,简单概括就是:

  • 其他 C#、Java、Pythone 语言已经有的高级功能,我 JS 也应该要有(诸如C# 和 Java 之类的语言支持将元数据添加到类型的属性或注释,以及用于读取元数据的反射API,而目前 JS 缺少这种能力)

  • 许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式向类中添加其他元数据。

  • 为了使各种工具和库能够推理出元数据,需要一种标准一致的方法;

  • 元数据不仅可以用在对象上,也可以通过相关捕获器用在 Proxy 上;

  • 对开发人员来说,定义新的元数据生成装饰器应该简洁易用;

2、使用

TypeScript 在 1.5+ 的版本已经支持 reflect-metadata,但是我们在使用的时候还需要额外进行安装,如下所示

  • npm i reflect-metadata --save

  • 在 tsconfig.json 里配置选项 emitDecoratorMetadata: true

关于 reflect-metadata 的基本使用 api 可以阅读 reflect-metadata 文档,其包含常见的增删改查基本功能,我们来看下其基本的使用示例,其中 Reflect.metadata 当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:

import "reflect-metadata";

@Reflect.metadata('classMetaData', 'A')
class SomeClass {
 @Reflect.metadata('methodMetaData', 'B')
 public someMethod(): string {
   return 'hello someMethod';
}
}

console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B

当然跟我们平时看到的 IOC 不同,我们进一步结合装饰器,如下所示,与前面的功能是一样的

import "reflect-metadata";

function classDecorator(): ClassDecorator {
 return target => {
   // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
   Reflect.defineMetadata('classMetaData', 'A', target);
};
}

function methodDecorator(): MethodDecorator {
 return (target, key, descriptor) => {
   // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
   Reflect.defineMetadata('methodMetaData', 'B', target, key);
};
}

@classDecorator()
class SomeClass {
 @methodDecorator()
 someMethod() {}
}

console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B'

3、design:类型元数据

在 TS 中的 reflect-metadata 的功能是经过增强的,其添加 "design:type"、"design:paramtypes" 和 "design:returntype" 这 3 个类型相关的元数据

  • design:type 表示被装饰的对象是什么类型, 比如是字符串、数字、还是函数等;

  • design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该 key;

  • design:returntype 表示被装饰对象的返回值属性, 比如字符串、数字或函数等;

示例如下所示

import "reflect-metadata";

@Reflect.metadata('type', 'class')
class A {  
 constructor(
   public name: string,
   public age: number
) { }  

 @Reflect.metadata(undefined, undefined)  
 method(name: string, age: number):boolean {    
   return true  
}
}

 const t1 = Reflect.getMetadata('design:type', A.prototype, 'method')
 const t2 = Reflect.getMetadata('design:paramtypes', A.prototype, 'method')
 const t3 = Reflect.getMetadata('design:returntype', A.prototype, 'method')
 
 console.log(t1)  // [Function: Function]
 console.log(...t2) // [Function: String] [Function: Number]
 console.log(t3) // [Function: Boolean]

三、IOC 容器实现

1、源码解读

我们可以从 github 上克隆 midway 仓库的代码到本地,然后进行代码阅读以及 debug 调试。本篇博文我们主要想探究下 midway 的依赖注入合控制反转是如何实现的,其主要源码存在于两个目录:packages/core 和 packages/decorator,其中 packages/core 包含依赖注入的核心实现,加载对象的class,同步、异步创建对象实例化,对象的属性绑定等。 IOC 容器就像是一个对象池,管理着每个对象实例的信息(Class Definition),所以用户无需关心什么时候创建,当用户希望拿到对象的实例 (Object Instance) 时,可以直接拿到依赖对象的实例,容器会 自动将所有依赖的对象都自动实例化。packages/core 中主要有以下几种,分别处理不同的逻辑:

  • AppliationContext 基础容器,提供了基础的增加定义和根据定义获取对象实例的能力;

  • MidwayContainer 用的最多的容器,做了上层封装,通过 bind 函数能够方便的生成类定义,midway 从此类开始扩展;

  • RequestContainer 用于请求链路上的容器,会自动销毁对象并依赖另一个容器创建实例;

packages/decorator 包含装饰器 provide.ts、inject.ts 的实现,在midwayjs中是有一个装饰器管理类DecoratorManager, 用来管理 midwayjs 的所有装饰器:

  • @provide() 的作用是简化绑定,能被 IOC 容器自动扫描,并绑定定义到容器上,对应的逻辑是绑定对象定义;

  • @inject() 的作用是将容器中的定义实例化成一个对象,并且绑定到属性中,这样,在调用的时候就可以访问到该属性。

2、简单实现

2.1、装饰器 Provider

实现装饰器 Provider 类,作用为将对应类注册到 IOC 容器中。

import 'reflect-metadata'
import * as camelcase from 'camelcase'
import { class_key } from './constant'

// Provider 装饰的类,表示要注册到 IOC 容器中
export function Provider (identifier?: string, args?: Array<any>) {
 return function (target: any) {
   // 类注册的唯一标识符
   identifier = identifier ?? camelcase(target.name)

   Reflect.defineMetadata(class_key, {
     id: identifier,  // 唯一标识符
     args: args || [] // 实例化所需参数
  }, target)
   return target
}
}

2.2、装饰器 Inject

实现装饰器 Inject 类,作用为将对应的类注入到对应的地方。

import 'reflect-metadata'
import { props_key } from './constant'

export function Inject () {
 return function (target: any, targetKey: string) {
   // 注入对象
   const annotationTarget = target.constructor
   let props = {}
   // 同一个类,多个属性注入类
   if (Reflect.hasOwnMetadata(props_key, annotationTarget)) {
     props = Reflect.getMetadata(props_key, annotationTarget)
  }

   //@ts-ignore
   props[targetKey] = {
     value: targetKey
  }

   Reflect.defineMetadata(props_key, props, annotationTarget)
}
}

2.3、管理容器 Container

管理容器 Container 的实现,用于绑定实例信息并且在对应的地方获取它们。

import 'reflect-metadata'
import { props_key } from './constant'

export class Container {
 bindMap = new Map()

 // 绑定类信息
 bind(identifier: string, registerClass: any, constructorArgs: any[]) {
   this.bindMap.set(identifier, {registerClass, constructorArgs})
}

 // 获取实例,将实例绑定到需要注入的对象上
 get<T>(identifier: string): T {
   const target = this.bindMap.get(identifier)
   if (target) {
     const { registerClass, constructorArgs } = target
     // 等价于 const instance = new registerClass([...constructorArgs])
     const instance = Reflect.construct(registerClass, constructorArgs)

     const props = Reflect.getMetadata(props_key, registerClass)
     for (let prop in props) {
       const identifier = props[prop].value
       // 递归进行实例化获取 injected object
       instance[prop] = this.get(identifier)
    }
     return instance
  }
}
}

2.4、加载类文件 load

启动时扫描所有文件,获取文件导出的所有类,然后根据元数据进行绑定。

import * as fs from 'fs'
import { resolve } from 'path'
import { class_key } from './constant'

// 启动时扫描所有文件,获取定义的类,根据元数据进行绑定
export function load(container: any, path: string) {
 const list = fs.readdirSync(path)
 for (const file of list) {
   if (/\.ts$/.test(file)) {
     const exports = require(resolve(path, file))

     for (const m in exports) {
       const module = exports[m]
       if (typeof module === 'function') {
         const metadata = Reflect.getMetadata(class_key, module)
         // register
         if (metadata) {
           container.bind(metadata.id, module, metadata.args)
        }
      }
    }
  }
}
}

2.5、示例类

三个示例类如下所示

// class A
import { Provider } from "../provide";
import { Inject } from "../inject";
import B from './classB'
import C from './classC'

@Provider('a')
export default class A {
 @Inject()
 private b: B

 @Inject()
 c: C

 print () {
   this.c.print()
}
}

// class B
import { Provider } from '../provide'

@Provider('b', [10])
export default class B {
 n: number
 constructor (n: number) {
   this.n = n
}
}

// class C
import { Provider } from '../provide'

@Provider()
export default class C {
 print () {
   console.log('hello')
}
}

2.6、初始化

我们能从以下示例结果中看到,我们已经实现了一个基本的 IOC 容器能力。

import { Container } from './container'
import { load } from './load'
import { class_path } from './constant'

const init =  function () {

 const container = new Container()
 // 通过加载,会先执行装饰器(设置元数据),
 // 再由 container 统一管理元数据中,供后续使用
 load(container, class_path)
 const a:any = container.get('a') // A { b: B { n: 10 }, c: C {} }
 console.log(a);
 a.c.print() // hello
}

init()

总结

本文的依次从 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器四个部分由零到一编写自定义 IOC 容器,希望对你有所启发。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

作者:我是你的超级英雄
来源:https://juejin.cn/post/7036895697865555982

0 个评论

要回复文章请先登录注册