Lumen中间件源码解析

1.中间件可以做什么

Lumen框架通过设置中间件可以实现业务与非业务逻辑的隔离。在构建api服务的过程中中间件可以做很多工作,例如:

  • 为本次请求生成单独的requestid,可一路透传,用来生成分布式的链路,也可用于在日志中串连单次请求的所有逻辑。
  • 校验web管理页面的token,判断用户登录信息是否失效。
  • 对HTTP的响应体进行压缩(对php来说,一般放在Nginx中使用gzip对响应结构进行压缩),从请求头中读取X-Forwarded-For 和 X-Real-IP,将http.Request中的RemoteAttr修改为得到的RealIP。
  • 打印请求处理日志,统计单次请求各个阶段的耗时工作。

2.Lumen中间件的设置

Lumen中间件分为两种,都需要在/bootstrap/app.php中进行设置

  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)进行设置

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

3.中间件的实现原理和源码剖析

Lumen中间件采用的是装饰器模式(当然也可以说成是责任链、管道模式)。和node.js框架koa2的洋葱模型效果一样。实现的都是对http请求的层层处理,最终返回给用户响应信息。

Lumen中间件处理流程示意图

3.1 实现原理

Lumen框架实现中间件使用到了管道和Closure(匿名函数类),源码不太好理解,笔者准备了一个小demo帮助大家理解:

<?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中的相关方法,例如:

public function getList(Request $r){
    ...//这里是业务逻辑
}
复制代码

接下来的array_reduce是重点,php官方是这样定义的:

array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。

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

猜你喜欢

转载自juejin.im/post/5dbc3f64e51d456f28370b51