前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Hyperf源码分析 - Http 路由

Hyperf源码分析 - Http 路由

原创
作者头像
黄振炼
修改2023-09-28 11:12:01
3030
修改2023-09-28 11:12:01
举报
文章被收录于专栏:HcmsHcms

FastRoute

在官方文档提到 默认情况下路由由 nikic/fast-route 提供支持,并由 hyperf/http-server 组件负责接入到 Hyperf 中,RPC 路由由对应的 hyperf/rpc-server 组件负责

路由收集

路由收集在服务启动,初始化服务注册服务事件的时候,在Hyperf 启动章节中有提到。在初始化事件回调类的时候,通过 createDispatcher方法初始化路由。在HttpServer的initCoreMiddleware

$this->routerDispatcher = $this->createDispatcher($serverName); 这个语句就是点对路由收集。

代码语言:javascript
复制
public function __construct()
  {
    $this->initAnnotationRoute(AnnotationCollector::list());
    $this->initConfigRoute();
  }

注解路由

拿到所有的 AutoController 和 Controller 注解类,同时获取该类的中间返回给到HttpServer调度。

代码语言:javascript
复制
protected function initAnnotationRoute(array $collector): void
  {
    foreach ($collector as $className => $metadata) {
      if (isset($metadata['_c'][AutoController::class])) {
        if ($this->hasControllerAnnotation($metadata['_c'])) {
          $message = sprintf('AutoController annotation can\'t use with Controller annotation at the same time in %s.', $className);
          throw new ConflictAnnotationException($message);
        }
        $middlewares = $this->handleMiddleware($metadata['_c']);
        $this->handleAutoController($className, $metadata['_c'][AutoController::class], $middlewares, $metadata['_m'] ?? []);
      }
      if (isset($metadata['_c'][Controller::class])) {
        $middlewares = $this->handleMiddleware($metadata['_c']);
        $this->handleController($className, $metadata['_c'][Controller::class], $metadata['_m'] ?? [], $middlewares);
      }
    }
  }

AutoController

  • 获取注解中路由前缀$prefix
  • $router 本服务的路由收集对象 RouteCollector
  • 自动将控制器方法注册成路由,默认请求方法是 ['GET', 'POST', 'HEAD']
  • 读取方法中间件
  • 默认路由:是 $defaultAction = "/index",也就是控制器中的index方法会默认注册空路由。
代码语言:javascript
复制
protected function handleAutoController(string $className, AutoController $annotation, array $middlewares = [], array $methodMetadata = []): void
  {
    $class = ReflectionManager::reflectClass($className);
    $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
    $prefix = $this->getPrefix($className, $annotation->prefix);
    $router = $this->getRouter($annotation->server);

    $autoMethods = ['GET', 'POST', 'HEAD'];
    $defaultAction = '/index';
    foreach ($methods as $method) {
      $options = $annotation->options;
      $path = $this->parsePath($prefix, $method);
      $methodName = $method->getName();
      if (substr($methodName, 0, 2) === '__') {
        continue;
      }

      $methodMiddlewares = $middlewares;
      // Handle method level middlewares.
      if (isset($methodMetadata[$methodName])) {
        $methodMiddlewares = array_merge($methodMiddlewares, $this->handleMiddleware($methodMetadata[$methodName]));
      }

      // Rewrite by annotation @Middleware for Controller.
      $options['middleware'] = array_unique($methodMiddlewares);

      $router->addRoute($autoMethods, $path, [$className, $methodName], $options);

      if (Str::endsWith($path, $defaultAction)) {
        $path = Str::replaceLast($defaultAction, '', $path);
        $router->addRoute($autoMethods, $path, [$className, $methodName], $options);
      }
    }
  }

Controller

  • 获取注解中路由前缀$prefix
  • $router 本服务的路由收集对象 RouteCollector
  • 遍历控制器中有注解的方法,获取方法中的注解信息,例如Mapping和Middleware注解。
  • 合并控制器的中间件、方法注解中间件和mapping中options定义的中间件
  • 拿到mapping 中的 $path 作为路由
代码语言:javascript
复制
protected function handleController(string $className, Controller $annotation, array $methodMetadata, array $middlewares = []): void
  {
    if (! $methodMetadata) {
      return;
    }
    $prefix = $this->getPrefix($className, $annotation->prefix);
    $router = $this->getRouter($annotation->server);

    $mappingAnnotations = [
      RequestMapping::class,
      GetMapping::class,
      PostMapping::class,
      PutMapping::class,
      PatchMapping::class,
      DeleteMapping::class,
      ];

    foreach ($methodMetadata as $methodName => $values) {
      $options = $annotation->options;
      $methodMiddlewares = $middlewares;
      // Handle method level middlewares.
      if (isset($values)) {
        $methodMiddlewares = array_merge($methodMiddlewares, $this->handleMiddleware($values));
      }

      // Rewrite by annotation @Middleware for Controller.
      $options['middleware'] = array_unique($methodMiddlewares);

      foreach ($mappingAnnotations as $mappingAnnotation) {
        /** @var Mapping $mapping */
        if ($mapping = $values[$mappingAnnotation] ?? null) {
          if (! isset($mapping->path) || ! isset($mapping->methods) || ! isset($mapping->options)) {
            continue;
          }
          $methodOptions = Arr::merge($options, $mapping->options);
          // Rewrite by annotation @Middleware for method.
          $methodOptions['middleware'] = $options['middleware'];
          $path = $mapping->path;

          if ($path === '') {
            $path = $prefix;
          } elseif ($path[0] !== '/') {
            $path = $prefix . '/' . $path;
          }
          $router->addRoute($mapping->methods, $path, [$className, $methodName], $methodOptions);
        }
      }
    }
  }

