惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Spread Privacy
Spread Privacy
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Forbes - Security
Forbes - Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Secure Thoughts
WordPress大学
WordPress大学
Microsoft Security Blog
Microsoft Security Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
Security Latest
Security Latest
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
S
SegmentFault 最新的问题
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
N
Netflix TechBlog - Medium
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
H
Hacker News: Front Page
C
Cybersecurity and Infrastructure Security Agency CISA
小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
AWS News Blog
AWS News Blog
AI
AI
G
GRAHAM CLULEY
IT之家
IT之家
P
Privacy & Cybersecurity Law Blog
L
Lohrmann on Cybersecurity
Last Week in AI
Last Week in AI
D
Docker
Recent Announcements
Recent Announcements
O
OpenAI News
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
S
Security @ Cisco Blogs
T
Troy Hunt's Blog
C
Check Point Blog
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
N
News and Events Feed by Topic

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 译|理解容器镜像层 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
深入 Hyperf:Inject 注解是如何工作的?
她和她的猫 · 2024-08-25 · via 文章列表 - 她和她的猫

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

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

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

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

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

Hyperf 生成的所有代理类都保存在 runtime/container/proxy 目录中,仔细观察一下就会发现,这个目录只包含了部分原始类的代理类。

由此可知,Hyperf 不会为所有类生成代理类

那么,Hyperf 会为哪些类生成代理类呢?答案是,所有需要被切面(Aspect)介入的类。

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

为了实现 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 目录下的代理类,而非原始类。

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

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

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

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

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

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

Inject 注解的回调函数是在 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 注解的属性,也就完成了属性值的自动注入。

总结

以上就是关于 Inject 的全部内容了,希望这篇文章能够帮助你更好地理解和使用这些组件。