-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: impl GlobalGraph build hook (#246)
<!-- 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]. --> - [ ] `npm test` passes - [ ] tests and/or benchmarks are included - [ ] documentation is changed or added - [ ] 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 --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added `CrossCutGraphHook` and `PointCutGraphHook` to enhance the module's capabilities. - Introduced functions for managing cross-cutting concerns and linking pointcut advice within a global graph structure. - **Bug Fixes** - Improved test coverage for cross-cutting concerns, ensuring robust functionality. - **Documentation** - New `package.json` files created for `hello-cross-cut` and `hello-point-cut` modules to define metadata. - **Refactor** - Restructured the `Hello` class to utilize pointcut advice effectively. - **Chores** - Updated utility classes to support new build hooks for enhanced functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Loading branch information
Showing
15 changed files
with
385 additions
and
192 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
export * from './src/EggPrototypeCrossCutHook'; | ||
export * from './src/EggObjectAopHook'; | ||
export * from './src/LoadUnitAopHook'; | ||
|
||
export * from './src/CrossCutGraphHook'; | ||
export * from './src/PointCutGraphHook'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { AspectMetaBuilder, CrosscutInfo, CrosscutInfoUtil } from '@eggjs/aop-decorator'; | ||
import { GraphNode } from '@eggjs/tegg-common-util'; | ||
import { | ||
ClassProtoDescriptor, | ||
GlobalGraph, | ||
ProtoDependencyMeta, | ||
ProtoNode, | ||
} from '@eggjs/tegg-metadata'; | ||
|
||
export function crossCutGraphHook(globalGraph: GlobalGraph) { | ||
for (const moduleNode of globalGraph.moduleGraph.nodes.values()) { | ||
for (const crossCutProtoNode of moduleNode.val.protos) { | ||
const protoNodes = findCrossCuttedClazz(globalGraph, crossCutProtoNode); | ||
if (!protoNodes) continue; | ||
for (const crossCuttedProtoNode of protoNodes) { | ||
const crossCuttedModuleNode = globalGraph.findModuleNode(crossCuttedProtoNode.val.proto.instanceModuleName); | ||
if (!crossCuttedModuleNode) continue; | ||
globalGraph.addInject( | ||
crossCuttedModuleNode, | ||
crossCuttedProtoNode, | ||
crossCutProtoNode, | ||
crossCutProtoNode.val.proto.name); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function findCrossCuttedClazz(globalGraph: GlobalGraph, protoNode: GraphNode<ProtoNode, ProtoDependencyMeta>) { | ||
const proto = protoNode.val.proto; | ||
if (!ClassProtoDescriptor.isClassProtoDescriptor(proto)) { | ||
return; | ||
} | ||
if (!CrosscutInfoUtil.isCrosscutAdvice(proto.clazz)) { | ||
return; | ||
} | ||
const crosscutInfoList = CrosscutInfoUtil.getCrosscutInfoList(proto.clazz); | ||
const result: GraphNode<ProtoNode, ProtoDependencyMeta>[] = []; | ||
// eslint-disable-next-line no-labels | ||
crosscut: for (const crosscutInfo of crosscutInfoList) { | ||
for (const protoNode of globalGraph.protoGraph.nodes.values()) { | ||
if (checkClazzMatchCrossCut(protoNode, crosscutInfo)) { | ||
result.push(protoNode); | ||
// eslint-disable-next-line no-labels | ||
break crosscut; | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
function checkClazzMatchCrossCut(protoNode: GraphNode<ProtoNode>, crosscutInfo: CrosscutInfo) { | ||
const proto = protoNode.val.proto; | ||
if (!ClassProtoDescriptor.isClassProtoDescriptor(proto)) { | ||
return; | ||
} | ||
const allMethods = AspectMetaBuilder.getAllMethods(proto.clazz); | ||
for (const method of allMethods) { | ||
if (crosscutInfo.pointcutInfo.match(proto.clazz, method)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { AspectMetaBuilder, PointcutAdviceInfoUtil } from '@eggjs/aop-decorator'; | ||
import { PrototypeUtil, QualifierUtil } from '@eggjs/core-decorator'; | ||
import { GraphNode } from '@eggjs/tegg-common-util'; | ||
import { | ||
ClassProtoDescriptor, | ||
GlobalGraph, | ||
ProtoDependencyMeta, | ||
ProtoNode, | ||
} from '@eggjs/tegg-metadata'; | ||
import assert from 'node:assert'; | ||
|
||
export function pointCutGraphHook(globalGraph: GlobalGraph) { | ||
for (const moduleNode of globalGraph.moduleGraph.nodes.values()) { | ||
for (const pointCuttedProtoNode of moduleNode.val.protos) { | ||
const pointCutAdviceProtoList = findPointCutAdvice(globalGraph, pointCuttedProtoNode); | ||
if (!pointCutAdviceProtoList) continue; | ||
for (const pointCutAdviceProto of pointCutAdviceProtoList) { | ||
globalGraph.addInject( | ||
moduleNode, | ||
pointCuttedProtoNode, | ||
pointCutAdviceProto, | ||
pointCutAdviceProto.val.proto.name); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function findPointCutAdvice(globalGraph: GlobalGraph, protoNode: GraphNode<ProtoNode, ProtoDependencyMeta>) { | ||
const proto = protoNode.val.proto; | ||
if (!ClassProtoDescriptor.isClassProtoDescriptor(proto)) { | ||
return; | ||
} | ||
const result: Set<GraphNode<ProtoNode, ProtoDependencyMeta>> = new Set(); | ||
const allMethods = AspectMetaBuilder.getAllMethods(proto.clazz); | ||
for (const method of allMethods) { | ||
const adviceInfoList = PointcutAdviceInfoUtil.getPointcutAdviceInfoList(proto.clazz, method); | ||
for (const { clazz } of adviceInfoList) { | ||
const property = PrototypeUtil.getProperty(clazz); | ||
assert(property, 'not found property'); | ||
const injectProto = globalGraph.findDependencyProtoNode(protoNode.val.proto, { | ||
objName: property.name, | ||
refName: property.name, | ||
qualifiers: [ | ||
...property?.qualifiers ?? [], | ||
...QualifierUtil.getProtoQualifiers(clazz), | ||
], | ||
}); | ||
if (injectProto) { | ||
result.add(injectProto); | ||
} | ||
} | ||
} | ||
return Array.from(result); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
core/aop-runtime/test/fixtures/modules/hello_cross_cut/CallTrace.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { AccessLevel, SingletonProto } from "@eggjs/core-decorator"; | ||
|
||
export interface CallTraceMsg { | ||
className: string; | ||
methodName: string; | ||
id: number; | ||
name: string; | ||
result?: string; | ||
adviceParams?: any; | ||
} | ||
|
||
@SingletonProto({ | ||
accessLevel: AccessLevel.PUBLIC, | ||
}) | ||
export class CallTrace { | ||
msgs: Array<CallTraceMsg> = []; | ||
|
||
addMsg(msg: CallTraceMsg) { | ||
this.msgs.push(msg); | ||
} | ||
} |
69 changes: 69 additions & 0 deletions
69
core/aop-runtime/test/fixtures/modules/hello_cross_cut/HelloCrossCut.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import assert from 'node:assert'; | ||
import { AccessLevel, Inject } from '@eggjs/core-decorator'; | ||
import { Advice, Crosscut } from '@eggjs/aop-decorator'; | ||
import { AdviceContext, IAdvice, PointcutType } from '@eggjs/tegg-types'; | ||
import { Hello } from '../hello_succeed/Hello'; | ||
import { CallTrace } from './CallTrace'; | ||
|
||
export const crosscutAdviceParams = { | ||
cross: Math.random().toString(), | ||
cut: Math.random().toString(), | ||
}; | ||
|
||
@Crosscut({ | ||
type: PointcutType.CLASS, | ||
clazz: Hello, | ||
methodName: 'hello', | ||
}, { adviceParams: crosscutAdviceParams }) | ||
@Advice({ | ||
accessLevel: AccessLevel.PUBLIC, | ||
}) | ||
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)})`; | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
core/aop-runtime/test/fixtures/modules/hello_cross_cut/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"name": "hello-cross-cut", | ||
"eggModule": { | ||
"name": "helloCrossCut" | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
core/aop-runtime/test/fixtures/modules/hello_point_cut/HelloPointCut.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import assert from 'node:assert'; | ||
import { AccessLevel, Inject } from '@eggjs/core-decorator'; | ||
import { Advice } from '@eggjs/aop-decorator'; | ||
import { AdviceContext, IAdvice } from '@eggjs/tegg-types'; | ||
import { Hello } from '../hello_succeed/Hello'; | ||
import { CallTrace } from '../hello_cross_cut/CallTrace'; | ||
|
||
export const pointcutAdviceParams = { | ||
point: Math.random().toString(), | ||
cut: Math.random().toString(), | ||
}; | ||
|
||
// 测试aop修改ctx的args的值 | ||
const TEST_CTX_ARGS_VALUE = 123; | ||
|
||
@Advice({ | ||
accessLevel: AccessLevel.PUBLIC, | ||
}) | ||
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, | ||
}); | ||
ctx.args = [...ctx.args, TEST_CTX_ARGS_VALUE]; | ||
} | ||
|
||
async afterReturn(ctx: AdviceContext<Hello>, result: any): Promise<void> { | ||
assert.ok(ctx.adviceParams); | ||
assert.deepStrictEqual(ctx.adviceParams, pointcutAdviceParams); | ||
assert.deepStrictEqual(ctx.args[ctx.args.length - 1], TEST_CTX_ARGS_VALUE); | ||
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)})`; | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
core/aop-runtime/test/fixtures/modules/hello_point_cut/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"name": "hello-point-cut", | ||
"eggModule": { | ||
"name": "helloPointCut" | ||
} | ||
} |
Oops, something went wrong.