Swoft 源码剖析 - Swoft 中 IOC 容器的实现原理

作者: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...

你可能感兴趣的:(php,swoole)