Lumen中間件源碼解析

1.中間件能夠作什麼

Lumen框架經過設置中間件能夠實現業務與非業務邏輯的隔離。在構建api服務的過程當中中間件能夠作不少工做,例如:php

  • 爲本次請求生成單獨的requestid,可一路透傳,用來生成分佈式的鏈路,也可用於在日誌中串連單次請求的全部邏輯。
  • 校驗web管理頁面的token,判斷用戶登陸信息是否失效。
  • 對HTTP的響應體進行壓縮(對php來講,通常放在Nginx中使用gzip對響應結構進行壓縮),從請求頭中讀取X-Forwarded-For 和 X-Real-IP,將http.Request中的RemoteAttr修改成獲得的RealIP。
  • 打印請求處理日誌,統計單次請求各個階段的耗時工做。

2.Lumen中間件的設置

Lumen中間件分爲兩種,都須要在/bootstrap/app.php中進行設置node

  1. 全局中間件
$app->middleware([
     App\Http\Middleware\ExampleMiddleware::class
 ]);
複製代碼
  1. 路由中間件(須要分配到路由當中)
$app->routeMiddleware([
     'auth' => App\Http\Middleware\Authenticate::class,
     'log' => App\Http\Middleware\Logger::class
]);
複製代碼

路由中間件須要在路由中(一般在./routes/web.php)進行設置web

$app->group(['middleware' => 'Example'], function () use ($app) {
   $app->get('age',['uses'=>'ExampleController@age']);
});
複製代碼

3.中間件的實現原理和源碼剖析

Lumen中間件採用的是裝飾器模式(固然也能夠說成是責任鏈、管道模式)。和node.js框架koa2的洋蔥模型效果同樣。實現的都是對http請求的層層處理,最終返回給用戶響應信息。bootstrap

Lumen中間件處理流程示意圖

3.1 實現原理

Lumen框架實現中間件使用到了管道和Closure(匿名函數類),源碼不太好理解,筆者準備了一個小demo幫助你們理解:api

<?php
interface Middleware
{
    public function handle(Closure $next);
}

class LogMiddleware implements Middleware
{
    public function handle(Closure $next)
    {
        echo "記錄請求日誌" . '<br/>';
        $next();
        echo "記錄響應日誌" . '<br/>';
    }
}

class ApiMiddleware implements Middleware
{
    public function handle(Closure $next)
    {
        echo "Apimiddleware校驗token" . '<br/>';
        $next();
    }
}

class RateLimitMiddleware implements Middleware
{
    public function handle(Closure $next)
    {
        echo "校驗流量限制" . '<br/>';
        $next();
    }
}

function carry($closures, $middleware)
{
    return function () use ($closures, $middleware) {
        return $middleware->handle($closures);
    };
}

function then()
{
    $middlewares = [new LogMiddleware(), new ApiMiddleware(), new RateLimitMiddleware()];
    $prepare = function () {
        echo '<b>用戶處理邏輯,返回響應結果</b>' . '<br/>';
    };
    $go = array_reduce(array_reverse($middlewares), 'carry', $prepare);
    $go();
}

then();
複製代碼

例子中建立了三個Middleware,都實現了Middleware接口。其中handle爲處理邏輯。handle方法接收一個匿名函數類(Closure),handle方法有本身的處理邏輯,這些處理邏輯能夠在匿名函數類執行以前,也能夠在它以後。(這也是洋蔥模型的精要,請求處理通過中間件到邏輯處理函數,返回響應後還會再次通過它)。LogMiddleware在匿名函數類執行以前和以後分別打印了信息;ApiMiddleware和RateLimitMiddleware則分別在匿名函數執行以前打印了信息。數組

咱們直接來看then函數:首先$middleware數組保存了一組中間件;$prepare是一個匿名函數,這裏主要是爲了模擬用戶處理邏輯,對應咱們Lumen中的業務入口Controller中的相關方法,例如:bash

public function getList(Request $r){
    ...//這裏是業務邏輯
}
複製代碼

