hyperf 注解

Hyperf的注解是一种内嵌在代码中的结构化配置指令

是什么

一种为类、方法、属性等代码声明部分添加结构化元数据的能力。它不是普通注释,而是框架可读取的“配置式语言”,目的是实现功能定义与功能使用的解耦

如何工作

注解本身只是元数据定义,其作用依赖于框架的收集与利用。

  • 收集:启动时,Hyperf的注解扫描器会读取代码,将所有注解的元数据收集到 Hyperf\Di\Annotation\AnnotationCollector 类(或你自定义的收集器)中。

  • 利用:在框架运行过程中,相关组件(如路由、依赖注入容器、AOP切面)会在需要时查询这些已收集的元数据,并执行相应逻辑。

注解的主要用途与分类

可应用于类、类方法和类属性

类别 典型注解 应用对象 主要用途与示例
路由与控制 #[Controller]、#[GetMapping] 类、方法 定义HTTP路由。#[GetMapping(path: “/user”)] 会将方法绑定到 /user 的GET请求。
依赖注入 #[Inject], #[Value] 属性 实现依赖自动注入。#[Inject] 注入对象,#[Value(“${config.key}”)] 注入配置值。
参数校验 #[Validation] 方法 自动验证请求参数,失败则抛出异常。
事件监听 #[Listener]`` 将类标记为事件监听器。
异步任务 #[Async], #[Crontab] 方法 声明异步执行方法或定时任务。
切面编程 #[Aspect] 标记一个类为AOP切面,用于拦截特定注解或方法。

自定义注解:从创建到应用

文档详细说明了自定义注解的完整流程,这是实现高度解耦业务逻辑的关键。其过程可以概括为下图:
image

    1. 创建注解类
      你需要创建一个继承 Hyperf\Di\Annotation\AbstractAnnotation 的类,并使用 #[Attribute] 定义其可用的目标。
      这个注解类,就像我们定义一个标签的样式和可写的参数.
<?php
// 文件:app/Annotation/Cacheable.php

namespace App\Annotation;

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

// 这个“标签”只能贴在类的方法上 (Attribute::TARGET_METHOD)
#[Attribute(Attribute::TARGET_METHOD)]
class Cacheable extends AbstractAnnotation
{
    // 定义标签上可写的参数:$prefix 缓存键前缀,$ttl 缓存时间(秒)
    public function __construct(public string $prefix = '', public int $ttl = 3600)
    {
        // 父类 AbstractAnnotation 会自动将接收到的参数(如 ['prefix'=>'user', 'ttl'=>100])
        // 赋值给这个对象的同名属性 $this->prefix 和 $this->ttl
    }
}

此刻,这个“标签”还只是个空壳,贴上去什么也不会发生。

    1. 使用注解
      创建后,即可在业务代码中像使用内置注解一样使用它
      现在,我们可以在任何服务类的方法上,贴上这个自定义的“标签”。
<?php
// 文件:app/Service/UserService.php

namespace App\Service;

use App\Annotation\Cacheable;

class UserService
{
    // 在 getUserInfo 方法上贴上标签,并写上参数:缓存前缀为 ‘user’, 有效时间100秒
    #[Cacheable(prefix: 'user', ttl: 100)]
    public function getUserInfo(int $userId): array
    {
        // 这是真实的、可能很耗时的数据库查询逻辑
        echo “正在查询数据库获取用户 {$userId} 的信息...\n”;
        return [
            ‘id’ => $userId,
            ’name’ => ‘张三’,
            ’email’ => ‘zhangsan@example.com’
        ];
    }
}

即使贴了标签,方法仍然会每次执行真实逻辑,因为还没有人来看这个标签并采取行动。

    1. 注解收集
      我们需要一个“登记员”,在项目启动时扫描所有代码,把Cacheable标签贴在哪儿、参数是什么,都记录到一个本子上(AnnotationCollector)。
      这个“登记”行为,我们在 Cacheable 注解类自身内部定义。修改第一步的 Cacheable.php 文件:
<?php
// 文件:app/Annotation/Cacheable.php (修改后)

namespace App\Annotation;

use Attribute;
use Hyperf\Di\Annotation\AbstractAnnotation;
use Hyperf\Di\Annotation\AnnotationCollector; // 引入收集器

#[Attribute(Attribute::TARGET_METHOD)]
class Cacheable extends AbstractAnnotation
{
    public function __construct(public string $prefix = '', public int $ttl = 3600) {}

