6、Hyperf 3 微服务系列 - 异常处理,返回相关格式的数据

作者: 温新

分类: 【Hyperf 3 微服务系列】

阅读: 1212

时间: 2023-05-20 12:29:23

hi,我是温新,一名 PHPer

Hyperf 3 微服务代码已上传至 Github:https://github.com/ziruchu/hyperf3-microservice-code

本篇文章将结合枚举类对返回消息进行统一封装。在封装之前先来看看项目目录。

# 现在有 3 个应用
note # 此篇文章暂未使用
note_consumer_user # 消费者
note_provider_user # 服务者

服务者统一返回数据

注意:当前位于 note_provider_user

第一步:安装枚举组件并编写枚举类

安装组件

composer require hyperf/constants

生成枚举类

php bin/hyperf.php gen:constant ResponseCode

编写枚举类

<?php
// app/Constants/ResponseCode.php
declare(strict_types=1);

namespace App\Constants;

use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;

#[Constants]
class ResponseCode extends AbstractConstants
{
    /**
     * @Message("Server Error!")
     */
    const SERVER_ERROR = 500;

    /**
     * @Message("success")
     */
    public const SUCCESS = 200;

    /**
     * @Message("error")
     */
    public const ERROR = 0;
}
第二步:创建公共统一数据返回类
<?php
// app/Tools/ResponseTool.php
namespace App\Tools;

use App\Constants\ResponseCode;

class ResponseTool
{
    public static function success(array $data = [])
    {
        return self::commonResuls(ResponseCode::SUCCESS, ResponseCode::getMessage(ResponseCode::SUCCESS), $data);
    }

    public static function error(int $code = ResponseCode::ERROR, string $message = '', array $data = [])
    {
        if (empty($message)) {
            return self::commonResuls($code, ResponseCode::getMessage($code), $data);
        } else {
            return  self::commonResuls($code, $message, $data);
        }
    }

    // 返回统一的数据
    public static function commonResuls(int $code, string $message, array $data)
    {
        return [
            'code'    => $code,
            'message' => $message,
            'data'    => $data
        ];
    }
}
第三步:修改 App\JsonRpc\Service\UserService.php 中的 getUserInfo 方法
<?php
// app/JsonRpc/Service/UserService.php

use App\Tools\ResponseTool;

public function getUserInfo(int $id)
{
    $user = User::query()->find($id);
    if (empty($user)) {
        throw new \RuntimeException('没有该用户');
    }

    return ResponseTool::success($user->toArray());
}    
第四步:postman 测试

获取存在的用户信息

POST请求 http://192.168.31.90:9600
请求参数
{
    "jsonrpc": "2.0",
    "method": "/user/getUserInfo",
    "params": {
        "id": 1
    },
    "id": "",
    "context": []
}

返回结果
{
    "jsonrpc": "2.0",
    "id": "",
    "result": {
        "code": 200,
        "message": "success",
        "data": {
            "id": 1,
            "name": "李四",
            "gender": 1,
            "created_at": "2023-03-21 04:37:43",
            "updated_at": "2023-03-21 04:37:43"
        }
    },
    "context": []
}

获取不存在的用户信息

POST请求 http://192.168.31.90:9600
请求参数
{
    "jsonrpc": "2.0",
    "method": "/user/getUserInfo",
    "params": {
        "id": 11
    },
    "id": "",
    "context": []
}

返回结果
{
    "jsonrpc": "2.0",
    "id": "",
    "error": {
        "code": -32000,
        "message": "没有该用户",
        "data": {
            "class": "RuntimeException",
            "code": 0,
            "message": "没有该用户"
        }
    },
    "context": []
}

当获取不存在的用户信息时,抛出异常。

异常处理

为了知道是哪台服务抛出了异常,现在需要对异常进行处理。

1、处理服务者远程调用异常

<?php
// app/Exception/Handler/JsonRpcExceptionHandler.php
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')]
    private string $appName;

    public function handle(Throwable $throwable, ResponseInterface $response)
    {
        /*
            $$responseContents 结构如下:

            Array
            (
                [jsonrpc] => 2.0
                [id] =>
                [error] => Array
                    (
                        [code] => -32000
                        [message] => 没有该用户
                        [data] => Array
                            (
                                [class] => RuntimeException
                                [code] => 0
                                [message] => 没有该用户
                            )
                    )

                [context] => Array()
            )
         */
        $responseContents = json_decode($response->getBody()->getContents(), true);
        $errorMessage     = $responseContents['error']['message'];
        if (! empty($responseContents['error'])) {
            $port    = 0;
            $host    = '';
            $config  = ApplicationContext::getContainer()->get(ConfigInterface::class);
            $servers = $config->get('server.servers');

            foreach ($servers as $server) {
                if ($server['name'] == 'jsonrpc-http') {
                    $port = $server['port'];
                    $host = $server['host'];
                    break;
                }
            }
            $responseContents['error']['message'] = $this->appName . '-' . $host .':'. $port . '-' . $errorMessage;
        }
        $data = json_encode($responseContents, JSON_UNESCAPED_UNICODE);

        return $response->withStatus(200)->withBody(new SwooleStream($data));
    }

    public function isValid(Throwable $throwable): bool
    {
        return true;
    }
}

2、注册异常

<?php
// config/autoload/exception.php
declare(strict_types=1);

