关于hyperf:记一次hyperf-微服务实践

72次阅读

共计 12245 个字符,预计需要花费 31 分钟才能阅读完成。

简略的业务模型图

这里对分层进行简略的阐明:

  • 接口层

    • 提供对立的 http 申请入口
    • 验证用户身份和申请的参数,对用户申请进行过滤。
    • 通过 rpc 调用业务层的办法来组织业务逻辑
    • 自身不对数据层进行间接操作
    • 从 consul/etcd 中发现服务提供方
  • 业务层

    • 实现业务逻辑,并且为接口层提供 rpc 调用服务
    • 定时工作,音讯队列的生产和生产
    • 应用数据层将业务的后果长久化保留到数据库中
    • 将服务注册到 consul/etcd 中
  • 数据层

    • mysql 数据库提供次要的数据存储能力和事务处理能力
    • mongo 数据库提供数据归档能力
    • amqp 提供音讯队列反对
    • elasticsearch 提供搜寻服务和日志存储
  • 公共服务

    • 接口层和业务层都可能会用到 redis 提供缓存
    • 接口层和业务层都须要进行日志的收集和长久化
  • 注册发现

    • 这里因为 hyperf 框架的反对,抉择应用 consul 作为服务的注册和发现
    • 开发阶段应用注册发现有很多不便,这里就通过 svc 的节点的形式进行 rpc 调用

示例源码

  • api 我的项目仓库:https://gitee.com/diablo7/hyp…
  • svc 我的项目仓库:https://gitee.com/diablo7/hyp…
  • 公共设施仓库:https://gitee.com/diablo7/docker

从官网 demo 开始说起

上面是官网实例的一个服务调用

<?php

namespace App\JsonRpc;

use Hyperf\RpcClient\AbstractServiceClient;

class CalculatorServiceConsumer extends AbstractServiceClient implements CalculatorServiceInterface
{
    protected $serviceName = 'CalculatorService';

    protected $protocol = 'jsonrpc-http';

    public function add(int $a, int $b): int
    {return $this->__request(__FUNCTION__, compact('a', 'b'));
    }
}

咱们看他的 __request 办法:

protected function __request(string $method, array $params, ?string $id = null)
{if (! $id && $this->idGenerator instanceof IdGeneratorInterface) {$id = $this->idGenerator->generate();
    }
    $response = $this->client->send($this->__generateData($method, $params, $id));
    if (is_array($response)) {$response = $this->checkRequestIdAndTryAgain($response, $id);

        if (array_key_exists('result', $response)) {return $response['result'];
        }
        if (array_key_exists('error', $response)) {return $response['error'];
        }
    }
    throw new RequestException('Invalid response.');
}

如果依照这个例子去组织代码你就会发现一个问题,如果 result 中也蕴含 error 外面的字段该怎么办?

比方:

  • 某个业务胜利的返回值中蕴含 codemessage, data 字段,那么依据返回值你怎么判断胜利还是失败,总不能要求业务中不能返回这三个字段吧。

你可能会想把 resutl 和 error 定义成雷同的数据结构,而后依据业务中的 code 的取值范畴定义不同的谬误

于是我扛起锄头写下上面的代码:

<?php
namespace App\Response;
/**
 * 用户服务的响应对象
 */
class ServiceResponse
{
    public $code;
    public $message;
    public $data;
    public function __construct($response)
    {$this->code = $response["code"];
        $this->message = $response["message"];
        $this->data = $response["data"];
    }
    // 判断是否申请胜利
    public function isOk(): bool
    {return $this->code == 0 ;}
    // 获取响应的音讯
    public function getMessage(): string
    {return $this->message;}
    // 获取响应的数据
    public function getData()
    {return $this->result;}
}

将响应的后果封装成一个 ServiceResponse 对象, 而后提供几个办法。

你感觉这样能够吗?能用然而不标准! 而且对于后果还有有各种 if 判断,感觉太蹩脚了

应用 rpc 的意义就在于像调用本地办法一样调用近程零碎的办法,当初这个样子几乎就是南辕北辙!

于是通过一番钻研忽然发现了一个 ServiceClient 继承了AbstractServiceClient

没错,这个才是响应的正确处理办法,原来作者造就思考到了!所以当你要申请一个服务的时候应该这样子:

<?php

namespace App\JsonRpc;

use Hyperf\RpcClient\ServiceClient;

class CalculatorServiceConsumer extends ServiceClient implements CalculatorServiceInterface
{}

那么,接下来咱们就动手做一个手机号登录的服务

