swoft| 源碼解讀系列二: 啓動階段, swoft 都幹了些啥?

date: 2018-8-01 14:22:17
title: swoft| 源碼解讀系列二: 啓動階段, swoft 都幹了些啥?
description: 閱讀 sowft 框架源碼, 瞭解 sowft 啓動階段的那些事兒php

小夥伴剛接觸 swoft 的時候會感受 壓力有點大, 更直觀的說法是 . 開發組是不同意 這個說法的, swoft 的代碼都是 php 實現的, 而 php 又是 世界上最好的語言, swoft 的代碼閱讀起來是很輕鬆的.html

以後開發組會用 系列源碼 解讀文章, 深刻解析 swoft. 咱們相信, 這會成爲一段輕鬆之旅.laravel

swoft 源碼解讀系列一: 好難! swoft demo 都跑不起來怎麼破? docker 瞭解一下唄~
swoft 源碼解讀系列二: 啓動階段, swoft 都幹了些啥?web

附上社區小夥伴 隨風 製做的流程圖:docker

附上社區小夥伴 **隨風** 製做的流程圖

程序入口

看過 官方文檔-服務啓動與管理 章節, 就知道 swoft 的入口時 php bin/swoft start, 用來啓動 http server. 運行這個命令, 就爲咱們打開了新世界的大門bootstrap

root@e38a7e5aff40 /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 的小夥伴, 就能看到熟悉的:segmentfault

  • master 進程
  • manager 進程
  • worker 進程
  • task-worker 進程

swoole-wiki 上的 運行流程圖進程/線程結構圖 值得細細品味, 這是咱們以後理解和使用 swoole 進行服務器開發的基礎, 這裏按下暫時不表.數組

而咱們爲了弄懂 swoft啓動階段都幹了些啥, 能夠直接運行 php bin/swoft, 慢慢 調試/輸出 便可. 是的, 沒有什麼高級技巧, var_dump() + die() 便可bash

使用工具閱讀源碼的小技巧

沒錯, 這個工具就是 phpstorm, 沒使用 phpstorm 的小夥伴趕忙用起來, 下面以 window 下的快捷鍵爲例:服務器

  • 快捷鍵說明: 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;

root@e38a7e5aff40 /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;

root@e38a7e5aff40 /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/propertiesbootScan 配置的命名空間
    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);
    }

簡單打印一下就能夠知道結果:

root@e38a7e5aff40 /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();
        }
    }

結果以下:

root@e38a7e5aff40 /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.

相關文章
相關標籤/搜索