简略的业务模型图

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

  • 接口层

    • 提供对立的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开始说起

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

<?phpnamespace 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的取值范畴定义不同的谬误

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

<?phpnamespace 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

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

<?phpnamespace App\JsonRpc;use Hyperf\RpcClient\ServiceClient;class CalculatorServiceConsumer extends ServiceClient implements CalculatorServiceInterface{   }

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

做一个手机号登录的业务

接口的定义

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

<?phpnamespace 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对立响应

<?phpnamespace 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服务

<?phpdeclare(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 ,        ]);    }}

服务提供者

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

<?phpnamespace 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中也会抛出这个异样。这样就实现了异样的传递

<?phpnamespace App\Exception;class LogicException extends \Exception{}

定义一套错误码

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

<?phpnamespace App\Constant;class Code{    const INVALID_PHONE_CODE = 10001;}

在服务中抛出LogicException异样

<?phpnamespace 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异样转换为响应

<?phpnamespace 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:创立出错的响应

<?phpdeclare(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是要害

<?phpdeclare(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

<?phpnamespace 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

<?phpuse 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,起初看了源码发现异常也是当做对象进行还原的。