Swoft 框架精华教程:Bean 定义的实例化
- 作者: 刘杰
- 来源: 技术那些事
- 阅读:91
- 发布: 2025-06-18 09:43
- 最后更新: 2025-08-24 17:15
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
单例 bean -
Bean::PROTOTYPE
原型 bean -
Bean::REQUEST
请求 bean -
Bean::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 = [];
}
标记了 @Bean 注解的实例,属性注入和 init 方法哪个优先级高
一个类如果用 @Bean 注解标记,那么这个bean实例被初始化时候,如果有属性注入,它会优先注入属性,然后在执行 init 方法。具体代码请参见:Container::newBean
方法。
php
private function newBean(string $beanName, string $id = '') {
// ...... 以上代码省略
// 先进行映射类对象的的实例化,也就是优先执行构造函数
$reflectObject = $this->newInstance($reflectClass, $constructArgs);
// 获取当前对象的注入属性
$propertyInjects = $objectDefinition->getPropertyInjections();
// 给当前对象属性注入相关的值或者 bean 实例
$this->newProperty($reflectObject, $reflectClass, $propertyInjects, $id);
// Alias name
// Fix: $aliasId !== $id for deny loop get
if ($alias && $alias !== $beanName) {
$this->aliases[$alias] = $beanName;
}
// 如果类中有 init 方法,调用类中的 init 方法
if ($reflectClass->hasMethod(self::INIT_METHOD)) {
$reflectObject->{self::INIT_METHOD}();
}
// 将当前类的实例化对象,根据 scope 放到容器中的不同对象池,并返回类的实例化对象
return $this->setNewBean($beanName, $scope, $reflectObject, $id);
}