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框架中间件有更深刻的认识!

相关文章
相关标签/搜索