一次 Hyperf 注解失效问题分析

yizhihongxing

问题环境

PHP: 8.0.13
Swoole: 4.6.2
Hyperf: 2.2.33
运行环境: Docker Desktop on WSL2  

文章会持续修订,转载请注明来源地址:https://her-cat.com/posts/2023/03/02/hyperf-annotation-failure-problem-analysis/

问题背景

有同事说我之前使用注解实现的某个功能有问题,具体表现就是有部分使用了注解的类没有被 Hyperf 收集到注解收集器中,导致出现了不符合预期的结果。

由于这个功能已经运行了一段时间,并且我在自己的电脑(Mac)上测试是正常的,找另外一个跟他同样使用 Windows + Docker 开发的同事进行测试也是正常的,所以可以排除业务代码和环境的问题。

简化后的代码如下:

#[Attribute(Attribute::TARGET_CLASS)]  
class CustomAnnotation extends AbstractAnnotation
{
}  
  
#[CustomAnnotation]  
class Foo
{  
}  
  
#[CustomAnnotation]  
class Bar
{  
}  

在上面的代码中,定义了一个注解类 CustomAnnotation,并且在两个类上使用了这个注解。期望的结果是 FooBar 都能够被 Hyperf 收集到注解收集器中,但实际上只有 Foo 被收集到了。

Foo 和 Bar 分别在不同的文件中,但是都在同一个目录下,该目录下的文件数量有 60+。

于是我俩开始在他的电脑上排查是不是 Hyperf 的问题。

源码分析

在 Hyperf 启动时, ClassLoader 类加载器会扫描项目中所有的类文件,并将元数据(注解与类之间的关系)收集到相应的注解收集器中,如果没有自定义注解收集器,则默认统一收集到 Hyperf\Di\Annotation\AnnotationCollector 类中。

下面是完成收集注解的主要逻辑:

  • 使用 symfony/finder 组件提供的 Finder 类遍历指定目录下所有的 PHP 类文件。
  • 通过反射读取每个文件中的类及其属性、方法上使用的注解。
  • 依次检查这些注解是否实现了 Hyperf\Di\Annotation\AnnotationInterface 接口,该接口定义了三个方法分别用于收集类、方法、属性的元数据。
  • 如果注解实现了该接口,根据注解使用位置调用相应的方法将其收集到注解收集器中。

完成收集后,我们就能使用注解收集器提供的静态方法的获取对应的元数据用于实现一些自定义的逻辑和功能。

第一步就是先检查类文件是否被 Finder 类读取到了,这部分的逻辑在 ReflectionManager::getAllClasses() 静态方法中。

public static function getAllClasses(array $paths): array  
{  
    $finder = new Finder();  
    // 设置读取指定目录下的 PHP 文件
    $finder->files()->in($paths)->name('*.php');  
    $parser = new Ast();  
  
    $reflectionClasses = [];  
    foreach ($finder as $file) {  
        try {  
	        // 解析文件内容获取类名称
            $stmts = $parser->parse($file->getContents());  
            if (! $className = $parser->parseClassByStmts($stmts)) {
	            // 没获取到说明没有定义类
                continue;  
            }
            $reflectionClasses[$className] = static::reflectClass($className);  
        } catch (\Throwable) {  
        }    
    }    
    return $reflectionClasses;  
}

将获取目录下文件的这段代码提出来单独进行测试。由于 Finder 类实现了 IteratorAggregate 接口,所以在上面的代码中可以直接对 Finder 类进行遍历,也可以使用 iterator_to_array() 函数直接获取迭代器的结果。

$finder = new Finder();  
// 设置读取指定目录下的 PHP 文件
$finder->files()->in('出现问题的目录路径')->name('*.php'); 
var_dump(iterator_to_array($finder));

通过观察打印的结果就发现了问题所在:没有读取到 Bar 的类文件。

当时就在想,这么流行的一个组件包总不能出现这么低级的 Bug 吧?抱着怀疑的心态继续分析 Finder 类实现迭代器的代码,最后将问题定位到了 PHP 内置的 RecursiveDirectoryIterator 类上,Finder 类实际上就是对 PHP 的这些类做了一层封装。

RecursiveDirectoryIterator 提供了一个用于递归迭代文件系统目录的功能,用这个类再次进行上面的测试,依然没有读取到 Bar 的类文件。

$iter = new RecursiveDirectoryIterator('出现问题的目录路径');
var_dump(iterator_to_array($iter));

于是,我又一次陷入了怀疑中,难道 PHP 实现的这个类有问题?还得继续看 PHP 的源码?我在犹豫了一会后打开了 Google,抱着肯定有人也遇到过这个问题的想法输入了「RecursiveDirectoryIterator bug」,按下回车,在短暂的页面加载后...

嘿,还真有人已经遇到过这个问题。

真相大白

