深入 Hyperf:Inject 注解是如何工作的?

周五的时候,我在 Hyperf 群里看到有群友提出了一个问题:为什么 Inject 注解在使用 new 关键字实例化类时依然能够生效?按理说,Inject 注解不是应该只在通过容器实例化类时才会起作用吗?这个问题引发了群友们的讨论和猜测,甚至有人感叹,Inject 注解的实现简直就是魔法!

对于这个问题,Hyperf 的作者作出了解答:新版本的注入机制通过代理类来实现,注解之所以在 new 关键字下依然有效,是因为实例化的实际上是代理类,而代理类的构造函数中包含了注入操作。

然而,如果我们继续深究,还会发现一些问题:Hyperf 是否为所有类都生成了代理类?又是如何在类实例化时拦截 new 关键字的行为,从而实现实例化的是代理类而非原始类?在属性值注入过程中,具体都执行了哪些操作?

由于微信群本身不太适合深入讨论这些复杂的问题,而且考虑到「深入 Hyperf」系列已经有半年多没更新了,所以我决定撰写这篇文章,逐一解答这些问题,带大家深入探索 Inject 注解的工作原理。

文章会持续修订,转载请注明来源地址:https://her-cat.com/posts/2024/08/25/how-does-hyperf-inject-annotation-work/

是否为所有类生成了代理类?

Hyperf 生成的所有代理类都保存在 runtime/container/proxy 目录中,仔细观察一下就会发现,该目录只包含了部分原始类的代理类,由此可知,Hyperf 不会为所有类生成代理类。那么,Hyperf 会为哪些类生成代理类呢?答案是,所有需要被切面(Aspect)介入的类。

在 Hyperf 中,可以通过切面(Aspect)介入到任意类的任意方法的执行流程中,从而改变或加强原方法的功能,这就是 AOP(Aspect Oriented Programming)面向切面编程。切面(Aspect)包含了要介入的目标,以及实现对原方法的修改加强处理。这里的介入目标包含了类/方法或者注解,意味着你可以通过类名/方法名称直接指定要介入的类/方法,或者通过注解间接地指定要介入的类/方法。

为了实现 Inject 自动注入的功能,Hyperf 添加了一个 Inject 切面,其介入的目标是使用了 Inject 注解的类。

class InjectAspect extends AbstractAspect
{
    public array $annotations = [
        Inject::class,
    ];

    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        // Do nothing, just to mark the class should be generated to the proxy classes.
        return $proceedingJoinPoint->process();
    }
}

一旦我们在某个类中使用了 Inject 注解,Hyperf 就会为这个类生成代理类。并且从注释中可以看到,Inject 切面不会修改原始方法的任何行为,只是用来标记需要为其生成代理类。

除了 Inject 切面以外,Hyperf 中还包含了很多其它的切面,你可以使用 AspectCollector::list() 获取这些切面。你也可以查看 Hyperf 文档 学习如何自定义切面,提高程序的可重用性以及开发效率。

为什么被实例化的是代理类?

这一切的关键在于 Hyperf 巧妙地利用了 Composer 类自动加载机制,让我们来了解一下其中的细节。

通常情况下,使用了 Composer 的框架都会在入口文件引入一个 /vendor/autoload.php 文件,以启用自动加载功能。在这个文件中,Composer 会通过 spl_autoload_register 函数向 PHP 注册自己的类加载器(ClassLoader)。当我们在 PHP 中使用了当前内存中尚未定义的类的时候(例如使用 new 关键字实例化某个类),PHP 会调用已注册的类加载器来加载这个类文件到内存中,完成类的自动加载。

在 Composer 的类加载器中,有一个 classMap 数组属性,其键是包含命名空间的类名,值是类的文件路径。示例如下:

array:5 [  
  "Hyperf\Cache\AnnotationManager" => "/code/project/vendor/composer/../hyperf/cache/src/AnnotationManager.php"  
  "Hyperf\Cache\Annotation\CacheAhead" => "/code/project/vendor/composer/../hyperf/cache/src/Annotation/CacheAhead.php"  
  "Hyperf\Cache\Annotation\CacheEvict" => "/code/project/vendor/composer/../hyperf/cache/src/Annotation/CacheEvict.php"  
  "Hyperf\Cache\Annotation\CachePut" => "/code/project/vendor/composer/../hyperf/cache/src/Annotation/CachePut.php"  
  "Hyperf\Cache\Annotation\Cacheable" => "/code/project/vendor/composer/../hyperf/cache/src/Annotation/Cacheable.php"  
]

当 Composer 的类加载器运行时,它首先会检查 classMap 数组中是否已经存在这个类。如果类不存在,加载器将按照 PSR-4 或 PSR-0 的规范依次查找类文件;如果存在或找到了类文件,就会使用 include 加载这个类文件。

从整个自动加载过程可以看出,只要在使用某个类之前,将其代理类的路径添加到 classMap 数组中,那么当我们在 PHP 中实例化这个类时,Composer 就会直接加载代理类,而不是原始类。

实际上,Hyperf 确实是这样实现的。在生成所有代理类后,Hyperf 将原始类与代理类的映射关系添加到 Composer 的 classMap 数组中:

$proxyFileDirPath = BASE_PATH . '/runtime/container/proxy/';
$composerLoader = Composer::getLoader();    
  
$scanner = new Scanner($config, $handler);  
$composerLoader->addClassMap( 
	// 在 scan 方法中完成了扫描与生成代理类
    $scanner->scan($composerLoader->getClassMap(), $proxyFileDirPath)  
);

因此,当你在 Hyperf 中使用那些已经生成了代理类的类时,加载的就是 /runtime/container/proxy 目录下的代理类,而非原始类。

Inject 自动注入的过程

属性值的注入操作是在代理类的构造函数中完成的,因此我们需要通过对比原始类与代理类的内容来进一步分析。

从图片中可以看出,代理类相较于原始类增加了以下内容:

  • 引入了 ProxyTraitPropertyHandlerTrait
  • 在构造函数中调用了 Trait 中的 __handlePropertyHandler 方法
  • 将原始类 user 方法的内容封装为匿名函数,并作为 self::__proxyCall 方法的参数

其中,ProxyTraitself::__proxyCall 是 AOP 功能的核心部分。然而,由于 Inject 切面并不会修改原始方法的行为,我们可以暂时忽略这部分内容,专注于属性值注入的过程。

当我们实例化某个类时,PHP 会自动调用这个类的构造函数。而构造函数中的 __handlePropertyHandler 方法也会随之被调用。由于类里面不仅仅包含当前类的属性,还可能包含 Trait 和继承自父类的属性,因此 __handlePropertyHandler 方法会通过 PropertyHandlerTrait 中的 __handle 方法,依次为当前类、Trait 以及父类的属性完成注入操作。

__handle 方法中,Hyperf 会遍历提供的属性列表,并根据属性上的 Inject 注解,从 PropertyHandlerManager 中找到相应的回调函数并调用。

Inect 注解的回调函数是在 Hyperf 启动时注册的,该函数会通过属性的类型名称(即类名)从容器中获取到相应的实例,然后通过反射将实例设置为该属性的值,从而完成注入。

$reflectionProperty = ReflectionManager::reflectProperty($currentClassName, $property);  
$reflectionProperty->setAccessible(true);  
$container = ApplicationContext::getContainer();  
if ($container->has($annotation->value)) {  
    $reflectionProperty->setValue($object, $container->get($annotation->value));  
} elseif ($annotation->required) {  
    throw new NotFoundException("No entry or class found for '{$annotation->value}'");  
}

当类实例化完成后,类里面所有需要注入的属性也就完成了注入。

总结

