从零到一编写 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;
复制代码
参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
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;
参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链;
propertyKey: 属性的名称;
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;
参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)。
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