Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从零到一编写 IOC 容器 #30

Open
fengshi123 opened this issue Dec 1, 2021 · 0 comments
Open

从零到一编写 IOC 容器 #30

fengshi123 opened this issue Dec 1, 2021 · 0 comments

Comments

@fengshi123
Copy link
Owner

前言

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

辛苦整理良久,还望手动点赞鼓励~
博客 github地址为:github.com/fengshi123/blog ,汇总了作者的所有博客,欢迎关注及 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 ,读者可以克隆下来进行调试运行。

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant