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

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


在这里插入图片描述

今日目标

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

一、环境准备与热重启配置(约 30 分钟)

我们继续基于上周末的 hyperf-app 项目进行。首先进入 Docker 容器并确保环境就绪。

cd swoole-course
docker-compose exec swoole bash
cd /var/www/hyperf-app
安装 Hyperf Watcher(热重启)

在开发过程中,每次修改代码都要手动重启服务很影响效率。Hyperf 官方提供了 hyperf/watcher 组件,它会监听文件变更并自动重启服务。

composer require 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]
public function index() {
    // 业务逻辑
}
// 访问这个接口时,控制台自动打印:方法执行耗时: 0.0234 秒

不需要修改业务代码,不需要手动 microtime(),这就是 AOP 的魅力。


三、实战:构建方法耗时统计注解(约 2.5 小时)

步骤 1:创建自定义注解类 Benchmark

app 目录下新建 Annotation 文件夹,然后创建 Benchmark.php

<?php
declare(strict_types=1);
namespace App\Annotation;

use Attribute;
use Hyperf\Di\Annotation\AbstractAnnotation;

/**
 * 标记一个方法需要被统计执行耗时
 * @Annotation
 * @Target({"METHOD"})
 */
#[Attribute(Attribute::TARGET_METHOD)]
class Benchmark extends AbstractAnnotation
{
    // 这里可以定义一些参数,比如日志级别,但我们简单化
}

解析

  • #[Attribute] 声明这是一个 PHP 8 注解,TARGET_METHOD 表示只能用于方法。
  • 继承 AbstractAnnotation 是为了被 Hyperf 的注解收集器识别,并参与 AOP 切入点的匹配。
步骤 2:创建切面类 BenchmarkAspect

app 目录下新建 Aspect 文件夹,创建 BenchmarkAspect.php

<?php
declare(strict_types=1);
namespace App\Aspect;

use App\Annotation\Benchmark;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;

#[Aspect]
class BenchmarkAspect extends AbstractAspect
{
    // 切入点:所有带有 Benchmark 注解的方法
    public array $annotations = [
        Benchmark::class,
    ];

    /**
     * Around 通知
     * @param ProceedingJoinPoint $proceedingJoinPoint 连接点对象,可以执行原方法
     * @return mixed
     */
    public function process(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]

<?php
namespace App\Controller;

use App\Annotation\Benchmark;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;

#[Controller]
class IndexController extends AbstractController
{
    #[RequestMapping(path: '/', methods: 'get')]
    #[Benchmark]
    public function index()
    {
        $user = $this->request->input('user', 'Hyperf');
        // 模拟一个耗时操作,比如 sleep 一段时间
        \Swoole\Coroutine\System::sleep(0.5); // 500ms
        return [
            'message' => "Hello {$user}.",
        ];
    }
    
    // 不加 Benchmark 的方法
    #[RequestMapping(path: '/health', methods: 'get')]
    public function health()
    {
        return ['status' => 'ok'];
    }
}

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

步骤 4:重启服务并验证

重新启动 Hyperf(用 php bin/hyperf.php startserver:watch 热重启):

php bin/hyperf.php start

然后访问首页:

curl http://localhost:9501/

你会看到控制台输出类似:

[Benchmark] App\Controller\IndexController::index() 执行耗时: 502.73 ms

而访问 /health 则不会有任何额外输出,证明 AOP 只拦截了标记的方法。

实验:你可以多打几个 #[Benchmark] 在不同的控制器方法上,观察不同方法的耗时。

步骤 5:扩展 Before 和 After 通知(可选)

为加深理解,我们再创建一个带 Before 和 After 的切面示例,用于权限检查。

创建 app/Annotation/AuthCheck.php

<?php
namespace App\Annotation;

use Attribute;
use Hyperf\Di\Annotation\AbstractAnnotation;

#[Attribute(Attribute::TARGET_METHOD)]
class AuthCheck extends AbstractAnnotation
{
}

创建 app/Aspect/AuthCheckAspect.php

<?php
namespace App\Aspect;

use App\Annotation\AuthCheck;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\HttpServer\Contract\RequestInterface;

#[Aspect]
class AuthCheckAspect extends AbstractAspect
{
    public array $annotations = [
        AuthCheck::class,
    ];

    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        // 获取 Request 对象(可从容器中获取,或者通过参数注入,这里简化演示)
        $request = \Hyperf\Utils\ApplicationContext::getContainer()->get(RequestInterface::class);
        $token = $request->header('Authorization', '');
        
        // Before 逻辑:校验 Token
        if ($token !== 'Bearer secret-token') {
            // 不调用原方法,直接返回 401
            return [
                'code' => 401,
                'message' => 'Unauthorized',
            ];
        }
        
        // 放行,执行原方法
        $result = $proceedingJoinPoint->process();
        
        // After 逻辑:可以在这里记录操作日志
        // 比如 $this->logger->info('User called ...');
        
        return $result;
    }
}

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

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

重启服务,测试:

curl http://localhost:9501/secure           # 返回401
curl -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 -n 1000 -c 100 http://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 服务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值