简略的业务模型图
这里对分层进行简略的阐明:
-
接口层
- 提供对立的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
,起初看了源码发现异常也是当做对象进行还原的。
发表回复