return [
    'handler' => [
        'jsonrpc-http' => [
            \App\Exception\Handler\JsonRpcExceptionHandler::class,
        ]
    ],
];

3、使用 postman 进行异常测试

# 获取一个不存在的用户信息
POST请求 http://192.168.31.90:9600
请求参数
{
    "jsonrpc": "2.0",
    "method": "/user/getUserInfo",
    "params": {
        "id": 11
    },
    "id": "",
    "context": []
}

# 查询结果
{
    "jsonrpc": "2.0",
    "id": "",
    "error": {
        "code": -32000,
        "message": "node_provider_user-0.0.0.0:9600-没有该用户",
        "data": {
            "class": "RuntimeException",
            "code": 0,
            "message": "没有该用户"
        }
    },
    "context": []
}

获取用户异常信息已经得到处理,同样的,也可以对创建用户进行处理。

4、创建用户信息异常处理

<?php
// app/JsonRpc/Service/UserService.php    
    
public function createUser(string $name, string $gender)
{
    if (empty($name)) {
        throw new \RuntimeException('用户名不能为空');
    }

    $user = User::query()->create([
        'name'   => $name,
        'gender' => $gender,
    ]);

    return  $user ? ResponseTool::success() : ResponseTool::error('创建用户失败');
}

消费者异常与数据处理

注意:现在已经切换到到了 note_consumer_user

上面已经对服务提供者异常信息进行了处理,现在我们来请求一下消费者看看是什么情况。

$ curl http://192.168.31.90:9501/users/show?jd=2
Internal Server Error.

当消费者获取一个不存在的用户信息时,直接报错,这并不友好,下面就对这种情况进行处理。处理的方式和服务提供者一样。

第一步:安装枚举组件并编写枚举类

安装组件

composer require hyperf/constants
第二步:代码复制

把服务者中的 Constants\ResponseCode.phpTools\ResponseTool.php 复制到消费者,同时对应好目录。

第三步:修改控制器方法

1、修改获取用户信息方法

<?php
// app/Controller/UserController.php
    
use App\Constants\ResponseCode;

// 获取用户信息
#[GetMapping('/users/show')]
public function getUserInfo()
{
    $id = (int) $this->request->input('id');
    $user = $this->userService->getUserInfo($id);

    if ($user['code'] != ResponseCode::SUCCESS) {
        throw new \RuntimeException($user['message']);
    }

    return ResponseTool::success($user['data']);
}

2、postman 测试

GET请求 http://192.168.31.90:9501/users/show?id=1
结果
{
    "code": 200,
    "message": "success",
    "data": {
        "id": 1,
        "name": "李四",
        "gender": 1,
        "created_at": "2023-03-21 04:37:43",
        "updated_at": "2023-03-21 04:37:43"
    }
}

3、改造创建用户方法

<?php

// 添加用户
#[PostMapping('/users/store')]
public function store()
{
    $name   = (string)$this->request->input('name', '');
    $gender = (int)$this->request->input('gender', 0);

    $user = $this->userService->createUser($name, $gender);
    if ($user['code'] != ResponseCode::SUCCESS) {
        throw new \RuntimeException($user['message']);
    }

    return ResponseTool::success($user['data']);
}

4、测试添加用户

说明:postman 中,请求参数使用 json
    
请求方法: http://192.168.31.90:9501/users/store
参数:
{
    "name":"好帅",
    "gender":2
}

返回结果
{
    "code": 200,
    "message": "success",
    "data": []
}

异常处理

1、创建异常处理类

<?php
// app/Exception/Handler/ApiExceptionHandler.php
    
namespace App\Exception\Handler;

use App\Exception\ApiException;
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 __construct(protected StdoutLoggerInterface $logger)
    {
    }

    public function handle(Throwable $throwable, ResponseInterface $response)
    {
        // 处理 RuntimeException 异常
        if ($throwable instanceof \RuntimeException) {
            return $this->exceptionHandle($throwable, $response);
        }

        $this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
        $this->logger->error($throwable->getTraceAsString());
        return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.'));
    }

    public function isValid(Throwable $throwable): bool
    {
        return true;
    }

    // 返回异常信息
    public function exceptionHandle(Throwable $throwable, ResponseInterface $response)
    {
        $data = json_encode([
            'code'    => $throwable->getCode(),
            'message' => $throwable->getMessage(),
        ], JSON_UNESCAPED_UNICODE);

        return $response->withAddedHeader('Content-Type', ' application/json; charset=UTF-8')
            ->withStatus(500)
            ->withBody(new SwooleStream($data));
    }
}

2、注册异常

<?php
// config/autoload/exception.php
    
declare(strict_types=1);

return [
    'handler' => [
        'http' => [
            // 在默认的异常类中埋进行远程调用异常处理
            App\Exception\Handler\AppExceptionHandler::class,
        ],
    ],
];

3、测试异常 - 获取不存在的用户信息

$ curl http://192.168.31.90:9501/users/show?jd=211
{"code":-32000,"message":"node_provider_user-0.0.0.0:9600-没有该用户"}

本文对服务者的调用是通过手动方式的,也可以注意到,使用注解服务时 publishTo 参数是没有填写的。目的是为了演示手动调用的过程。

到这里,就已经使用了微服务,没错,就是已经使用了。

后面我们将使用服务注册与自动发现。

我是温新,本篇文章结束。

请登录后再评论