php ioc容器,PHP 在Swoole中使用双IoC容器实现无污染的依赖注入
容器(container)技術(shù)(可以理解為全局的工廠方法), 已經(jīng)是現(xiàn)代項目的標(biāo)配. 基于容器, 可以進一步實現(xiàn)控制反轉(zhuǎn), 依賴注入. Laravel 的巨大成功就是構(gòu)建在它非常強大的IoC容器 illuminate/container 基礎(chǔ)上的. 而 PSR-11 定義了標(biāo)準(zhǔn)的 container , 讓更多的 PHP 項目依賴容器實現(xiàn)依賴解耦, 面向接口編程.
另一方面, PHP 天生一個進程響應(yīng)一次請求的模型, 已經(jīng)不能完全適應(yīng)開發(fā)的需要. 于是 Swoole, reactPHP, roadrunner 也越來越流行. 它們共同的特點是一個 php worker 進程在生命周期內(nèi)要響應(yīng)多個請求, 甚至同一時間同時運行多個請求 (協(xié)程).
在這些引擎上使用傳統(tǒng)只考慮單請求的容器技術(shù), 就容易發(fā)生單例相互污染, 內(nèi)存泄露等問題 (姑且稱之為”IoC容器的請求隔離問題” ). 于是出現(xiàn)了各種策略以解決之.
多輪對話機器人框架 CommuneChatbot 使用 swoole 做通信引擎, 同時非常廣泛地使用了容器和依賴注入. 在本項目中使用了 “雙容器策略” 來解決 “請求隔離問題” .
所謂”雙容器策略”, 總結(jié)如下:
同時運行 “進程級容器” 與 “請求級容器”
“進程級容器” :
傳統(tǒng)的IoC 容器, 例如 Illuminate/container
“請求級容器” :
所有工廠方法注冊到容器的靜態(tài)屬性上
在 worker 進程初始化階段 注冊服務(wù)
每個請求到來后, 實例化一個請求容器.
請求中生成的單例, 掛載到容器的動態(tài)屬性上.
持有”進程級容器”, 當(dāng)綁定不存在時, 到”進程級容器” 上查找之.
請求結(jié)束時進行必要清理, 防止內(nèi)存泄露
解決方案的代碼在 https://github.com/thirdgerb/container 創(chuàng)建了一個 composer 包 commune/container
容器的”請求隔離”問題
關(guān)于容器, 控制反轉(zhuǎn)與依賴注入
為防止部分讀者不了這些概念, 簡單說明一下.
所謂容器, 相當(dāng)于一個全局的工廠. 可以在這里 “注冊” 各種服務(wù)的工廠方法, 再使用容器統(tǒng)一地獲取. 例如
1 $container = newContainer();2
3 //綁定一個單例
4 $container->singleton(5
//綁定對象的ID, 通常是 interface, 以實現(xiàn)面向接口編程.
6
UserInterface::class,
7
//生成實例的工廠方法.
8
function() {9
return new class implementsUserInterface{};10 }11 );12
13 //從容器中獲取實例
14 $user = $container->get(UserInterfacle::class);15
16 $user instanceof UserInterface; //true
當(dāng)一個類的實例在容器中生成, 或者一個方法被容器調(diào)用時, 就可以方便地實現(xiàn)依賴注入.
簡單來說, 容器通過反射機制可獲取目標(biāo)方法的依賴 ( laravel 用反射來獲取 typehint 類型約束, 而 Swoft項目似乎與spring 相似, 是從注釋上獲取的).
然后容器查找是否已注冊了 依賴 (dependency) 的實現(xiàn) (resolver), 如果已注冊, 就從容器中生成該依賴, 再注入給目標(biāo)方法.
具有依賴注入能力的容器, 我們稱之為 IoC (控制反轉(zhuǎn)) 容器. 關(guān)于IoC 容器的好處不是本文重點, 先跳過去了.
IoC 容器的請求隔離問題
容器最典型的應(yīng)用場景之一, 就是持有單例. 但在 swoole 等引擎上, 一個 worker 進程要響應(yīng)多個請求, 單例的數(shù)據(jù)就容易相互污染.
例如我們把 session 的數(shù)據(jù)放在 一個 SessionInterface 中, 每個邏輯調(diào)用時都用容器來取:
$sessionInstance = container()->make(SessionInterface::class);
由于單例在容器內(nèi)只生成一次, 那第二次請求時, 容器會給出第一次請求的session單例, 從而邏輯就亂套了.
所以容器要運行在 swoole 等引擎上, 必須做到請求與請求相隔離.
常見的解決策略
由于 Laravel 等使用了IoC 容器的項目能帶來極好的工程體驗, 而Swoole 能帶來極大的性能提升, 于是有許多試圖結(jié)合兩者的項目, 都面臨了 “請求隔離問題”.
我個人看到過的解決策略有以下三種, 都能一定程度解決問題, 但也有美中不足之處.
克隆策略:
方案: 每次請求, 克隆一個新的 container
問題:
要遞歸地 clone 屬性, 才能避免淺拷貝導(dǎo)致的污染
無法區(qū)分進程共享的單例, 和請求隔離的單例.
清洗策略:
方案: 每次請求結(jié)束時, 主動清洗掉已注冊的單例
問題:
定義類時就要考慮清洗邏輯, 可能要實現(xiàn)interface, 耦合較重
swoole 發(fā)展到協(xié)程后, 同時可能相應(yīng)多個請求, 清晰策略失效了.
重新注冊:
方案: 每個請求到來時, 實例化一個新容器, 重新注冊所有服務(wù)
問題:
注冊服務(wù)其實開銷很大, 尤其是需要大量讀文件的初始化(比如翻譯組件)
無法區(qū)分進程共享的單例, 和請求隔離的單例.
利用不了 swoole 的優(yōu)勢, 比起多進程模型只少了 composer autoloader 的加載.
CommuneChatbot 遇到的請求隔離問題
總結(jié)
以上是生活随笔為你收集整理的php ioc容器,PHP 在Swoole中使用双IoC容器实现无污染的依赖注入的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle11g备份出错,Oracle
- 下一篇: SSH框架(Struts+Spring+