简单的业务模型图
这里对分层进行简单的说明:
接口层
- 提供统一的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开始说起
下面是官方实例的一个服务调用
__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的取值范围定义不同的错误
于是我扛起锄头写下下面的代码:
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
没错,这个才是响应的正确处理方法,原来作者造就考虑到了!所以当你要请求一个服务的时候应该这样子:
那么,接下来我们就动手做一个手机号登录的服务
做一个手机号登录的业务
接口的定义
首先我们先定义一套远程方法的接口,同时下发给服务的提供者和服务的消费者
API统一响应
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服务
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 ,
]);
}
}
服务提供者
这里都省略了数据库操作和业务逻辑部分,直接返回了结果
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中也会抛出这个异常。这样就实现了异常的传递
定义一套错误码
定义错误码是很有必要的,尤其是多语言的项目,可以根据错误码返回对应语言的文字提示
在服务中抛出LogicException
异常
rand(111111111,9999999999),
"user_name" => sprintf("user-%s",uniqid()),
];
}
.......
}
API层将LogicException
异常转换为响应
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:创建出错的响应
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
是关键
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
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
new SerializerFactory(Serializer::class),//(必须这样写)
];
并且 还要在composer.json
中导入 symfony/serializer (^5.0)
和 symfony/property-access (^5.0)
如果不配置的话LogicException
异常是不能传递的,起初我以为只有rpc调用结果需要返回对象时才需要,所以没有配置,结果导致无法抛出LogicException
,后来看了源码发现异常也是当做对象进行还原的。