PHP Swoft2 框架精华系列:Bean 定义的实例化

Bean 定义的实例化

Definition 的定义,我们都知道,主要在两个位置,一个是各个组件的 Autoloader 中的 beans() 方法中。另一个是在 app/bean.php 文件中。也就是所谓的核心 bean 的定义。那这些 bean 是如何实例化的呢?相关的配置信息又是如何注入到 bean 实例中的呢?

核心 bean Definition 的解析

bean 的来源

核心 bean 的实例化,在启动过程中,属于 bean 进程对象实例处理的阶段。

php 复制代码
$handler     = new BeanHandler();
// 获取核心 bean 的定义
$definitions = $this->getDefinitions();
$parsers     = AnnotationRegister::getParsers();
$annotations = AnnotationRegister::getAnnotations();
// 将核心 bean 的定义给 BeanFactory(内部给了 Container)
BeanFactory::addDefinitions($definitions);
BeanFactory::addAnnotations($annotations);
BeanFactory::addParsers($parsers);
BeanFactory::setHandler($handler);
// BeanFactory 初始化过程,就是 Container 容器对以上来源不同的 bean 的实例化过程
BeanFactory::init();

$stats = BeanFactory::getStats();

从以上代码可以看到,bean 来源主要是两个:

  • 核心 bean 的定义
    • 组件中 Autoloader::beans 方法中的返回的 bean 定义(优先级低)
    • app/bean.php文件中的 bean 定义(优先级高)
  • 注解
    • 注解的解析器
    • 注解的内容
bean 定义的解析代码跟踪入口

我们从 BeanFatory::init()开始说起。这个bean 工厂的初始化,其实是内部容器 Container 的一个初始化过程。这个过程又分为了三个部分:

  • 注解的解析
  • bean 定义的解析
  • 初始化 bean

DefinitionObjParser->parseDefinitions() 进行解析。

可以参考一下几个关键函数:

  1. new ObjectDefinition($beanName, $className) 创建 bean 定义对象
  2. DefinitionObjParser->updateObjectDefinitionByDefinition 进一步设置 bean 定义对象中的构造参数注入信息、属性注入信息、
bean 定义的主要构成

通过 DefinitionObjParser->parseDefinition 函数可以得出一个 bean 的定义的主要组成:

  • class:用来实例化 bean 的类
  • 无键名的数组:用来配置 bean 的构造函数参数。
    • 属性值和构造函数参数值,都可以通过 ${xxx}这种形式 , 注入Bean和引用配置信息
      • ${dbTest}表示引用一个名为 dbTest 的 bean。
      • ${.config.db.name}表示引用主配置 base.php中的 db['name'] 或者 db.php 中的 name 键。
      • ${.db.name}等同于上方配置(config 会被自动去掉)
  • 键名:和绑定的 class 类的成员属性名称对应
  • __option 可选的注入信息
    • scope:表示当前 bean 实例的作用范围。
      • Bean::SINGLETON 单例 bean
      • Bean::PROTOTYPE 原型 bean
      • Bean::REQUEST 请求 bean
      • Bean::SESSION 会话 bean
    • alias:表示 bean 的别名,后续再取用的时候,通过 bean 名称或者别名都可以获取 bean。
php 复制代码
db => [
    'class'  => Database::class, // 对应 bean 实例化的类
    'dsn'    => 'mysql:dbname=dbname;host=127.0.0.1', // 对应 Database::class 中的 dsn 属性
    'config' => [	// 对应 Database::class 中的 config 属性
        // fetch array
        'fetchMode' => PDO::FETCH_ASSOC, // config 中的一个项
    ],
    [
        'arg1' => '${.arg1}', // 表示引用主配置文件信息 arg1 项
        'arg2' => '${.config.arg2}' // 表示引用主配置文件 arg2 项
    ]
    '__option' => [
        'aaa' => 'xxx'
    ]
]

以上只是一个示例,实际 Database 类并没有__construct函数,找了半天没有找到合适的示例,只能造个假的了,但是规则是一样的。

注意以上定义中,键名应该都用字符串类型的数据,否则可能会导致实例化后对应的参数注入失败。

配置信息的注入

bean 定义中的键值的部分(不管是属性,还是构造函数),如果是以美元符号开头的字符串${xxx},表示数据是一个引用值。

${.xxx}花括号中为点开始的数据,表示引用的配置。不是以点开始的表示引用的是一个 bean 实例。

如果不是以美元符号和花括号表示的引用值,则表示这个是一个固定值,会直接注入到对应的属性或者参数中。

Bean 的实例化

DefinitionObjParser 结构说明

bean 定义的解析之后,所有的 bean 的定义数据会存储在 DefinitionObjParser 中。

  • classNames:存储同一个类对应的不同的 bean(多个不同的bean ,可以是同一个类的实例化对象)。
  • objectDefinitions:存储每个 beanName 对应的全部的定义信息。
  • definitions:存储用户的 bean 的定义(未经解析过的)。
  • alias:关联数组,存储每个别名对应的原始的 beanName。

以上数据结构示例如下:

