作者:bromine
链接:https://www.jianshu.com/p/a23...
來源:简书
著作权归作者所有,本文已获得作者授权转载,并对原文进行了重新的排版。
Swoft Github: https://github.com/swoft-clou...
前言
Swoft为应用提供一个完整的IOC容器作为依赖管理方案 ,是Swoft AOP功能,RPC模块等功能的实现基础 。
他主要解决的功能有三个:
1. 避免了麻烦地手工管理对象间种种嵌套依赖。
2. 对象的依赖关系不再在编译期确定,提供了运行期改变行为的更多弹性。
3. 对象可以不再依赖具体实现,而是依赖抽象的接口或者抽象类
对依赖管理有兴趣的同学可以查阅马丁大叔的这篇文章
服务定位器
Bean通过类级别注解@Bean
定义,Bean定义后程序可以直接通过App::getBean()
获取到一个Bean的实例。
App::getBean()
提供 服务定位器 式的依赖管理方式,用于可以通过访问服务定位器获取特定的实例,服务定位器解决了"实例构造,实例间依赖管理,具体实现类选择"的问题,并对用户屏蔽相关细节。
Container->set()
方法是App::getBean()
底层实际创建bean的方法。原理是通过反射和各种注解(参考注解章节)提供的信息和方法构造Bean的一个代理对象。
//Swoft\Bean\Container.php
/**
* 创建Bean
*
* @param string $name 名称
* @param ObjectDefinition $objectDefinition bean定义
* @return object
* @throws \ReflectionException
* @throws \InvalidArgumentException
*/
private function set(string $name, ObjectDefinition $objectDefinition)
{
// bean创建信息
$scope = $objectDefinition->getScope();
$className = $objectDefinition->getClassName();
$propertyInjects = $objectDefinition->getPropertyInjections();
$constructorInject = $objectDefinition->getConstructorInjection();
//ref属性重定向依赖查找,一般用于在Interface这种需要具体实现类的Bean上,用于指定实际使用的实现类
if (!empty($objectDefinition->getRef())) {
$refBeanName = $objectDefinition->getRef();
return $this->get($refBeanName);
}
// 构造函数参数注入
$constructorParameters = [];
if ($constructorInject !== null) {
$constructorParameters = $this->injectConstructor($constructorInject);
}
$reflectionClass = new \ReflectionClass($className);
$properties = $reflectionClass->getProperties();
// 通过反射new实例
$isExeMethod = $reflectionClass->hasMethod($this->initMethod);
$object = $this->newBeanInstance($reflectionClass, $constructorParameters);
// 属性注入
$this->injectProperties($object, $properties, $propertyInjects);
// 执行Swoft Bean约定的初始化方法`init()`
if ($isExeMethod) {
$object->{$this->initMethod}();
}
//动态代理,具体见AOP章节
if (!$object instanceof AopInterface) {
$object = $this->proxyBean($name, $className, $object);
}
// 单例处理
if ($scope === Scope::SINGLETON) {
$this->singletonEntries[$name] = $object;
}
return $object;
}
依赖注入
相对于 服务定位器,依赖注入是一种更加先进的依赖管理实践。
在服务定位器模式中,客户端需要调用服务定位器本身,对服务定位器本身存在依赖;
在依赖注入模式中,客户端和依赖注入管理器之间关系也是控制反转的,客户端并不知道依赖管理器的存在,由依赖管理器调用客户端并注入具体的依赖对象。
Swoft的依赖注入管理方案基于服务定位器。提供的注入方式有三种:
属性注入
/**
* @Reference("user")
* @var \App\Lib\MdDemoInterface
*/
private $mdDemoService;
/**
* @Inject()
* @var \App\Models\Logic\UserLogic
*/
private $logic;
/**
* the name of pool
*
* @Value(name="${config.service.user.name}", env="${USER_POOL_NAME}")
* @var string
*/
protected $name = "";
上面@Reference,@Inject,@value三者是典型的属性注入用的注解声明,在一个Bean类中声明这三种注解的属性会分别被注入特定的Rpc客户端代理对象 , 普通的Bean代理对象 ,和配置文件配置值。
属性注入元信息的解析
Bean的各个属性的注入信息是在注解搜集阶段完成的,即在Swoft的启动阶段就已经完成
//Swoft\Bean\Wrapper\AbstractWrapper.php
/**
* 属性解析
*
* @param array $propertyAnnotations
* @param string $className
* @param string $propertyName
* @param mixed $propertyValue
*
* @return array
*/
private function parsePropertyAnnotations(array $propertyAnnotations, string $className, string $propertyName, $propertyValue)
{
$isRef = false;
$injectProperty = "";
// 没有任何注解
if (empty($propertyAnnotations) || !isset($propertyAnnotations[$propertyName])
|| !$this->isParseProperty($propertyAnnotations[$propertyName])
) {
return [null, false];
}
// 属性注解解析
foreach ($propertyAnnotations[$propertyName] as $propertyAnnotation) {
$annotationClass = get_class($propertyAnnotation);
if (!in_array($annotationClass, $this->getPropertyAnnotations())) {
continue;
}
// 使用具体的解析器(如ValueParser,ReferenceParser等)解析注入元信息
$annotationParser = $this->getAnnotationParser($propertyAnnotation);
if ($annotationParser === null) {
$injectProperty = null;
$isRef = false;
continue;
}
list($injectProperty, $isRef) = $annotationParser->parser($className, $propertyAnnotation, $propertyName, "", $propertyValue);
}
return [$injectProperty, $isRef];
}
$isRef
决定属性需要注入一个Bean还是一个标量值$injectProperty
指代该属性要注入的Bean名或者具体标量值
这两者最终会封装进一个Swoft\Bean\ObjectDefinition
对象中并保存在AnnotationResource->$definitions
中
属性注入
属性注入在调用服务定位器App::getBean()
生成Bean的时候进行,此时服务定位器根据之前解析到的$isRef
,$injectProperty
信息注入特定的值到属性中。
// Swoft\Bean\Container.php
/**
* 注入属性
*
* @param mixed $object
* @param \ReflectionProperty[] $properties $properties
* @param mixed $propertyInjects
* @throws \InvalidArgumentException
*/
private function injectProperties($object, array $properties, $propertyInjects)
{
foreach ($properties as $property) {
//...
// 属性是数组
if (\is_array($injectProperty)) {
$injectProperty = $this->injectArrayArgs($injectProperty);
}
// 属性是bean引用
if ($propertyInject->isRef()) {
$injectProperty = $this->get($injectProperty);
}
if ($injectProperty !== null) {
$property->setValue($object, $injectProperty);
}
}
属性注入依赖于服务定位器,如果一个对象是由用户手动new出来的,将不会获得属性注入功能。
方法参数注入
Swoft有很多框架按照约定直接调用Bean的特定方法的地方,如框架会在收到web请求的时候调用Controllert的某个action方法,如果有合适的AOP连接点会调用对应的通知方法.....
在这些框架调用的种种方法中基本都支持方法参数注入,Swoft会根据参数类型,参数名等规则自动给方法的参数填充合适的值。
方法注入的实现较为零散,每个方法注入点都会有类似的代码处理注入的数据,这里看一下action的注入处理。action的参数注入处理代码在HandlerAdapter->bindParams()
中
//Swoft\Http\Server\Route\HandlerAdapter.php
/**
* binding params of action method
*
* @param ServerRequestInterface $request request object
* @param mixed $handler handler
* @param array $matches route params info
*
* @return array
* @throws \ReflectionException
*/
private function bindParams(ServerRequestInterface $request, $handler, array $matches)
{
if (\is_array($handler)) {
list($controller, $method) = $handler;
$reflectMethod = new \ReflectionMethod($controller, $method);
$reflectParams = $reflectMethod->getParameters();
} else {
$reflectMethod = new \ReflectionFunction($handler);
$reflectParams = $reflectMethod->getParameters();
}
$bindParams = [];
// $matches = $info['matches'] ?? [];
$response = RequestContext::getResponse();
// binding params
foreach ($reflectParams as $key => $reflectParam) {
$reflectType = $reflectParam->getType();
$name = $reflectParam->getName();
// 未定义参数类型直接使用$matches对应值
if ($reflectType === null) {
if (isset($matches[$name])) {
$bindParams[$key] = $matches[$name];
} else {
$bindParams[$key] = null;
}
continue;
}
/**
* @notice \ReflectType::getName() is not supported in PHP 7.0, that is why use __toString()
*/
$type = $reflectType->__toString();
//若类型的特定类型如Request/Response,直接注入对应对象,否则注入类型转换后的$matches对应值
if ($type === Request::class) {
$bindParams[$key] = $request;
} elseif ($type === Response::class) {
$bindParams[$key] = $response;
} elseif (isset($matches[$name])) {
$bindParams[$key] = $this->parserParamType($type, $matches[$name]);//类型强转处理
} else {
$bindParams[$key] = $this->getDefaultValue($type);//提供一个指定类型的默认值(等价于0)
}
}
return $bindParams;
}
$matches
对应的是REST模板型路由特定字段的具体值,举个例子。若实际访问/user/100
,其匹配的路由为/user/{uid}
,则$matches
会存储['uid'=>'100']
信息。
其他 方法参数注入点 的实现大同小异
构造器注入
Swoft当前的构造器注入实现尚不完整,可能还有变动,这里就先不说了。
Swoft源码剖析系列目录: https://segmentfault.com/a/11...