后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载Midway 框架内置了一套与场景无关的通用方法拦截器AOP / Aspect机制用于在 Controller、Service、自定义类等任意 Class 方法上统一编写前置校验、后置处理、异常捕获、返回值改写等横切逻辑。本文基于 Midway v3.0.0 官方文档并结合仓库源码完整讲解 Aspect 装饰器的用法、五个切面方法before/around/afterReturn/afterThrow/after的生命周期与执行顺序读完即可在自己的 Midway 项目中落地无侵入的拦截器。为什么需要拦截器AOP在实际业务中我们经常需要一段全局统一处理逻辑例如统一处理错误、统一转换返回格式、统一记录日志等。虽然 Web 场景下可以借助 Web 中间件Middleware完成部分工作但中间件能力无法在其他场景如非 HTTP 的定时任务、消息队列消费者等中复用。为此Midway 设计了一套通用的方法拦截器Aspect即切面让同样的横切逻辑可以在不同场景下统一编写。拦截器与传统的 Web 中间件、装饰器Decorator不同它是 Midway 框架自身提供的能力在整体执行序列中处于中间位置并且可以拦截任意 Class 的方法——包括 Controller 方法、Service 方法等。这意味着我们不需要侵入业务代码不需要在业务文件里加装饰器也不需要在主流程前后加入可见代码就能为方法注入额外的处理逻辑。需要强调的是拦截器能力非常强大必须谨慎、正确地使用否则容易造成难以排查的副作用。同时拦截器固定为单例Singleton实例。:::caution 在存在继承的情况下拦截器不会对父类的方法生效原因详见下文源码分析。 :::快速上手编写第一个拦截器拦截器通常放置在项目的src/aspect目录下。以一个拦截 Controller 方法的例子开始首先创建src/controller/home.ts➜ my_midway_app tree . ├── src │ │── aspect ## 拦截器目录 │ │ └── report.ts │ └── controller ## Web Controller 目录 │ └── home.ts ├── test ├── package.json └── tsconfig.json// src/controller/home.ts import { Controller, Get } from midwayjs/core; Controller(/) export class HomeController { Get(/) async home() { return Hello Midwayjs!; } }接着创建src/aspect/report.ts编写一个拦截HomeController的切面// src/aspect/report.ts import { Aspect, IMethodAspect, JoinPoint } from midwayjs/core; import { HomeController } from ../controller/home; Aspect(HomeController) export class ReportInfo implements IMethodAspect { async before(point: JoinPoint) { console.log(before home router run); } }项目启动后访问首页路由时控制台便会输出before home router run。可以看到我们既没有 hack 进 Controller 的代码也没有在业务文件中添加任何装饰器更没有在主流程前后加入可见代码——所有增强逻辑都被隔离在src/aspect目录的独立类中实现了真正的关注点分离。Aspect 装饰器源码解析从源码 packages/core/src/decorator/common/aspect.ts 可以看到Aspect接受三个参数export function Aspect( aspectTarget: any | any[], match?: string | (() boolean), priority?: number ) { return function (target) { DecoratorManager.saveModule(ASPECT_KEY, target); const aspectTargets [].concat(aspectTarget); for (const aspectTarget of aspectTargets) { MetadataManager.attachMetadata( ASPECT_KEY, { aspectTarget, match, priority }, target ); } Scope(ScopeEnum.Singleton)(target); Provide()(target); }; }第一个参数aspectTarget要拦截的目标类可以是单个类也可以是类数组下文详述第二个参数match方法匹配规则字符串通配符或函数用于精确匹配目标类中的特定方法第三个参数priority拦截器优先级默认 0数字越大优先级越高。同时装饰器内部会自动为切面类附加Scope(ScopeEnum.Singleton)与Provide()这正是拦截器固定为单例实例的底层保证——每个切面类在整个应用生命周期内只被实例化一次。切面生命周期五个拦截方法方法拦截器可以拦截整个方法调用过程拦截方式包含五个可选的切面方法。仓库中 packages/core/src/interface.ts 对IMethodAspect接口的定义如下export interface IMethodAspect { after?(joinPoint: JoinPoint, result: any, error: Error); afterReturn?(joinPoint: JoinPoint, result: any): any; afterThrow?(joinPoint: JoinPoint, error: Error): void; before?(joinPoint: JoinPoint): void; around?(joinPoint: JoinPoint): any; }方法说明before方法调用前执行around包裹方法的执行前后afterReturn内容正确返回时执行afterThrow抛出异常时执行after最终执行无论正确还是错误简单理解这五个方法与try/catch/finally的对应关系如下try { // before // around 或 invokeMethod原方法调用 // afterReturn } catch(err) { // afterThrow } finally { // after }各方法的能力对比如下修改输入参数调用原方法获取返回值修改返回值获取错误拦截并抛出错误before√√around√√√√√√afterReturn√√afterThrow√√after√√源码中的执行顺序印证拦截器的执行编排位于 packages/core/src/service/aspectService.ts 的interceptPrototypeMethod方法中其会对被拦截方法的原型方法进行整体替换包装并根据原方法是异步还是同步走两条几乎相同的分支同步分支省略awaittry { await aspectObject.before?.(joinPoint); if (aspectObject.around) { result await aspectObject.around(joinPoint); } else { result await originMethod.call(this, ...joinPoint.args); } joinPoint.proceed undefined; const resultTemp await aspectObject.afterReturn?.(joinPoint, result); result typeof resultTemp undefined ? result : resultTemp; return result; } catch (err) { joinPoint.proceed undefined; error err; if (aspectObject.afterThrow) { await aspectObject.afterThrow(joinPoint, error); } else { throw err; } } finally { await aspectObject.after?.(joinPoint, result, error); }从中可以提炼出几个关键实现事实执行顺序固定before→around或原方法→afterReturn异常时走afterThrow最终总会执行after与文档中的伪代码完全一致proceed仅存在于 before/around 阶段在调用afterReturn之前joinPoint.proceed已被置为undefined防止误用afterThrow的兜底行为如果切面定义了afterThrow抛出的错误将被其接住除非在其中再次抛出如果未定义afterThrow错误会原样向上抛出返回值覆盖规则afterReturn若返回undefined则保留原结果否则以返回值覆盖原结果。JoinPoint可修改的方法调用点JoinPoint是拦截器中传递的方法调用点对象其定义同样位于 packages/core/src/interface.tsexport interface JoinPoint { methodName: string; target: any; args: any[]; proceed?(...args: any[]): any; proceedIsAsyncFunction?: boolean; }参数说明methodName被拦截的方法名target方法被调用时的实例args原始方法调用的参数proceed原方法本身仅存在于 before 和 around 中补充相比文档中的接口定义当前仓库源码的JoinPoint额外提供了proceedIsAsyncFunction字段用于标记被拦截原方法是否为异步函数proceed也被标记为可选属性因为如前文所述它只会在 before/around 阶段存在。before修改输入参数与参数校验我们经常需要修改输入参数、或在before阶段做参数校验使其符合程序执行的逻辑。例如// src/controller/home.ts Controller(/) export class HomeController { Get(/) async home(data1, data2) { return data1 data2; // 因为方法被拦截这里的返回值是 3 } } // src/aspect/report.ts Aspect(HomeController, home) // 只拦截 home 方法 export class ReportInfo implements IMethodAspect { async before(point: JoinPoint) { console.log(point.args); // 拦截的是 Controller 方法原始参数是 [ctx, next] point.args [1, 2]; // 修改参数 } }注意通过直接改写joinPoint.args数组即可完成参数替换源码中originMethod.call(this, ...joinPoint.args)正是读取修改后的args来调用原方法。在仓库测试 fixture packages/core/test/fixtures/base-app-aspect/src/aspect/a.ts 中同样通过point.args [ddd, cccc]演示了该用法。around完整包裹方法调用around是一个全能方法可以完整包裹整个方法调用过程——既能拿到参数、调用原方法也能读取/修改返回值、捕获/重新抛出错误// src/controller/home.ts Controller(/) export class HomeController { Get(/) async home() { return hello; } } // src/aspect/report.ts Aspect(HomeController, home) // 只拦截 home 方法 export class ReportInfo implements IMethodAspect { async around(point: JoinPoint) { const result await point.proceed(...point.args); // 执行原方法 return result world; } }最终 Controller 将返回hello world。fixture packages/core/test/fixtures/base-app-aspect/src/aspect/b.ts 中给出了around的完整写法proceed前后分别打日志。afterReturn直接修改返回值afterReturn会多一个返回结果参数。如果只需要修改返回值可以直接使用它——上面的around例子用afterReturn改写更简单// src/controller/home.ts Controller(/) export class HomeController { Get(/) async home() { return hello; } } // src/aspect/report.ts Aspect(HomeController, home) // 只拦截 home 方法 export class ReportInfo implements IMethodAspect { async afterReturn(point: JoinPoint, result) { return result world; } }afterThrow拦截异常afterThrow用于拦截错误。它拿到error参数后可以自行判断并处理// src/controller/home.ts Controller(/) export class HomeController { Get(/) async home() { throw new Error(custom error); } } // src/aspect/report.ts Aspect(HomeController, home) export class ReportInfo implements IMethodAspect { async afterThrow(point: JoinPoint, error) { if(/not found/.test(error.message)) { throw new Error(another error); } else { console.error(got custom error); } } }afterThrow可以拦截错误相应地它在过程中不能返回结果一般用于记录错误日志或者如上面的例子在满足条件时抛出新的错误以转换错误类型。仓库测试 packages/core/test/fixtures/base-app-aspect/src/aspect/c.ts 即演示了错误信息为bbb时抛出ccc的转换逻辑对应的测试断言见下文。after最终处理after用于执行最终处理无论方法成功还是失败都会执行非常适合统计成功/失败次数、清理资源等收尾任务// src/controller/home.ts Controller(/) export class HomeController { Get(/) async home() { throw new Error(custom error); } } // src/aspect/report.ts Aspect(HomeController, home) export class ReportInfo implements IMethodAspect { async after(point: JoinPoint, result, error) { if(error) { console.error(error); } else { console.log(result); } } }异步问题如果被拦截的方法是异步的那么原则上before等所有切面方法也都应该是异步的反之如果被拦截方法是同步的切面方法也应该是同步的。源码中正是通过Types.isAsyncFunction判断被拦截方法分别生成带await的异步包装与不带await的同步包装// src/controller/home.ts Controller(/) export class HomeController { Get(/) async home() { // 这里是异步的那么下面的 before 也应是异步的 } } // src/aspect/report.ts Aspect(HomeController, home) export class ReportInfo implements IMethodAspect { async before(point: JoinPoint) { } }// src/controller/home.ts Controller(/) export class HomeController { Get(/) home() { // 这里是同步的那么下面的 before 也应是同步的 } } // src/aspect/report.ts Aspect(HomeController, home) export class ReportInfo implements IMethodAspect { before(point: JoinPoint) { } }应用到多个类Aspect装饰器的第一个参数可以传入数组同时指定多个目标类这些类的所有方法都会被拦截Aspect([HomeController, APIController]) export class ReportInfo implements IMethodAspect { async before(point: JoinPoint) { } }源码层面Aspect装饰器内部通过[].concat(aspectTarget)将参数归一为数组并对每个目标类分别写入元数据见 packages/core/src/decorator/common/aspect.ts。特定方法匹配picomatch 通配符多数场景下我们只需要拦截某个类中的特定方法。Aspect的第二个参数是一个通配符字符串其匹配规则使用 picomatch一个轻量级 glob 匹配库。仓库中 packages/core/src/service/aspectService.ts 通过pm(aspectData.match)将匹配字符串编译为匹配函数再逐一对目标类原型上的方法名进行匹配。假设我们的方法如下// src/controller/home.ts import { Controller, Get } from midwayjs/core; Controller(/) export class HomeController { Get(/1) async hello1() { return Hello Midwayjs!; } Get(/2) async hello2() { return Hello Midwayjs, too!; } }那么配置以下规则时只有hello2方法会被匹配到Aspect([HomeController], *2) export class ReportInfo implements IMethodAspect { async before(point: JoinPoint) { console.log(hello method with suffix 2); } }除了字符串通配符match参数也可以传返回布尔值的函数见AspectMetadata接口定义中的match?: string | (() boolean)实现更灵活的动态匹配。执行顺序与优先级洋葱模型当多个拦截器同时作用于同一个方法时可能会出现顺序混乱的问题。如果多个拦截器分散在两个文件里它们的默认顺序是随机的因此需要显式指定优先级。Aspect的第三个参数用于指定拦截器优先级默认值为0。数字越大优先级越高意味着该拦截器越先被注册而先注册的拦截器其方法越晚被调用——整体呈洋葱模型。整个拦截过程分为两个阶段先注册、后执行。注册过程优先级高的先注册被包在更内层MyAspect2 (priority1) 先注册 → 内层 MyAspect1 (priority0) 后注册 → 外层包裹 MyAspect2执行过程外层 before 先执行洋葱模型MyAspect1.before → MyAspect2.before → 原方法 → MyAspect2.after → MyAspect1.after对应代码如下Aspect([HomeController]) export class MyAspect1 implements IMethodAspect { before(point: JoinPoint) { console.log(111); } } Aspect([HomeController], *, 1) // 在这里设置优先级 export class MyAspect2 implements IMethodAspect { before(point: JoinPoint) { console.log(222); } }执行输出为111 222这一行为在源码中有明确对应MidwayAspectService.loadAspect()会先收集所有注册了ASPECT_KEY的切面模块再按(next.priority || 0) - (pre.priority || 0)降序排序然后依次调用addAspect。而interceptPrototypeMethod每次都会用新的包装函数整体替换目标类的原型方法因此后注册的切面包装在前先注册的切面被包在内层——最终形成上述洋葱模型详见 packages/core/src/service/aspectService.ts。一些限制1. 拦截器对父类方法不生效由于addAspect中通过Object.getOwnPropertyNames(module.prototype)只枚举目标类自身定义的原型方法getOwnPropertyNames不包含继承来的属性因此父类中声明的方法不会被拦截同时若方法描述符不可写descriptor.writable false也会被跳过。结合前面的全部内容可以总结出拦截器的核心使用守则明确要拦截的目标类与方法范围必要时用通配符收窄、尽量只使用最小能力的切面方法如仅改返回值用 afterReturn、明确声明优先级、保持切面方法与目标方法异步/同步一致。源码与测试印证为了进一步验证上述机制仓库中提供了完整的测试 fixture 与断言fixture 目录 packages/core/test/fixtures/base-app-aspect/src/aspect/ 中的a.ts、b.ts、c.ts分别演示了参数修改、*2方法匹配 优先级、afterThrow错误转换目标类 packages/core/test/fixtures/base-app-aspect/src/home.ts 包含继承父类的Home用于验证父类方法不受拦截以及抛错的UserController测试用例 packages/core/test/baseFramework.test.ts 中的should test aspect decorator断言home.hello()返回hello worlddddccccfff——证明before中修改的args真实生效home.hello2()返回hello worldcccppp——证明*2匹配与优先级排序正确userController.getUser()抛出的bbb错误被afterThrow转换为ccc链式调用test1().test2().getUser1()返回before test user验证切面在链式调用中同样生效单元测试 packages/core/test/service/aspectService.test.ts 则从容器服务层验证了MidwayAspectService对普通方法与异步方法的拦截均正常。通过本文的讲解你可以基于Aspect装饰器在 Midway 项目中构建统一的日志记录、参数校验、鉴权、缓存、错误归一化等横切能力并结合源码理解其底层实现做到安全、正确地使用这一强大的 AOP 机制。赞分享后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载相关推荐中文多模态预训练模型全景指南从 WenLan 到 QA-CLIP 的 13 个模型全解析awesome-pretrained-chinese-nlp-models 仓库中文多模态预训练模型全景指南从 WenLan 到 QA CLIP 的 13 个模型全解析awesome pretrained chinese nlp mod后端微服务云原生Midway 拦截器AOP实战指南基于 Aspect 的方法级切面编程Midway 拦截器AOP实战指南基于 Aspect 的方法级切面编程 Midway 框架内置了一套通用方法拦截器AOP/切面能力用于在不同场景下后端微服务云原生Hyperf框架中Aspect切面执行顺序详解Hyperf框架中Aspect切面执行顺序详解 引言为什么需要关注切面执行顺序 在Hyperf框架的AOPAspect Oriented Programm后端Web框架微服务RPC框架异步编程上一篇从零搭建安全技术博客用hve-notes构建专业量子通信内容平台下一篇PD Stepper核心功能解析TMC2209静音驱动与ESP32-S3无线控制的完美结合创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考