Skip to content

面向切面

基本概念

awe-axios还实现了面向切面编程(AOP)的功能,通过@Before@After等装饰器,可以对请求前、请求后、请求错误等阶段进行拦截,并对请求进行处理。

切面类

使用@Aspect可以定义切面类。切面类中的方法可以在目标方法的不同执行阶段产生影响。定义切面类方式如下:

typescript
// 定义切面类
@Aspect()
class Logger {
  @Before('getUser*')
  log(ctx: AspectContext) {
    console.log('before getUser*');
  }
}

切点表达式

aop的核心在于明确在什么方法、方法什么执行阶段进行切入。这个方法就是切入点(或者说叫做切入位置),切入点需要使用切入点表达式来表示。

切入点表达式其实就是就是指定切入位置的字符串,通过字符串的方式指定切入位置。其语法为:[模块名].[类名].(方法名),并且这些字符串都支持用*作为通配符表示任意字符。比如:

  1. getUser*:表示所有以getUser开头的方法
  2. UserApi.getUser*:表示UserApi类中所有以getUser开头的方法
  3. UserApi.getUserById:表示UserApi类中getUserById方法
  4. UserApi.*:表示UserApi类中所有的方法
  5. user.UserApi.getUserById:表示user模块中UserApi类中getUserById方法
  6. *:表示所有的方法

缓存优化

awe-axios对使用过的切点表达式进行函数记忆缓存,避免重复执行,提高性能。

切入时机

切入时机指的是在什么阶段进行切入,比如:

@Before

@Before装饰器用于在方法调用前进行拦截

ts
@Aspect(1)
class Logger {
  @Before('getUser*')
  log(ctx: AspectContext) {
    // 调用方法前打印before getUser*
    console.log('before getUser*');
  }
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);

@After

@After装饰器用于在方法调用后进行拦截

ts
@Aspect(1)
class Logger {
  @After('getUser*')
  logAfter(ctx: AspectContext) {
    console.log('after getUser*');
  }
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);

@Around

@Around装饰器用于在方法调用前后进行拦截

ts
@Aspect(1)
class Logger {
  @Around('getUser*')
  logAround(ctx: AspectContext, adviceChain: AdviceChain) {
    console.log('around before getUser*');
    const result = adviceChain.proceed(ctx);
    console.log('arount after getUser*');
    return result;
  }
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);

注意

  1. @Around装饰器必须要有返回值,否则会报错。
  2. 你必须要调用adviceChain.proceed(ctx)方法来手动推进执行链的执行,否则不会执行目标方法。

@AfterReturning

@AfterReturning装饰器用于在方法调用成功后进行拦截,它可以获取到方法的返回值,并进行处理。

ts
@Aspect(1)
class Logger {
  @AfterReturning('getUser*')
  logAfterReturning(ctx: AspectContext, result: any) {
    console.log(result);
    console.log('afterReturning getUser*');
  }
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);

@AfterThrowing

@AfterThrowing装饰器用于在方法调用失败后进行拦截,它可以获取到方法的错误信息,并进行处理。

ts
@Aspect(1)
class Logger {
  @AfterThrowing('getUser*')
  logAfterThrowing(ctx: AspectContext, error: any) {
    console.log('出错了');
    console.log('afterThrowing getUser*');
  }
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);

切点上下对象

切点上下对象(AspectContext)是awe-axios中用于保存切点信息的对象,也就是在上面的方法中第一个参数ctx。它包含了如下信息:

ts
export class AspectContext {
  /**
   * 原方法
   */
  method: Function;
  /**
   * 原方法this
   */
  target: any;
  /**
   * 原方法参数
   */
  args: any[];

  /**
   * axios配置
   */
  axiosConfig?: HttpRequestConfig;
}

所以,你可以通过ctx.methodctx.targetctx.args获取到原方法、原方法 this、原方法参以及axios配置数等。所以你可以利用这些信息更加精确的实现拦截功能。

可复用的切入表达式

某些切入点表达式你可能会经常用到,所以awe-axios支持提供可复用的切入表达式。可复用的切入点表达式是一个函数,只需要返回切点表达式即可,如下所示:

