【基于 Swoole+Hyperf 的微服务实战】 第二周·周一: 注解与 AOP 切面编程

发布时间:2026/8/24 13:55:16
【基于 Swoole+Hyperf 的微服务实战】 第二周·周一: 注解与 AOP 切面编程 今天主题是 Hyperf 的核心利器注解与 AOP 切面编程。如果说第一周我们是在探索 Swoole 协程的底层原理和手工搭建服务那么从今天起你将体验到框架的强大魔法——只需一个注解就能自动为方法添加缓存、日志、事务等横切逻辑极大提升开发效率并保持代码整洁。今日目标彻底理解 Hyperf 注解的运作机制包括如何定义、如何被扫描和解析。理解面向切面编程AOP的概念连接点、切面、通知Advice类型。亲手编写一个Benchmark注解配合Around通知实现无侵入的方法耗时统计。将注解应用于控制器方法并通过真实请求验证 AOP 的生效。学会使用 Hyperf 的watcher组件实现代码热重启告别手动CtrlC。一、环境准备与热重启配置约 30 分钟我们继续基于上周末的hyperf-app项目进行。首先进入 Docker 容器并确保环境就绪。cdswoole-coursedocker-composeexecswoolebashcd/var/www/hyperf-app安装 Hyperf Watcher热重启在开发过程中每次修改代码都要手动重启服务很影响效率。Hyperf 官方提供了hyperf/watcher组件它会监听文件变更并自动重启服务。composerrequire hyperf/watcher--dev安装完毕后发布配置文件php bin/hyperf.php vendor:publish hyperf/watcher此时会在config/autoload/下生成watcher.php一般默认配置即可监听app,config等目录。以后我们可以直接使用以下命令启动带热更新的服务php bin/hyperf.php server:watch注意Watcher 会监控文件变化并重启 Worker 进程仅在开发环境使用。现在我们还是先手动操作以加深理解后文中会提示如何使用热重启。二、知识核心注解原理与 AOP 模型约 1.5 小时1. Hyperf 注解是如何工作的注解Annotation本质是类/方法/属性的元数据。在 PHP 8 中原生支持了 AttributesHyperf 同时兼容 Doctrine 传统注解和 PHP 8 Attributes。我们之后统一使用 Attributes。加载机制框架启动时Hyperf\Di组件会扫描所有的注解类通常在app/目录下。解析器AnnotationCollector将收集到的注解元数据哪个类的哪个方法用了什么注解存储起来。在依赖注入容器构建实例时AOP 代理生成器会介入如果检测到某个类的方法匹配了切面切入点就会生成一个代理子类通过继承 协程化的动态代理在调用时织入切面逻辑。因此你从容器中获取的控制器、Service 等实际上已经是代理对象而非原始对象。简单类比就好像你给某个方法贴上“监控”标签框架在运行时看到这个标签就自动在方法前后包了一层计时代码而你本身的业务代码毫不知情。2. AOP 核心概念切面Aspect横切逻辑的封装比如日志、事务、权限。在 Hyperf 中对应一个切面类Aspect后缀需要实现Hyperf\Di\Aop\AbstractAspect。连接点Joinpoint程序执行过程中的某个点例如方法调用。Hyperf 中切面的切入点主要针对方法调用。切入点Pointcut一组连接点的集合通过注解或者表达式定义。Hyperf 通过$classes、$annotations属性来筛选要拦截的类或方法。通知Advice切面在特定连接点执行的动作。Around环绕通知可以在方法执行前后、甚至跳过或替换原方法。功能最强。Before前置通知在方法执行前执行。After后置通知在方法正常返回后执行。AfterThrowing异常通知方法抛出异常后执行。我们今天的重点是Around因为它最常用可以完全控制执行流程。3. 我们即将实现的效果// 在控制器方法上添加一句注解#[Benchmark]publicfunctionindex(){// 业务逻辑}// 访问这个接口时控制台自动打印方法执行耗时: 0.0234 秒不需要修改业务代码不需要手动microtime()这就是 AOP 的魅力。三、实战构建方法耗时统计注解约 2.5 小时步骤 1创建自定义注解类Benchmark在app目录下新建Annotation文件夹然后创建Benchmark.php?phpdeclare(strict_types1);namespaceApp\Annotation;useAttribute;useHyperf\Di\Annotation\AbstractAnnotation;/** * 标记一个方法需要被统计执行耗时 * Annotation * Target({METHOD}) */#[Attribute(Attribute::TARGET_METHOD)]classBenchmarkextendsAbstractAnnotation{// 这里可以定义一些参数比如日志级别但我们简单化}解析#[Attribute]声明这是一个 PHP 8 注解TARGET_METHOD表示只能用于方法。继承AbstractAnnotation是为了被 Hyperf 的注解收集器识别并参与 AOP 切入点的匹配。步骤 2创建切面类BenchmarkAspect在app目录下新建Aspect文件夹创建BenchmarkAspect.php?phpdeclare(strict_types1);namespaceApp\Aspect;useApp\Annotation\Benchmark;useHyperf\Di\Annotation\Aspect;useHyperf\Di\Aop\AbstractAspect;useHyperf\Di\Aop\ProceedingJoinPoint;#[Aspect]classBenchmarkAspectextendsAbstractAspect{// 切入点所有带有 Benchmark 注解的方法publicarray$annotations[Benchmark::class,];/** * Around 通知 * param ProceedingJoinPoint $proceedingJoinPoint 连接点对象可以执行原方法 * return mixed */publicfunctionprocess(ProceedingJoinPoint$proceedingJoinPoint){// 1. 记录开始时间$startmicrotime(true);// 2. 获取被调用方法的名称和类名便于日志输出$className$proceedingJoinPoint-className;$methodName$proceedingJoinPoint-methodName;// 3. 执行原方法并获取返回值$result$proceedingJoinPoint-process();// 4. 计算耗时$endmicrotime(true);$costround(($end-$start)*1000,2);// 毫秒// 5. 输出日志或使用 Loggerecho[Benchmark]{$className}::{$methodName}() 执行耗时:{$cost}ms.PHP_EOL;// 6. 必须返回原方法的返回值否则调用方收不到数据return$result;}}关键点#[Aspect]注解标记该类为一个切面优先级可由priority属性控制默认为 0。$annotations数组定义了切入点所有被Benchmark注解标记的方法。核心方法process接收ProceedingJoinPoint参数它包含了被调用的类、方法、参数等信息。调用$proceedingJoinPoint-process()会执行原始方法并返回结果。我们必须返回原始结果否则接口将无响应。步骤 3应用注解到控制器打开app/Controller/IndexController.php或任意控制器在某个方法上加上#[Benchmark]?phpnamespaceApp\Controller;useApp\Annotation\Benchmark;useHyperf\HttpServer\Annotation\Controller;useHyperf\HttpServer\Annotation\RequestMapping;#[Controller]classIndexControllerextendsAbstractController{#[RequestMapping(path:/,methods:get)]#[Benchmark]publicfunctionindex(){$user$this-request-input(user,Hyperf);// 模拟一个耗时操作比如 sleep 一段时间\Swoole\Coroutine\System::sleep(0.5);// 500msreturn[messageHello{$user}.,];}// 不加 Benchmark 的方法#[RequestMapping(path:/health,methods:get)]publicfunctionhealth(){return[statusok];}}别忘了在文件顶部引入use App\Annotation\Benchmark;。步骤 4重启服务并验证重新启动 Hyperf用php bin/hyperf.php start或server:watch热重启php bin/hyperf.php start然后访问首页curlhttp://localhost:9501/你会看到控制台输出类似[Benchmark] App\Controller\IndexController::index() 执行耗时: 502.73 ms而访问/health则不会有任何额外输出证明 AOP 只拦截了标记的方法。实验你可以多打几个#[Benchmark]在不同的控制器方法上观察不同方法的耗时。步骤 5扩展 Before 和 After 通知可选为加深理解我们再创建一个带 Before 和 After 的切面示例用于权限检查。创建app/Annotation/AuthCheck.php?phpnamespaceApp\Annotation;useAttribute;useHyperf\Di\Annotation\AbstractAnnotation;#[Attribute(Attribute::TARGET_METHOD)]classAuthCheckextendsAbstractAnnotation{}创建app/Aspect/AuthCheckAspect.php?phpnamespaceApp\Aspect;useApp\Annotation\AuthCheck;useHyperf\Di\Annotation\Aspect;useHyperf\Di\Aop\AbstractAspect;useHyperf\Di\Aop\ProceedingJoinPoint;useHyperf\HttpServer\Contract\RequestInterface;#[Aspect]classAuthCheckAspectextendsAbstractAspect{publicarray$annotations[AuthCheck::class,];publicfunctionprocess(ProceedingJoinPoint$proceedingJoinPoint){// 获取 Request 对象可从容器中获取或者通过参数注入这里简化演示$request\Hyperf\Utils\ApplicationContext::getContainer()-get(RequestInterface::class);$token$request-header(Authorization,);// Before 逻辑校验 Tokenif($token!Bearer secret-token){// 不调用原方法直接返回 401return[code401,messageUnauthorized,];}// 放行执行原方法$result$proceedingJoinPoint-process();// After 逻辑可以在这里记录操作日志// 比如 $this-logger-info(User called ...);return$result;}}在某个接口上添加#[AuthCheck]#[RequestMapping(path:/secure,methods:get)]#[AuthCheck]publicfunctionsecure(){return[secretdata];}重启服务测试curlhttp://localhost:9501/secure# 返回401curl-HAuthorization: Bearer secret-tokenhttp://localhost:9501/secure# 成功通过这个例子你看到了 AOP 在权限验证中的应用完全解耦了业务逻辑和安全逻辑。四、成果测试与验证约 1 小时1. 基准测试清单检验项方法通过标准注解定义与扫描查看启动日志无报错无未识别的注解错误Benchmark 切面生效curl访问带注解的接口查看终端输出终端输出包含[Benchmark]日志及耗时无注解方法不受影响curl /health终端无 Benchmark 日志Around 通知返回值正确curl返回内容与预期一致返回{message:Hello Hyperf.}Auth 切面拦截无 Token 访问/secure返回 401带正确 Token 返回数据响应码及内容符合预期热重启可用修改注解后无需手动重启服务自动生效php bin/hyperf.php server:watch下修改文件刷新接口立即变化2. 并发压测观察 AOP 性能影响使用ab对首页进行 1000 请求并发测试ab-n1000-c100http://localhost:9501/检查终端每个请求应该都会打印一次 Benchmark 日志且 QPS 因模拟的 0.5 秒延时不会高但观察协程并发处理能力。AOP 的代理调用开销非常小微秒级不会成为瓶颈。3. 调试技巧如果发现注解没生效通常是以下原因没有在切面类上标记#[Aspect]或忘记在$annotations中添加注解类。注解类没有被AbstractAnnotation子类化导致扫描器忽略。重启服务时缓存未清理可以删除runtime/container后重启。确保控制器是通过容器获取的Hyperf 默认就是如果手动new则不会代理。五、今日作业与学习产出提交代码将Benchmark注解、BenchmarkAspect切面以及修改过的控制器提交到 Git。学习笔记画出 Hyperf AOP 的代理生成时序图注解扫描 → 收集器 → 代理类生成 → 容器注入代理 → 方法调用织入。实战拓展修改Benchmark注解增加一个$minCost参数只有执行时间超过该值如 100ms时才输出日志。编写一个Cache注解配合 Around 通知实现简单的方法结果缓存存到 Redis 或本地数组体验 AOP 的强大。思考题如果在一个方法上同时应用了Benchmark和AuthCheck它们的执行顺序是怎样的如何控制多个切面的优先级提示通过切面类的priority属性通过今天的学习你不仅掌握了注解和 AOP 的使用更理解了 Hyperf 框架如何在不侵入代码的情况下增强功能。这种思想将贯穿整个微服务开发——中间件、限流、熔断、事务等全部基于此。明天我们将继续深入用中间件和验证器加固我们的 API 服务。