hyperf微服务新课首发,连载期间5折优惠,截止到2021-09-30。去购买

hyperf从零开始构建微服务(三)—— hyperf统一响应
91 0 0

阅读目录

上一节课我们说到,consumer 对外抛出的结果,又是字符串又是对象,还动不动直接 Internal Server Error. 数据格式的不统一非常不友好。

为了规范,我们制定了一个简单的标准,统一返回带有code,message,data的数据格式。

源码已上传至github,https://github.com/bailangzhan/hyperf-rpc

服务提供者统一响应

我们先针对provider统一处理,正常情况下我们手动处理也可以解决问题,比如

【App\JsonRpc\UserService::getUserInfo】

public function getUserInfo(int $id)
{
    $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
 */
#[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\Config\Annotation\Value;
use Hyperf\Contract\ConfigInterface;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Utils\ApplicationContext;
use Psr\Http\Message\ResponseInterface;
use Throwable;
class JsonRpcExceptionHandler extends ExceptionHandler
{
    /**
     * @Value("app_name")
     * @var $appName
     */
    private $appName;
    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'] .= " - {$this->appName}:{$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": []
}

同样,UserService::createUser方法也可以快速处理。

public function createUser(string $name, int $gender)
{
    if (empty($name)) {
        throw new \RuntimeException("name不能为空");
    }
    $result = User::query()->create([
        'name' => $name,
        'gender' => $gender,
    ]);
    return $result ? Result::success() : Result::error("fail");
}

如此一来,服务提供者统一返回的数据格式我们就处理好了。

服务消费者统一响应

在我们不做任何处理的时候,请求一个不存在的用户信息

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,
        ],
    ],
];

格式化输出
【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 方法进行改写如下

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接口下尝试使用:

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 \Bailangzhan\Result\Result::success($result['data']);
}

postman请求测试发现接口正常,其他接口以及 provider 大家可以参考修改。

目前为止,我们已经搭建了一个小的项目,下一节我们开始考虑微服务的问题。