配置路由

  • 通过 require_once 引入配置文件 protected $routes = [BASE_PATH . '/config/routes.php'];
  • 在配置文件中,通过 Hyperf\HttpServer\Router\Router 一系列静态方法进行处理。
    • 用一个静态变量(static::$factory)存储 DispatcherFactory
    • 通过魔术方法 __callStatic 拿到可以收集路由的 $router 对象 $router = static::$factory->getRouter(static::$serverName);
代码语言:javascript
复制
public function initConfigRoute()
  {
    Router::init($this);
    foreach ($this->routes as $route) {
      if (file_exists($route)) {
        require_once $route;
      }
    }
  }
代码语言:javascript
复制
class Router
{
    /**
     * @var string
     */
    protected static $serverName = 'http';

    /**
     * @var DispatcherFactory
     */
    protected static $factory;

    public static function __callStatic($name, $arguments)
    {
        $router = static::$factory->getRouter(static::$serverName);
        return $router->{$name}(...$arguments);
    }

    public static function addServer(string $serverName, callable $callback)
    {
        static::$serverName = $serverName;
        call($callback);
        static::$serverName = 'http';
    }

    public static function init(DispatcherFactory $factory)
    {
        static::$factory = $factory;
    }
}

路由组成

查看每一个路由器的组成,找到 Hyperf\HttpServer\Router\RouteCollector中的 addRoute看出。主要由

  • $httpMethod:请求方法,get、post等
  • $route:路由路径,包含参数匹配规则。
  • $handler:路由处理回调,例如控制器方法。
  • $options:其他参数,在这里最重要是用来存储每个路由对应处理的中间件。

路由匹配

HttpServer 中有讲到,所有的http请求都是固定有一个核心中间件 CoreMiddleware 处理的,在中间件处理之前,会先执行中间件的调度器 dispatch

这个调度器就是将请求获得的请求方法,请求uri 通过路由调度器获得匹配的路由数组 $routes,

再实例化一个Hyperf定义的路由调度器。并将这个调度器挂载请求对象中。

代码语言:javascript
复制
public function dispatch(ServerRequestInterface $request): ServerRequestInterface
  {
    $routes = $this->dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());
    $dispatched = new Dispatched($routes);

    return Context::set(ServerRequestInterface::class, $request->withAttribute(Dispatched::class, $dispatched));
  }

FastRoute\Dispatcher 调度器

在 DispatcherFactory 处理类中,使用 GroupCountBased 作为服务路由调度器,GroupCountBased基础 RegexBasedAbstract 中的 dispatch 方法。所以这个 dispatch 方法就是将 请求方法,和请求Uri解析成指定对应路由的核心方法。最终解析结果就是 $routes 具体的格式就是

  • NOT_FOUND 未匹配到合适的路由
  • METHOD_NOT_ALLOWED 有路由,但是请求方法不合适
  • FOUND 完全匹配到路由
    • 路由处理方法(例如控制器业务)
    • 参数数组
代码语言:javascript
复制
*     [Dispatcher::NOT_FOUND]
*     [Dispatcher::METHOD_NOT_ALLOWED, ['GET', 'OTHER_ALLOWED_METHODS']]
*     [Dispatcher::FOUND, $handler, ['varName' => 'value', ...]]

Hyperf 定义的路由调度器

这个调度器其实只是做了一个封装,让后续的业务方法调用而已。

代码语言:javascript
复制
class Dispatched
{
    /**
     * @var int
     */
    public $status;

    /**
     * @var null|Handler
     */
    public $handler;

    /**
     * @var array
     */
    public $params = [];

    /**
     * Dispatches against the provided HTTP method verb and URI.
     *
     * @param array $array with one of the following formats:
     *
     *     [Dispatcher::NOT_FOUND]
     *     [Dispatcher::METHOD_NOT_ALLOWED, ['GET', 'OTHER_ALLOWED_METHODS']]
     *     [Dispatcher::FOUND, $handler, ['varName' => 'value', ...]]
     */
    public function __construct(array $array)
    {
        $this->status = $array[0];
        switch ($this->status) {
            case Dispatcher::METHOD_NOT_ALLOWED:
                $this->params = $array[1];
                break;
            case Dispatcher::FOUND:
                $this->handler = $array[1];
                $this->params = $array[2];
                break;
        }
    }

    public function isFound(): bool
    {
        return $this->status === Dispatcher::FOUND;
    }
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • FastRoute
  • 路由收集
    • 注解路由
      • AutoController
      • Controller
    • 配置路由
    • 路由组成
    • 路由匹配
      • FastRoute\Dispatcher 调度器
        • Hyperf 定义的路由调度器
        相关产品与服务
        消息队列 TDMQ
        消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档