Repository: artusjs/core
Pull request #18 (2022-04-06 13:00:11)

feat: 将 Application 基类整合 IoC Container
- 之前全局共用一个 Container 和 Application，且组合形式比较松散，使用起来比较难懂，现重构为继承形式，为上层框架包装提供些许便利
- Class(OOP) 形式声明 Hook 的格式进行调整，相关写法改为 reflect-metadata 模式，IoC 模型对接更彻底

noahziheng: @hyj1991 改 Loader 的时候发现 Hook 定义这块问题比较大，我改了个比较彻底的版本，要麻烦再看一下（doge

Patch:
diff --git a/package.json b/package.json
index cc58ff1..6d73940 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
     "typescript": "^4.6.2"
   },
   "dependencies": {
-    "@artus/injection": "^0.1.0",
+    "@artus/injection": "^0.1.1",
     "@artus/pipeline": "^0.1.2",
     "deepmerge": "^4.2.2",
     "js-yaml": "^4.1.0"
diff --git a/src/application.ts b/src/application.ts
index 7a0ee7b..ac450fd 100644
--- a/src/application.ts
+++ b/src/application.ts
@@ -1,51 +1,85 @@
-import { Container, Injectable, Inject } from '@artus/injection';
-import { artusContainer } from '.';
-import { ARTUS_TRIGGER_ID } from './constraints';
+import path from 'path';
+import { Constructable, Container } from '@artus/injection';
+import { ArtusInjectEnum } from './constraints';
 import { ArtusStdError, ExceptionHandler, initException } from './exception';
 import { HookFunction, LifecycleManager } from './lifecycle';
 import { LoaderFactory, Manifest } from './loader';
-import { Trigger } from './trigger';
-import { Application } from './types';
+import { Application, ApplicationInitOptions, ApplicationLifecycle } from './types';
+import Trigger from './trigger';
 
-@Injectable()
-export class ArtusApplication implements Application {
-  @Inject(ARTUS_TRIGGER_ID)
-  // @ts-ignore
-  public trigger: Trigger;
-
-  @Inject(ExceptionHandler)
-  // @ts-ignore
-  public exceptionHandler: ExceptionHandler;
+export const appExtMap = new Set<Constructable<ApplicationLifecycle>>();
 
+export class ArtusApplication implements Application {
   public manifest?: Manifest;
-  private container: Container = artusContainer;
   public config?: Record<string, any>;
-  private lifecycleManager: LifecycleManager;
 
-  constructor() {
-    this.lifecycleManager = new LifecycleManager(this);
+  protected container: Container;
+  protected lifecycleManager: LifecycleManager;
+  protected loaderFactory: LoaderFactory;
+  protected defaultClazzLoaded: boolean = false;
 
+  constructor(opts?: ApplicationInitOptions) {
+    this.container = new Container(opts?.containerName ?? ArtusInjectEnum.DefaultContainerName);
+    this.lifecycleManager = new LifecycleManager(this, this.container);
+    this.loaderFactory = LoaderFactory.create(this.container);
+
+    // Default Hook Register
     this.registerHook('didLoad', initException);
+    this.initExtension();
 
     process.on('SIGINT', () => this.close());
     process.on('SIGTERM', () => this.close());
   }
 
+  get trigger(): Trigger {
+    return this.container.get(ArtusInjectEnum.Trigger);
+  }
+
+  async loadDefaultClass() {
+    // load Artus default clazz
+    this.container.set({ id: ArtusInjectEnum.Application, value: this });
+
+    await this.loaderFactory.loadManifest({
+      rootDir: __dirname,
+      items: [
+        {
+          loader: 'module',
+          path: path.resolve(__dirname, './trigger')
+        },
+        {
+          loader: 'module',
+          path: path.resolve(__dirname, './exception/handler')
+        }
+      ]
+    });
+
+    this.defaultClazzLoaded = true;
+  }
 
   async load(manifest: Manifest) {
+    if (!this.defaultClazzLoaded) {
+      await this.loadDefaultClass();
+    }
+
+    // Load user manifest
     this.manifest = manifest;
-    const loaderFactory = await LoaderFactory.create(this.container)
 
     await this.lifecycleManager.emitHook('configWillLoad');
-    const config = await loaderFactory.loadConfig(manifest);
+    const config = await this.loaderFactory.loadConfig(manifest);
     this.config = config;
     await this.lifecycleManager.emitHook('configDidLoad', config);
 
-    await loaderFactory.loadManifest(manifest);
+    await this.loaderFactory.loadManifest(manifest);
     await this.lifecycleManager.emitHook('didLoad');
     return this;
   }
 
+  async initExtension() {
+    for (const appExtClazz of appExtMap) {
+      this.lifecycleManager.batchRegisterHookByClass(appExtClazz);
+    }
+  }
+
   async run() {
     await this.lifecycleManager.emitHook('willReady'); // 通知协议实现层启动服务器
     await this.lifecycleManager.emitHook('didReady');
@@ -60,11 +94,11 @@ export class ArtusApplication implements Application {
   }
 
   throwException(code: string): void {
-    this.exceptionHandler.throw(code);
+    (this.container.get(ExceptionHandler) as ExceptionHandler).throw(code);
   }
 
   createException(code: string): ArtusStdError {
-    return this.exceptionHandler.create(code);
+    return (this.container.get(ExceptionHandler) as ExceptionHandler).create(code);
   }
 }
 
diff --git a/src/constraints.ts b/src/constraints.ts
index 45ff8ff..26eb0e2 100644
--- a/src/constraints.ts
+++ b/src/constraints.ts
@@ -1,16 +1,22 @@
 export const DEFAULT_LOADER = 'module';
 
-export const ARTUS_DEFAULT_CONTAINER = 'artus#default_container';
-export const ARTUS_TRIGGER_ID = 'artus#trigger';
+export const enum ArtusInjectEnum {
+  Application = 'artus#application',
+  Trigger = 'artus#trigger',
+  DefaultContainerName = 'artus#default_container',
+}
 
 export const ARTUS_EXCEPTION_DEFAULT_LOCALE = 'en';
 
-export const HOOK_META_SYMBOL = Symbol('HookMeta');
-
 export const ARTUS_SERVER_ENV = 'ARTUS_SERVER_ENV';
 
 export enum ARTUS_DEFAULT_CONFIG_ENV {
   DEV = 'development',
   PROD = 'production',
   DEFAULT = 'default',
-}
\ No newline at end of file
+};
+
+export const HOOK_CONSTRUCTOR_PARAMS = 'appHook:constructorParams';
+export const HOOK_CONSTRUCTOR_PARAMS_APP = 'appHook:constructorParams:app';
+export const HOOK_CONSTRUCTOR_PARAMS_CONTAINER = 'appHook:constructorParams:container';
+export const HOOK_NAME_META_PREFIX = 'hookName:';
diff --git a/src/decorator.ts b/src/decorator.ts
index 9b22028..f0bee7d 100644
--- a/src/decorator.ts
+++ b/src/decorator.ts
@@ -1,15 +1,14 @@
-// import { Injectable } from '@artus/injection';
-import { artusContainer, getArtusApplication } from '.';
-import { HOOK_META_SYMBOL } from './constraints';
-import { Application } from './types';
+import {
+  HOOK_CONSTRUCTOR_PARAMS,
+  HOOK_CONSTRUCTOR_PARAMS_APP,
+  HOOK_CONSTRUCTOR_PARAMS_CONTAINER,
+  HOOK_NAME_META_PREFIX
+} from './constraints';
+import { appExtMap } from './application';
 
-export function ApplicationHookClass(): ClassDecorator {
+export function ApplicationExtension(): ClassDecorator {
   return (target: any) => {
-    // TODO: 待替换为 injection 提供的属性方法
-    target.constructor.prototype.app = getArtusApplication();
-    artusContainer.set({
-      type: target
-    });
+    appExtMap.add(target);
   };
 };
 
@@ -18,9 +17,25 @@ export function ApplicationHook(hookName?: string): PropertyDecorator {
     if (typeof propertyKey === 'symbol') {
       throw new Error(`hookName is not support symbol [${propertyKey.description}]`);
     }
-    const app: Application = getArtusApplication();
-    const hookFn = target[propertyKey];
-    hookFn[HOOK_META_SYMBOL] = target.constructor;
-    app.registerHook(hookName ?? propertyKey, hookFn);
+    Reflect.defineMetadata(`${HOOK_NAME_META_PREFIX}${propertyKey}`, hookName ?? propertyKey, target.constructor);
   };
-};
\ No newline at end of file
+};
+
+const WithApplicationExtensionConstructorParams = (tag: string): ParameterDecorator => {
+  return (target: any, _propertyKey: string|symbol, parameterIndex: number) => {
+    const paramsMd = Reflect.getOwnMetadata(HOOK_CONSTRUCTOR_PARAMS, target) ?? [];
+    paramsMd[parameterIndex] = tag;
+    Reflect.defineMetadata(HOOK_CONSTRUCTOR_PARAMS, paramsMd, target);
+  };
+}
+
+export function WithApplication(): ParameterDecorator {
+  return WithApplicationExtensionConstructorParams(HOOK_CONSTRUCTOR_PARAMS_APP);
+}
+
+export function WithContainer(): ParameterDecorator {
+  return WithApplicationExtensionConstructorParams(HOOK_CONSTRUCTOR_PARAMS_CONTAINER);
+}
+
+export * from './loader/decorator';
+export * from './trigger/decorator';
diff --git a/src/exception/error.ts b/src/exception/error.ts
index 314e852..518c7c4 100644
--- a/src/exception/error.ts
+++ b/src/exception/error.ts
@@ -20,7 +20,7 @@ export class ArtusStdError extends Error {
 
   static registerCode(code: string, exceptionItem: ExceptionItem): void {
     if (this.errorCodeMap.has(code)) {
-      console.warn(`[Artus-Exception] Register error-code failed, code is existed.(${code})`);
+      console.log(`[Artus-Exception] Register error-code failed, code is existed.(${code})`);
       return;
     }
     this.errorCodeMap.set(code, exceptionItem);
diff --git a/src/exception/handler.ts b/src/exception/handler.ts
index bfb143c..485cd46 100644
--- a/src/exception/handler.ts
+++ b/src/exception/handler.ts
@@ -1,6 +1,6 @@
 import { ArtusStdError } from './error';
 
-export class ExceptionHandler {
+export default class ExceptionHandler {
   throw(code: string): void {
     throw new ArtusStdError(code);
   }
diff --git a/src/exception/index.ts b/src/exception/index.ts
index 63c0994..0d9a67c 100644
--- a/src/exception/index.ts
+++ b/src/exception/index.ts
@@ -1,3 +1,8 @@
+import ExceptionHandler from './handler';
+
 export * from './error';
-export * from './handler';
 export * from './appHook';
+
+export {
+  ExceptionHandler
+};
diff --git a/src/index.ts b/src/index.ts
index 13eaaf9..fd6e1b2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,29 +1,21 @@
+
+
 export {
   Inject,
   Injectable
 } from '@artus/injection';
 
+export {
+  ArtusInjectEnum
+} from './constraints';
 export * from './loader';
 export * from './lifecycle';
 export * from './exception';
 export * from './application';
-export * from './trigger';
+
+import Trigger from './trigger';
+export { Trigger };
 
 export type {
   Manifest
 } from './loader/types';
-
-// IoC
-import { Container } from '@artus/injection';
-import { ArtusApplication } from './application';
-import { ARTUS_DEFAULT_CONTAINER } from './constraints';
-import { Trigger } from './trigger';
-import { ExceptionHandler } from './exception';
-
-export const artusContainer = new Container(ARTUS_DEFAULT_CONTAINER);
-
-artusContainer.set({ type: Trigger });
-artusContainer.set({ type: ExceptionHandler });
-artusContainer.set({ type: ArtusApplication });
-
-export const getArtusApplication = (): ArtusApplication => artusContainer.get(ArtusApplication);
diff --git a/src/lifecycle/index.ts b/src/lifecycle/index.ts
index e1cd94e..2df32b8 100644
--- a/src/lifecycle/index.ts
+++ b/src/lifecycle/index.ts
@@ -1,12 +1,17 @@
-import { artusContainer } from '..';
-import { HOOK_META_SYMBOL } from '../constraints';
+import { Constructable, Container } from '@artus/injection';
 import { Application } from '../types';
+import {
+  HOOK_CONSTRUCTOR_PARAMS,
+  HOOK_CONSTRUCTOR_PARAMS_APP,
+  HOOK_CONSTRUCTOR_PARAMS_CONTAINER,
+  HOOK_NAME_META_PREFIX
+} from '../constraints';
 
 export type HookFunction = <T = unknown>(hookProps : {
   app: Application,
   lifecycleManager: LifecycleManager,
   payload?: T
-}) => Promise<void>;
+}) => void|Promise<void>;
 
 export class LifecycleManager {
   hookList: string[] = [
@@ -19,9 +24,11 @@ export class LifecycleManager {
   ];
   hookFnMap: Map<string, HookFunction[]> = new Map();
   private app: Application;
+  private container: Container;
 
-  constructor(app: Application) {
+  constructor(app: Application, container: Container) {
     this.app = app;
+    this.container = container;
   }
 
   insertHook(existHookName: string, newHookName: string) {
@@ -43,6 +50,24 @@ export class LifecycleManager {
     }
   }
 
+  batchRegisterHookByClass(hookClazz: Constructable<any>) {
+    const fnMetaKeys = Reflect.getMetadataKeys(hookClazz);
+    const constructorParams = Reflect.getMetadata(HOOK_CONSTRUCTOR_PARAMS, hookClazz) ?? [];
+    const paramsMap = {
+      [HOOK_CONSTRUCTOR_PARAMS_APP]: this.app,
+      [HOOK_CONSTRUCTOR_PARAMS_CONTAINER]: this.container
+    };
+    const hookClazzInstance = new hookClazz(...constructorParams.map((param) => paramsMap[param]));
+    for (const fnMetaKey of fnMetaKeys) {
+      if (typeof fnMetaKey !== 'string' || !fnMetaKey.startsWith(HOOK_NAME_META_PREFIX)) {
+        continue;
+      }
+      const hookName = Reflect.getMetadata(fnMetaKey, hookClazz);
+      const propertyKey = fnMetaKey.slice(HOOK_NAME_META_PREFIX.length);
+      this.registerHook(hookName, hookClazzInstance[propertyKey].bind(hookClazzInstance));
+    }
+  }
+
   async emitHook<T = unknown>(hookName: string, payload?: T) {
     if (!this.hookFnMap.has(hookName)) {
       return;
@@ -52,15 +77,7 @@ export class LifecycleManager {
       return;
     }
     for (const hookFn of fnList) {
-      let that: any;
-      if (hookFn[HOOK_META_SYMBOL]) {
-        try {
-          that = artusContainer.get(hookFn[HOOK_META_SYMBOL]);
-        } catch (error) {
-          // SEEME: 临时行为，待 injection 的 get 提供参数允许 id 不存在
-        }
-      }
-      await hookFn.call(that, {
+      await hookFn({
         app: this.app,
         lifecycleManager: this,
         payload
diff --git a/src/loader/factory.ts b/src/loader/factory.ts
index 45d2962..bb7e1fe 100644
--- a/src/loader/factory.ts
+++ b/src/loader/factory.ts
@@ -16,7 +16,7 @@ export class LoaderFactory {
     this.container = container;
   }
 
-  static create(container): LoaderFactory {
+  static create(container: Container): LoaderFactory {
     return new LoaderFactory(container);
   }
 
diff --git a/src/loader/impl/module.ts b/src/loader/impl/module.ts
index a0df1cb..c636d30 100644
--- a/src/loader/impl/module.ts
+++ b/src/loader/impl/module.ts
@@ -1,4 +1,4 @@
-import { Container } from '@artus/injection';
+import { Container, InjectableDefinition } from '@artus/injection';
 import { DefineLoader } from '../decorator';
 import { ManifestItem, Loader } from '../types';
 
@@ -14,12 +14,14 @@ class ModuleLoader implements Loader {
     try {
       const moduleObj = await import(item.path);
       const moduleClazz = moduleObj?.default ?? moduleObj;
-      this.container.set({
-        // id: moduleClazz,
-        id: item.id,
+      const opts: Partial<InjectableDefinition> = {
         path: item.path,
         type: moduleClazz
-      });
+      };
+      if (item.id) {
+        opts.id = item.id;
+      }
+      this.container.set(opts);
     } catch (error) {
       console.error(error);
     }
diff --git a/src/trigger/decorator.ts b/src/trigger/decorator.ts
new file mode 100644
index 0000000..32daf87
--- /dev/null
+++ b/src/trigger/decorator.ts
@@ -0,0 +1,9 @@
+import { Injectable, ScopeEnum } from '@artus/injection';
+import { ArtusInjectEnum } from '../constraints';
+
+export function DefineTrigger(): ClassDecorator {
+  return (target:any) => Injectable({
+    id: ArtusInjectEnum.Trigger,
+    scope: ScopeEnum.SINGLETON
+  })(target);
+};
diff --git a/src/trigger.ts b/src/trigger/index.ts
similarity index 64%
rename from src/trigger.ts
rename to src/trigger/index.ts
index 72c0737..179baba 100644
--- a/src/trigger.ts
+++ b/src/trigger/index.ts
@@ -1,12 +1,8 @@
-import { Injectable, ScopeEnum } from "@artus/injection";
-import { Input, Context, MiddlewareInput, Pipeline } from "@artus/pipeline";
-import { ARTUS_TRIGGER_ID } from "./constraints";
+import { Input, Context, MiddlewareInput, Pipeline } from '@artus/pipeline';
+import { DefineTrigger } from './decorator';
 
-@Injectable({
-  id: ARTUS_TRIGGER_ID,
-  scope: ScopeEnum.SINGLETON
-})
-export class Trigger {
+@DefineTrigger()
+export default class Trigger {
   private pipeline: Pipeline;
 
   constructor() {
diff --git a/src/types.ts b/src/types.ts
index bb19bb6..875ac16 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,15 +1,27 @@
-import { ExceptionHandler } from '.';
 import { HookFunction } from './lifecycle';
 import { Manifest } from './loader';
-import { Trigger } from './trigger';
+import Trigger from './trigger';
 
-export interface Application {
-  trigger: Trigger;
-  exceptionHandler: ExceptionHandler;
+export interface ApplicationLifecycle {
+  configWillLoad?: HookFunction;
+  configDidLoad?: HookFunction;
+  didLoad?: HookFunction;
+  willReady?: HookFunction;
+  didReady?: HookFunction;
+  beforeClose?: HookFunction;
+}
+
+export interface ApplicationInitOptions {
+  containerName?: string;
+}
 
+export interface Application {
   manifest?: Manifest;
+  config?: Record<string, any>;
+
+  get trigger(): Trigger;
 
-  load(manifest: Manifest): Promise<Application>;
+  load(manifest: Manifest): Promise<this>;
   run(): Promise<void>;
   registerHook(hookName: string, hookFn: HookFunction): void;
 }


Test patch:
diff --git a/test/app.test.ts b/test/app.test.ts
index b2f9b95..56d2fcc 100644
--- a/test/app.test.ts
+++ b/test/app.test.ts
@@ -1,8 +1,6 @@
 import 'reflect-metadata';
 import axios from 'axios';
 import assert from 'assert';
-import { getArtusApplication } from '../src';
-import { ARTUS_SERVER_ENV } from '../src/constraints';
 
 describe('test/app.test.ts', () => {
   describe('app koa with ts', () => {
@@ -20,8 +18,7 @@ describe('test/app.test.ts', () => {
           main,
           isListening
         } = await import('./fixtures/app-koa-with-ts/src/bootstrap');
-        const app = getArtusApplication();
-        await main();
+        const app = await main();
         const testResponse = await axios.get('http://127.0.0.1:3000');
         assert(testResponse.status === 200);
         assert(testResponse.data === 'Hello Artus');
@@ -32,14 +29,4 @@ describe('test/app.test.ts', () => {
       }
     });
   });
-
-  describe('app with config', () => {
-    it('should config load on application', async () => {
-      process.env[ARTUS_SERVER_ENV] = 'production'
-      const { main } = await import('./fixtures/app-with-config/app');
-      const app = getArtusApplication();
-      await main();
-      expect(app.config).toEqual({ name: 'test-for-config', test: 1, arr: [ 4, 5, 6 ] })
-    });
-  });
 });
diff --git a/test/config.test.ts b/test/config.test.ts
new file mode 100644
index 0000000..188637e
--- /dev/null
+++ b/test/config.test.ts
@@ -0,0 +1,13 @@
+import 'reflect-metadata';
+import { ARTUS_SERVER_ENV } from '../src/constraints';
+
+describe('test/app.test.ts', () => {
+  describe('app with config', () => {
+    it('should config load on application', async () => {
+      process.env[ARTUS_SERVER_ENV] = 'production'
+      const { main } = await import('./fixtures/app-with-config/app');
+      const app = await main();
+      expect(app.config).toEqual({ name: 'test-for-config', test: 1, arr: [ 4, 5, 6 ] })
+    });
+  });
+});
diff --git a/test/exception.test.ts b/test/exception.test.ts
index fb4e224..89f6967 100644
--- a/test/exception.test.ts
+++ b/test/exception.test.ts
@@ -1,6 +1,5 @@
 import 'reflect-metadata';
 import assert from 'assert';
-import { getArtusApplication } from '../src';
 import { ArtusStdError, ErrorCodeUtils, ExceptionHandler } from '../src/exception';
 import { ExceptionItem } from '../src/exception/types';
 
@@ -67,45 +66,49 @@ describe('test/app.test.ts', () => {
 
   describe('app test for ts and yaml', () => {
     it('should run app', async () => {
-      const {
-        main
-      } = await import('./fixtures/exception-with-ts-yaml/app');
-      const app = getArtusApplication();
-      await main();
-
       try {
-        app.throwException('ARTUS:GLOBAL_TEST');
-      } catch (error) {
+        const {
+          main
+        } = await import('./fixtures/exception-with-ts-yaml/app');
+        const app = await main();
+
+        try {
+          app.throwException('ARTUS:GLOBAL_TEST');
+        } catch (error) {
+          assert(error instanceof ArtusStdError);
+          assert(error.code === 'ARTUS:GLOBAL_TEST');
+          assert(error.desc === '全局测试错误，仅用于单元测试');
+          assert(error.detailUrl === 'https://github.com/artusjs/spec');
+        }
+        try {
+          app.throwException('APP:TEST_ERROR');
+        } catch (error) {
+          assert(error instanceof ArtusStdError);
+          assert(error.code === 'APP:TEST_ERROR');
+          assert(error.desc === '这是一个测试用的错误');
+          assert(error.detailUrl === 'https://github.com/artusjs');
+        }
+        try {
+          process.env.ARTUS_ERROR_LOCALE = 'en';
+          app.throwException('ARTUS:GLOBAL_TEST_I18N');
+        } catch (error) {
+          assert(error instanceof ArtusStdError);
+          assert(error.code === 'ARTUS:GLOBAL_TEST_I18N');
+          assert(error.desc === 'This is a test exception, only valid in unit-test');
+          assert(error.detailUrl === 'https://github.com/artusjs/spec');
+        }
+
+        const error = app.createException('ARTUS:GLOBAL_TEST');
         assert(error instanceof ArtusStdError);
         assert(error.code === 'ARTUS:GLOBAL_TEST');
         assert(error.desc === '全局测试错误，仅用于单元测试');
         assert(error.detailUrl === 'https://github.com/artusjs/spec');
-      }
-      try {
-        app.throwException('APP:TEST_ERROR');
-      } catch (error) {
-        assert(error instanceof ArtusStdError);
-        assert(error.code === 'APP:TEST_ERROR');
-        assert(error.desc === '这是一个测试用的错误');
-        assert(error.detailUrl === 'https://github.com/artusjs');
-      }
-      try {
-        process.env.ARTUS_ERROR_LOCALE = 'en';
-        app.throwException('ARTUS:GLOBAL_TEST_I18N');
+
+        await app.close();
       } catch (error) {
-        assert(error instanceof ArtusStdError);
-        assert(error.code === 'ARTUS:GLOBAL_TEST_I18N');
-        assert(error.desc === 'This is a test exception, only valid in unit-test');
-        assert(error.detailUrl === 'https://github.com/artusjs/spec');
+        console.error(error);
+        throw error;
       }
-
-      const error = app.createException('ARTUS:GLOBAL_TEST');
-      assert(error instanceof ArtusStdError);
-      assert(error.code === 'ARTUS:GLOBAL_TEST');
-      assert(error.desc === '全局测试错误，仅用于单元测试');
-      assert(error.detailUrl === 'https://github.com/artusjs/spec');
-
-      await app.close();
     });
   });
 });
diff --git a/test/fixtures/app-koa-with-ts/src/app.ts b/test/fixtures/app-koa-with-ts/src/app.ts
index b310fd4..9cb9aea 100644
--- a/test/fixtures/app-koa-with-ts/src/app.ts
+++ b/test/fixtures/app-koa-with-ts/src/app.ts
@@ -1,48 +1,50 @@
 import { DefaultContext } from 'koa';
 import { Server } from 'http';
-import { Inject } from '@artus/injection';
-import { ArtusApplication, artusContainer } from '../../../../src';
-import { ApplicationHook, ApplicationHookClass } from '../../../../src/decorator';
+import { Container } from '@artus/injection';
 import { Context, Input } from '@artus/pipeline';
+
+import { ArtusApplication } from '../../../../src';
+import { ApplicationExtension, ApplicationHook, WithApplication, WithContainer } from '../../../../src/decorator';
+import { ApplicationLifecycle } from '../../../../src/types';
+
 import KoaApplication from './koaApp';
 import TestController from './controllers/test';
-import { ARTUS_TRIGGER_ID } from '../../../../src/constraints';
-import { HttpTrigger } from './httpTrigger';
 
 export let server: Server;
 
-artusContainer.set({ type: KoaApplication });
-
-@ApplicationHookClass()
-export default class ApplicationHookExtension {
-  @Inject(ArtusApplication)
-  // @ts-ignore
+@ApplicationExtension()
+export class ApplicationHookExtension implements ApplicationLifecycle {
   app: ArtusApplication;
+  container: Container;
 
-  @Inject(ARTUS_TRIGGER_ID)
-  // @ts-ignore
-  trigger: HttpTrigger;
+  constructor(@WithApplication() app: ArtusApplication, @WithContainer() container: Container) {
+    this.app = app;
+    this.container = container;
+  }
 
-  @Inject(KoaApplication)
-  // @ts-ignore
-  koaApp: KoaApplication;
+  get koaApp(): KoaApplication {
+    return this.container.get(KoaApplication);
+  }
 
   @ApplicationHook()
   async didLoad() {
-    this.trigger.use(async (ctx: Context) => {
-      const testController = artusContainer.get(TestController);
-      ctx.output.data = await testController.index();
+    this.app.trigger.use(async (ctx: Context) => {
+      ctx.output.data = await this.container.get(TestController).index();
     });
   }
 
-  @ApplicationHook()
-  willReady() {
+  @ApplicationHook('willReady')
+  setKoaMiddleware() {
     this.koaApp.use(async (koaCtx: DefaultContext) => {
       const input = new Input();
       const { req, res } = koaCtx;
       input.params = { koaCtx, req, res };
-      await this.trigger.startPipeline(input);
+      await this.app.trigger.startPipeline(input);
     });
+  }
+
+  @ApplicationHook('willReady')
+  startKoaServer() {
     server = this.koaApp.listen(3000);
   }
 
diff --git a/test/fixtures/app-koa-with-ts/src/bootstrap.ts b/test/fixtures/app-koa-with-ts/src/bootstrap.ts
index 4b04a50..953af78 100644
--- a/test/fixtures/app-koa-with-ts/src/bootstrap.ts
+++ b/test/fixtures/app-koa-with-ts/src/bootstrap.ts
@@ -1,22 +1,29 @@
 import path from 'path';
-import { artusContainer, ArtusApplication, getArtusApplication } from '../../../../src';
-import { HttpTrigger } from './httpTrigger';
+import { ArtusApplication } from '../../../../src';
 import { server } from './app';
 
-artusContainer.set({ type: HttpTrigger });
-
 async function main() {
-  const app: ArtusApplication = getArtusApplication();
+  const app: ArtusApplication = new ArtusApplication();
   await app.load({
     rootDir: __dirname,
     items: [
       {
         loader: 'module',
-        path: path.resolve(__dirname, './controllers/test.ts')
+        path: path.resolve(__dirname, './koaApp')
+      },
+      {
+        loader: 'module',
+        path: path.resolve(__dirname, './httpTrigger')
+      },
+      {
+        loader: 'module',
+        path: path.resolve(__dirname, './controllers/test')
       }
     ]
   });
   await app.run();
+
+  return app;
 };
 
 const isListening = () => server.listening;
diff --git a/test/fixtures/app-koa-with-ts/src/httpTrigger.ts b/test/fixtures/app-koa-with-ts/src/httpTrigger.ts
index ba63370..577c6d4 100644
--- a/test/fixtures/app-koa-with-ts/src/httpTrigger.ts
+++ b/test/fixtures/app-koa-with-ts/src/httpTrigger.ts
@@ -1,13 +1,9 @@
-import { Injectable, ScopeEnum } from "@artus/injection";
-import { Context, Next } from "@artus/pipeline";
-import { Trigger } from "../../../../src";
-import { ARTUS_TRIGGER_ID } from "../../../../src/constraints";
+import { Context, Next } from '@artus/pipeline';
+import { Trigger } from '../../../../src';
+import { DefineTrigger } from '../../../../src/decorator';
 
-@Injectable({
-  id: ARTUS_TRIGGER_ID,
-  scope: ScopeEnum.SINGLETON
-})
-export class HttpTrigger extends Trigger {
+@DefineTrigger()
+export default class HttpTrigger extends Trigger {
   constructor() {
     super();
     // first of all
diff --git a/test/fixtures/app-koa-with-ts/tsconfig.json b/test/fixtures/app-koa-with-ts/tsconfig.json
index 2597e01..2c9fb13 100644
--- a/test/fixtures/app-koa-with-ts/tsconfig.json
+++ b/test/fixtures/app-koa-with-ts/tsconfig.json
@@ -1,7 +1,9 @@
 {
   "compilerOptions": {
     "esModuleInterop": true,
-    "experimentalDecorators": true
+    "experimentalDecorators": true,
+    "target": "ES6",
+    "moduleResolution": "node"
   },
   "rootDir": "./src",
   "exclude": []
diff --git a/test/fixtures/app-with-config/app.ts b/test/fixtures/app-with-config/app.ts
index e87a976..7aa0b74 100644
--- a/test/fixtures/app-with-config/app.ts
+++ b/test/fixtures/app-with-config/app.ts
@@ -1,16 +1,17 @@
 import path from 'path';
 import { Server } from 'http';
 import Koa from 'koa';
-import { getArtusApplication } from '../../../src';
-import { ApplicationHook, ApplicationHookClass } from '../../../src/decorator';
+import { ArtusApplication } from '../../../src';
+import { ApplicationExtension, ApplicationHook } from '../../../src/decorator';
+import { ApplicationLifecycle } from '../../../src/types';
 
 const rootDir = path.resolve(__dirname, './');
 
 let server: Server;
 const koaApp = new Koa();
 
-@ApplicationHookClass()
-export class ApplicationHookExtension {
+@ApplicationExtension()
+export class ApplicationHookExtension implements ApplicationLifecycle {
   testStr: string = 'Hello Artus';
 
   @ApplicationHook()
@@ -29,7 +30,7 @@ export class ApplicationHookExtension {
 }
 
 async function main() {
-  const app = getArtusApplication();
+  const app = new ArtusApplication();
   await app.load({
     rootDir,
     items: [
@@ -43,6 +44,7 @@ async function main() {
       }
     ]
   });
+  return app;
 };
 
 
diff --git a/test/fixtures/exception-with-ts-yaml/app.ts b/test/fixtures/exception-with-ts-yaml/app.ts
index 1483ba6..e439949 100644
--- a/test/fixtures/exception-with-ts-yaml/app.ts
+++ b/test/fixtures/exception-with-ts-yaml/app.ts
@@ -1,13 +1,14 @@
 import { Server } from 'http';
 import Koa from 'koa';
-import { getArtusApplication } from '../../../src';
-import { ApplicationHook, ApplicationHookClass } from '../../../src/decorator';
+import { ArtusApplication } from '../../../src';
+import { ApplicationExtension, ApplicationHook } from '../../../src/decorator';
+import { ApplicationLifecycle } from '../../../src/types';
 
 let server: Server;
 const koaApp = new Koa();
 
-@ApplicationHookClass()
-export class ApplicationHookExtension {
+@ApplicationExtension()
+export class ApplicationHookExtension implements ApplicationLifecycle {
   testStr: string = 'Hello Artus';
 
   @ApplicationHook()
@@ -26,12 +27,13 @@ export class ApplicationHookExtension {
 }
 
 async function main() {
-  const app = getArtusApplication();
+  const app = new ArtusApplication();
   await app.load({
     rootDir: __dirname,
     items: []
   });
   await app.run();
+  return app;
 };
 
 const isListening = () => server.listening;
diff --git a/test/fixtures/trigger-event/app.ts b/test/fixtures/trigger-event/app.ts
index 8ecd0fc..ada1cfb 100644
--- a/test/fixtures/trigger-event/app.ts
+++ b/test/fixtures/trigger-event/app.ts
@@ -1,19 +1,20 @@
-import { Inject } from '@artus/injection';
-import { artusContainer, ArtusApplication, getArtusApplication } from '../../../src';
-import { ApplicationHook, ApplicationHookClass } from '../../../src/decorator';
-import { Context, Input, Next } from '@artus/pipeline';
-import { EventTrigger } from './eventTrigger';
+import  path from 'path';
 import { EventEmitter } from 'events';
+import { ArtusApplication } from '../../../src';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../src/decorator';
+import { Context, Input, Next } from '@artus/pipeline';
+import { ApplicationLifecycle } from '../../../src/types';
 
-artusContainer.set({ type: EventTrigger });
 let event = new EventEmitter();
 
-@ApplicationHookClass()
-export class ApplicationHookExtension {
-  @Inject(ArtusApplication)
-  // @ts-ignore
+@ApplicationExtension()
+export class ApplicationHookExtension implements ApplicationLifecycle {
   app: ArtusApplication;
 
+  constructor(@WithApplication() app: ArtusApplication) {
+    this.app = app;
+  }
+
   @ApplicationHook()
   async didLoad() {
     this.app.trigger.use(async (ctx: Context, next: Next) => {
@@ -61,12 +62,19 @@ export class ApplicationHookExtension {
 }
 
 async function main() {
-  const app: ArtusApplication = getArtusApplication();
+  const app: ArtusApplication = new ArtusApplication();
   await app.load({
     rootDir: __dirname,
-    items: []
+    items: [
+      {
+        loader: 'module',
+        path: path.resolve(__dirname, './eventTrigger')
+      }
+    ]
   });
   await app.run();
+
+  return app;
 };
 
 function pub(e: 'e1' | 'e2', p: any) {
diff --git a/test/fixtures/trigger-event/eventTrigger.ts b/test/fixtures/trigger-event/eventTrigger.ts
index fcae043..ea8f35c 100644
--- a/test/fixtures/trigger-event/eventTrigger.ts
+++ b/test/fixtures/trigger-event/eventTrigger.ts
@@ -1,13 +1,9 @@
-import { Injectable, ScopeEnum } from "@artus/injection";
-import { Context, Next } from "@artus/pipeline";
-import { Trigger } from "../../../src";
-import { ARTUS_TRIGGER_ID } from "../../../src/constraints";
+import { Context, Next } from '@artus/pipeline';
+import { Trigger } from '../../../src';
+import { DefineTrigger } from '../../../src/decorator';
 
-@Injectable({
-  id: ARTUS_TRIGGER_ID,
-  scope: ScopeEnum.SINGLETON
-})
-export class EventTrigger extends Trigger {
+@DefineTrigger()
+export default class EventTrigger extends Trigger {
   constructor() {
     super();
     // first of all
diff --git a/test/fixtures/trigger-event/tsconfig.json b/test/fixtures/trigger-event/tsconfig.json
new file mode 100644
index 0000000..2597e01
--- /dev/null
+++ b/test/fixtures/trigger-event/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "esModuleInterop": true,
+    "experimentalDecorators": true
+  },
+  "rootDir": "./src",
+  "exclude": []
+}
\ No newline at end of file
diff --git a/test/fixtures/trigger-http/app.ts b/test/fixtures/trigger-http/app.ts
index 7ae4424..c7c79b9 100644
--- a/test/fixtures/trigger-http/app.ts
+++ b/test/fixtures/trigger-http/app.ts
@@ -1,20 +1,21 @@
+import path from 'path';
 import { Server } from 'http';
-import { Inject } from '@artus/injection';
-import { artusContainer, ArtusApplication, getArtusApplication } from '../../../src';
-import { ApplicationHook, ApplicationHookClass } from '../../../src/decorator';
-import { HttpTrigger } from './httpTrigger';
+import { ArtusApplication } from '../../../src';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../src/decorator';
 import http from 'http';
 import { Context, Input } from '@artus/pipeline';
+import { ApplicationLifecycle } from '../../../src/types';
 
-artusContainer.set({ type: HttpTrigger });
 let server: Server;
 
-@ApplicationHookClass()
-export class ApplicationHookExtension {
-  @Inject(ArtusApplication)
-  // @ts-ignore
+@ApplicationExtension()
+export class ApplicationHookExtension implements ApplicationLifecycle {
   app: ArtusApplication;
 
+  constructor(@WithApplication() app: ArtusApplication) {
+    this.app = app;
+  }
+
   @ApplicationHook()
   async didLoad() {
     this.app.trigger.use(async (ctx: Context) => {
@@ -41,12 +42,19 @@ export class ApplicationHookExtension {
 }
 
 async function main() {
-  const app: ArtusApplication = getArtusApplication();
+  const app: ArtusApplication = new ArtusApplication();
   await app.load({
     rootDir: __dirname,
-    items: []
+    items: [
+      {
+        loader: 'module',
+        path: path.resolve(__dirname, './httpTrigger')
+      }
+    ]
   });
   await app.run();
+
+  return app;
 };
 
 const isListening = () => server.listening;
diff --git a/test/fixtures/trigger-http/httpTrigger.ts b/test/fixtures/trigger-http/httpTrigger.ts
index c2961af..ef9d492 100644
--- a/test/fixtures/trigger-http/httpTrigger.ts
+++ b/test/fixtures/trigger-http/httpTrigger.ts
@@ -1,14 +1,10 @@
 import { Stream } from 'stream';
-import { Injectable, ScopeEnum } from "@artus/injection";
-import { Context, Next } from "@artus/pipeline";
-import { Trigger } from "../../../src";
-import { ARTUS_TRIGGER_ID } from "../../../src/constraints";
+import { Context, Next } from '@artus/pipeline';
+import { Trigger } from '../../../src';
+import { DefineTrigger } from '../../../src/decorator';
 
-@Injectable({
-  id: ARTUS_TRIGGER_ID,
-  scope: ScopeEnum.SINGLETON
-})
-export class HttpTrigger extends Trigger {
+@DefineTrigger()
+export default class HttpTrigger extends Trigger {
   constructor() {
     super();
     // first of all
diff --git a/test/fixtures/trigger-timer/app.ts b/test/fixtures/trigger-timer/app.ts
index 7a42c46..0c10737 100644
--- a/test/fixtures/trigger-timer/app.ts
+++ b/test/fixtures/trigger-timer/app.ts
@@ -1,10 +1,9 @@
-import { Inject } from '@artus/injection';
-import { artusContainer, ArtusApplication, getArtusApplication } from '../../../src';
-import { ApplicationHook, ApplicationHookClass } from '../../../src/decorator';
+import path from 'path';
+import { ArtusApplication } from '../../../src';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../src/decorator';
 import { Context, Input } from '@artus/pipeline';
-import { TimerTrigger } from './timerTrigger';
+import { ApplicationLifecycle } from '../../../src/types';
 
-artusContainer.set({ type: TimerTrigger });
 let timers: any[] = [];
 let execution = {
   task1: {
@@ -17,12 +16,14 @@ let execution = {
   }
 };
 
-@ApplicationHookClass()
-export class ApplicationHookExtension {
-  @Inject(ArtusApplication)
-  // @ts-ignore
+@ApplicationExtension()
+export class ApplicationHookExtension implements ApplicationLifecycle {
   app: ArtusApplication;
 
+  constructor(@WithApplication() app: ArtusApplication) {
+    this.app = app;
+  }
+
   @ApplicationHook()
   async didLoad() {
     this.app.trigger.use(async (ctx: Context) => {
@@ -64,12 +65,19 @@ export class ApplicationHookExtension {
 }
 
 async function main() {
-  const app: ArtusApplication = getArtusApplication();
+  const app: ArtusApplication = new ArtusApplication();
   await app.load({
     rootDir: __dirname,
-    items: []
+    items: [
+      {
+        loader: 'module',
+        path: path.resolve(__dirname, './timerTrigger')
+      }
+    ]
   });
   await app.run();
+
+  return app;
 };
 
 function getTaskExecution() {
diff --git a/test/fixtures/trigger-timer/timerTrigger.ts b/test/fixtures/trigger-timer/timerTrigger.ts
index 8487966..1260b4c 100644
--- a/test/fixtures/trigger-timer/timerTrigger.ts
+++ b/test/fixtures/trigger-timer/timerTrigger.ts
@@ -1,13 +1,9 @@
-import { Injectable, ScopeEnum } from "@artus/injection";
-import { Context, Next } from "@artus/pipeline";
-import { Trigger } from "../../../src";
-import { ARTUS_TRIGGER_ID } from "../../../src/constraints";
+import { Context, Next } from '@artus/pipeline';
+import { Trigger } from '../../../src';
+import { DefineTrigger } from '../../../src/decorator';
 
-@Injectable({
-  id: ARTUS_TRIGGER_ID,
-  scope: ScopeEnum.SINGLETON
-})
-export class TimerTrigger extends Trigger {
+@DefineTrigger()
+export default class TimerTrigger extends Trigger {
   constructor() {
     super();
     // first of all
diff --git a/test/fixtures/trigger-timer/tsconfig.json b/test/fixtures/trigger-timer/tsconfig.json
new file mode 100644
index 0000000..2597e01
--- /dev/null
+++ b/test/fixtures/trigger-timer/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "esModuleInterop": true,
+    "experimentalDecorators": true
+  },
+  "rootDir": "./src",
+  "exclude": []
+}
\ No newline at end of file
diff --git a/test/loader.test.ts b/test/loader.test.ts
index 3ee7e3b..168dc71 100644
--- a/test/loader.test.ts
+++ b/test/loader.test.ts
@@ -11,7 +11,7 @@ describe('test/loader.test.ts', () => {
       const loaderFactory = LoaderFactory.create(container);
       const manifest = require('./fixtures/module-with-ts/src/index').default;
       await loaderFactory.loadManifest(manifest);
-      assert(container.get('testServiceA').testMethod() === 'Hello Artus');
+      assert((container.get('testServiceA') as any).testMethod() === 'Hello Artus');
     });
   });
   describe('module with js', () => {
@@ -25,7 +25,7 @@ describe('test/loader.test.ts', () => {
           return container.get(properName);
         }
       });
-      assert(container.get('testServiceA').testMethod(appProxy) === 'Hello Artus');
+      assert((container.get('testServiceA') as any).testMethod(appProxy) === 'Hello Artus');
     });
   });
 });
diff --git a/test/trigger/event.test.ts b/test/trigger/event.test.ts
index 04885cf..34b6883 100644
--- a/test/trigger/event.test.ts
+++ b/test/trigger/event.test.ts
@@ -1,15 +1,13 @@
 import 'reflect-metadata';
 import assert from 'assert';
-import { getArtusApplication } from '../../src';
 
-describe('test/trigger/timer.test.ts', () => {
-  it('[trigger with timer] should run succeed', async () => {
+describe('test/trigger/event.test.ts', () => {
+  it('[trigger with event] should run succeed', async () => {
     const {
       main,
       pub
     } = await import('../fixtures/trigger-event/app');
-    const app = getArtusApplication();
-    await main();
+    const app = await main();
     let e1Result, e2Result;
     pub('e1', {
       cb(res) {
diff --git a/test/trigger/http.test.ts b/test/trigger/http.test.ts
index e907d34..7ee6ef0 100644
--- a/test/trigger/http.test.ts
+++ b/test/trigger/http.test.ts
@@ -1,7 +1,6 @@
 import 'reflect-metadata';
 import axios from 'axios';
 import assert from 'assert';
-import { getArtusApplication } from '../../src';
 
 describe('test/trigger/http.test.ts', () => {
   it('should run succeed', async () => {
@@ -9,8 +8,7 @@ describe('test/trigger/http.test.ts', () => {
       main,
       isListening
     } = await import('../fixtures/trigger-http/app');
-    const app = getArtusApplication();
-    await main();
+    const app = await main();
     const testResponse = await axios.get('http://127.0.0.1:3001');
     await axios.get('http://127.0.0.1:3001');
     await axios.get('http://127.0.0.1:3001');
diff --git a/test/trigger/timer.test.ts b/test/trigger/timer.test.ts
index 7643e8d..ad585b9 100644
--- a/test/trigger/timer.test.ts
+++ b/test/trigger/timer.test.ts
@@ -1,6 +1,5 @@
 import 'reflect-metadata';
 import assert from 'assert';
-import { getArtusApplication } from '../../src';
 
 describe('test/trigger/timer.test.ts', () => {
   it('[trigger with timer] should run succeed', async () => {
@@ -8,12 +7,10 @@ describe('test/trigger/timer.test.ts', () => {
       main,
       getTaskExecution,
     } = await import('../fixtures/trigger-timer/app');
-    const app = getArtusApplication();
-    await main();
+    const app = await main();
     await new Promise(resolve => setTimeout(resolve, 3000));
     const execution = getTaskExecution();
     assert(execution);
-    console.log(execution);
     // task1
     const task1ExecutionCount = execution.task1.count;
     assert(task1ExecutionCount > 10);