在前几条搜索结果中,赫然发现有人在 PHP 官方的 Bug 系统反馈了这个问题:RecursiveDirectoryIterator returns incorrect results for Docker Desktop on WSL2,并贴心的附带了可以复现问题的代码。

下面是精简过后的复现代码。

$filesPath = __DIR__.'/files';  
  
if (! mkdir($filesPath) && ! is_dir($filesPath)) {  
    throw new \RuntimeException(sprintf('Directory "%s" was not created', 'files'));  
}  
  
$max = 1;  
$stop = 5000;  
  
// 生成测试文件,模拟目录中文件较多的情况  
foreach(range(1, $stop) as $index) {  
    $message = sprintf("creating %s\n", $index);  
    echo $message;  
    file_put_contents(__DIR__ . '/files/file' . $index, str_repeat('A', 100));  
}  
  
$iter = new \RecursiveDirectoryIterator($filesPath, FilesystemIterator::KEY_AS_PATHNAME|FilesystemIterator::CURRENT_AS_FILEINFO|FilesystemIterator::SKIP_DOTS);  
var_dump(iterator_count($iter));
// 打印出来的数字小于 5000 说明复现成功了

PHP 官方给出了回复:这是 WSL 的 Bug,并提供了相关的 issue:WSL2: Seek of directory entry by lseek does not work on v9fs。里面的实际输出跟我们发现这个问题时的打印结果几乎一模一样,感兴趣的可以去看看。

有人可能会问,lseek() 函数跟 RecursiveDirectoryIterator 类有什么关系吗 ?

当然有!将上面的代码保存到 test.php 文件,然后执行 strace php test.php 命令查看 PHP 代码的系统调用情况。