    // 当扫描器发现这个注解用在某个类的方法上时,会自动调用此方法
    public function collectMethod(string $className, ?string $target): void
    {
        // 核心:将当前注解对象($this)的信息,登记到“总账本”中。
        // 登记的信息是:“在 $className 类的 $target 方法上,有一个 Cacheable 注解,它的参数是 $this。”
        AnnotationCollector::collectMethod($className, $target, static::class, $this);
    }
}

现在,“登记员”工作了。项目启动后,AnnotationCollector 里就有了所有 Cacheable 的使用记录。

    1. 让“标签”真正起作用(实现功能 - AOP切面)
      在方法执行时,拦截它,先查缓存,如果有就返回,没有才执行方法并存入缓存
      在Hyperf中,这个“拦截并增强”的功能,通常由 AOP切面(Aspect) 来实现。
<?php
// 文件:app/Aspect/CacheableAspect.php

namespace App\Aspect;

use App\Annotation\Cacheable;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Context\Context; // 这里简单用Context模拟缓存,实际应用请用Redis等
use Hyperf\Di\Annotation\AnnotationCollector; // 引入总账本

// 声明这是一个切面,专门拦截带有 @Cacheable 注解的方法
#[Aspect]
class CacheableAspect extends AbstractAspect
{
    // 要拦截的注解或类
    public array $annotations = [
        Cacheable::class,
    ];

    // 环绕通知:在目标方法前后执行
    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        // 1. 获取当前被拦截的方法所属的类和方法名
        $className = $proceedingJoinPoint->className;
        $methodName = $proceedingJoinPoint->methodName;

        // 2. 去“总账本” (AnnotationCollector) 里查询这个方法的 Cacheable 注解元数据
        /** @var null|Cacheable $annotation */
        $annotation = AnnotationCollector::getMethodAnnotation($className, $methodName, Cacheable::class);

        // 如果该方法没有 Cacheable 注解(理论上不会,因为被$this->annotations过滤了),直接执行原方法
        if (!$annotation) {
            return $proceedingJoinPoint->process();
        }

        // 3. 根据注解参数生成唯一的缓存键 (这里拼接了前缀和方法参数)
        $args = $proceedingJoinPoint->arguments['keys'];
        $cacheKey = sprintf(‘%s:%s:%s’, $annotation->prefix, $methodName, md5(json_encode($args)));

        // 4. 尝试从缓存中获取 (这里用Hyperf的Context内存存储简单演示)
        if (Context::has($cacheKey)) {
            echo “从缓存中获取数据...\n”;
            return Context::get($cacheKey);
        }

        // 5. 缓存不存在,则执行原始方法(例如:查询数据库)
        echo “执行真实业务逻辑...\n”;
        $result = $proceedingJoinPoint->process();

        // 6. 将结果存入缓存,并设置TTL (此处Context无TTL,仅为演示。实际请用Redis)
        Context::set($cacheKey, $result);
        // 实际代码: $this->redis->setex($cacheKey, $annotation->ttl, serialize($result));

        // 7. 返回结果
        return $result;
    }
}
    1. 配置自定义收集器
      编辑 config/autoload/annotations.php 文件:
<?php
return [
    ‘scan’ => [
        ‘collectors’ => [
            // 添加你的自定义注解类到收集器数组
            \App\Annotation\Cacheable::class,
        ],
    ],
];
    1. 最后
      当以上所有步骤完成后,你的 UserService::getUserInfo 方法行为就完全改变了:
// 第一次调用:执行真实逻辑,并缓存
$user1 = $userService->getUserInfo(1);
// 输出:正在查询数据库获取用户 1 的信息... / 执行真实业务逻辑...

// 第二次调用(100秒内):直接返回缓存结果
$user2 = $userService->getUserInfo(1);
// 输出:从缓存中获取数据...

自定义注解的本质是:1. 定义标记 -> 2. 收集标记 -> 3. 利用标记。
这个过程实现了 “声明”与“实现”的完美分离。业务开发者在写 UserService 时,只需关心“这里需要被缓存”(声明),而缓存具体怎么实现(AOP切面中的Key生成、Redis操作、TTL处理)则由另一部分基础设施代码负责。这使得代码极其干净、可维护,且缓存逻辑可以统一修改。

posted @ 2026-01-14 16:55  py卡卡  阅读(1)  评论(0)    收藏  举报