Skip to content

Commit

Permalink
feat: add aop runtime/dynamic inject runtime for standalone (#149)
Browse files Browse the repository at this point in the history
<!--
Thank you for your pull request. Please review below requirements.
Bug fixes and new features should include tests and possibly benchmarks.
Contributors guide:
https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md

感谢您贡献代码。请确认下列 checklist 的完成情况。
Bug 修复和新功能必须包含测试,必要时请附上性能测试。
Contributors guide:
https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md
-->

##### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to
[x]. -->

- [x] `npm test` passes
- [x] tests and/or benchmarks are included
- [ ] documentation is changed or added
- [x] commit message follows commit guidelines

##### Affected core subsystem(s)
<!-- Provide affected core subsystem(s). -->


##### Description of change
<!-- Provide a description of the change below this comment. -->

<!--
- any feature?
- close https://github.com/eggjs/egg/ISSUE_URL
-->
  • Loading branch information
killagu authored Aug 29, 2023
1 parent 77eaf38 commit 6091fc6
Show file tree
Hide file tree
Showing 20 changed files with 429 additions and 3 deletions.
3 changes: 2 additions & 1 deletion standalone/standalone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"@eggjs/tegg-lifecycle": "^3.17.0",
"@eggjs/tegg-loader": "^3.17.0",
"@eggjs/tegg-metadata": "^3.17.0",
"@eggjs/tegg-runtime": "^3.17.0"
"@eggjs/tegg-runtime": "^3.17.0",
"@eggjs/tegg-aop-runtime": "^3.17.0"
},
"publishConfig": {
"access": "public"
Expand Down
4 changes: 4 additions & 0 deletions standalone/standalone/src/ConfigSourceLoadUnitHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
} from '@eggjs/tegg';
import { ConfigSourceQualifier, ConfigSourceQualifierAttribute } from './ConfigSource';

