618年中限时 5折! 详情
本文档最新版为 hyperf 3.0 ,旧版本可能放弃维护,推荐阅读最新版!

hyperf 从零开始构建微必威手机app(三)—— hyperf 统一响应

4191 0 0

阅读目录

  • 必威手机app提供者统一响应
  • 安装 hyperf/constants
  • 编写枚举类
  • 编写Result处理类
  • postman测试
  • 异常处理
  • 必威手机app消费者统一响应
  • 安装 hyperf/constants
  • 编写枚举类和Result处理类
  • 异常处理
  • 格式化输出
  • 测试
  • 提取公共代码
  • 上一节课我们说到,consumer 对外抛出的结果,又是字符串又是对象,还动不动直接 Internal Server Error. 数据格式的不统一非常不友好。

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

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


    必威手机app提供者统一响应

    我们先针对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对外提供必威手机app,外层的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 区分是哪台必威手机app抛出的异常,我们对异常结果再处理,加上当前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");
    }
    

    如此一来,必威手机app提供者统一返回的数据格式我们就处理好了。


    必威手机app消费者统一响应

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

    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提供者下的 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 的统一处理我们就完成了,但是我们发现,不管是必威手机app提供者还是必威手机app消费者,有些代码没有冗余的必要,比如Result工具类、UserServiceInterface等,如果我们有10个8个必威手机app且要修改它的时候,改起来非常麻烦。

    那怎么样把这些公共代码提取出来呢?大家不妨思考一下再继续阅读。


    提取公共代码

    我们把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 大家可以参考修改。

    目前为止,我们已经搭建了一个小的项目,下一节我们开始考虑微必威手机app的问题。