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