Skip to content

Latest commit

 

History

History
280 lines (176 loc) · 13.3 KB

README.md

File metadata and controls

280 lines (176 loc) · 13.3 KB

koatty_container

Typescript中IOC容器的实现,支持DI(依赖注入)以及 AOP (切面编程)。参考Spring IOC的实现机制,用Typescript实现了一个IOC容器,在应用启动的时候,自动分类装载组件,并且根据依赖关系,注入相应的依赖。它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。

Version npm npm Downloads

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创建实例并持有的方式。仔细观察,会发现以下缺点:

  • 实例化一个组件,要先实例化依赖的组件,强耦合

  • 每个组件都需要实例化一个依赖组件,没有复用

  • 很多组件需要销毁以便释放资源,例如DataSource,但如果该组件被多个组件共享,如何确保它的使用方都已经全部被销毁

  • 随着更多的组件被引入,需要共享的组件写起来会更困难,这些组件的依赖关系会越来越复杂

如果一个系统有大量的组件,其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。

因此,核心问题是:

  • 1、谁负责创建组件?
  • 2、谁负责根据依赖关系组装组件?
  • 3、销毁时,如何按依赖顺序正确销毁?

解决这一问题的核心方案就是IoC。

组件分类

根据组件的不同应用场景,Koatty把Bean分为 'COMPONENT' | 'CONTROLLER' | 'MIDDLEWARE' | 'SERVICE' 四种类型。

  • COMPONENT 扩展类、第三方类属于此类型,例如 Plugin,ORM持久层等

  • 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) //依赖TestAspect切面类, 能够获取path参数
  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)的入参