限时 5折! 详情

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

5930 0 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 大家可以参考修改。

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