以上就是关于 Inject 工作原理的全部内容了,深入这些底层实现,不仅有助于解决疑难问题,还能在使用框架是更加地得心应手。希望本文能够帮助你更好地理解和应用这些机制。周五的时候,我在 Hyperf 群里看到有群友提出了一个问题:为什么 Inject 注解在使用 new 关键字实例化类时依然能够生效?按理说,Inject 注解不是应该只在通过容器实例化类时才会起作用吗?这个问题引发了群友们的讨论和猜测,甚至有人感叹,Inject 注解的实现简直就是魔法!

对于这个问题,Hyperf 的作者作出了解答:新版本的注入机制通过代理类来实现,注解之所以在 new 关键字下依然有效,是因为实例化的实际上是代理类,而代理类的构造函数中包含了注入操作。

然而,如果我们继续深究,还会发现一些问题:Hyperf 是否为所有类都生成了代理类?又是如何在类实例化时拦截 new 关键字的行为,从而实现实例化的是代理类而非原始类?在属性值注入过程中,具体都执行了哪些操作?

由于微信群本身不太适合深入讨论这些复杂的问题,而且考虑到「深入 Hyperf」系列已经有半年多没更新了,所以我决定撰写这篇文章,逐一解答这些问题,带大家深入探索 Inject 注解的工作原理。

是否为所有类生成了代理类?

Hyperf 生成的所有代理类都保存在 runtime/container/proxy 目录中,仔细观察一下就会发现,该目录只包含了部分原始类的代理类,由此可知,Hyperf 不会为所有类生成代理类。那么,Hyperf 会为哪些类生成代理类呢?答案是,所有需要被切面(Aspect)介入的类。

在 Hyperf 中,可以通过切面(Aspect)介入到任意类的任意方法的执行流程中,从而改变或加强原方法的功能,这就是 AOP(Aspect Oriented Programming)面向切面编程。切面(Aspect)包含了要介入的目标,以及实现对原方法的修改加强处理。这里的介入目标包含了类/方法或者注解,意味着你可以通过类名/方法名称直接指定要介入的类/方法,或者通过注解间接地指定要介入的类/方法。

为了实现 Inject 自动注入的功能,Hyperf 添加了一个 Inject 切面,其介入的目标是使用了 Inject 注解的类。

class InjectAspect extends AbstractAspect
{
    public array $annotations = [
        Inject::class,
    ];

    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        // Do nothing, just to mark the class should be generated to the proxy classes.
        return $proceedingJoinPoint->process();
    }
}

一旦我们在某个类中使用了 Inject 注解,Hyperf 就会为这个类生成代理类。并且从注释中可以看到,Inject 切面不会修改原始方法的任何行为,只是用来标记需要为其生成代理类。

除了 Inject 切面以外,Hyperf 中还包含了很多其它的切面,你可以使用 AspectCollector::list() 获取这些切面。你也可以查看 Hyperf 文档 学习如何自定义切面,提高程序的可重用性以及开发效率。

为什么被实例化的是代理类?

这一切的关键在于 Hyperf 巧妙地利用了 Composer 类自动加载机制,让我们来了解一下其中的细节。

通常情况下,使用了 Composer 的框架都会在入口文件引入一个 /vendor/autoload.php 文件,以启用自动加载功能。在这个文件中,Composer 会通过 spl_autoload_register 函数向 PHP 注册自己的类加载器(ClassLoader)。当我们在 PHP 中使用了当前内存中尚未定义的类的时候(例如使用 new 关键字实例化某个类),PHP 会调用已注册的类加载器来加载这个类文件到内存中,完成类的自动加载。

在 Composer 的类加载器中,有一个 classMap 数组属性,其键是包含命名空间的类名,值是类的文件路径。示例如下:

array:5 [  
  "Hyperf\Cache\AnnotationManager" => "/code/project/vendor/composer/../hyperf/cache/src/AnnotationManager.php"  
  "Hyperf\Cache\Annotation\CacheAhead" => "/code/project/vendor/composer/../hyperf/cache/src/Annotation/CacheAhead.php"  
  "Hyperf\Cache\Annotation\CacheEvict" => "/code/project/vendor/composer/../hyperf/cache/src/Annotation/CacheEvict.php"  
  "Hyperf\Cache\Annotation\CachePut" => "/code/project/vendor/composer/../hyperf/cache/src/Annotation/CachePut.php"  
  "Hyperf\Cache\Annotation\Cacheable" => "/code/project/vendor/composer/../hyperf/cache/src/Annotation/Cacheable.php"  
]

当 Composer 的类加载器运行时,它首先会检查 classMap 数组中是否已经存在这个类。如果类不存在,加载器将按照 PSR-4 或 PSR-0 的规范依次查找类文件;如果存在或找到了类文件,就会使用 include 加载这个类文件。

从整个自动加载过程可以看出,只要在使用某个类之前,将其代理类的路径添加到 classMap 数组中,那么当我们在 PHP 中实例化这个类时,Composer 就会直接加载代理类,而不是原始类。

实际上,Hyperf 确实是这样实现的。在生成所有代理类后,Hyperf 将原始类与代理类的映射关系添加到 Composer 的 classMap 数组中:

$proxyFileDirPath = BASE_PATH . '/runtime/container/proxy/';
$composerLoader = Composer::getLoader();    
  
$scanner = new Scanner($config, $handler);  
$composerLoader->addClassMap( 
	// 在 scan 方法中完成了扫描与生成代理类
    $scanner->scan($composerLoader->getClassMap(), $proxyFileDirPath)  
);

因此,当你在 Hyperf 中使用那些已经生成了代理类的类时,加载的就是 /runtime/container/proxy 目录下的代理类,而非原始类。

Inject 自动注入的过程

属性值的注入操作是在代理类的构造函数中完成的,因此我们需要通过对比原始类与代理类的内容来进一步分析。

从图片中可以看出,代理类相较于原始类增加了以下内容:

  • 引入了 ProxyTraitPropertyHandlerTrait
  • 在构造函数中调用了 Trait 中的 __handlePropertyHandler 方法
  • 将原始类 user 方法的内容封装为匿名函数,并作为 self::__proxyCall 方法的参数

其中,ProxyTraitself::__proxyCall 是 AOP 功能的核心部分。然而,由于 Inject 切面并不会修改原始方法的行为,我们可以暂时忽略这部分内容,专注于属性值注入的过程。

当我们实例化某个类时,PHP 会自动调用这个类的构造函数。而构造函数中的 __handlePropertyHandler 方法也会随之被调用。由于类里面不仅仅包含当前类的属性,还可能包含 Trait 和继承自父类的属性,因此 __handlePropertyHandler 方法会通过 PropertyHandlerTrait 中的 __handle 方法,依次为当前类、Trait 以及父类的属性完成注入操作。

__handle 方法中,Hyperf 会遍历提供的属性列表,并根据属性上的 Inject 注解,从 PropertyHandlerManager 中找到相应的回调函数并调用。

Inect 注解的回调函数是在 Hyperf 启动时注册的,该函数会通过属性的类型名称(即类名)从容器中获取到相应的实例,然后通过反射将实例设置为该属性的值,从而完成注入。

$reflectionProperty = ReflectionManager::reflectProperty($currentClassName, $property);  
$reflectionProperty->setAccessible(true);  
$container = ApplicationContext::getContainer();  
if ($container->has($annotation->value)) {  
    $reflectionProperty->setValue($object, $container->get($annotation->value));  
} elseif ($annotation->required) {  
    throw new NotFoundException("No entry or class found for '{$annotation->value}'");  
}

当类实例化完成后,类里面所有需要注入的属性也就完成了注入。

总结

以上就是关于 Inject 工作原理的全部内容了,深入这些底层实现,不仅有助于解决疑难问题,还能在使用框架是更加地得心应手。希望本文能够帮助你更好地理解和应用这些机制。

posted @ 2024-10-19 17:42  她和她的猫_her-cat  阅读(20)  评论(0编辑  收藏  举报