做一个手机号登录的业务

接口的定义

首先咱们先定义一套近程办法的接口,同时下发给服务的提供者和服务的消费者

<?php

namespace App\Service;

interface UserBaseServiceInterface
{
    /**
     * 手机登录查看,返回查看后果
     * @param string $phone
     * @return int [0: 不存在,1: 已存在,2: 已解冻]
     */
    public function phoneLoginCheck(string $phone) :int;
    /**
     * 发送手机验证码
     * @param string $phone
     * @return bool
     */
    public function sendLoginPhoneCode(string $phone): bool;
    /**
     * 应用手机验证码登录
     * @param string $phone
     * @param string $code
     * @return array
     */
    public function loginWithPhoneCode(string $phone,string $code): array;
    /**
     * 依据 ID 获取用户信息
     * @param int $id
     * @return array
     */
    public function getUserInfoById(int $id) :array;
}

API 对立响应

<?php

namespace App\Ability;


use Hyperf\Validation\Validator;

trait StandardApiResponse
{
    /**
     * 胜利的响应
     * @param array $data
     * @param string $message
     * @return array
     */
    public function success(array $data = [] ,string $message = '')
    {
        return [
            "code" => 0 ,
            "msg" => $message,
            "data" => $data,
        ];
    }

    /**
     * 出错的响应
     * @param int $code
     * @param string $message
     * @param array $data
     * @return array
     */
    public function error(int $code , string $message = '', array $data = [] )
    {
        return [
            "code" => $code ,
            "msg" => $message,
            "data" => $data,
        ];
    }

    /**
     * 验证失败
     * @param Validator $validator
     * @return array
     */
    public function fails(Validator $validator)
    {
        return [
            "code" => 100,
            "msg" => "validate error !",
            "data" => ["errors" => $validator->errors()
            ],
        ];
    }
}

API 调用 rpc 服务

<?php

declare(strict_types=1);

namespace App\Controller\Auth;

use App\Ability\StandardApiResponse;
use App\Achieve\JwtSubject;
use App\Controller\AbstractController;
use App\Service\UserBaseServiceInterface;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Validation\ValidatorFactory;
use HyperfExt\Jwt\JwtFactory;

class PhoneController extends AbstractController
{
    use StandardApiResponse;
    /**
     * 手机登录
     * @param RequestInterface $request
     * @return array|\Psr\Http\Message\ResponseInterface
     */
    public function login(RequestInterface $request)
    {$validator = $this->container->get(ValidatorFactory::class)->make($request->all(),[
            "phone" => "required",
            "code" => "required",
        ]);

        if($validator->fails()){return $this->fails($validator);
        }
        $params = $validator->validated();

        $result = make(UserBaseServiceInterface::class)->loginWithPhoneCode($params["phone"],$params["code"]);

        // 登录失败会抛出异样,能走到这里就是胜利的,接下来给用户生成 jwt-token
        $jwt = $this->container->get(JwtFactory::class)->make();

        return $this->success(["token" => $jwt->fromSubject(new JwtSubject($result["user_id"],$result)),
        ]);
    }

    /**
     * 手机号码检测
     * @param RequestInterface $request
     * @return array
     */
    public function check(RequestInterface $request)
    {$validator = $this->container->get(ValidatorFactory::class)->make($request->all(),["phone" => "required",]);

        if($validator->fails()){return $this->fails($validator);
        }
        $params = $validator->validated();

        $result = make(UserBaseServiceInterface::class)->phoneLoginCheck($params["phone"]);

        return $this->success(["check" => $result,]);
    }

    /**
     * 发送手机验证码
     * @param RequestInterface $request
     * @return array
     */
    public function code(RequestInterface $request)
    {$validator = $this->container->get(ValidatorFactory::class)->make($request->all(),["phone" => "required",]);

        if($validator->fails()){return $this->fails($validator);
        }
        $params = $validator->validated();

        $result = make(UserBaseServiceInterface::class)->sendLoginPhoneCode($params["phone"]);

        return $this->success(["send_at" => $result ? time() : 0 ,
        ]);
    }
}

服务提供者

这里都省略了数据库操作和业务逻辑局部,间接返回了后果

<?php

namespace App\JsonRpc;

use App\Service;
use Hyperf\RpcServer\Annotation\RpcService;
use Hyperf\Contract\ContainerInterface;

/**
 * @RpcService(name="user.base", protocol="jsonrpc", server="jsonrpc")
 */
class UserBaseService implements Service\UserBaseServiceInterface
{
    /**
     * @var ContainerInterface
     */
    public $container;