...省略其他部分...
openat(AT_FDCWD, "/home/ubuntu/files", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 4
fstat(4, {st_mode=S_IFDIR|0775, st_size=135168, ...}) = 0
brk(0x55d84733f000)                     = 0x55d84733f000
getdents(4, /* 1024 entries */, 32768)  = 32752
lseek(4, 0, SEEK_SET)                   = 0
getdents(4, /* 1024 entries */, 32768)  = 32752
getdents(4, /* 1024 entries */, 32768)  = 32768
getdents(4, /* 1024 entries */, 32768)  = 32768
getdents(4, /* 1024 entries */, 32768)  = 32768
getdents(4, /* 906 entries */, 32768)   = 28992
getdents(4, /* 0 entries */, 32768)     = 0
write(1, "int(5000)\n", 10int(5000)
)             = 10
close(3)                                = 0
close(4)                                = 0
...省略其他部分...

可以看到,RecursiveDirectoryIterator 类在底层中调用了 lseek() 函数,它的作用是设置文件偏移量。lseek(4, 0, SEEK_SET) 表示将文件偏移量设置为 0,即文件开头的位置,该函数无法工作会导致下次操作依然使用的是原来的文件偏移量。

Linux 中万物皆为文件,包括目录。

用 PHP 代码来举个例子,这里使用 PHP 的 rewinddir() 函数代替 lseek() 函数,实际上底层调用的还是 lseek() 函数。

$dh = opendir(__DIR__ . '/files');  
  
echo '开始读取目录中的所有文件:' . PHP_EOL;  
while (($file = readdir($dh)) !== false) {  
    echo 'filename:' . $file . PHP_EOL;  
}
  
echo '再次读取目录中的所有文件:' . PHP_EOL;  
// 这时文件偏移量已经到达文件的末尾,再次读取目录将不会有任何输出,模拟 lseek() 函数无法工作的情况 
while (($file = readdir($dh)) !== false) {  
    echo 'filename:' . $file . PHP_EOL;  
}  
  
// 将文件偏移量重置到文件的开头  
rewinddir($dh);  
  
echo '重置偏移量后读取目录中的所有文件:' . PHP_EOL;  
// 与第一次读取的结果相同,模拟 lseek() 函数正常工作的情况
while (($file = readdir($dh)) !== false) {
    echo 'filename:' . $file . PHP_EOL;  
}  
  
closedir($dh);

在 WSL2 以外的系统中运行以上代码,可以得到与预期一致的结果。那么在 WSL2 中运行的结果是什么?

解决问题

当然,最好是 WSL 官方能够修复这个问题,但是从有人提出这个问题到现在已经快三年了依然没有被解决的情况来看,不知道得等到猴年马月。

提问的作者也给出了一种解决方案,开启 Hyper V。但是经过测试后发现开启 Hyper V 依然会出现这个问题,所以最后直接从 WSL2 回滚到 WSL1,从另一种「根本上」解决这个问题。

总结

等等,文章开头不是说已经排除是环境的问题了吗?怎么最后又是环境的问题了?

是的,这是由于我当时并没有问清楚,只是确认了另一个同事是用 Docker 运行的,我怎么也没想到他是本地运行了个虚拟机,然后在虚拟机里面运行 Docker...

当然,后面的源码分析也不是一点作用都没有,至少将问题的范围从 Hyperf 框架缩小到了 Finder 类,再到 RecursiveDirectoryIterator 类。否则直接 Google 搜索「Hyperf 注解失效」是很难找到正确答案的。

在这篇文章中,讲述了我排查「Hyperf 注解失效」问题的过程,整个排查过程看似一气呵成,但实际上要曲折得多,甚至一度觉得这是个玄学问题。

最后,没有 Bug 的程序是不存在的,不要过度迷信那些看似很可靠的系统。

原文链接:https://www.cnblogs.com/her-cat/p/hyperf-annotation-failure-problem-analysis.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:一次 Hyperf 注解失效问题分析 - Python技术站

(0)
上一篇 2023年4月17日
下一篇 2023年4月17日

相关文章

  • Golang 之协程的用法讲解

    Golang 之协程的用法讲解 什么是协程 协程(Coroutines),也称为轻量级线程(Light Weight Thread),是一种用户态线程,不依赖于操作系统内核,由程序自己实现调度,可以在一条线程中运行多个协程,协程之间可以独立运行,也可以通过通道(Channel)进行通信和同步。协程通常用于实现事件驱动、异步编程、并发计算等技术领域。 协程的用…

    PHP 2023年5月27日
    00
  • php实现登录页面的简单实例

    下面是详细的“php实现登录页面的简单实例”的攻略。 创建数据库和数据表 首先要创建一个数据库,然后在数据库中创建一个数据表,该数据表存储用户的账号和密码信息。可以通过以下 SQL 语句来创建用户表: CREATE TABLE users ( id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VA…

    PHP 2023年5月27日
    00
  • php的urlencode()URL编码函数浅析

    PHP的urlencode()函数 urlencode()函数是PHP内置的URL编码函数,用于将字符串编码为符合URL规范的格式,包括将一些特殊字符转换为URL编码形式。该函数一般用于将URL参数进行编码,防止出现乱码等问题。 urlencode()函数的语法 urlencode()函数的语法非常简单,只需要传入一个字符串作为参数即可,例如: $url =…

    PHP 2023年5月23日
    00
  • PHP如何通过表单直接提交大文件详解

    当我们需要上传大文件时,常常会遇到上传失败,上传慢等问题,这时我们就可以通过表单直接提交大文件的方式来解决。 以下是PHP通过表单直接提交大文件的详细攻略: 修改php.ini配置文件 在服务器上找到php.ini文件,然后找到以下几个参数并修改: upload_max_filesize = 64M post_max_size = 64M max_execu…

    PHP 2023年5月26日
    00
  • PHP网页缓存技术优点及代码实例

    下面就是“PHP网页缓存技术优点及代码实例”的完整攻略。 1. 什么是网页缓存技术? 网页缓存技术可以把一些查询可以一直重用的结果缓存起来,并在下一次查询时返回缓存的结果,从而减轻服务器的负担,提高用户体验。 2. 网页缓存技术的优点 网页缓存技术有很多优点: 2.1 提高网站访问速度 网页缓存技术可以将一些重复的数据保存在内存中,从而加快网站的访问速度。 …

    PHP 2023年5月27日
    00
  • 简单谈谈php浮点数精确运算

    题目:简单谈谈PHP浮点数精确运算 为了精确处理浮点数,我们应该使用“BC函数库”和“GMP函数库”以及PHP7引入的“内置函数 intdiv() ”。 BC函数库的使用 BC函数库是一个高精度数学函数库,可以进行任意精度数字的加、减、乘、除等运算。 具体用法 BC函数库的使用需要用户开启PHP.ini配置文件中的 “bcmath” 扩展,加入 extens…

    PHP 2023年5月26日
    00
  • php解决缓存击穿的问题

    缓存击穿是指缓存中没有的数据,而查询非常频繁的数据,导致大量的请求落到了数据库上,因此很容易导致数据库连接数暴增,甚至导致宕机。 下面是 PHP 解决缓存击穿问题的一般解决方案: // 获取 Key $key = ‘my_key’; // 根据 Key 从 Redis 中获取数据 $data = $redis->get($key); // 如果数据不存…

    PHP 2023年4月17日
    00
  • php生成WAP页面

    下面我将详细介绍如何用 PHP 生成 WAP 页面的完整攻略。 什么是 WAP 页面 WAP(无线应用协议)页面是一种专门为移动设备设计的网页,由于移动设备的屏幕较小,因此 WAP 页面通常都需要进行特殊的设计和适配。 生成 WAP 页面的步骤 生成 WAP 页面的步骤如下: 创建一个 PHP 文件,例如 index.php。 在 PHP 文件中编写 HTM…

    PHP 2023年5月30日
    00
合作推广
合作推广
分享本页
返回顶部