Skip to content

Commit

Permalink
feat: impl GlobalGraph build hook (#246)
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]. -->

- [ ] `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
killagu authored Oct 10, 2024
1 parent f01fb63 commit 48fce45
Show file tree
Hide file tree
Showing 15 changed files with 385 additions and 192 deletions.
2 changes: 1 addition & 1 deletion core/aop-decorator/src/AspectMetaBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class AspectMetaBuilder {
return aspectList;
}

private static getAllMethods(clazz): PropertyKey[] {
static getAllMethods(clazz): PropertyKey[] {
const methodSet = new Set<string>();
function getMethods(obj) {
if (obj) {
Expand Down
3 changes: 3 additions & 0 deletions core/aop-runtime/index.ts
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';
63 changes: 63 additions & 0 deletions core/aop-runtime/src/CrossCutGraphHook.ts
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;
}
54 changes: 54 additions & 0 deletions core/aop-runtime/src/PointCutGraphHook.ts
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);
}
12 changes: 11 additions & 1 deletion core/aop-runtime/test/aop-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import { EggPrototypeLifecycleUtil, LoadUnitFactory, LoadUnitLifecycleUtil } fro
import type { LoadUnitInstance } from '@eggjs/tegg-types';
import { CrosscutAdviceFactory } from '@eggjs/aop-decorator';
import { CoreTestHelper, EggTestContext } from '../../test-util';
import { CallTrace, Hello, crosscutAdviceParams, pointcutAdviceParams } from './fixtures/modules/hello_succeed/Hello';
import { Hello } from './fixtures/modules/hello_succeed/Hello';
import { crosscutAdviceParams } from './fixtures/modules/hello_cross_cut/HelloCrossCut';
import { pointcutAdviceParams } from './fixtures/modules/hello_point_cut/HelloPointCut';
import { EggObjectAopHook } from '../src/EggObjectAopHook';
import { LoadUnitAopHook } from '../src/LoadUnitAopHook';
import { EggPrototypeCrossCutHook } from '../src/EggPrototypeCrossCutHook';
import { crossCutGraphHook } from '../src/CrossCutGraphHook';
import { pointCutGraphHook } from '../src/PointCutGraphHook';
import { CallTrace } from './fixtures/modules/hello_cross_cut/CallTrace';

describe('test/aop-runtime.test.ts', () => {
describe('succeed call', () => {
Expand All @@ -31,6 +36,11 @@ describe('test/aop-runtime.test.ts', () => {
modules = await CoreTestHelper.prepareModules([
path.join(__dirname, '..'),
path.join(__dirname, 'fixtures/modules/hello_succeed'),
path.join(__dirname, 'fixtures/modules/hello_point_cut'),
path.join(__dirname, 'fixtures/modules/hello_cross_cut'),
], [
crossCutGraphHook,
pointCutGraphHook,
]);
});

Expand Down
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);
}
}
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)})`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "hello-cross-cut",
"eggModule": {
"name": "helloCrossCut"
}
}
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)})`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "hello-point-cut",
"eggModule": {
"name": "helloPointCut"
}
}
Loading

0 comments on commit 48fce45

Please sign in to comment.