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()
进行解析。
可以参考一下几个关键函数:
new ObjectDefinition($beanName, $className)
创建 bean 定义对象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
单例 beanBean::PROTOTYPE
原型 beanBean::REQUEST
请求 beanBean::SESSION
会话 bean
- alias:表示 bean 的别名,后续再取用的时候,通过 bean 名称或者别名都可以获取 bean。
- scope:表示当前 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 = [];
}
Swoft2 框架精华教程:Controller 组件解析,使用说明
Swoft2 框架精华教程:Config 配置解析,使用说明