Skip to content

Commit c53df00

Browse files
committed
feat: impl aop
1 parent 47f22d6 commit c53df00

59 files changed

Lines changed: 1721 additions & 5 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/aop-decorator/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Change Log
2+
3+
All notable changes to this project will be documented in this file.
4+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

core/aop-decorator/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# `@eggjs/aop-decorator`
2+
3+
# Usage
4+
5+
Please read [@eggjs/tegg-aop-plugin](../../plugin/aop/README.md)

core/aop-decorator/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export * from './src/decorator/Advice';
2+
export * from './src/decorator/Pointcut';
3+
export * from './src/decorator/Cosscut';
4+
export * from './src/model/Aspect';
5+
export * from './src/model/PointcutInfo';
6+
export * from './src/util/AdviceInfoUtil';
7+
export * from './src/util/CrosscutInfoUtil';
8+
export * from './src/util/PointcutAdviceInfoUtil';
9+
export * from './src/util/AspectInfoUtil';
10+
export * from './src/AspectMetaBuilder';
11+
export * from './src/CrosscutAdviceFactory';

core/aop-decorator/package.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@eggjs/aop-decorator",
3+
"version": "0.1.19",
4+
"description": "tegg aop decorator",
5+
"keywords": [
6+
"tegg",
7+
"aop",
8+
"typescript",
9+
"egg"
10+
],
11+
"author": "killagu <[email protected]>",
12+
"homepage": "https://github.com/eggjs/tegg",
13+
"repository": {
14+
"type": "git",
15+
"url": "[email protected]:eggjs/tegg.git",
16+
"directory": "core/aop-decorator"
17+
},
18+
"dependencies": {
19+
"@eggjs/core-decorator": "^0.1.19",
20+
"@eggjs/tegg-common-util": "^0.1.19",
21+
"@eggjs/tegg-metadata": "^0.1.19"
22+
},
23+
"scripts": {
24+
"clean": "tsc -b --clean",
25+
"tsc": "npm run clean && tsc -p ./tsconfig.json",
26+
"tsc:pub": "npm run clean && tsc -p ./tsconfig.pub.json",
27+
"prepublishOnly": "npm run tsc:pub",
28+
"autod": "autod"
29+
},
30+
"publishConfig": {
31+
"access": "public"
32+
},
33+
"engines": {
34+
"node": ">=14.0.0"
35+
},
36+
"license": "MIT",
37+
"main": "dist/index.js",
38+
"files": [
39+
"dist/**/*.js",
40+
"dist/**/*.d.ts"
41+
],
42+
"bugs": {
43+
"url": "https://github.com/eggjs/tegg/issues"
44+
}
45+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { CrosscutAdviceFactory } from './CrosscutAdviceFactory';
2+
import { EggProtoImplClass } from '@eggjs/core-decorator';
3+
import { Aspect, AspectBuilder } from './model/Aspect';
4+
import { PointcutAdviceInfoUtil } from './util/PointcutAdviceInfoUtil';
5+
6+
export class AspectMetaBuilder {
7+
private readonly clazz: EggProtoImplClass;
8+
private readonly crosscutAdviceFactory: CrosscutAdviceFactory;
9+
10+
constructor(clazz: EggProtoImplClass, options: {
11+
crosscutAdviceFactory: CrosscutAdviceFactory;
12+
}) {
13+
this.clazz = clazz;
14+
this.crosscutAdviceFactory = options.crosscutAdviceFactory;
15+
}
16+
17+
build(): Array<Aspect> {
18+
const aspectList: Array<Aspect> = [];
19+
const methods = AspectMetaBuilder.getAllMethods(this.clazz);
20+
for (const method of methods) {
21+
const aspect = this.doBuildMethodAspect(method);
22+
if (aspect) {
23+
aspectList.push(aspect);
24+
}
25+
}
26+
return aspectList;
27+
}
28+
29+
private static getAllMethods(clazz): PropertyKey[] {
30+
const methodSet = new Set<string>();
31+
function getMethods(obj) {
32+
if (obj) {
33+
const names = Object.getOwnPropertyNames(obj);
34+
for (const name of names) {
35+
if (obj[name] instanceof Function) {
36+
methodSet.add(name);
37+
}
38+
}
39+
getMethods(Object.getPrototypeOf(obj));
40+
}
41+
}
42+
43+
getMethods(clazz.prototype);
44+
45+
return Array.from(methodSet);
46+
}
47+
48+
private doBuildMethodAspect(method: PropertyKey): Aspect | undefined {
49+
const crosscutAdviceList = this.crosscutAdviceFactory.getAdvice(this.clazz, method);
50+
// decorator execute in reverse order
51+
const pointcutAdviceList = PointcutAdviceInfoUtil.getPointcutAdviceInfoList(this.clazz, method);
52+
if (!crosscutAdviceList.length && !pointcutAdviceList.length) return;
53+
const aspectBuilder = new AspectBuilder(this.clazz, method);
54+
for (const advice of crosscutAdviceList) {
55+
aspectBuilder.addAdvice(advice);
56+
}
57+
for (const advice of pointcutAdviceList) {
58+
aspectBuilder.addAdvice(advice);
59+
}
60+
return aspectBuilder.build();
61+
}
62+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import assert from 'assert';
2+
import { EggProtoImplClass } from '@eggjs/core-decorator';
3+
import { IAdvice } from './decorator/Advice';
4+
import { CrosscutInfoUtil } from './util/CrosscutInfoUtil';
5+
import { AdviceInfo } from './model/Aspect';
6+
7+
export class CrosscutAdviceFactory {
8+
private readonly crosscutAdviceClazzList: Array<EggProtoImplClass<IAdvice>> = [];
9+
10+
registerCrossAdviceClazz(clazz: EggProtoImplClass<IAdvice>) {
11+
assert(CrosscutInfoUtil.isCrosscutAdvice(clazz), `clazz ${clazz.name} is not crosscut advice`);
12+
this.crosscutAdviceClazzList.push(clazz);
13+
}
14+
15+
getAdvice(clazz: EggProtoImplClass, method: PropertyKey): Array<AdviceInfo> {
16+
const result: Array<AdviceInfo> = [];
17+
for (const crosscutAdviceClazz of this.crosscutAdviceClazzList) {
18+
const crosscutInfoList = CrosscutInfoUtil.getCrosscutInfoList(crosscutAdviceClazz);
19+
for (const crosscutInfo of crosscutInfoList) {
20+
if (crosscutInfo.pointcutInfo.match(clazz, method)) {
21+
result.push(crosscutInfo.adviceInfo);
22+
}
23+
}
24+
}
25+
return result;
26+
}
27+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
AccessLevel,
3+
EggProtoImplClass,
4+
ObjectInitType,
5+
Prototype,
6+
PrototypeParams,
7+
} from '@eggjs/core-decorator';
8+
import { AdviceInfoUtil } from '../util/AdviceInfoUtil';
9+
10+
export interface AdviceContext<T = object> {
11+
that: T;
12+
method: PropertyKey;
13+
args: any[];
14+
}
15+
16+
/**
17+
* execute order:
18+
* 1. beforeCall
19+
* 1. around
20+
* 1. afterReturn/afterThrow
21+
* 1. afterFinally
22+
*/
23+
export interface IAdvice<T = object> {
24+
/**
25+
* call before function
26+
* @param ctx
27+
*/
28+
beforeCall?(ctx: AdviceContext<T>): Promise<void>;
29+
30+
/**
31+
* call after function succeed
32+
*/
33+
afterReturn?(ctx: AdviceContext<T>, result: any): Promise<void>;
34+
35+
/**
36+
* call after function throw error
37+
*/
38+
afterThrow?(ctx: AdviceContext<T>, error: Error): Promise<void>;
39+
40+
/**
41+
* always call after function done
42+
*/
43+
afterFinally?(ctx: AdviceContext<T>): Promise<void>;
44+
45+
/**
46+
* execute the function
47+
* the only one can modify the function return value
48+
*/
49+
around?(ctx: AdviceContext<T>, next: () => Promise<any>): Promise<any>;
50+
}
51+
52+
const defaultAdviceParam = {
53+
accessLevel: AccessLevel.PUBLIC,
54+
initType: ObjectInitType.CONTEXT,
55+
};
56+
57+
export function Advice(param?: PrototypeParams) {
58+
return function(constructor: EggProtoImplClass<IAdvice>) {
59+
AdviceInfoUtil.setIsAdvice(true, constructor);
60+
const func = Prototype({
61+
...defaultAdviceParam,
62+
...param,
63+
});
64+
func(constructor);
65+
};
66+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { EggProtoImplClass } from '@eggjs/core-decorator';
2+
import { IAdvice } from './Advice';
3+
import { CrosscutInfo, CrosscutInfoUtil } from '../util/CrosscutInfoUtil';
4+
import {
5+
ClassPointInfo,
6+
CustomPointcutCallback,
7+
CustomPointInfo,
8+
NamePointInfo,
9+
PointcutType,
10+
} from '../model/PointcutInfo';
11+
12+
export interface CrosscutOptions {
13+
// 默认值 100
14+
order?: number;
15+
}
16+
17+
const defaultCrossOptions = {
18+
order: 100,
19+
};
20+
21+
// TODO type check for methodName
22+
export interface ClassCrosscutParam {
23+
type: PointcutType.CLASS;
24+
clazz: EggProtoImplClass;
25+
methodName: PropertyKey;
26+
}
27+
28+
export interface NameCrosscutParam {
29+
type: PointcutType.NAME;
30+
className: RegExp;
31+
methodName: RegExp;
32+
}
33+
34+
35+
export interface CustomCrosscutParam {
36+
type: PointcutType.CUSTOM;
37+
callback: CustomPointcutCallback;
38+
}
39+
40+
export type CrosscutParam = ClassCrosscutParam | NameCrosscutParam | CustomCrosscutParam;
41+
42+
export function Crosscut(param: CrosscutParam, options?: CrosscutOptions) {
43+
return function(constructor: EggProtoImplClass<IAdvice>) {
44+
let crosscutInfo: CrosscutInfo;
45+
if (param.type === PointcutType.CLASS) {
46+
crosscutInfo = {
47+
pointcutInfo: new ClassPointInfo(param.clazz, param.methodName),
48+
adviceInfo: {
49+
clazz: constructor,
50+
order: options?.order ?? defaultCrossOptions.order,
51+
},
52+
};
53+
} else if (param.type === PointcutType.NAME) {
54+
crosscutInfo = {
55+
pointcutInfo: new NamePointInfo(param.className, param.methodName),
56+
adviceInfo: {
57+
clazz: constructor,
58+
order: options?.order ?? defaultCrossOptions.order,
59+
},
60+
};
61+
} else {
62+
crosscutInfo = {
63+
pointcutInfo: new CustomPointInfo(param.callback),
64+
adviceInfo: {
65+
clazz: constructor,
66+
order: options?.order ?? defaultCrossOptions.order,
67+
},
68+
};
69+
}
70+
CrosscutInfoUtil.setIsCrosscutAdvice(true, constructor);
71+
CrosscutInfoUtil.addCrosscutInfo(crosscutInfo, constructor);
72+
};
73+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { IAdvice } from './Advice';
2+
import { EggProtoImplClass } from '@eggjs/core-decorator';
3+
import { PointcutAdviceInfoUtil } from '../util/PointcutAdviceInfoUtil';
4+
import assert from 'assert';
5+
import { AdviceInfoUtil } from '../util/AdviceInfoUtil';
6+
7+
export interface PointcutOptions {
8+
// default is 1000
9+
order?: number;
10+
}
11+
12+
const defaultPointcutOptions = {
13+
order: 1000,
14+
};
15+
16+
export function Pointcut(adviceClazz: EggProtoImplClass<IAdvice>, options?: PointcutOptions) {
17+
return function(target: any, propertyKey: PropertyKey) {
18+
assert(AdviceInfoUtil.isAdvice(adviceClazz), `class ${adviceClazz} has no @Advice decorator`);
19+
const targetClazz = target.constructor as EggProtoImplClass;
20+
const methodName = propertyKey as string;
21+
PointcutAdviceInfoUtil.addPointcutAdviceInfo({
22+
clazz: adviceClazz,
23+
order: options?.order ?? defaultPointcutOptions.order,
24+
}, targetClazz, methodName);
25+
};
26+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { EggProtoImplClass } from '@eggjs/core-decorator';
2+
import { IAdvice } from '../decorator/Advice';
3+
4+
export interface AdviceInfo {
5+
clazz: EggProtoImplClass<IAdvice>;
6+
order: number;
7+
}
8+
9+
export interface AspectAdvice {
10+
name: string;
11+
clazz: EggProtoImplClass<IAdvice>;
12+
}
13+
14+
export class Aspect {
15+
readonly clazz: EggProtoImplClass;
16+
readonly method: PropertyKey;
17+
readonly adviceList: readonly AspectAdvice[];
18+
19+
constructor(clazz: EggProtoImplClass, method: PropertyKey, adviceList: readonly AspectAdvice[]) {
20+
this.clazz = clazz;
21+
this.method = method;
22+
this.adviceList = adviceList;
23+
}
24+
}
25+
26+
export class AspectBuilder {
27+
readonly clazz: EggProtoImplClass;
28+
readonly method: PropertyKey;
29+
private readonly adviceList: Array<AdviceInfo>;
30+
31+
constructor(clazz: EggProtoImplClass, method: PropertyKey) {
32+
this.clazz = clazz;
33+
this.method = method;
34+
this.adviceList = [];
35+
}
36+
37+
addAdvice(adviceInfo: AdviceInfo) {
38+
this.adviceList.push(adviceInfo);
39+
}
40+
41+
build(): Aspect {
42+
this.adviceList.sort((a, b) => a.order - b.order);
43+
const aspectAdviceList = this.adviceList.map((t, i) => {
44+
return {
45+
clazz: t.clazz,
46+
name: this.adviceName(t.clazz, i),
47+
};
48+
});
49+
return new Aspect(this.clazz, this.method, aspectAdviceList);
50+
}
51+
52+
private adviceName(advice: EggProtoImplClass<IAdvice>, index: number) {
53+
return `${this.clazz.name}#${String(this.method)}#${advice.name}#${index}`;
54+
}
55+
}

0 commit comments

Comments
 (0)