/**
* Hook for inject moduleConfig.
* Add default qualifier value is current module name.
*/
export class ConfigSourceLoadUnitHook implements LifecycleHook<LoadUnitLifecycleContext, LoadUnit> {
async preCreate(ctx: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise<void> {
const classList = ctx.loader.load();
Expand Down
26 changes: 26 additions & 0 deletions standalone/standalone/src/LoadUnitInnerClassHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LifecycleHook } from '@eggjs/tegg-lifecycle';
import {
EggPrototypeFactory,
LoadUnit,
LoadUnitLifecycleContext,
EggPrototypeCreatorFactory,
} from '@eggjs/tegg-metadata';
import { EggProtoImplClass } from '@eggjs/tegg';
import { EggObjectFactory } from '@eggjs/tegg-dynamic-inject-runtime';

const INNER_CLASS_LIST = [
EggObjectFactory,
];

export class LoadUnitInnerClassHook implements LifecycleHook<LoadUnitLifecycleContext, LoadUnit> {
async postCreate(_: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise<void> {
if (loadUnit.type === 'StandaloneLoadUnitType') {
for (const clazz of INNER_CLASS_LIST) {
const protos = await EggPrototypeCreatorFactory.createProto(clazz as EggProtoImplClass, loadUnit);
for (const proto of protos) {
EggPrototypeFactory.instance.registerPrototype(proto, loadUnit);
}
}
}
}
}
42 changes: 40 additions & 2 deletions standalone/standalone/src/Runner.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import { ModuleConfigUtil, ModuleReference, RuntimeConfig } from '@eggjs/tegg-common-util';
import {
EggPrototype,
EggPrototype, EggPrototypeLifecycleUtil,
LoadUnit,
LoadUnitFactory,
LoadUnitLifecycleUtil,
} from '@eggjs/tegg-metadata';
import {
ContextHandler,
EggContainerFactory, EggContext,
EggContainerFactory, EggContext, EggObjectLifecycleUtil,
LoadUnitInstance,
LoadUnitInstanceFactory,
ModuleLoadUnitInstance,
} from '@eggjs/tegg-runtime';
import { EggProtoImplClass, PrototypeUtil } from '@eggjs/tegg';
import { StandaloneUtil, MainRunner } from '@eggjs/tegg/standalone';
import { CrosscutAdviceFactory } from '@eggjs/tegg/aop';
import { EggObjectAopHook, EggPrototypeCrossCutHook, LoadUnitAopHook } from '@eggjs/tegg-aop-runtime';

import { EggModuleLoader } from './EggModuleLoader';
import { InnerObject, StandaloneLoadUnit, StandaloneLoadUnitType } from './StandaloneLoadUnit';
import { StandaloneContext } from './StandaloneContext';
import { StandaloneContextHandler } from './StandaloneContextHandler';
import { ModuleConfigHolder, ModuleConfigs } from './ModuleConfigs';
import { ConfigSourceQualifierAttribute } from './ConfigSource';
import { ConfigSourceLoadUnitHook } from './ConfigSourceLoadUnitHook';
import { LoadUnitInnerClassHook } from './LoadUnitInnerClassHook';

export interface RunnerOptions {
/**
Expand All @@ -40,6 +44,12 @@ export class Runner {
private runnerProto: EggPrototype;
private configSourceEggPrototypeHook: ConfigSourceLoadUnitHook;

private readonly loadUnitInnerClassHook: LoadUnitInnerClassHook;
private readonly crosscutAdviceFactory: CrosscutAdviceFactory;
private readonly loadUnitAopHook: LoadUnitAopHook;
private readonly eggPrototypeCrossCutHook: EggPrototypeCrossCutHook;
private readonly eggObjectAopHook: EggObjectAopHook;

loadUnits: LoadUnit[] = [];
loadUnitInstances: LoadUnitInstance[] = [];
innerObjects: Record<string, InnerObject[]>;
Expand Down Expand Up @@ -96,6 +106,20 @@ export class Runner {
this.loadUnitLoader = new EggModuleLoader(this.moduleReferences);
const configSourceEggPrototypeHook = new ConfigSourceLoadUnitHook();
LoadUnitLifecycleUtil.registerLifecycle(configSourceEggPrototypeHook);

this.loadUnitInnerClassHook = new LoadUnitInnerClassHook();
LoadUnitLifecycleUtil.registerLifecycle(this.loadUnitInnerClassHook);

// TODO refactor with egg module
// aop runtime
this.crosscutAdviceFactory = new CrosscutAdviceFactory();
this.loadUnitAopHook = new LoadUnitAopHook(this.crosscutAdviceFactory);
this.eggPrototypeCrossCutHook = new EggPrototypeCrossCutHook(this.crosscutAdviceFactory);
this.eggObjectAopHook = new EggObjectAopHook();

EggPrototypeLifecycleUtil.registerLifecycle(this.eggPrototypeCrossCutHook);
LoadUnitLifecycleUtil.registerLifecycle(this.loadUnitAopHook);
EggObjectLifecycleUtil.registerLifecycle(this.eggObjectAopHook);
}

async init() {
Expand Down Expand Up @@ -165,5 +189,19 @@ export class Runner {
if (this.configSourceEggPrototypeHook) {
LoadUnitLifecycleUtil.deleteLifecycle(this.configSourceEggPrototypeHook);
}

if (this.loadUnitInnerClassHook) {
LoadUnitLifecycleUtil.deleteLifecycle(this.loadUnitInnerClassHook);
}

if (this.eggPrototypeCrossCutHook) {
EggPrototypeLifecycleUtil.deleteLifecycle(this.eggPrototypeCrossCutHook);
}
if (this.loadUnitAopHook) {
LoadUnitLifecycleUtil.deleteLifecycle(this.loadUnitAopHook);
}
if (this.eggObjectAopHook) {
EggObjectLifecycleUtil.deleteLifecycle(this.eggObjectAopHook);
}
}
}
182 changes: 182 additions & 0 deletions standalone/standalone/test/fixtures/aop-module/Hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import {
ContextProto,
Inject,
SingletonProto,
} from '@eggjs/tegg';
import {
Advice,
AdviceContext,
Crosscut,
IAdvice,
Pointcut,
PointcutType,
} from '@eggjs/tegg/aop';
import assert from 'assert';

export interface CallTraceMsg {
className: string;
methodName: string;
id: number;
name: string;
result?: string;
adviceParams?: any;
}

@SingletonProto()
export class CallTrace {
msgs: Array<CallTraceMsg> = [];

addMsg(msg: CallTraceMsg) {
this.msgs.push(msg);
}
}

export const pointcutAdviceParams = {
point: Math.random()
.toString(),
cut: Math.random()
.toString(),
};

@Advice()
export class PointcutAdvice implements IAdvice<Hello> {
@Inject()
callTrace: CallTrace;

async beforeCall(ctx: AdviceContext<Hello>): Promise<void> {
assert.ok(ctx.adviceParams);
assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams);
this.callTrace.addMsg({
className: PointcutAdvice.name,
methodName: 'beforeCall',
id: ctx.that.id,
name: ctx.args[0],
adviceParams: ctx.adviceParams,
});
}

async afterReturn(ctx: AdviceContext<Hello>, result: any): Promise<void> {
assert.ok(ctx.adviceParams);
assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams);
this.callTrace.addMsg({
className: PointcutAdvice.name,
methodName: 'afterReturn',
id: ctx.that.id,
name: ctx.args[0],
result,
adviceParams: ctx.adviceParams,
});
}

async afterThrow(ctx: AdviceContext<Hello, any>, error: Error): Promise<void> {
assert.ok(ctx.adviceParams);
assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams);
this.callTrace.addMsg({
className: PointcutAdvice.name,
methodName: 'afterThrow',
id: ctx.that.id,
name: ctx.args[0],
result: error.message,
adviceParams: ctx.adviceParams,
});
}

async afterFinally(ctx: AdviceContext<Hello>): Promise<void> {
assert.ok(ctx.adviceParams);
assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams);
this.callTrace.addMsg({
className: PointcutAdvice.name,
methodName: 'afterFinally',
id: ctx.that.id,
name: ctx.args[0],
adviceParams: ctx.adviceParams,
});
}

async around(ctx: AdviceContext<Hello>, next: () => Promise<any>): Promise<any> {
assert.ok(ctx.adviceParams);
assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams);
ctx.args[0] = `withPointAroundParam(${ctx.args[0]})`;
const result = await next();
return `withPointAroundResult(${result}${JSON.stringify(pointcutAdviceParams)})`;
}
}

@ContextProto()
export class Hello {
id = 233;

@Pointcut(PointcutAdvice, { adviceParams: pointcutAdviceParams })
async hello(name: string) {
return `hello ${name}`;
}

@Pointcut(PointcutAdvice, { adviceParams: pointcutAdviceParams })
async helloWithException(name: string) {
throw new Error(`ops, exception for ${name}`);
}

}

export const crosscutAdviceParams = {
cross: Math.random()
.toString(),
cut: Math.random()
.toString(),
};

@Crosscut({
type: PointcutType.CLASS,
clazz: Hello,
methodName: 'hello',
}, { adviceParams: crosscutAdviceParams })
@Advice()
export class CrosscutAdvice implements IAdvice<Hello, String> {
@Inject()
callTrace: CallTrace;

async beforeCall(ctx: AdviceContext<Hello, {}>): Promise<void> {
assert.ok(ctx.adviceParams);
assert.deepStrictEqual(ctx.adviceParams, crosscutAdviceParams);
this.callTrace.addMsg({
className: CrosscutAdvice.name,
methodName: 'beforeCall',
id: ctx.that.id,
name: ctx.args[0],
adviceParams: ctx.adviceParams,
});
}

async afterReturn(ctx: AdviceContext<Hello>, result: any): Promise<void> {
assert.ok(ctx.adviceParams);
assert.deepStrictEqual(ctx.adviceParams, crosscutAdviceParams);
this.callTrace.addMsg({
className: CrosscutAdvice.name,
methodName: 'afterReturn',
id: ctx.that.id,
name: ctx.args[0],
result,
adviceParams: ctx.adviceParams,
});
}

async afterFinally(ctx: AdviceContext<Hello>): Promise<void> {
assert.ok(ctx.adviceParams);
assert.deepStrictEqual(ctx.adviceParams, crosscutAdviceParams);
this.callTrace.addMsg({
className: CrosscutAdvice.name,
methodName: 'afterFinally',
id: ctx.that.id,
name: ctx.args[0],
adviceParams: ctx.adviceParams,
});
}

async around(ctx: AdviceContext<Hello>, next: () => Promise<any>): Promise<any> {
assert.ok(ctx.adviceParams);
assert.deepStrictEqual(ctx.adviceParams, crosscutAdviceParams);
ctx.args[0] = `withCrosscutAroundParam(${ctx.args[0]})`;
const result = await next();
return `withCrossAroundResult(${result}${JSON.stringify(ctx.adviceParams)})`;
}
}
15 changes: 15 additions & 0 deletions standalone/standalone/test/fixtures/aop-module/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ContextProto, Inject } from '@eggjs/tegg';
import { Runner, MainRunner } from '@eggjs/tegg/standalone';
import { Hello } from './Hello';


@Runner()
@ContextProto()
export class Foo implements MainRunner<string> {
@Inject()
hello: Hello;

async main(): Promise<string> {
return await this.hello.hello('aop');
}
}
6 changes: 6 additions & 0 deletions standalone/standalone/test/fixtures/aop-module/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "aop-module",
"eggModule": {
"name": "aopModule"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export abstract class AbstractContextHello {
abstract hello(): string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export abstract class AbstractSingletonHello {
abstract hello(): string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum ContextHelloType {
FOO = 'FOO',
BAR = 'BAR',
}

export enum SingletonHelloType {
FOO = 'FOO',
BAR = 'BAR',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ContextProto, Inject, EggObjectFactory } from '@eggjs/tegg';
import { ContextHelloType, SingletonHelloType } from './FooType';
import { AbstractContextHello } from './AbstractContextHello';
import { AbstractSingletonHello } from './AbstractSingletonHello';

@ContextProto()
export class HelloService {
@Inject()
private readonly eggObjectFactory: EggObjectFactory;

async hello(): Promise<string[]> {
const helloImpls = await Promise.all([
this.eggObjectFactory.getEggObject(AbstractContextHello, ContextHelloType.FOO),
this.eggObjectFactory.getEggObject(AbstractContextHello, ContextHelloType.BAR),
this.eggObjectFactory.getEggObject(AbstractSingletonHello, SingletonHelloType.FOO),
this.eggObjectFactory.getEggObject(AbstractSingletonHello, SingletonHelloType.BAR),
]);
const msgs = helloImpls.map(helloImpl => helloImpl.hello());
return msgs;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ContextHelloType } from '../FooType';
import { ImplDecorator, QualifierImplDecoratorUtil } from '@eggjs/tegg';
import { AbstractContextHello } from '../AbstractContextHello';

export const CONTEXT_HELLO_ATTRIBUTE = 'CONTEXT_HELLO_ATTRIBUTE';

export const ContextHello: ImplDecorator<AbstractContextHello, typeof ContextHelloType> =
QualifierImplDecoratorUtil.generatorDecorator(AbstractContextHello, CONTEXT_HELLO_ATTRIBUTE);

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SingletonHelloType } from '../FooType';
import { ImplDecorator, QualifierImplDecoratorUtil } from '@eggjs/tegg';
import { AbstractSingletonHello } from '../AbstractSingletonHello';

export const SINGLETON_HELLO_ATTRIBUTE = 'SINGLETON_HELLO_ATTRIBUTE';

export const SingletonHello: ImplDecorator<AbstractSingletonHello, typeof SingletonHelloType> =
QualifierImplDecoratorUtil.generatorDecorator(AbstractSingletonHello, SINGLETON_HELLO_ATTRIBUTE);
Loading

0 comments on commit 6091fc6

Please sign in to comment.