依赖注入
Midway 中使用了非常多的依赖注入的特性,通过装饰器的轻量特性,让依赖注入变的优雅,从而让开发过程变的便捷有趣。
依赖注入是 Java Spring 体系中非常重要的核心,我们用简单的做法讲解这个能力。
我们举个例子,以下面的函数目录结构为例。
.
├── package.json
├── src
│ ├── controller # 控制器目录
│ │ └── user.controller.ts
│ └── service # 服务目录
│ └── user.service.ts
└── tsconfig.json
在上面的示例中,提供了两个文件, user.controller.ts
和 user.service.ts
。
下面的示例,为了展示完整的功能,我们会写完整的 @Provide
装饰器,而在实际使用中,如果有其他装饰器(比如 @Controller
)的情况下, @Provide
可以被省略。
为了解释方便,我们将它合并到了一起,内容大致如下。
import { Provide, Inject, Get } from '@midwayjs/core';
// user.controller.ts
@Provide() // 实际可省略
@Controller()
export class UserController {
@Inject()
userService: UserService;
@Get('/')
async get() {
const user = await this.userService.getUser();
console.log(user); // world
}
}
// user.service.ts
@Provide()
export class UserService {
async getUser() {
return 'world';
}
}
抛开所有装饰器,你可以看到这是标准的 Class 写法,没有其他多余的内容,这也是 Midway 体系的核心能力,依赖注入最迷人的地方。
@Provide
的作用是告诉 依赖注入容器,我需要被容器所加载。 @Inject
装饰器告诉容器,我需要将某个实例注入到属性上。
通过这两个装饰器的搭配,我们可以方便的在任意类中拿到实例对象,就像上面的 this.userService
。
注意:实际使用中,如果有其他装饰器(比如 @Controller
)的情况下 @Provide
经常被省略。
依赖注入 原理
我们以下面的伪代码举例,在 Midway 体系启动阶段,会创建一个依赖注入容器(MidwayContainer),扫描所有用户代码(src)中的文件,将拥有 @Provide
装饰器的 Class,保存到容器中。
/***** 下面为 Midway 内部代码 *****/
const container = new MidwayContainer();
container.bind(UserController);
container.bind(UserService);
这里的依赖注入容器类似于一个 Map。Map 的 key 是类对应的标识符(比如 类名的驼峰形式字符串),Value 则是 类本身。
在请求时,会动态实例化这些 Class,并且处理属性的赋值,比如下面的伪代码,很容易理解。
/***** 下面为依赖注入容器伪代码 *****/
const userService = new UserService();
const userController = new UserController();
userController.userService = userService;
经过这样,我们就能拿到完整的 userController
对象了,实际的代码会稍微不一样。
MidwayContainer 有 getAsync
方法,用来异步处理对象的初始化(很多依赖都是有异步初始化的需求),自动属性赋值,缓存,返回对象,将上面的流程合为同一个。
/***** 下面为依赖注入容器内部代码 *****/
// 自动 new UserService();
// 自动 new UserController();
// 自动赋值 userController.userService = await container.getAsync(UserService);
const userController = await container.getAsync(UserController);
await userController.handler(); // output 'world'
以上就是依赖注入的核心过程,创建实例。
此外,这里还有一篇名为 《这一次,教你从零开始写一个 IoC 容器》的文章,欢迎扩展阅读。
依赖注入作用域
默认的未指定或者未声明的情况下,所有的 @Provide
出来的 Class 的作用域都为 请求作用域。这意味着这些 Class ,会在**每一次请求第一次调用时被 实例化(new),请求结束后实例销毁。**我们默认情况下的控制器(Controller)和服务(Service)都是这种作用域。
在 Midway 的依赖注入体系中,有三种作用域。
作用域 | 描述 |
---|---|
Singleton | 单例,全局唯一(进程级别) |
Request | 默认,请求作用域,生命周期绑定 请求链路,实例在请求链路上唯一,请求结束立即销毁 |
Prototype | 原型作用域,每次调用都会重复创建一个新的对象 |
不同的作用域有不同的作用,**单例 **可以用来做进程级别的数据缓存,或者数据库连接等只需要执行一次的工作,同时单例由于全局唯一,只初始化一次,所以调用的时候速度比较快。而 **请求作用域 **则是大部分需要获取请求参数和数据的服务的选择,**原型作用域 **使用比较少,在一些特殊的场景下也有它独特的作用。
配置作用域
如果我们需要将一个对象定义为其他两种作用域,需要额外的配置。Midway 提供了 @Scope
装饰器来定义一个类的作用域。下面的代码就将我们的 user 服务变成了一个全局唯一的实例。
// service
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
@Provide()
@Scope(ScopeEnum.Singleton)
export class UserService {
//...
}
注意,所有的入口类,比如 Controller,均为请求作用域,不支持修改。大部分情况下,只需要调整 Service 即可。
单例作用域
在显式配置后,某个类的作用域就可以变成单例作用域。。
// service
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
@Provide()
@Scope(ScopeEnum.Singleton)
export class UserService {
//...
}
后续不管获取这个类的实例多少次,在 同一个进程下,都是同一个实例。
比如基于上面的单例服务,下面两个注入的 userService
属性是同一个实例:
@Provide()
export class A {
@Inject()
userService: UserService
//...
}
@Provide()
export class B {
@Inject()
userService: UserService
//...
}
在 v3.10 版本之后,可以使用单例装饰器来简化原来的写法。
import { Singleton } from '@midwayjs/core';
@Singleton()
class UserService {
// ...
}
请求作用域
默认情况下,代码中编写的类均为 请求作用域。
在每个协议入口框架会自动创建一个请求作用域下的依赖注入容器,所有创建的实例都会绑定当前协议的上下文。
比如:
- http 请求进来的时候,会创建一个请求作用域,每个 Controller 都是在请求路由时动态创建
- 定时器触发,也相当于创建了请求作用域 ctx,我们可以通过@Inject()ctx可以拿到这个请求作用域。
默认为请求作用域的目的是为了和请求上下文关联,显式传递 ctx 更为安全可靠,方便调试。
所以在请求作用域中,我们可以通过 @Inject()
来注入当前的 ctx 对象。
import { Controller, Provide, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Provide() // 实际可省略
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
//...
}
我们的 @Inject
装饰器也是在 当前类的作用域 下去寻找对象来注入的。比如,在 Singleton
作用域下,由于和请求不关联 ,默认没有 ctx
对象,所以注入 ctx 是不对的 。
@Provide()
@Scope(ScopeEnum.Singleton)
export class UserService {
@Inject()
ctx; // undefined
//...
}
作用域固化
当作用域被设置为单例(Singleton)之后,整个 Class 注入的对象在第一次实例化之后就已经被固定了,这意味着,单例中注入的内容不能是其他作用域。
我们来举个例子。
// 这个类是默认的请求作用域(Request)
@Provide() // 实际可省略
@Controller()
export class HomeController {
@Inject()
userService: UserService;
}
// 设置了单例,进程级别唯一
@Provide()
@Scope(ScopeEnum.Singleton)
export class UserService {
async getUser() {
// ...
}
}
调用的情况如下。
这种情况下,不论调用 HomeController
多少次,每次请求的 HomeController
实例是不同的,而 UserService
都会固定的那个。
我们再来举个例子演示单例中注入的服务是否还会保留原有作用域。
这里的 DBManager
我们特地设置成请求作用域,来演示一下特殊场景。
// 这个类是默认的请求作用域(Request)
@Provide()
export class HomeController {
@Inject()
userService: UserService;
}
// 设置了单例,进程级别唯一
@Provide()
@Scope(ScopeEnum.Singleton)
export class UserService {
@Inject()
dbManager: DBManager;
async getUser() {
// ...
}
}
// 未设置作用域,默认是请求作用域(这里用来验证单例链路下,后续的实例都被缓存的场景)
@Provide()
export class DBManager {
}