【基于 Swoole+Hyperf 的微服务实战】 第二周·周一: 注解与 AOP 切面编程
2026/8/24 13:54:39 网站建设 项目流程

今天主题是 Hyperf 的核心利器:注解与 AOP 切面编程。如果说第一周我们是在探索 Swoole 协程的底层原理和手工搭建服务,那么从今天起,你将体验到框架的强大魔法——只需一个注解,就能自动为方法添加缓存、日志、事务等横切逻辑,极大提升开发效率并保持代码整洁。


今日目标

  1. 彻底理解 Hyperf 注解的运作机制,包括如何定义、如何被扫描和解析。
  2. 理解面向切面编程(AOP)的概念:连接点、切面、通知(Advice)类型。
  3. 亲手编写一个@Benchmark注解,配合Around通知,实现无侵入的方法耗时统计。
  4. 将注解应用于控制器方法,并通过真实请求验证 AOP 的生效。
  5. 学会使用 Hyperf 的watcher组件实现代码热重启,告别手动Ctrl+C

一、环境准备与热重启配置(约 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 中,原生支持了 Attributes,Hyperf 同时兼容 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_types=1);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_types=1);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. 记录开始时间$start=microtime(true);// 2. 获取被调用方法的名称和类名(便于日志输出)$className=$proceedingJoinPoint->className;$methodName=$proceedingJoinPoint->methodName;// 3. 执行原方法,并获取返回值$result=$proceedingJoinPoint->process();// 4. 计算耗时$end=microtime(true);$cost=round(($end-$start)*1000,2);// 毫秒// 5. 输出日志(或使用 Logger)echo"[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['message'=>"Hello{$user}.",];}// 不加 Benchmark 的方法#[RequestMapping(path:'/health',methods:'get')]publicfunctionhealth(){return['status'=>'ok'];}}

别忘了在文件顶部引入use App\Annotation\Benchmark;

步骤 4:重启服务并验证

重新启动 Hyperf(用php bin/hyperf.php startserver: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['code'=>401,'message'=>'Unauthorized',];}// 放行,执行原方法$result=$proceedingJoinPoint->process();// After 逻辑:可以在这里记录操作日志// 比如 $this->logger->info('User called ...');return$result;}}

在某个接口上添加#[AuthCheck]

#[RequestMapping(path:'/secure',methods:'get')]#[AuthCheck]publicfunctionsecure(){return['secret'=>'data'];}

重启服务,测试:

curlhttp://localhost:9501/secure# 返回401curl-H"Authorization: Bearer secret-token"http://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则不会代理。

五、今日作业与学习产出

  1. 提交代码:将Benchmark注解、BenchmarkAspect切面以及修改过的控制器提交到 Git。
  2. 学习笔记:画出 Hyperf AOP 的代理生成时序图:注解扫描 → 收集器 → 代理类生成 → 容器注入代理 → 方法调用织入。
  3. 实战拓展
    • 修改Benchmark注解,增加一个$minCost参数,只有执行时间超过该值(如 100ms)时才输出日志。
    • 编写一个@Cache注解,配合 Around 通知实现简单的方法结果缓存(存到 Redis 或本地数组),体验 AOP 的强大。
  4. 思考题:如果在一个方法上同时应用了@Benchmark@AuthCheck,它们的执行顺序是怎样的?如何控制多个切面的优先级?(提示:通过切面类的priority属性)

通过今天的学习,你不仅掌握了注解和 AOP 的使用,更理解了 Hyperf 框架如何在不侵入代码的情况下增强功能。这种思想将贯穿整个微服务开发——中间件、限流、熔断、事务等全部基于此。明天我们将继续深入,用中间件和验证器加固我们的 API 服务。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询