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

今日目标
- 彻底理解 Hyperf 注解的运作机制,包括如何定义、如何被扫描和解析。
- 理解面向切面编程(AOP)的概念:连接点、切面、通知(Advice)类型。
- 亲手编写一个
@Benchmark注解,配合Around通知,实现无侵入的方法耗时统计。 - 将注解应用于控制器方法,并通过真实请求验证 AOP 的生效。
- 学会使用 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 start 或 server: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则不会代理。
五、今日作业与学习产出
- 提交代码:将
Benchmark注解、BenchmarkAspect切面以及修改过的控制器提交到 Git。 - 学习笔记:画出 Hyperf AOP 的代理生成时序图:注解扫描 → 收集器 → 代理类生成 → 容器注入代理 → 方法调用织入。
- 实战拓展:
- 修改
Benchmark注解,增加一个$minCost参数,只有执行时间超过该值(如 100ms)时才输出日志。 - 编写一个
@Cache注解,配合 Around 通知实现简单的方法结果缓存(存到 Redis 或本地数组),体验 AOP 的强大。
- 修改
- 思考题:如果在一个方法上同时应用了
@Benchmark和@AuthCheck,它们的执行顺序是怎样的?如何控制多个切面的优先级?(提示:通过切面类的priority属性)
通过今天的学习,你不仅掌握了注解和 AOP 的使用,更理解了 Hyperf 框架如何在不侵入代码的情况下增强功能。这种思想将贯穿整个微服务开发——中间件、限流、熔断、事务等全部基于此。明天我们将继续深入,用中间件和验证器加固我们的 API 服务。

9万+

被折叠的 条评论
为什么被折叠?



