Swoft 源碼剖析 - Swoft 中 IOC 容器的實現原理

做者:bromine
連接:https://www.jianshu.com/p/a23...
來源:簡書
著做權歸做者全部,本文已得到做者受權轉載,並對原文進行了從新的排版。
Swoft Github: https://github.com/swoft-clou...php

前言

Swoft爲應用提供一個完整的IOC容器做爲依賴管理方案 ,是Swoft AOP功能,RPC模塊等功能的實現基礎 。
他主要解決的功能有三個:
1. 避免了麻煩地手工管理對象間種種嵌套依賴。
2. 對象的依賴關係再也不在編譯期肯定,提供了運行期改變行爲的更多彈性。
3. 對象能夠再也不依賴具體實現,而是依賴抽象的接口或者抽象類
對依賴管理有興趣的同窗能夠查閱馬丁大叔的這篇文章 <Inversion of Control Containers and the Dependency Injection pattern>html

服務定位器

Bean經過類級別註解@Bean定義,Bean定義後程序能夠直接經過App::getBean()獲取到一個Bean的實例。git

App::getBean()提供 服務定位器 式的依賴管理方式,用於能夠經過訪問服務定位器獲取特定的實例,服務定位器解決了"實例構造,實例間依賴管理,具體實現類選擇"的問題,並對用戶屏蔽相關細節。github

Container->set()方法是App::getBean()底層實際建立bean的方法。原理是經過反射和各類註解(參考註解章節)提供的信息和方法構造Bean的一個代理對象。web

//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;
}

依賴注入

相對於 服務定位器,依賴注入是一種更加先進的依賴管理實踐。segmentfault

服務定位器模式中,客戶端須要調用服務定位器自己,對服務定位器自己存在依賴;
依賴注入模式中,客戶端和依賴注入管理器之間關係也是控制反轉的,客戶端並不知道依賴管理器的存在,由依賴管理器調用客戶端並注入具體的依賴對象。數組

Swoft的依賴注入管理方案基於服務定位器。提供的注入方式有三種:app

屬性注入

/**
 * @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會根據參數類型,參數名等規則自動給方法的參數填充合適的值。

<?php
//App\Controllers\RouteController.php;
/**
 *  這個例子中,除了Request 和Response 是固定的注入特定結構的對象,其餘參數都是根據路由規則注入
 * @RequestMapping(route="user/{uid}/book/{bid}/{bool}/{name}")
 *
 * @param bool                $bool  參考RequestMapping
 * @param Request  $request     
 * @param int                 $bid  
 * @param string              $name
 * @param int                 $uid
 * @param Response $response
 *
 * @return array
 */
public function funcArgs(bool $bool, Request $request, int $bid, string $name, int $uid, Response $response)
{
    //...
}

方法注入的實現較爲零散,每一個方法注入點都會有相似的代碼處理注入的數據,這裏看一下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...
相關文章
相關標籤/搜索