ts
function reusableExp() {
  return 'getUser*';
}
@Aspect(1)
class Logger {
  @Before(reusableExp)
  log(ctx: AspectContext) {
    console.log('before getUser*');
  }
  @After(reusableExp)
  logAfter(ctx: AspectContext) {
    console.log('after getUser*');
    console.log(ctx.axiosConfig);
  }
}
@Component()
@HttpApi('http://localhost:3000/api/users')
class UserApi {
  @Post({
    url: '/pages',
    headers: {
      'Content-Type': 'application/json',
    },
    mock: async ({ request }) => {
      const data = await request.json();
      const { page, size } = data as { page: number; size: number };
      return HttpResponse.json({
        message: 'ok',
        data: { id: 1, name: '张三' },
      });
    },
  })
  getUserPages(@BodyParam() data: { page: number; size: number }): any {}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages({ page: 1, size: 10 })();
console.log(data);

执行顺序

切入时机执行顺序

我们可能会很疑惑,这些切入时机的执行顺序,我们先来看看这个案例:

ts
@Component()
@HttpApi('http://localhost:3000/api/users')
class UserApi {
  @Get({
    url: '/pages',
    mock: () => {
      return HttpResponse.json({
        data: 'hello world',
      });
    },
  })
  getUserPages(): any {}
  getUsers(): any {}
}
@Aspect(1)
class Logger {
  @Before('getUser*')
  log(ctx: AspectContext) {
    console.log('before getUser*');
  }
  @After('getUser*')
  logAfter(ctx: AspectContext) {
    console.log('after getUser*');
  }
  @Around('getUser*')
  logAround(ctx: AspectContext, adviceChain: AdviceChain) {
    console.log('around before getUser*');
    const result = adviceChain.proceed(ctx);
    console.log('arount after getUser*');
    return result;
  }
  @AfterReturning('getUser*')
  logAfterReturning(ctx: AspectContext, result: any) {
    console.log('result');
    console.log('afterReturning getUser*');
  }
  @AfterThrowing('getUser*')
  logAfterThrowing(ctx: AspectContext, error: any) {
    console.log('afterThrowing getUser*');
  }
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);

这个案例执行结果为:

json
around before getUser*
before getUser*
after getUser*
[AsyncFunction (anonymous)]
afterReturning getUser*
arount after getUser*

所以切入时机的执行顺序为:环绕前置通知->前置通知->后置通知->目标函数执行->返回通知->异常通知->环绕后置通知

切面类的执行顺序

当有多个切面类时awe-axios支持为切面类添加优先级序号,其默认值为 5,值越小优先级越高则越先执行。如果优先级相同则顺序是随机的,这个我们不做讨论。如果优先级不同但是有多个切面类,那么执行顺序又是怎么样呢?我们来看下下面的代码:

ts
function reusableExp() {
  return 'getUser*';
}
@Aspect(1)
class Logger {
  @Before(reusableExp)
  log(ctx: AspectContext) {
    console.log('before getUser*');
  }
  @After(reusableExp)
  logAfter(ctx: AspectContext) {
    console.log('after getUser*');
  }
  @Around('getUser*')
  logAround(ctx: AspectContext, adviceChain: AdviceChain) {
    console.log('around before getUser*');
    const result = adviceChain.proceed(ctx);
    console.log('arount after getUser*');
    return result;
  }
  @AfterReturning('getUser*')
  logAfterReturning(ctx: AspectContext, result: any) {
    console.log('result');
    console.log('afterReturning getUser*');
  }
  @AfterThrowing('getUser*')
  logAfterThrowing(ctx: AspectContext, error: any) {
    console.log('afterThrowing getUser*');
  }
}
@Aspect(2)
class Logger2 {
  @Before(reusableExp)
  log(ctx: AspectContext) {
    console.log('2before getUser*');
  }
  @After(reusableExp)
  logAfter(ctx: AspectContext) {
    console.log('2after getUser*');
  }
  @Around('getUser*')
  logAround(ctx: AspectContext, adviceChain: AdviceChain) {
    console.log('2around before getUser*');
    const result = adviceChain.proceed(ctx);
    console.log('2arount after getUser*');
    return result;
  }
  @AfterReturning('getUser*')
  logAfterReturning(ctx: AspectContext, result: any) {
    console.log('result');
    console.log('2afterReturning getUser*');
  }
  @AfterThrowing('getUser*')
  logAfterThrowing(ctx: AspectContext, error: any) {
    console.log('2afterThrowing getUser*');
  }
}
@Component()
@HttpApi('http://localhost:3000/api/users')
class UserApi {
  @Post({
    url: '/pages',
    headers: {
      'Content-Type': 'application/json',
    },
    mock: async ({ request }) => {
      const data = await request.json();
      const { page, size } = data as { page: number; size: number };
      return HttpResponse.json({
        message: 'ok',
        data: { id: 1, name: '张三' },
      });
    },
  })
  getUserPages(@BodyParam() data: { page: number; size: number }): any {}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages({ page: 1, size: 10 })();
console.log(data);

所以当有多个切面类时,执行顺序为:

ts
around before getUser*
2around before getUser*
before getUser*
2before getUser*
after getUser*
2after getUser*
result
afterReturning getUser*
result
2afterReturning getUser*
2arount after getUser*
arount after getUser*