深入 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 自动注入的过程
属性值的注入操作是在代理类的构造函数中完成的,因此我们需要通过对比原始类与代理类的内容来进一步分析。
从图片中可以看出,代理类相较于原始类增加了以下内容:
- 引入了
ProxyTrait
和PropertyHandlerTrait
- 在构造函数中调用了 Trait 中的
__handlePropertyHandler
方法 - 将原始类
user
方法的内容封装为匿名函数,并作为self::__proxyCall
方法的参数
其中,ProxyTrait
和 self::__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 自动注入的过程
属性值的注入操作是在代理类的构造函数中完成的,因此我们需要通过对比原始类与代理类的内容来进一步分析。
从图片中可以看出,代理类相较于原始类增加了以下内容:
- 引入了
ProxyTrait
和PropertyHandlerTrait
- 在构造函数中调用了 Trait 中的
__handlePropertyHandler
方法 - 将原始类
user
方法的内容封装为匿名函数,并作为self::__proxyCall
方法的参数
其中,ProxyTrait
和 self::__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 工作原理的全部内容了,深入这些底层实现,不仅有助于解决疑难问题,还能在使用框架是更加地得心应手。希望本文能够帮助你更好地理解和应用这些机制。