- 后端
- Web框架
- 微服务
- RPC框架
- 异步编程
【免费下载链接】hyperf
🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
本篇技术指南围绕 Hyperf 协程框架的注解(Annotation)机制展开,系统讲解注解在类、类方法、类属性上的使用方式,以及ignore_annotations、自定义注解、注解收集器与class_map类映射等进阶能力。读完本文,你将能够熟练运用 Hyperf 内置注解(如Controller、RequestMapping、Inject、Value),并具备从零编写自定义注解、按需接管注解收集逻辑、甚至无侵入替换框架核心类的能力。
注解基础:Hyperf 为什么如此依赖注解
注解是 Hyperf 中非常强大的一项功能,它允许以注解的形式大幅减少配置量,并实现许多开箱即用的便利能力。整个框架的控制器路由、依赖注入、AOP 切面、RPC 服务注册等功能,都建立在注解机制之上。
什么是注解:嵌入代码的声明式元数据
注解功能为代码中的声明部分提供了添加结构化、机器可读元数据的能力。注解的目标可以是类、方法、函数、参数、属性以及类常量,通过 PHP 的反射 API,可以在运行时获取注解所定义的元数据。因此,注解可以理解为一种直接嵌入代码的配置式语言。
通过注解的使用,应用中"实现功能"与"使用功能"得以相互解耦。某种程度上,注解可以与接口(interface)和其实现(implementation)的关系相类比——但接口与实现是代码相关的,注解则与声明额外信息和配置相关;接口只能由类来实现,而注解可以声明到方法、函数、参数、属性甚至类常量中,因此注解比接口更加灵活。
一个简单的例子可以说明这种灵活性:假设接口ActionHandler代表应用中的一个操作,其中部分 handler 实现需要setup,部分不需要。如果要求所有类都实现ActionHandler接口并实现setUp()方法,那么不需要 setup 的类也必须写出空实现;而改用注解后,只有真正需要 setup 的类才声明相应注解,并且可以多次使用。
注解是如何发挥作用的:收集器 + 扫描器 + 消费方
注解本身只是元数据定义,必须配合应用程序才能发挥作用。在 Hyperf 中,注解内的数据会被收集到Hyperf\Di\Annotation\AnnotationCollector类中供应用程序使用(当然,根据实际情况也可以收集到你自定义的类中),随后在这些注解本身希望发挥作用的业务位置,对已收集的注解元数据进行读取和利用,最终实现期望的功能。
从源码结构看,注解的完整生命周期由三个核心角色组成(相关实现位于 src/di/src/Annotation):
- 扫描器(Scanner):框架启动时,Scanner::collect() 会对扫描路径内的每个类做反射解析,分别遍历类的注解、属性注解、方法注解以及类常量注解,并逐一触发对应注解对象的收集方法。也就是说,除了文档中常说的类、类方法、类属性三类目标外,源码层面还支持类常量上的注解。
- 收集器(Collector):
AnnotationCollector是框架默认的元数据容器(详见下文"利用注解数据"一节)。 - 消费方:路由管理器、依赖注入容器、AOP 代理等组件在初始化时,从收集器中读取元数据并转化为实际行为。
忽略注解:与第三方注解工具和平共处
在某些场景下,我们希望忽略某些注解。典型场景是接入自动生成文档的工具——不少此类工具都是通过注解的形式定义文档结构内容的,而这些注解可能并不符合 Hyperf 的使用方式。此时可以在config/autoload/annotations.php中将相关注解设置为忽略:
use JetBrains\PhpStorm\ArrayShape; return [ 'scan' => [ // ignore_annotations 数组内的注解都会被注解扫描器忽略 'ignore_annotations' => [ ArrayShape::class, ], ], ];该配置从源码层面可以得到印证:ScanConfig在实例化时会读取config/autoload/annotations.php与各组件ConfigProvider中的annotations.scan.ignore_annotations配置(见 src/di/src/Annotation/ScanConfig.php#L93-L96),而 AnnotationReader::getAttributes() 在通过反射解析每个属性时,会先判断注解类名是否位于忽略列表内,若命中则直接continue跳过,不进行实例化与收集。另外,hyperf/di组件自身的ConfigProvider默认将mixin加入忽略列表(见 src/di/src/ConfigProvider.php#L57-L59),因此业务代码中即使出现mixin这类注解也不会干扰框架运行。
注解的三种典型使用场景
注解一共有 3 种常见应用对象,分别是类、类方法和类属性。下面逐一介绍,并对应到 Hyperf 内置注解的真实实现。
类注解:Controller 与 AutoController 的典范
类注解定义在class关键词上方的注释块(属性声明)内。Hyperf 中常用的Controller和AutoController就是类注解的使用典范。下面的示例表明ClassAnnotation注解应用于Foo类:
<?php #[ClassAnnotation] class Foo {}从源码看,src/http-server/src/Annotation/Controller.php#L18-L23 中的Controller注解声明了prefix(路由前缀)、server(所属服务,默认http)与options三个参数;AutoController 在Controller基础上额外支持defaultMethods参数,用于指定需要自动注册为路由的默认方法集合。
类方法注解:RequestMapping 路由映射的典范
类方法注解定义在方法上方的注释块内。RequestMapping就是类方法注解的使用典范,下面的示例表明MethodAnnotation注解应用于Foo::bar()方法:
<?php class Foo { #[MethodAnnotation] public function bar() { // some code } }Hyperf 内置的 RequestMapping 通过#[Attribute(Attribute::TARGET_METHOD)]声明仅允许作用于方法,构造函数接受path(路由路径)、methods(HTTP 方法,默认['GET', 'POST'])与options。源码中定义并导出了GET、POST、PUT、PATCH、DELETE、HEADER、OPTIONS七个方法常量,且methods参数兼容字符串与数组两种写法——传入字符串时会被按逗号切分并自动转为大写(如'get, post'会被规范化为['GET', 'POST'])。
类属性注解:Value 与 Inject 依赖注入的典范
类属性注解定义在属性上方的注释块内。Value和Inject就是类属性注解的使用典范,下面的示例表明PropertyAnnotation注解应用于Foo类的$bar属性:
<?php class Foo { #[PropertyAnnotation] private $bar; }其中Inject是依赖注入的核心注解(见 src/di/src/Annotation/Inject.php):通过#[Attribute(Attribute::TARGET_PROPERTY)]限定作用于属性,构造参数包括value(目标类,缺省时自动从属性类型或 PHPDoc 推断)、required(是否必需,默认true)与lazy(是否懒加载,默认false,开启时会将目标类名加上HyperfLazy\前缀交给懒加载代理处理)。而Value注解(见 src/config/src/Annotation/Value.php)则用于将配置项直接注入到类属性中,构造参数为配置键key,由ValueAspect在运行时读取配置并写入属性。
注解参数传递方式
注解参数共支持以下几种传递形式:
- 传递主要的单个参数:
#[DemoAnnotation('value')] - 传递字符串参数:
#[DemoAnnotation(key1: 'value1', key2: 'value2')] - 传递数组参数:
#[DemoAnnotation(key: ['value1', 'value2'])]
自定义注解:从零编写一个注解类
当内置注解无法满足业务需求时,可以自定义注解。整体分为三个步骤:创建注解类、按需实现收集逻辑、配置收集器以支持缓存。
创建注解类:继承 AbstractAnnotation
<?php namespace App\Annotation; use Attribute; use Hyperf\Di\Annotation\AbstractAnnotation; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class Foo extends AbstractAnnotation { public function __construct(public array $bar, public int $baz = 0) { } }使用注解类:
<?php use App\Annotation\Foo; #[Foo(bar: [1, 2], baz: 3)] class IndexController extends AbstractController { // 利用注解数据 }注意#[Attribute(...)]中的目标限定:Attribute::TARGET_CLASS、Attribute::TARGET_METHOD、Attribute::TARGET_PROPERTY等可以按位或组合,约束该注解允许出现的位置。
抽象类做了什么:参数自动分配与自动收集
在上面的示例中,注解类继承了Hyperf\Di\Annotation\AbstractAnnotation抽象类。对于注解类来说,这不是必须的——真正必须的是实现Hyperf\Di\Annotation\AnnotationInterface接口;抽象类的作用在于提供极简的定义方式,它已经替你实现了两项非常便捷的功能:
- 注解参数自动分配到类属性:构造函数中使用
public修饰的具名参数会自动成为注解对象的公开属性,配合toArray()方法(见 src/di/src/Annotation/AbstractAnnotation.php#L21-L29)可将注解数据序列化为数组,方便存储与传输。 - 根据注解使用位置自动按规则收集到
AnnotationCollector:collectClass、collectClassConstant、collectMethod、collectProperty四个方法在抽象类中均已默认实现,直接调用AnnotationCollector对应方法完成收集(见 src/di/src/Annotation/AbstractAnnotation.php#L31-L49)。
因此,大多数自定义注解只需像上面的Foo一样定义构造参数即可,无需编写任何收集代码。
自定义注解收集器:实现 AnnotationInterface
如果默认的收集规则不满足需求(例如要把元数据收集到自己的容器中),可以在注解类内重写收集逻辑。收集注解的具体执行流程由Hyperf\Di\Annotation\AnnotationInterface约束,该接口要求实现以下方法(见 src/di/src/Annotation/AnnotationInterface.php):
public function collectClass(string $className): void;—— 当注解定义在类上被扫描到时触发public function collectClassConstant(string $className, ?string $target): void;—— 当注解定义在类常量上被扫描到时触发public function collectMethod(string $className, ?string $target): void;—— 当注解定义在类方法上被扫描到时触发public function collectProperty(string $className, ?string $target): void;—— 当注解定义在类属性上被扫描到时触发
其中?string $target参数表示注解所在的具体方法名、属性名或类常量名。也就是说,虽然使用层面的注解目标常概括为"类、类方法、类属性"三类,但接口层面对类常量同样提供了收集入口,Scanner在扫描时也会遍历类的反射常量并调用collectClassConstant(见 src/di/src/Annotation/Scanner.php#L67-L74)。
注册收集器以启用缓存
因为框架实现了注解收集器缓存功能,所以需要将自定义收集器配置到annotations.scan.collectors中,框架才能自动缓存收集好的注解并在下次启动时复用。如果没有配置对应的收集器,自定义注解只有在首次启动server时生效,再次启动时不会生效。
<?php return [ // 注意在 config/autoload 文件下的配置文件则无 annotations 这一层 'annotations' => [ 'scan' => [ 'collectors' => [ CustomCollector::class, ], ], ], ];从源码可以理解这背后的机制:Scanner::scan()在扫描完成后,会把每个收集器serialize()出的数据连同代理类映射一起写入runtime/container/scan.cache(见 src/di/src/Annotation/Scanner.php#L122-L134);下次启动时若扫描缓存可用(config/config.php中scan_cacheable为true或app_env为prod,见 ScanConfig.php#L123-L131),则直接反序列化缓存数据并调用对应收集器的deserialize()恢复元数据。只有被列在collectors中的收集器才会参与缓存读写,未注册的自定义收集器自然会在二次启动时"丢失"首次收集的数据。
另外,hyperf/di自身在ConfigProvider中默认注册了AnnotationCollector与AspectCollector两个收集器(见 src/di/src/ConfigProvider.php#L52-L55),这就是框架内置注解能够在多进程、多次启动场景下稳定生效的原因。
读取注解元数据:AnnotationCollector 静态 API
在没有自定义注解收集方法时,注解的元数据默认统一收集在Hyperf\Di\Annotation\AnnotationCollector类内。通过该类的静态方法,可以方便地获取对应元数据用于逻辑判断或功能实现。其内部数据结构按类维度组织,分别用_c(类)、_cc(类常量)、_p(属性)、_m(方法)四个键存放不同位置的注解(见 src/di/src/Annotation/AnnotationCollector.php#L21-L39),并提供以下常用读取 API:
| 方法 | 作用 |
|---|---|
getClassAnnotation(string $class, string $annotation) | 获取某个类上的指定注解实例 |
getClassAnnotations(string $class) | 获取某个类上的全部注解 |
getClassesByAnnotation(string $annotation) | 按注解反查所有使用它的类 |
getClassMethodAnnotation(string $class, string $method) | 获取某方法上的全部注解 |
getMethodsByAnnotation(string $annotation) | 按注解反查所有使用方法(返回包含class、method、annotation的数组) |
getClassPropertyAnnotation(string $class, string $property) | 获取某属性上的全部注解 |
getPropertiesByAnnotation(string $annotation) | 按注解反查所有使用属性的类 |
getClassConstantAnnotation(string $class, string $constant) | 获取某类常量上的全部注解 |
getClassConstantsByAnnotation(string $annotation) | 按注解反查所有使用类常量的类 |
getContainer() | 获取整个元数据容器 |
这些 API 的正确性由单元测试直接验证:在 src/di/tests/Annotation/ScannerTest.php#L102-L131 的testCollect用例中,框架对一个同时标注了类注解、类常量注解、属性注解和方法注解的测试类(见 src/di/tests/Stub/Collect/Foo.php)执行Scanner::collect()后,依次断言上述各类读取方法返回的注解实例与反查结果完全一致——这可以作为你编写自定义注解读取逻辑时的参考范式。
ClassMap 功能:无侵入替换框架类
框架提供了class_map配置,可以方便地直接替换需要加载的类,实现"不改动框架源码、不改动业务调用方"的定制。
class_map 的配置原理
class_map位于annotations.scan配置下,格式为"需要映射的类名 => 类所在的文件地址"。ScanConfig会将其读取为classMap(见 src/di/src/Annotation/ScanConfig.php#L96),而Scanner::collect()在收集某类注解前会先检查class_map:如果原类已被动态替换(即原类文件路径与映射地址不一致),则跳过原类的收集,避免新旧实现元数据冲突(见 src/di/src/Annotation/Scanner.php#L38-L44)。
实战:自动复制协程上下文
下面以实现"自动复制协程上下文"为例,演示class_map的完整用法。核心诉求是:使用co()、parallel()等方法创建子协程时,自动把父协程上下文(如Request)中的数据复制到子协程,避免子协程中拿不到请求上下文。
第一步:实现一个用于复制上下文的Coroutine类。其中create()方法可以将父类的上下文复制到子类当中。为了避免命名冲突,约定使用class_map作为文件夹名,后跟要替换的命名空间对应的文件夹及文件,例如class_map/Hyperf/Coroutine/Coroutine.php(本示例摘自官方业务骨架示例项目):
<?php declare(strict_types=1); namespace App\Kernel\Context; use App\Kernel\Log\AppendRequestIdProcessor; use Hyperf\Context\Context; use Hyperf\Contract\StdoutLoggerInterface; use Hyperf\Engine\Coroutine as Co; use Psr\Container\ContainerInterface; use Psr\Http\Message\ServerRequestInterface; use Throwable; class Coroutine { protected LoggerInterface $logger; public function __construct(protected ContainerInterface $container) { $this->logger = $container->get(StdoutLoggerInterface::class); } /** * @return int Returns the coroutine ID of the coroutine just created. * Returns -1 when coroutine create failed. */ public function create(callable $callable): int { $id = Co::id(); $coroutine = Co::create(function () use ($callable, $id) { try { // Shouldn't copy all contexts to avoid socket already been bound to another coroutine. Context::copy($id, [ AppendRequestIdProcessor::REQUEST_ID, ServerRequestInterface::class, ]); $callable(); } catch (Throwable $throwable) { $this->logger->warning((string) $throwable); } }); try { return $coroutine->getId(); } catch (Throwable $throwable) { $this->logger->warning((string) $throwable); return -1; } } }第二步:实现一个与Hyperf\Coroutine\Coroutine一模一样的同名类。其中create()方法替换成上述实现,其余静态方法保持原语义不变(如id()、defer()、sleep()、parentId()、inCoroutine()、stats()、exists()),文件位于class_map/Hyperf/Coroutine/Coroutine.php:
<?php declare(strict_types=1); namespace Hyperf\Coroutine; use App\Kernel\Context\Coroutine as Go; use Hyperf\Contract\StdoutLoggerInterface; use Hyperf\Engine\Coroutine as Co; use Hyperf\Engine\Exception\CoroutineDestroyedException; use Hyperf\Engine\Exception\RunningInNonCoroutineException; use Throwable; class Coroutine { /** * Returns the current coroutine ID. * Returns -1 when running in non-coroutine context. */ public static function id(): int { return Co::id(); } public static function defer(callable $callable): void { Co::defer(static function () use ($callable) { try { $callable(); } catch (Throwable $exception) { di()->get(StdoutLoggerInterface::class)->error((string) $exception); } }); } public static function sleep(float $seconds): void { usleep(intval($seconds * 1000 * 1000)); } /** * Returns the parent coroutine ID. * Returns 0 when running in the top level coroutine. * @throws RunningInNonCoroutineException when running in non-coroutine context * @throws CoroutineDestroyedException when the coroutine has been destroyed */ public static function parentId(?int $coroutineId = null): int { return Co::pid($coroutineId); } /** * @return int Returns the coroutine ID of the coroutine just created. * Returns -1 when coroutine create failed. */ public static function create(callable $callable): int { return di()->get(Go::class)->create($callable); } public static function inCoroutine(): bool { return Co::id() > 0; } public static function stats(): array { return Co::stats(); } public static function exists(int $id): bool { return Co::exists($id); } }第三步:配置class_map完成替换:
<?php declare(strict_types=1); use Hyperf\Coroutine\Coroutine; return [ 'scan' => [ 'paths' => [ BASE_PATH . '/app', ], 'ignore_annotations' => [ 'mixin', ], 'class_map' => [ // 需要映射的类名 => 类所在的文件地址 Coroutine::class => BASE_PATH . '/class_map/Hyperf/Coroutine/Coroutine.php', ], ], ];配置完成后,co()和parallel()等方法创建子协程时,就会自动拿到父协程上下文中的数据,例如Request。这与 Hyperf 协程组件的调用链完全吻合:全局函数co()与go()最终都调用Coroutine::create()(见 src/coroutine/src/Functions.php#L48-L63),而parallel()则基于Parallel类批量创建协程(见 src/coroutine/src/Functions.php#L24-L31),它们都经过被替换的Hyperf\Coroutine\Coroutine静态入口;上下文复制则依赖Hyperf\Context\Context的按协程 ID 读写机制(见 src/context/src/Context.php)。作为对比,框架原生的 src/coroutine/src/Coroutine.php#L96-L105 已经提供了fork(callable, array $keys)方法实现"带指定上下文键的协程创建",class_map方案则适用于需要统一拦截create()入口、按全局规则复制上下文的场景。
小结
注解是 Hyperf 声明式编程的基石。掌握本文内容后,你可以:
- 理解注解作为"嵌入代码的配置式语言"的本质,以及"扫描器 → 收集器 → 消费方"的完整工作链路;
- 熟练使用
ignore_annotations与第三方注解工具共存,避免扫描器误解析; - 在类、类方法、类属性(以及类常量)三个位置正确使用注解,并理解
Controller、RequestMapping、Inject、Value等内置注解的参数语义; - 通过继承
AbstractAnnotation快速创建自定义注解,或实现AnnotationInterface定制收集逻辑,并配置collectors使注解在扫描缓存开启后依然生效; - 借助
AnnotationCollector的静态 API 读取元数据,驱动自己的路由、AOP 或业务逻辑; - 使用
class_map无侵入替换框架类,实现如自动复制协程上下文等定制能力。
如需进一步实践,可深入阅读 src/di/src/Annotation 目录下的扫描器与收集器实现,以及 src/di/tests/Annotation/ScannerTest.php 中完整的注解收集测试用例。
- 后端
- Web框架
- 微服务
- RPC框架
- 异步编程
【免费下载链接】hyperf
🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
相关推荐
Hyperf 注解(Annotation)完全指南:从基础概念到自定义注解与 ClassMap 实战
Hyperf 注解(Annotation)完全指南:从基础概念到自定义注解与 ClassMap 实战 本文基于 Hyperf 官方文档《Annotation》编
后端Web框架微服务RPC框架异步编程Hyperf 注解(Annotation)机制完全指南:从底层原理到自定义注解与 ClassMap 实战
Hyperf 注解(Annotation)机制完全指南:从底层原理到自定义注解与 ClassMap 实战 Hyperf 是一个基于 Swoole 的高性能协程框
后端微服务Hyperf 注解(Annotation)完整指南:从元数据机制到自定义注解与 ClassMap 的实战解析
Hyperf 注解(Annotation)完整指南:从元数据机制到自定义注解与 ClassMap 的实战解析 注解是 Hyperf 框架中非常强大的一项功能,它
后端Web框架微服务RPC框架异步编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考