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