    public function __construct(ContainerInterface $container)
    {$this->container = $container;}

    public function phoneLoginCheck(string $phone): int
    {
        // 没有注册
        return 0;
    }


    public function sendLoginPhoneCode(string $phone): bool
    {
        $code = "123456";

        if(env("APP_ENV") == "prod" ){
            // 在服务中调用服务
            $this->container->get(Service\SmsCodeServiceInterface::class)->sendLoginVerifyCode($phone,$code);
        }
        return true;
    }


    public function loginWithPhoneCode(string $phone, string $code): array
    {
        //TODO 查看短信验证码,依据手机号查找用户

        return ["user_id" => rand(111111111,9999999999),
            "user_name" => sprintf("user-%s",uniqid()),
        ];
    }

    public function getUserInfoById(int $id): array
    {
        //TODO  依据 id 查找用户
        return [
            "user_id" => $id,
            "user_name" => sprintf("user-%s",uniqid()),
            "level" => 99,
        ];
    }
}

小结

至此咱们就组织起了一个残缺的逻辑:通过 api 层调用服务 svc 层而后将后果响应给接口。

然而,业务并不总是胜利的,api 层尽管对参数进行了校验,却有可能会呈现业务逻辑的异样。

例如:

  • 应用手机验证码进行登录,然而手机号被零碎拉黑了,不能失常登录
  • 当提交一笔订单的时候,商品库存有余了,无奈下单

这么这些业务逻辑的谬误怎么通知调用者?应用数组返回吗?不!如果这样做了消费者又要陷入到各种逻辑的判断中了,所以这里咱们应用抛出异样的形式进行返回。只有能让异样也像调用本地办法一排抛出,咱们就能够在 api 层中针对性的捕捉各种业务上的异样,而后将异样转换为 api 的响应格局,如此这般咱们就能够在消费者中只解决胜利的逻辑。

接下来咱们就退出异样的解决逻辑

定义一个异样类

在 api 和 svc 中定义一个 App\Exception\LogicException 类,专门解决业务中呈现的各种异样。
api 应用 rpc 调用的服务中抛出这个异样时,api 中也会抛出这个异样。这样就实现了异样的传递

<?php

namespace App\Exception;

class LogicException extends \Exception
{}

定义一套错误码

定义错误码是很有必要的,尤其是多语言的我的项目,能够依据错误码返回对应语言的文字提醒

<?php

namespace App\Constant;

class Code
{const INVALID_PHONE_CODE = 10001;}

在服务中抛出 LogicException 异样

<?php

namespace App\JsonRpc;

use App\Constant\Code;
use App\Exception\LogicException;
use App\Service;
use Hyperf\RpcServer\Annotation\RpcService;
use Hyperf\Contract\ContainerInterface;

/**
 * @RpcService(name="user.base", protocol="jsonrpc", server="jsonrpc")
 */
class UserBaseService implements Service\UserBaseServiceInterface
{
    ......
        
    public function loginWithPhoneCode(string $phone, string $code): array
    {
        //TODO 查看短信验证码,依据手机号查找用户
        if(true){throw new LogicException("谬误的手机验证码!",Code::INVALID_PHONE_CODE,);
        }
        return ["user_id" => rand(111111111,9999999999),
            "user_name" => sprintf("user-%s",uniqid()),
        ];
    }
    
    .......
}

API 层将 LogicException 异样转换为响应

<?php

namespace App\Exception\Handler;

use App\Ability\StandardApiResponse;
use App\Exception\LogicException;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use Throwable;

/**
 * 将逻辑异样转换为规范异样
 */
class LogicExceptionHandler extends ExceptionHandler
{
    use StandardApiResponse;

    public function handle(\Throwable $throwable, ResponseInterface $response)
    {$this->stopPropagation();
        $code = $throwable->getCode();
        $message = $throwable->getMessage();
        $data = [];

        $content = json_encode($this->error($code,$message,$data),JSON_UNESCAPED_UNICODE);

        return $response
            ->withAddedHeader('content-type', 'application/json; charset=utf-8')
            ->withBody(new SwooleStream($content));
    }

    public function isValid(Throwable $throwable): bool
    {return $throwable instanceof LogicException;}
}

要害代码

rpc 服务中的异样解决

ResponseBuilder:创立出错的响应

<?php
declare(strict_types=1);

namespace Hyperf\JsonRpc;