php 复制代码
// 假如以下配置为 bean.php 定义 beanName1 和 beanName2(Autoloader::beans() 无配置)
return [
    'beanName1' => [
        'class' => Test::class,
        '__option' => [
            'scope' => Bean::SINGLETON
        ]
    ],
    'beanName2' => [
        'class' => Test::class,
        '__option' => [
            'scope' => Bean::SINGLETON
        ]
    ]
];
// classNames 结构如下:
class DefinitionObjParser {
    // merge 后的定义数组(bean.php 和 Autoloader::beans() merge 的结果)
    protected $definitions = [
        'beanName1' => [
            'class' => Test::class,
            '__option' => [
                'scope' => Bean::SINGLETON
            ]
    	],
        'beanName2' => [
            'class' => Test::class,
            '__option' => [
                'scope' => Bean::SINGLETON
            ]
        ]
    ];
    // 记录每个定义中类关联的所有实例名称
	protected $classNames[
        Test::class => ['beanName1', 'beanName2'],
    ];
    // 记录 ObjectDefinition 对象集合
    protected $objectDefinitions = [
        // 不同的实例名称关联一个 ObjectDefinition 实例
        'beanName1' => new ObjectDefinition('beanName1', Test::class),
        'beanName2' => new ObjectDefinition('beanName2', Test::class),
    ];
}
Bean 实例化过程细节解析
definition 的合并

将 bean.php 和 Autoloader::beans() 获取到的 bean 配置进行合并。同一个 bean 实例名称的,合并为一个实例配置。

bean.php 配置的优先级要高于 Autoloader::beans() 中配置。

php 复制代码
// 实际的合并过程可以理解为如下:
$definitions = array_merge(Autoloader::beans(), require bean.php);
将用户定义的 definitions 转换成 ObjectDefinition

DefinitionObjParser->parseDefinitions()解析 bean 定义的两个重要函数,

  • createObjectDefinition

    当发现一个新的 bean 定义的时候会生成一个 ObjectDefinition 对象

  • resetObjectDefinition

    当同一个 bean 名字已经存在一个 ObjectDefinition 对象时候,要更新这个对象的结构

转成成 ObjectDefinition 后,这些集合数据存储在 DefinitionObjParser->objectDefinitions

注意:DefinitionObjParser->parseDefinitions() 后,其中的 classNames 结构被复制到了 Container 中。他们两个结构是一致的。

将 ObjectDefinition 转换成 bean 实例,存储到容器中

遍历上一步中DefinitionObjParser->objectDefinitions,将每个 bean 按照 scope 不同,分别放到不同的对象池中。

Container->newBean(beanName) 对每个 bean 进行实例化。

Container->setNewBean(string $beanName, string $scope, $object, string $id = '') 此函数按照生存期(scope)不同放到容器的不同的对象池中,如下:

  • singletonPool
  • prototypePool
  • requestPool
  • sessionPool

配置 Bean 定义常见问题

通过构造函数实例化的 bean

如果配置的是通过构造函数进行初始化的 bean,这个时候尤其小心,因为 bean 的最终定义是通过

array_merge('组件Autolaod中定义', 'bean.php 文件中定义') 进行合并的,且构造函数参数默认是 Definition 数组中的索引号为 0 的数组。多次配置后,经过merge 会生成多组构造函数参数。

如果在 Autolaoder.php beans 方法中配置过构造函数,但是 在 bean.php 中(或者其他任何能够配置bean定义的地方)再次配置了构造函数。这个时候,array_merge 会让第二次的配置,索引自动变为1,那么在实际这个定义进行实例化的时候,就会多出一组构造函数,无法与任何属性,对应上。进而抛出一个异常:

php 复制代码
InvalidArgumentException(code:0) Property key from definition must be string

所以,同一个bean实例,构造函数实例化的bean 不管在任何地方配置,构造数数组,只能配置一次

如果想要配置其他实例,更改 bean 名称与之前的 bean 名称不同即可,这样实例化后是两个 bean 示例,也不会影响。

单例 bean 和 prototype bean 配置差异

通过以上实例化 bean 的细节可知,不同的 beanName 生成的实例是不一样的,即使 class 是同一个。这个跟配置定义时候的生存期 scope 也无关。

通过不同 beanName 获取的实例是不同的。但是通过类全路径名称获取到的是,当前类关联的所有类实例中的最后一个。如下示例:

php 复制代码
// 通过 bean(Test::class) 获取到的是 bean2 对应的实例。
class Container {
    private $classNames = [
        Test::class => ['bean1', 'bean2']
    ]
        
    private $singletonPool = [
        'bean1' => new Object,
        'bean2' => new Object,
    ];

    private $prototypePool = [];
}

PHP Swoft2 开源框架系列教程专栏推荐

Swoft2 框架精华教程:Validator 校验器详解

Swoft 框架精华教程:Devtool 详解

Swoft2 框架精华教程:Controller 组件解析,使用说明

Swoft2 框架精华教程:Config 配置解析,使用说明

Swoft2 框架精华教程:CLog 使用篇

Swoft2 框架精华教程:数据库 Migration

Swoft2 框架精华教程:数据库操作

Swoft2 框架精华教程: Swoft 组件开发单元测试

Swoft2 框架精华教程:面向切面编程(Aspect)

Swoft2 框架精华教程:Annotation 注解机制详解

Swoft 框架精华教程:Bean 定义的实例化

相关推荐
BingoGo12 小时前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack12 小时前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack1 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo1 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack3 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理3 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082853 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php
WeiXin_DZbishe3 天前
基于django在线音乐数据采集的设计与实现-计算机毕设 附源码 22647
javascript·spring boot·mysql·django·node.js·php·html5
longxiangam3 天前
Composer 私有仓库搭建
php·composer
上海云盾-高防顾问3 天前
DNS异常怎么办?快速排查+解决指南
开发语言·php