hyperf 从零开始构建微服务(三)—— hyperf 统一响应(3.0)
阅读目录
上一节课我们说到,consumer 对外抛出的结果,又是字符串又是对象,还动不动直接 Internal Server Error. 数据格式的不统一非常不友好。
为了规范,我们制定了一个简单的标准,统一返回带有code,message,data的数据格式。
源码已上传至github,https://github.com/bailangzhan/hyperf3-rpc
服务提供者统一响应
我们先针对 shop_provider_user 统一处理,正常情况下我们手动处理也可以解决问题,比如【App\JsonRpc\UserService::getUserInfo】方法
/** * @param int $id * @return array */ public function getUserInfo(int $id): array { $user = User::query()->find($id); if (empty($user)) { throw new \RuntimeException("user not found"); } return [ 'code' => 200, 'message' => 'success', 'data' => $user->toArray(), ]; }
但每次都这样写非常麻烦,下面我们基于 hyperf/constants 进行简单的封装。
安装 hyperf/constants
composer require hyperf/constants:*
生成枚举类
php bin/hyperf.php gen:constant ErrorCode
修改后的 App\Constants\ErrorCode.php 如下
<?php declare(strict_types=1); namespace App\Constants; use Hyperf\Constants\AbstractConstants; use Hyperf\Constants\Annotation\Constants; #[Constants] class ErrorCode extends AbstractConstants { /** * @Message("Server Error!") */ const SERVER_ERROR = 500; /** * @Message("success") */ public const SUCCESS = 200; /** * @Message("error") */ public const ERROR = 0; }
定义 Result 处理类
新建【App\Tools\Result.php】
<?php namespace App\Tools; use App\Constants\ErrorCode; class Result { public static function success($data = []) { return static::result(ErrorCode::SUCCESS, ErrorCode::getMessage(ErrorCode::SUCCESS), $data); } public static function error($message = '', $code = ErrorCode::ERROR, $data = []) { if (empty($message)) { return static::result($code, ErrorCode::getMessage($code), $data); } else { return static::result($code, $message, $data); } } protected static function result($code, $message, $data) { return [ 'code' => $code, 'message' => $message, 'data' => $data, ]; } }
测试
现在我们重新修改 App\JsonRpc\UserService::getUserInfo 方法如下
use App\Tools\Result; public function getUserInfo(int $id) { $user = User::query()->find($id); if (empty($user)) { throw new \RuntimeException("user not found"); } return Result::success($user->toArray()); }
重新请求 user/getUserInfo 测试下
POST请求 http://127.0.0.1:9600 请求参数 { "jsonrpc": "2.0", "method": "/user/getUserInfo", "params": { "id": 1 }, "id": "61025bc35e07d", "context": [] } 结果 { "jsonrpc": "2.0", "id": "61025bc35e07d", "result": { "code": 200, "message": "success", "data": { "id": 1, "name": "zhangsan", "gender": 3, "created_at": "1630187123", "updated_at": "1630187123" } }, "context": [] }
因为 provider 对外提供服务,外层的 jsonrpc 格式是固定的,consumer 拿到的数据取决于 result 字段,所以满足了我们制定的标准。
请求一个不存在的记录测试下,比如 id=100
POST请求 http://127.0.0.1:9600 请求参数 { "jsonrpc": "2.0", "method": "/user/getUserInfo", "params": { "id": 100 }, "id": "61025bc35e07d", "context": [] } 结果 { "jsonrpc": "2.0", "id": "61025bc35e07d", "error": { "code": -32000, "message": "user not found", "data": { "class": "RuntimeException", "code": 0, "message": "user not found" } }, "context": [] }
可以看到我们抛出的 RuntimeException 被 hyperf 主动接管,这也是我们想要的。
异常处理
provider 后面我们会做集群处理,为了方便 consumer 区分是哪台服务抛出的异常,我们对异常结果再处理,加上当前 server 的信息。
新建【App\Exception\Handler\JsonRpcExceptionHandler.php】
<?php declare(strict_types=1); namespace App\Exception\Handler; use Hyperf\Contract\ConfigInterface; use Hyperf\Contract\StdoutLoggerInterface; use Hyperf\ExceptionHandler\ExceptionHandler; use Hyperf\HttpMessage\Stream\SwooleStream; use Hyperf\Utils\ApplicationContext; use Psr\Http\Message\ResponseInterface; use Throwable; class JsonRpcExceptionHandler extends ExceptionHandler { public function handle(Throwable $throwable, ResponseInterface $response) { $responseContents = $response->getBody()->getContents(); $responseContents = json_decode($responseContents, true); if (!empty($responseContents['error'])) { $port = null; $config = ApplicationContext::getContainer()->get(ConfigInterface::class); $servers = $config->get('server.servers'); foreach ($servers as $k => $server) { if ($server['name'] == 'jsonrpc-http') { $port = $server['port']; break; } } $responseContents['error']['message'] .= " - {$config->get('app_name')}:{$port}"; } $data = json_encode($responseContents, JSON_UNESCAPED_UNICODE); return $response->withStatus(200)->withBody(new SwooleStream($data)); } public function isValid(Throwable $throwable): bool { return true; } }
修改 config/autoload/exceptions.php 文件,定义异常处理类
<?php declare(strict_types=1); return [ 'handler' => [ 'jsonrpc-http' => [ App\Exception\Handler\JsonRpcExceptionHandler::class, ], ], ];
重新请求一个不存在的记录
POST请求 http://127.0.0.1:9600 请求参数 { "jsonrpc": "2.0", "method": "/user/getUserInfo", "params": { "id": 100 }, "id": "61025bc35e07d", "context": [] } 结果 { "jsonrpc": "2.0", "id": "61025bc35e07d", "error": { "code": -32000, "message": "user not found - shop_provider_user:9600", "data": { "class": "RuntimeException", "code": 0, "message": "user not found" } }, "context": [] }
可以很清晰的看到是部署在9600端口的 shop_provider_user 服务抛出的异常。
同样,UserService::createUser 方法也可以快速处理。
public function createUser(string $name, int $gender): array { if (empty($name)) { throw new \RuntimeException("name不能为空"); } $result = User::query()->create([ 'name' => $name, 'gender' => $gender, ]); return $result ? Result::success() : Result::error("fail"); }
UserServiceInterface::createUser 自行修改。
如此一来,服务提供者统一返回的数据格式我们就处理好了。
服务消费者统一响应
启动 shop_consumer_user,在我们不做任何处理的时候,请求一个不存在的用户信息
GET请求 http://127.0.0.1:9501/user/getUserInfo?id=100 浏览器输出 Internal Server Error.
安装 hyperf/constants
cd shop_consumer_user composer require hyperf/constants:*
编写枚举类和Result处理类
复制服务提供者下的 App\Constants\ErrorCode.php 和 App\Tools\Result.php 到 shop_consumer_user/app 目录下。
异常处理
config/autoload/exceptions.php 文件内定义的异常处理类
<?php declare(strict_types=1); return [ 'handler' => [ 'http' => [ Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, App\Exception\Handler\AppExceptionHandler::class, ], ], ];
注意 consumer 对外提供的是 http 服务,所有 handle 里面配置的是 http。
格式化输出
修改【App\Exception\Handler\AppExceptionHandler.php】如下:
<?php declare(strict_types=1); namespace App\Exception\Handler; use Hyperf\Contract\StdoutLoggerInterface; use Hyperf\ExceptionHandler\ExceptionHandler; use Hyperf\HttpMessage\Stream\SwooleStream; use Psr\Http\Message\ResponseInterface; use Throwable; class AppExceptionHandler extends ExceptionHandler { public function handle(Throwable $throwable, ResponseInterface $response) { // 格式化输出 $data = json_encode([ 'code' => $throwable->getCode(), 'message' => $throwable->getMessage(), ], JSON_UNESCAPED_UNICODE); // 阻止异常冒泡 $this->stopPropagation(); return $response ->withAddedHeader('Content-Type', ' application/json; charset=UTF-8') ->withStatus(500) ->withBody(new SwooleStream($data)); //return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); } public function isValid(Throwable $throwable): bool { return true; } }
测试
修改 UserController::getUserInfo 方法如下:
use App\Tools\Result; use App\Constants\ErrorCode; public function getUserInfo() { $id = (int) $this->request->input('id'); $result = $this->userServiceClient->getUserInfo($id); if ($result['code'] != ErrorCode::SUCCESS) { throw new \RuntimeException($result['message']); } return Result::success($result['data']); }
postman 分别对正常请求和异常请求测试下
GET请求 http://127.0.0.1:9501/user/getUserInfo?id=1 结果 { "code": 200, "message": "success", "data": { "id": 1, "name": "zhangsan", "gender": 3, "created_at": "1630187123", "updated_at": "1630187123" } } GET请求 http://127.0.0.1:9501/user/getUserInfo?id=100 { "code": -32000, "message": "user not found - shop_provider_user:9600" }
AppExceptionHandler 类可以根据自己的需要进行自定义。
同样的,createUser 方法我们也处理如下
public function createUser() { $name = (string) $this->request->input('name', ''); $gender = (int) $this->request->input('gender', 0); $result = $this->userServiceClient->createUser($name, $gender); if ($result['code'] != ErrorCode::SUCCESS) { throw new \RuntimeException($result['message']); } return Result::success($result['data']); }
针对 consumer 的统一处理我们就完成了,但是我们发现,不管是服务提供者还是服务消费者,有些代码没有冗余的必要,比如 Result 类、UserServiceInterface 等,如果我们有10个8个服务都要修改的时候,岂不是非常麻烦。
那怎么样把这些公共代码提取出来呢?大家不妨思考一下再继续阅读。
提取公共代码
我们把 Result 类和 ErrorCode 类提取出来形成一个基于 composer 的公共组件,修改代码的时候,只需要针对源组件包修改发布,需要的模块通过 composer 安装即可。
由于大部分代码都是复用的,这里就不贴代码了。
下面是一个基于hyperf ConfigProvider 机制实现的组件,暂时只支持 hyperf 框架下使用,并没有去兼容通用性。
源码参考 https://github.com/bailangzhan/hyperf-result,通过 composer 安装
composer require bailangzhan/hyperf-result:*
我们在消费者的 UserController::getUserInfo 接口下尝试使用:
/** * @return array|void */ public function getUserInfo() { $id = (int) $this->request->input('id'); $result = $this->userServiceClient->getUserInfo($id); if ($result['code'] != \Bailangzhan\Result\ErrorCode::SUCCESS) { throw new \RuntimeException($result['message']); } return \Bailangzhan\Result\Result::success($result['data']); }
postman请 求测试发现接口正常,其他接口以及 provider 大家可以参考修改。
目前为止,我们已经搭建了一个小的项目,下一节我们开始考虑微服务的问题。
- 评论区