接下來的array_reduce是重點,php官方是這樣定義的:閉包

array_reduce() 將回調函數 callback 迭代地做用到 array 數組中的每個單元中,從而將數組簡化爲單一的值。app

array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed
複製代碼

參數框架

array 輸入的 array。

callback callback ( mixed $carry , mixed $item ) : mixed carry 攜帶上次迭代裏的值; 若是本次迭代是第一次,那麼這個值是 initial。

item 攜帶了本次迭代的值。

initial 若是指定了可選參數 initial,該參數將在處理開始前使用,或者當處理結束,數組爲空時的最後一個結果。

看完了官方的定義,咱們就比較好理解demo中的carry函數就是中間件的迭代函數了,$prepare會作爲carry函數首次迭代時的第一個參數;carry函數的返回值使用到了閉包

return function () use ($closures, $middleware) {
        return $middleware->handle($closures);
    };
複製代碼

咱們知道函數內部的變量都是局部變量,除非使用global聲明。閉包的使用使得函數內部可使用外部變量。這裏咱們就把上一層中間件的實例和它用到的參數引用了進來。

存儲中間件的數組$middleware爲何要使用array_reverse()函數反轉呢?

由於array_reduce迭代以後,匿名函數所處的位置和數組原來的位置恰好是相反的。通過一次reverse以後,再次通過array_reduce的迭代,造成了一個符合咱們預期的執行鏈路。像洋蔥同樣,一層層的包裹。最後只須要執行then()函數,就能實現咱們上圖所表示的邏輯鏈路了,打印的結果爲:

記錄請求日誌
Apimiddleware校驗token
校驗流量限制
用戶處理邏輯,返回響應結果
記錄響應日誌
複製代碼

若是你們對上述中間件實現過程還不是太明白,建議回過頭來再讀一遍,將demo拷貝到本地運行調試。相信你們明白了這個例子之後,閱讀下面lumen中間件源碼的理解會有一種豁然開朗的感受。

3.2 源碼剖析

框架的入口文件(./app/public/index.php)很是簡單:

$app = require __DIR__.'/../bootstrap/app.php';

$app->run();
複製代碼

run函數是在Trait中定義的:

trait RoutesRequests
{
    protected $middleware = [];

    protected $routeMiddleware = [];
    
    ......
    public function run($request = null)
    {
        $response = $this->dispatch($request);

        if ($response instanceof SymfonyResponse) {
            $response->send();
        } else {
            echo (string) $response;
        }

        if (count($this->middleware) > 0) {
            $this->callTerminableMiddleware($response);
        }
    }
    
    ......
}
複製代碼

框架中的中間件存儲在成員變量$middleware和$routeMiddleware中,咱們接着看dispatch函數:

trait RoutesRequests
{
......
public function dispatch($request = null)
    {
        //解析路由和請求方法
        [$method, $pathInfo] = $this->parseIncomingRequest($request);

        try {
            //啓動服務註冊
            $this->boot();
            //經過向中間件分發請求處理獲得響應信息
            return $this->sendThroughPipeline($this->middleware, function ($request) use ($method, $pathInfo) {
                $this->instance(Request::class, $request);

                if (isset($this->router->getRoutes()[$method.$pathInfo])) {
                    return $this->handleFoundRoute([true, $this->router->getRoutes()[$method.$pathInfo]['action'], []]);
                }

                return $this->handleDispatcherResponse(
                    $this->createDispatcher()->dispatch($method, $pathInfo)
                );
            });
        } catch (Throwable $e) {
            return $this->prepareResponse($this->sendExceptionToHandler($e));
        }
    }
......    
複製代碼

接下來的sendThroughPipeline實現了中間件處理:

trait RoutesRequests
{
......
protected function sendThroughPipeline(array $middleware, Closure $then)
    {
        if (count($middleware) > 0 && ! $this->shouldSkipMiddleware()) {
            return (new Pipeline($this))
                ->send($this->make('request'))
                ->through($middleware)
                ->then($then);
        }

        return $then($this->make('request'));
    }
......
}
複製代碼

咱們只看有邏輯中間件的狀況,先來看then函數(/vendor/illuminate/pipeline/Pipeline.php):

public function then(Closure $destination)
    {
        $pipeline = array_reduce(
            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
        );

        return $pipeline($this->passable);
    }
複製代碼

到這裏,讀者大概就明白了,實現原理和咱們上述demo基本是同樣的。只不過這裏的迭代函數更加的複雜一些,咱們來看一下:

protected function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                if (is_callable($pipe)) {
                  
                    return $pipe($passable, $stack);
                } elseif (! is_object($pipe)) {
                    [$name, $parameters] = $this->parsePipeString($pipe);

                    $pipe = $this->getContainer()->make($name);

                    $parameters = array_merge([$passable, $stack], $parameters);
                } else {
                    $parameters = [$passable, $stack];
                }

