一次 Hyperf 注解失效问题分析

问题环境

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日

相关文章

  • PHP设计模式之迭代器模式的使用

    迭代器模式是什么? 迭代器模式是一种行为型设计模式,它允许你在不暴露对象内部结构的情况下遍历其中的元素。通过该模式,你可以在使用不同集合(例如树形和线性)的对象时使用相同的代码。 迭代器模式的使用: 实现迭代器模式的第一步是要创建一个迭代器接口,它至少应该具有访问元素和返回下一个元素的方法。该接口可以根据特定需要进行扩展。在 PHP 中,我们可以使用 SPL…

    PHP 2023年5月27日
    00
  • PHP 字符串分割和比较

    针对“PHP 字符串分割和比较”,我可以提供以下攻略。 分割字符串 explode() 函数:用于将字符串分割成数组。函数实现如下: array explode (string $delimiter, string $string [, int $limit = PHP_INT_MAX] ) 其中,$delimiter 表示指定的分割符号,$string 表…

    PHP 2023年5月26日
    00
  • PHP导出EXCEL快速开发指南–PHPEXCEL的使用详解

    PHP导出EXCEL快速开发指南–PHPEXCEL的使用详解 简介 PHP作为一种非常流行的Web开发语言,其在数据处理方面有着出色的表现。PHPEXCEL是PHP中一款非常强大的导出Excel工具,它支持导出各种格式的Excel表格,并且提供了丰富的样式和数据处理功能。 本文将介绍PHPEXCEL的使用方法,并提供两个示例来说明如何在PHP中使用PHPE…

    PHP 2023年5月26日
    00
  • 兼容PHP5的PHP目录管理函数库

    介绍PHP目录管理函数库的兼容性问题 PHP目录管理函数库通常用于文件的读写、复制、删除等操作。然而,该函数库在某些服务器上可能会因为PHP版本的问题而出现兼容性问题。为此,我们需要一套兼容PHP5的PHP目录管理函数库。 步骤一:确定PHP版本 在进行PHP目录管理函数库的使用前,首先要确定我们的服务器上安装的PHP版本是否是5或以上版本。若不满足,可以考…

    PHP 2023年5月23日
    00
  • php取出数组单个值的方法

    对于如何取出PHP数组单个值,有以下几种方法: 1. 数组下标访问 PHP数组中的每一个元素都有自己的下标,可以通过以下格式来访问数组中的单个元素: $array = array(‘apple’, ‘banana’, ‘orange’); echo $array[0]; //输出apple echo $array[1]; //输出banana echo $a…

    PHP 2023年5月26日
    00
  • php版银联支付接口开发简明教程

    下面是关于“php版银联支付接口开发简明教程”的完整攻略。 一、前置知识 在开始使用银联支付接口进行开发之前,需要掌握以下知识: PHP基础知识 网络编程基础知识 HTTP协议基础知识 rsa加密算法基础知识 二、准备工作 在进行银联支付接口开发之前,需要进行以下准备工作: 申请商户号和商户秘钥 下载工具包并解压 了解银联支付接口开发文档 三、接口集成 引入…

    PHP 2023年5月26日
    00
  • PHP创建文件及写入数据(覆盖写入,追加写入)的方法详解

    PHP创建文件及写入数据(覆盖写入,追加写入)的方法详解 创建文件 可以使用PHP的fopen()函数来创建文件,下面是示例代码: $filename = "example.txt"; $file = fopen($filename, "w") or die("无法创建文件"); fclose($f…

    PHP 2023年5月26日
    00
  • php创建桌面快捷方式实现方法

    下面我将为您详细讲解“PHP创建桌面快捷方式实现方法”的完整攻略。 1. 获取桌面路径 一般情况下,桌面的路径可以在Windows系统注册表中获取。代码如下: /** * 获取桌面路径 * * @return string or null */ function getDesktopPath() { $reg_path = ‘SOFTWARE\\Micros…

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