简略的业务模型图
这里对分层进行简略的阐明:
-
接口层
- 提供对立的 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 外面的字段该怎么办?
比方:
- 某个业务胜利的返回值中蕴含
code
,message
,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
, 起初看了源码发现异常也是当做对象进行还原的。