koatty_container
Typescript中IOC容器的实现,支持DI(依赖注入)以及 AOP (切面编程)。参考Spring IOC的实现机制,用Typescript实现了一个IOC容器,在应用启动的时候,自动分类装载组件,并且根据依赖关系,注入相应的依赖。它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。
IOC容器
IoC全称Inversion of Control,直译为控制反转。不是什么技术,而是一种设计思想。在OO开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。
如何理解好Ioc呢?理解好Ioc的关键是要明确“谁控制谁,控制什么,为何是反转(有反转就应该有正转了),哪些方面反转了”,那我们来深入分析一下:
传统OO程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
听着比较难以理解是不是,我们来举例说明,我们假定一个在线书店,通过BookService获取书籍:
export class BookService {
private config: DataConfig = new DataConfig();
private dataSource: DataSource = new MysqlDataSource(config);
protected constructor() {
}
public getBook(long bookId): Book {
try {
const conn = this.dataSource.getConnection();
...
return book;
} catch (err){
throw Error("message");
}
}
}
为了从数据库查询书籍,BookService持有一个DataSource。为了实例化一个DataSource,又不得不实例化一个DataConfig。
现在,我们继续编写UserService获取用户:
export class UserService {
private config: DataConfig = new DataConfig();
private dataSource: DataSource = new MysqlDataSource(config);
public getUser(userId: number):User {
try {
const conn = this.dataSource.getConnection();
...
return user;
} catch (err){
throw Error("message");
}
}
}
因为UserService也需要访问数据库,因此,我们不得不也实例化一个DataSource。
在处理用户购买的CartController中,我们需要实例化UserService和BookService:
export class CartController extends {
private bookService = new BookService();
private userService = new UserService();
...
}
类似的,在购买历史HistoryController中,也需要实例化UserService和BookService:
export class HistoryController extends {
private bookService = new BookService();
private userService = new UserService();
...
}
上述每个组件都采用了一种简单的通过new创建实例并持有的方式。仔细观察,会发现以下缺点:
如果一个系统有大量的组件,其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。
因此,核心问题是:
- 1、谁负责创建组件?
- 2、谁负责根据依赖关系组装组件?
- 3、销毁时,如何按依赖顺序正确销毁?
解决这一问题的核心方案就是IoC。
组件分类
根据组件的不同应用场景,Koatty把Bean分为 'COMPONENT' | 'CONTROLLER' | 'MIDDLEWARE' | 'SERVICE' 四种类型。
循环依赖
随着项目规模的扩大,很容易出现循环依赖。koatty_container解决循环依赖的思路是延迟加载。koatty_container在 app
上绑定了一个 appReady
事件,用于延迟加载产生循环依赖的bean, 在使用IOC的时候需要进行处理:
app.emit("appReady");
注意:虽然延迟加载能够解决大部分场景下的循环依赖,但是在极端情况下仍然可能装配失败,解决方案:
1、尽量避免循环依赖,新增第三方公共类来解耦互相依赖的类
2、使用IOC容器获取类的原型(getClass),自行实例化
API
通过组件加载的Loader,在项目启动时,会自动分析并装配Bean,自动处理好Bean之间的依赖问题。IOC容器提供了一系列的API接口,方便注册以及获取装配好的Bean。
reg(target: T, options?: ObjectDefinitionOptions): T;
reg(identifier: string, target: T, options?: ObjectDefinitionOptions): T;
注册Bean到IOC容器。
- target 类或者类的实例
- identifier 别名,默认使用类名。如果自定义,从容器中获取也需要使用自定义别名
- options Bean的配置,包含作用域、生命周期、类型等等
get(identifier: string, type?: CompomentType, args?: any[]): any;
从容器中获取Bean。
- identifier 别名,默认使用类名。如果自定义,从容器中获取也需要使用自定义别名
- type 'COMPONENT' | 'CONTROLLER' | 'MIDDLEWARE' | 'SERVICE' 四种类型。
- args 构造方法入参,如果传入参数,获取的Bean默认生命周期为Prototype,否则为单例Singleton
getClass(identifier: string, type?: CompomentType): Function;
从容器中获取类的原型。
- identifier 别名,默认使用类名。如果自定义,从容器中获取也需要使用自定义别名
- type 'COMPONENT' | 'CONTROLLER' | 'MIDDLEWARE' | 'SERVICE' 四种类型。
getInsByClass(target: T, args?: any[]): T;
根据class类获取容器中的实例
- target 类
- args 构造方法入参,如果传入参数,获取的Bean默认生命周期为Prototype,否则为单例Singleton
AOP切面
Koatty基于IOC容器实现了一套切面编程机制,利用装饰器以及内置特殊方法,在bean装载到IOC容器内的时候,通过嵌套函数的原理进行封装,简单而且高效。
切点声明类型
通过@Before、@After、@BeforeEach、@AfterEach装饰器声明的切点
声明方式 | 依赖Aspect切面类 | 能否使用类作用域 | 入参依赖切点方法 |
---|
装饰器声明 | 依赖 | 不能 | 依赖 |
依赖Aspect切面类: 需要创建对应的Aspect切面类才能使用
能否使用类作用域: 能不能使用切点所在类的this指针
入参依赖切点方法: 装饰器声明切点所在方法的入参同切面共享
例如:
@Controller('/')
export class TestController extends BaseController {
app: App;
ctx: KoattyContext;
@Autowired()
protected TestService: TestService;
@Before(TestAspect)
async test(path: string){
}
}
创建切面类
使用koatty_cli
进行创建:
koatty aspect test
自动生成的模板代码:
import { Aspect } from "koatty";
import { App } from '../App';
@Aspect()
export class TestAspect {
app: App;
run() {
console.log('TestAspect');
}
}
装饰器
类装饰器
装饰器名称 | 参数 | 说明 | 备注 |
---|
@Aspect() | identifier 注册到IOC容器的标识,默认值为类名。 | 声明当前类是一个切面类。切面类在切点执行,切面类必须实现run方法供切点调用 | 仅用于切面类 |
@BeforeEach() | aopName 切点执行的切面类名 | 为当前类声明一个切面,在当前类每一个方法("constructor", "init", "__before", "__after"除外)执行之前执行切面类的run方法。 | |
@AfterEach() | aopName 切点执行的切面类名 | 为当前类声明一个切面,在当前每一个方法("constructor", "init", "__before", "__after"除外)执行之后执行切面类的run方法。 | |
| | | |
属性装饰器
装饰器名称 | 参数 | 说明 | 备注 |
---|
@Autowired() | identifier 注册到IOC容器的标识,默认值为类名 cType 注入bean的类型 constructArgs 注入bean构造方法入参。如果传递该参数,则返回request作用域的实例 isDelay 是否延迟加载。延迟加载主要是解决循环依赖问题 | 从IOC容器自动注入bean到当前类 | |
@Values() | val 属性值, 值类型同属性类型一致 defaultValue 被定义时,当val值为undefined、null、NaN时取值defaultValue型 | val值可以是一个函数,取值函数结果 | |
方法装饰器
装饰器名称 | 参数 | 说明 | 备注 |
---|
@Before(aopName: string) | aopName 切点执行的切面类名 | 为当前方法声明一个切面,在当前方法执行之前执行切面类的run方法。 | |
@After() | aopName 切点执行的切面类名 | 为当前方法声明一个切面,在当前方法执行之后执行切面类的run方法。 | |
参数装饰器
装饰器名称 | 参数 | 说明 | 备注 |
---|
@Inject() | paramName 构造方法入参名(形参) cType 注入bean的类型 | 该装饰器使用类构造方法入参来注入依赖, 如果和 @Autowired() 同时使用, 可能会覆盖autowired注入的相同属性 | 仅用于构造方法(constructor)的入参 |