use Hyperf\Contract\PackerInterface;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Rpc\Contract\DataFormatterInterface;
use Hyperf\Utils\Context;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class ResponseBuilder
{
    ......
 
    public function buildErrorResponse(ServerRequestInterface $request, int $code, \Throwable $error = null): ResponseInterface
    {$body = new SwooleStream($this->formatErrorResponse($request, $code, $error));
        return $this->response()->withHeader('content-type', 'application/json')->withBody($body);
    }

    protected function formatErrorResponse(ServerRequestInterface $request, int $code, \Throwable $error = null): string
    {[$code, $message] = $this->error($code, $error ? $error->getMessage() : null);
        $response = $this->dataFormatter->formatErrorResponse([$request->getAttribute('request_id'), $code, $message, $error]);
        return $this->packer->pack($response);
    }
}

dataFormatter: 谬误数据的格式化,NormalizerInterface是要害

<?php
declare(strict_types=1);

namespace Hyperf\JsonRpc;

use Hyperf\Contract\NormalizerInterface;
use Hyperf\Rpc\Context;

class NormalizeDataFormatter extends DataFormatter
{
    /**
     * @var NormalizerInterface
     */
    private $normalizer;

    public function __construct(NormalizerInterface $normalizer, Context $context)
    {
        $this->normalizer = $normalizer;

        parent::__construct($context);
    }

    public function formatRequest($data)
    {$data[1] = $this->normalizer->normalize($data[1]);
        return parent::formatRequest($data);
    }

    public function formatResponse($data)
    {$data[1] = $this->normalizer->normalize($data[1]);
        return parent::formatResponse($data);
    }

    public function formatErrorResponse($data)
    {if (isset($data[3]) && $data[3] instanceof \Throwable) {$data[3] = ['class' => get_class($data[3]),
                'attributes' => $this->normalizer->normalize($data[3]),
            ];
        }
        return parent::formatErrorResponse($data);
    }
}

这里咱们能够看出一个抛出异样的响应被转换成这个样子:

{
    "jsonrpc": "2.0",
    "id": "6234809b91ff9",
    "error": {
        "code": -32000,
        "message": "谬误的手机验证码!",
        "data": {
            "class": "App\\Exception\\LogicException",
            "attributes": {
                "message": "谬误的手机验证码!",
                "code": 10001,
                "file": "/opt/www/app/JsonRpc/UserBaseService.php",
                "line": 49
            }
        }
    },
    "context": []}

API 中 RPC 响应后果解决

这里将异样信息进行还原并抛出,要害还是NormalizerInterface

<?php

namespace Hyperf\RpcClient;

class ServiceClient extends AbstractServiceClient
{
    // 省略不相干代码.....
    
    protected function __request(string $method, array $params, ?string $id = null)
    {if ($this->idGenerator instanceof IdGeneratorInterface && ! $id) {$id = $this->idGenerator->generate();
        }
        $response = $this->client->send($this->__generateData($method, $params, $id));
        if (! is_array($response)) {throw new RequestException('Invalid response.');
        }
        $response = $this->checkRequestIdAndTryAgain($response, $id);
        if (array_key_exists('result', $response)) {$type = $this->methodDefinitionCollector->getReturnType($this->serviceInterface, $method);
            if ($type->allowsNull() && $response['result'] === null) {return null;}

            return $this->normalizer->denormalize($response['result'], $type->getName());
        }
        if ($code = $response['error']['code'] ?? null) {$error = $response['error'];
            // Denormalize exception.
            $class = Arr::get($error, 'data.class');
            $attributes = Arr::get($error, 'data.attributes', []);
            if (isset($class) && class_exists($class) && $e = $this->normalizer->denormalize($attributes, $class)) {if ($e instanceof \Throwable) {throw $e;}
            }
            // Throw RequestException when denormalize exception failed.
            throw new RequestException($error['message'] ?? '', $code, $error['data'] ?? []);
        }

        throw new RequestException('Invalid response.');
    }
}

要害配置

官网文档提到要 在 dependencies.php配置NormalizerInterface

<?php
use Hyperf\Utils\Serializer\SerializerFactory;
use Hyperf\Utils\Serializer\Serializer;

return [Hyperf\Contract\NormalizerInterface::class => new SerializerFactory(Serializer::class),//(必须这样写)];

并且 还要在 composer.json 中导入 symfony/serializer (^5.0)symfony/property-access (^5.0)

如果不配置的话 LogicException 异样是不能传递的,起初我认为只有 rpc 调用后果须要返回对象时才须要,所以没有配置,后果导致无奈抛出LogicException, 起初看了源码发现异常也是当做对象进行还原的。

正文完
 0