[eggjs/egg][RFC] tegg 增加 aop 支持

2025-11-04 464 views
3
背景

在业务开发中,经常需要处理一些通用的逻辑,比如说:

在一个方法执行时,打印是否处理成功、耗时的日志 在一个方法执行时,保证其是事物的 在某些类型的方法执行时,增加通用的处理 思路 Advice

通过 Advice 注解来申明一个实现,可以在 Pointcut 中使用,或者对其添加 Crosscut 来实现通用的 Advice

注解定义
interface AdviceContext<T=object> {
  that: T;
  method: PropertyKey;
  args: ...any[];
}

interface IAdvice<T=object> {
  // 在函数调用前执行
  beforeCall?(ctx: AdviceContext<T>): Promise<void>;
  // 在函数执行成功之后执行
  afterReturn?(ctx: AdviceContext<T>, result: any): Promise<void>;
  // 在函数抛错之后执行
  afterThrow?(ctx: AdviceContext<T>, error: Error): Promise<void>;
  // 无论函数成功还是失败都会执行
  afterFinally?(ctx: AdviceContext<T>): Promise<void>;
  // 拦截函数执行
  around?(ctx: AdviceContext<T>, block: () => Promise<any>): Promise<any>;
}

function Advice() {
  return function(advice: IAdvice) {
  }
}
使用方式
@Advice()
class LogAdvice implement IAdvice {
  beforeCall(ctx: AdviceContext) {
    console.log('before call');
  }
}
执行顺序 beforeCall around(block start) function call around(block end) afterReturn afterThrow afterFinally Pointcut

在方法上加入 Pointcut 注解来添加 Advice。可以通过添加多个 Pointcut 来实现添加多个 Advice

注解定义

interface PointcutOptions { // 默认值 1000 order?: number; }

function Pointcut(advice: IAdvice, options?: PointcutOptions) {
}
使用方式
@ContextProto()
class Foo {
  @Prointcut(LogAdvice)
  async hello() {
    console.log('hello');
  }
}
Crosscut

在业务开发中,经常会遇到通用的拦截需求,比如说日志或者异常处理。这种情况下使用 Pointcut 的方式就不够了,手动添加很容易遗漏。

注解定义
interface CrosscutOptions {
 // 默认值 100
 order?: number;
} 

enum CrosscutType {
  // 通过基类来拦截
  CLASS = 'CLASS',
  // 通过类名 + 方法名正则的方式来拦截
  NAME = 'NAME';
  // 通过自定义函数的方式来拦截
  // 函数必须是静态定义的,动态定义de
  CUSTOM = 'CUSTOM';
}

interface CrosscutParam {
  type: CrosscutType;
}

interface ClassCrosscutParam extends CrosscutParam {
  type: CrosscutType.CLASS;
  clazz: new (...args: any[]) => object;
}

interface NameCrosscutParam extends CrosscutParam {
  type: CrosscutType.NAME;
  className: RegExp;
  methodName: RegExp;
}

interface CustomCrossCutParam extends CrosscutParam {
  type: CrosscutType.CUSTOM;
  callback: (clazz: new (...args: any[]) => object, method: PropertyKey): boolean;
}

function (param: CrosscutParam, options?: CrosscutOptions) {
  return function (aspect: IAspect) {}
}
使用方式
@CrossCut({
  type: CrosscutType.NAME;
  className: /.*Proxy/,
  method: /.*/,
})
@Aspect()
class ProxyCallLog {
  beforeCall(ctx: AdviceContext) {
    console.log('before call');
  }
}

回答

9

多个 Aspect 的写法是啥样的。。

9

afterReturn 可能需要修改返回值,比如判断了 resp.success 后返回 resp.data,外面不再需要解构一下

3

afterReturn 可能需要修改返回值,比如判断了 resp.success 后返回 resp.data,外面不再需要解构一下

我想只有 around 可以对函数的返回值进行修改。其他的都不行。

8

多个 Aspect 的写法是啥样的。。

@ContextProto()
class Foo {
  @Prointcut(LogAdvice)
  @Prointcut(Transaction)
  async hello() {
    console.log('hello');
  }
}
9

afterReturn 可能需要修改返回值,比如判断了 resp.success 后返回 resp.data,外面不再需要解构一下

理解错意思了。这个修改函数返回类型肯定不行,ts 类型检查都过不了。

6

Prointcut 是否增加一个工厂(validator 可以基于这个实现),比如

const LogProintcut = ProintcutFactory.create(LogAdvice)

@ContextProto()
class Foo {
  @LogProintcut()
  async hello() {
    console.log('hello');
  }
}
6

Prointcut 是否增加一个工厂(validator 可以基于这个实现),比如

const LogProintcut = ProintcutFactory.create(LogAdvice)

@ContextProto()
class Foo {
  @LogProintcut()
  async hello() {
    console.log('hello');
  }
}

感觉可以,直接上手写一个吧。

2

https://github.com/eggjs/tegg/pull/28

0

没有aspectj好用。。

9

aspectj

展开说说。可以看看有什么优化的点?