首页 > 其他分享 >深入 Hyperf:Inject 注解是如何工作的?

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

时间:2024-10-19 17:46:55浏览次数:1  
标签:切面 代理 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 工作原理的全部内容了,深入这些底层实现,不仅有助于解决疑难问题,还能在使用框架是更加地得心应手。希望本文能够帮助你更好地理解和应用这些机制。

标签:切面,代理,Hyperf,Inject,注解,加载
From: https://www.cnblogs.com/her-cat/p/18476408/how-does-hyperf-inject-annotation-work

相关文章

  • SpringBoot 项目的方法名是否添加@Transactional注解,以及SQL语句(SQLServer数据库)是
    项目改用SpringDataJDBC并手动配置DataSource之后,@Transactional注解一直不起作用。这两天研究了一下,注解不起作用,主要是没有配置TransactionManager的事,配置完TransactionManager之后,@Transactional注解就起作用了。但是配置完又发现,用jdbcTemplate.queryForList()方法执......
  • spring注解解析与configurationClassPostProcessor(1)
    上个章节讲解了spring启动时解析spring.xml的流程,本章主要解析对注解的解析;目前我们常用的是AnnotationConfigApplicationContext,其中MyApp就是启动类ApplicationContextctx=newAnnotationConfigApplicationContext(MyApp.class);this()方法中可以看到reader为Annotated......
  • 【Springboot】注解EqualsAndHashCode
    先看问题,如图所示注解解释@EqualsAndHashCode作用与子类上callSuper=true,根据子类自身的字段值和从父类继承的字段值来生成hashcode,当两个子类对象比较时,只有子类对象的本身的字段值和继承父类的字段值都相同,equals方法的返回值是true。callSuper=false,根据子类......
  • springboot使用自定义注解将对象注入容器中
    在SpringBoot中,你可以通过自定义注解和Spring的`BeanPostProcessor`来将对象注入到Spring容器中。以下是一个简单的实现步骤:1.**创建自定义注解**:importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.Reten......
  • laravel 中实现注解注入
    laravel中实现注解注入创建注解类<?phpdeclare(strict_types=1);namespaceApp\Support\Attributes;#[\Attribute(\Attribute::TARGET_PROPERTY)]readonlyclassInjection{publicfunction__construct(public?string$propertyType=null,p......
  • spring上 -基于注解配置bean,动态代理,AOP笔记
     用的是jdk8,spring框架里jar包的下载可以自己搜到注解用到的jar包。  60,注解配置Bean快速入门 基本介绍 代码结构: UserDao.javapackagecom.hspedu.spring.component;importorg.springframework.stereotype.Repository;/**使用@Repository标识该......
  • Python入门-面相对象——class(类)、封装、继承、多态、类型注解
    面向对象面向对象就是设计一个类,基于类创建对象,并使用创建出来的类完成具体的工作面向对象的三大特性:封装、继承、多态面向对象基本概述:属性:名词,用来描述事物的外在特征的,例如:姓名,性别,年龄,身高,体重...行为:动词,表示事物能够做什么,例如:......
  • 【攻防世界】Web_python_template_injection
    Web_python_template_injection:python模板漏洞python的flask模板注入的题思路比较固定,Jinja2模板引擎中,{{}}是变量包裹标识符。{{}}并不仅仅可以传递变量,还可以执行一些简单的表达式。1.先判断是否存在注入{{config}}2.获取基本类:{{''.__class__.__mro__}}.__class......
  • Spring注解之 @Autowired @Qualifier
    在Spring框架中,@Autowired和@Qualifier是两个常用注解,用于依赖注入(DependencyInjection)时选择和管理Spring容器中的Bean。1.@Autowired@Autowired注解用于自动注入依赖项。Spring容器会自动将符合类型的Bean注入到带有@Autowired注解的字段、构造器或方法......
  • Java中注解的学习
    元注解目录元注解什么是元注解5种元注解@Retention@Documented@Target@Inherited@Repeatable什么是元注解元注解是用于定义注解的注解(或者说元注解是一种基本注解,它能够应用到其它的注解上面);元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的标签进行......