                $response = method_exists($pipe, $this->method)
                                ? $pipe->{$this->method}(...$parameters)
                                : $pipe(...$parameters);

                return $response instanceof Responsable
                            ? $response->toResponse($this->getContainer()->make(Request::class))
                            : $response;
            };
        };
    }
複製代碼

注意咱們第二個參數傳入的是:$this->carry();因此實際的迭代函數是第一個return後的匿名對象。這個匿名對象返回的匿名對象(第二個return)接收一個參數$passable,並使用閉包引用了外層$middleware的實例(匿名函數)。

$pipeline($this->passable)中的$this->passable是什麼呢?正是send方法傳入的參數(Request):

class Pipeline implements PipelineContract
{
......
 public function send($passable)
    {
        $this->passable = $passable;

        return $this;
    }
......    
}
複製代碼

那麼$this->carry()中的這個返回值$pipe($passable, $stack);也讓咱們聯想到了中間件中的handle方法了吧:

class ExampleMiddleware
{
 public function handle($request, Closure $next)
    {
        ......
        return $next($request);
        ......
    }
}
複製代碼

array_reduce中的第三個參數咱們也來簡單看一下吧:

protected function prepareDestination(Closure $destination)
    {
        return function ($passable) use ($destination) {
            return $destination($passable);
        };
    }
複製代碼

$passable就是Request對象,$destination是傳入的參數,就是dispatch方法中返回值中的第二個參數,是匿名函數:

return $this->sendThroughPipeline($this->middleware, function ($request) use ($method, $pathInfo) {
                $this->instance(Request::class, $request);

                if (isset($this->router->getRoutes()[$method.$pathInfo])) {
                    return $this->handleFoundRoute([true, $this->router->getRoutes()[$method.$pathInfo]['action'], []]);
                }

                return $this->handleDispatcherResponse(
                    $this->createDispatcher()->dispatch($method, $pathInfo)
                );
            });
複製代碼

這個匿名函數最終返回的結果是「服務容器」根據$method和$pathInfo到路由中解析出來具體的執行邏輯(哪一個Controller的哪一個方法)實例。也就是$destination($passable)對應的是:

class ExampleController extends Controller
{
    public function getList(Request $r)
    {
       .... 
    }
複製代碼

到這裏,相信讀者都已經明白了Lumen中間件的實現原理.

4.小結

中間件是API框架必不可少的一個模塊,其目的都是爲了解耦非業務邏輯代碼和業務邏輯代碼,實現效果是相似於洋蔥同樣的請求處理模型;不一樣的語言有不一樣的特性,node.js框架Ko2是採用的回調函數的形式實現了中間件,只不過其封裝的更加優雅並容易使用;Go語言的特性提倡組合大於繼承,Gin框架利用組合方法的方式也實現了相似Ko2框架中間件的模式。本節講了Lumen框架如何實現中間件,基於php的特性,實現起來確實複雜了一些,相信讀完本文,讀者可以對API框架中間件有更深刻的認識!

相關文章
相關標籤/搜索