swoft| 原始碼解讀系列二: 啟動階段, swoft 都幹了些啥?
date: 2018-8-01 14:22:17
title: swoft| 原始碼解讀系列二: 啟動階段, swoft 都幹了些啥?
description: 閱讀 sowft 框架原始碼, 瞭解 sowft 啟動階段的那些事兒
小夥伴剛接觸 swoft 的時候會感覺 壓力有點大, 更直觀的說法是 難. 開發組是不贊成 難 這個說法的, swoft 的程式碼都是 php 實現的, 而 php 又是 世界上最好的語言, swoft 的程式碼閱讀起來是很輕鬆的.
之後開發組會用 系列原始碼 解讀文章, 深入解析 swoft. 我們相信, 這會成為一段輕鬆之旅.
swoft 原始碼解讀系列一: 好難! swoft demo 都跑不起來怎麼破? docker 瞭解一下唄~
swoft 原始碼解讀系列二: 啟動階段, swoft 都幹了些啥?
附上社群小夥伴 隨風 製作的流程圖:
程式入口
看過 官方文件-服務啟動與管理 章節, 就知道 swoft 的入口時 php bin/swoft start
, 用來啟動 http server. 執行這個命令, 就為我們打開了新世界的大門
[email protected] /v/w/s/swoft# ps aux PID USER TIME COMMAND 1 root 0:00 php -a 708 root 0:01 php-swoft master process (bin/swoft) 709 root 0:00 php-swoft manager process 711 root 0:01 php-swoft task process 712 root 0:01 php-swoft worker process 713 root 0:49 php-swoft reload process 779 root 0:00 ps aux
熟悉 swoole-wiki 的小夥伴, 就能看到熟悉的:
- master 程序
- manager 程序
- worker 程序
- task-worker 程序
swoole-wiki 上的 執行流程圖 和 程序/執行緒結構圖 值得細細品味, 這是我們之後理解和使用 swoole 進行伺服器開發的基礎, 這裡按下暫時不表.
而我們為了弄懂 swoft啟動階段都幹了些啥, 可以直接執行 php bin/swoft
, 慢慢 除錯/輸出 即可. 是的, 沒有什麼高階技巧, var_dump() + die()
即可
使用工具閱讀原始碼的小技巧
沒錯, 這個工具就是 phpstorm
- 快捷鍵說明: C->ctrl A->alt S->shift
- C-b / C-滑鼠點選: 跳轉到方法/函式定義的地方
- C-A-左右方向鍵: 切換游標前後所在的位置
- C-e: 檢視最近開啟的檔案
- C-q: 檢視函式的註釋說明(知道寫註釋有多重要了吧)
- C-p: 檢視函式的引數(還是註釋的重要性)
還有很多好用的功能, 請檢視選單欄的 navigate
選單欄, 去發現驚喜吧~
PS: 註釋! 註釋! 註釋!
入口原始碼: bootstrap
bin/swoft
檔案很簡單:
#!/usr/bin/env php
<?php
require_once __DIR__ . '/bootstrap.php';
$console = new \Swoft\Console\Console();
$console->run();
我們先來看 bin/bootstrap.php
:
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/config/define.php';
// init the factory of bean
\Swoft\Bean\BeanFactory::init();
/* @var \Swoft\Bootstrap\Boots\Bootable $bootstrap*/
$bootstrap = \Swoft\App::getBean(\Swoft\Bootstrap\Bootstrap::class);
$bootstrap->bootstrap();
第一步載入 composer 的 autoload 檔案, 使用 composer 的同學應該都知道吧, 不過你知道 autoload 的原理麼?
第二步是 config/define.php
檔案, 我們進去看看:
// Project base path
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
// Register alias
$aliases = [
'@root' => BASE_PATH,
];
\Swoft\App::setAliases($aliases);
做了 2 件事:
- 定義 PHP 常量
- swoft 的別名機制
swoft 的第一個特性 -- 別名機制 來了. 挺新鮮的詞兒, 本質很簡單 -- 字串替換 而已, 比如上面我們設定 @root
, 我們直接列印看看:
$tmp = \Swoft\App::getAlias('@root');
var_dump($tmp);die;
[email protected] /v/w/s/swoft# php bin/swoft
string(21) "/var/www/swoole/swoft"
使用看看:
$tmp1 = \Swoft\App::getAlias('@root');
$tmp2 = \Swoft\App::getAlias('@root/foo/bar');
var_dump($tmp1, $tmp2);die;
[email protected] /v/w/s/swoft# php bin/swoft
string(21) "/var/www/swoole/swoft"
string(29) "/var/www/swoole/swoft/foo/bar"
目前 swoft 中的別名機制在用在 檔案目錄/路徑 上, 熟悉 yii框架 的小夥伴知道, yii中別名機制用的場景更多一些, 還能拼接 url 等地方. 不過不管使用多少場景, 本質都是 字串替換.
那為什麼不直接使用 PHP常量 這種常規方式, 而要使用別名機制呢? 別名機制不是更優雅麼
框架核心: BeanFactory
到了框架的核心部分了, 閱讀這塊的程式碼要有耐心一點:
// init the factory of bean
\Swoft\Bean\BeanFactory::init();
進入 init()
, 先看第一個:
$properties = self::getProperties(); // 獲取 property 配置
var_dump($properties);die;
看原始碼和除錯驗證輔助: 讀取 config/properties
下的配置(檔案), merge
到同一個數組裡了
再看第二步, 核心的核心, 容器Container 來了, 這裡不再贅述 依賴注入DI/控制反轉IoC 等基礎知識, 不熟悉的小夥伴要去補補哦~
self::$container = new Container();
self::$container->setProperties($properties);
self::$container->autoloadServerAnnotation();
/**
* Register the annotation of server
*/
public function autoloadServerAnnotation()
{
$bootScan = $this->getScanNamespaceFromProperties('bootScan'); // 獲取 property 配置中的 bootScan 配置項
var_dump($bootScan);
$resource = new ServerAnnotationResource($this->properties);
$resource->addScanNamespace($bootScan); // 關鍵在這一句, 要掃描哪些名稱空間(檔案)
$definitions = $resource->getDefinitions();
var_dump($definitions);die;
$this->definitions = array_merge($definitions, $this->definitions);
}
重點來看看 $resource->addScanNamespace($bootScan)
註解的前半生: 要掃描哪些檔案
$resource->addScanNamespace($bootScan)
繼承了抽象基類繞了一下, 最後其實走到了這裡
<?php
namespace Swoft\Bean\Resource;
use Swoft\App;
use Swoft\Helper\ComponentHelper;
/**
* The annotation resource of server
*/
class ServerAnnotationResource extends AnnotationResource
{
/**
* Register the scaned namespace
*/
public function registerNamespace() // 繼承了抽象基類繞了一下, 最後其實走到了這裡
{
$swoftDir = dirname(__FILE__, 5); // 預設掃描路徑, swoft 框架各個元件目錄
var_dump($swoftDir);
var_dump(App::getAlias('@vendor/swoft')); // 使用 alias 可以得出一樣的結果, 可以思考一下為什麼這裡不用別名機制呢?
$componentDirs = scandir($swoftDir);
foreach ($componentDirs as $component) {
if ($component == '.' || $component == '..') {
continue;
}
$componentDir = $swoftDir . DS . $component;
$componentCommandDir = $componentDir . DS . 'src';
if (! is_dir($componentCommandDir)) {
continue;
}
$ns = ComponentHelper::getComponentNamespace($component, $componentDir);
$this->componentNamespaces[] = $ns;
// console component
if ($component == $this->consoleName) { // console 元件特殊處理
$this->scanNamespaces[$ns] = $componentCommandDir;
continue;
}
foreach ($this->serverScan as $dir) { // 預定義的名稱空間
$scanDir = $componentCommandDir . DS . $dir;
if (!is_dir($scanDir)) {
continue;
}
$scanNs = $ns . "\\" . $dir;
$this->scanNamespaces[$scanNs] = $scanDir;
}
}
}
}
/**
* @var array
*/
protected $serverScan
= [
'Command',
'Bootstrap',
'Aop',
];
// $this->scanNamespaces 的內容示例
["Swoft\WebSocket\Server\Bootstrap"]=>
string(65) "/var/www/swoole/swoft/vendor/swoft/websocket-server/src/Bootstrap"
恭喜你, 到這裡你已經理解了一半的註解功能:
- swoft 框架是由一個一個功能元件組成, 詳細內容可以移步 swoft框架元件化改造
- 預設掃描註解包含 2 部分內容:
config/properties
下bootScan
配置的名稱空間
swoft所有元件下的Command Bootstrap Aop
名稱空間, 其中console
元件特殊處理
如果到這裡你感覺比較難理解, 你需要補充一下基礎知識:
- composer 基礎知識: autoload 機制, 名稱空間
- swoft 元件相關知識, 在 composer 基礎知識之上
另外, 上面加的測試程式碼 var_dump(App::getAlias('@vendor/swoft'));
, 可以思考一下 swoft 的別名機制就是為了解決 路徑問題, 為什麼這裡又不用呢?
註解的後半生: 掃描出的結果
$definitions = $resource->getDefinitions();
對應的內容:
/**
* 獲取已解析的配置beans
*
* @return array
* <pre>
* [
* 'beanName' => ObjectDefinition,
* ...
* ]
* </pre>
*/
public function getDefinitions()
{
// 獲取掃描的PHP檔案
$classNames = $this->registerLoaderAndScanBean(); // 掃描上一步註冊進來的名稱空間
$fileClassNames = $this->scanFilePhpClass(); // 額外配置的掃描檔案, 大家可以嘗試一下在哪配置的哦
$classNames = array_merge($classNames, $fileClassNames); // 獲取到所有需要掃面的類
foreach ($classNames as $className) {
$this->parseBeanAnnotations($className); // 解析bean註解
}
$this->parseAnnotationsData(); // 解析註解資料, 存放到 $this->definitions 中
return $this->definitions; // 最後, 我們使用這個就可以獲取到註解解析出來的了類啦
}
// 看一看註解解析出來的例子
["Swoft\WebSocket\Server\Bootstrap\CoreBean"]=>
object(Swoft\Bean\ObjectDefinition)#126 (7) {
["name":"Swoft\Bean\ObjectDefinition":private]=>
string(41) "Swoft\WebSocket\Server\Bootstrap\CoreBean"
["className":"Swoft\Bean\ObjectDefinition":private]=>
string(41) "Swoft\WebSocket\Server\Bootstrap\CoreBean"
["scope":"Swoft\Bean\ObjectDefinition":private]=>
int(1)
["ref":"Swoft\Bean\ObjectDefinition":private]=>
string(0) ""
["constructorInjection":"Swoft\Bean\ObjectDefinition":private]=>
NULL
["propertyInjections":"Swoft\Bean\ObjectDefinition":private]=>
array(0) {
}
["methodInjections":"Swoft\Bean\ObjectDefinition":private]=>
array(0) {
}
}
這裡隱藏了掃描不同型別註解的細節, 因為我們後面閱讀不同元件原始碼時會一一遇到, 這裡只要理解大致原理即可
後面的 2 句比較簡單:
$definition = self::getServerDefinition();
self::$container->addDefinitions($definition);
/**
* @return array
* @throws \InvalidArgumentException
*/
private static function getServerDefinition(): array
{
$file = App::getAlias('@console');
$configDefinition = [];
if (\is_readable($file)) {
$configDefinition = require_once $file;
}
$coreBeans = self::getCoreBean(BootBeanCollector::TYPE_SERVER);
var_dump($coreBeans);die;
return ArrayHelper::merge($coreBeans, $configDefinition);
}
簡單列印一下就可以知道結果:
[email protected] /v/w/s/swoft# php bin/swoft
array(1) {
["commandRoute"]=>
array(1) {
["class"]=>
string(35) "Swoft\Console\Router\HandlerMapping"
}
}
大功告成: 初始化 Bean
self::$container->initBeans(); // 進去檢視
/**
* @throws \InvalidArgumentException
* @throws \ReflectionException
*/
public function initBeans()
{
$autoInitBeans = $this->properties['autoInitBean'] ?? false;
if (!$autoInitBeans) {
return;
}
// 迴圈初始化
foreach ($this->definitions as $beanName => $definition) {
$this->get($beanName);
}
}
/**
* 獲取一個bean
*
* @param string $name 名稱
*
* @return mixed
* @throws \ReflectionException
* @throws \InvalidArgumentException
*/
public function get(string $name)
{
// 已經建立
if (isset($this->singletonEntries[$name])) { // 單例, 初始化過就直接返回
return $this->singletonEntries[$name];
}
// 未定義
if (!isset($this->definitions[$name])) {
throw new \InvalidArgumentException(sprintf('Bean %s not exist', $name));
}
/* @var ObjectDefinition $objectDefinition */
$objectDefinition = $this->definitions[$name];
return $this->set($name, $objectDefinition); // 沒有初始化則進行初始化
}
/**
* 建立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();
if ($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);
// 執行初始化方法
if ($isExeMethod) {
$object->{$this->initMethod}();
}
if (!$object instanceof AopInterface) {
$object = $this->proxyBean($name, $className, $object);
}
// 單例處理
if ($scope === Scope::SINGLETON) {
$this->singletonEntries[$name] = $object;
}
return $object;
}
Bean 初始化的所有細節都在這裡了:
- 註解解析後獲取到的類相關的所有資訊
- 注入建構函式(construct)
- 初始化類(new), 此時會執行建構函式
- 注入屬性(property)
- 執行初始化方法, 這就是為什麼 Bean 裡面定義的
init()
也會執行的 - AOP處理, 找到實際代理的類
- 單例處理
- 返回生成好的 Bean 物件
到這裡 整個 swoft 核心中的核心 就已經呈現在你面前了, 總結起來也很簡單:
- swoft啟動時要去哪裡掃描註解
- swoft掃描到的註解如何初始化 Bean
有了 \Swoft\Bean\BeanFactory::init();
以後, 我們需要使用 Bean, 只需要:
\Swoft\Bean\BeanFactory::getBean('xxx');
// 下面的寫法只是一層封裝而已
\Swoft\App::getBean('xxx');
/**
* get bean
*
* @param string $name 名稱
*
* @return mixed
*/
public static function getBean(string $name)
{
return ApplicationContext::getBean($name);
}
bootstrap階段的最後: 各項配置
通過在合適的地方列印:
/* @var \Swoft\Bootstrap\Boots\Bootable $bootstrap*/
$bootstrap = \Swoft\App::getBean(\Swoft\Bootstrap\Bootstrap::class);
var_dump($bootstrap);
$bootstrap->bootstrap();
/**
* bootstrap
*/
public function bootstrap()
{
$bootstraps = BootstrapCollector::getCollector(); // 需要執行哪些 bootstrap
var_dump($bootstraps);die;
$temp = \array_column($bootstraps, 'order');
\array_multisort($temp, SORT_ASC, $bootstraps);
foreach ($bootstraps as $bootstrapBeanName => $name){
/* @var Bootable $bootstrap*/
$bootstrap = App::getBean($bootstrapBeanName);
$bootstrap->bootstrap();
}
}
結果如下:
[email protected] /v/w/s/swoft# php bin/swoft
object(Bootstrap_5b6dd8716a6dc)#209 (1) {
["__handler_5b6dd8716a6dc":"Bootstrap_5b6dd8716a6dc":private]=>
object(Swoft\Proxy\Handler\AopHandler)#188 (1) { # 用到了 aop
["target":"Swoft\Proxy\Handler\AopHandler":private]=>
object(Swoft\Bootstrap\Bootstrap)#186 (0) {
}
}
}
array(3) { # 真正執行的 bootstrap
["Swoft\Bootstrap\Boots\InitPhpEnv"]=> # init php env
array(2) {
["name"]=>
string(0) ""
["order"]=>
int(2)
}
["Swoft\Bootstrap\Boots\LoadEnv"]=> # 載入 .env 檔案
array(2) {
["name"]=>
string(0) ""
["order"]=>
int(1)
}
["Swoft\Bootstrap\Boots\LoadInitConfiguration"]=> # 載入 config 目錄的其他配置
array(2) {
["name"]=>
string(0) ""
["order"]=>
int(3)
}
}
至此, bootstrap 階段的所有工作就完成了
swoft 中的 bean 到底是啥咧
回答 bean 是啥之前, 先記住: 一切皆物件
我們使用對面物件的方式來對問題進行抽象, 並使用抽象出來的類例項化後的物件來解決問題, 而例項化後的物件, 就是 swoft 中一個又一個的 Bean
回顧我們整個 bootstrap 階段, 可以概括為自動化做了 2 件事情:
- 根據預設的註解掃描機制, 例項化 Bean
- 根據
config/
.env
等配置中中的 bean/property, 對 swoft 中的 Bean 進行配置(例項化 Bean, 或者配置 Bean 的 property)
這樣通過配置來示例化類和配置物件屬性的方式, 在 php 框架中大型其道, 典型的如 yii/laravel.
原文地址:https://segmentfault.com/a/1190000015966514