ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【基于 Swoole+Hyperf 的微服务实战】 第三周·周五:统一 RPC 异常处理与跨服务 Token 传递

【基于 Swoole+Hyperf 的微服务实战】 第三周·周五:统一 RPC 异常处理与跨服务 Token 传递 第三周 周五统一 RPC 异常处理与跨服务 Token 传递今日目标理解微服务中 RPC 异常传播的挑战学会定义业务异常类区分系统异常与业务异常。在RPC 服务端实现统一异常处理将业务异常以结构化的 JSON 返回给客户端避免泄露敏感堆栈信息。在RPC 客户端捕获远程异常转换为本地可理解的业务异常或降级处理保持代码整洁。实现跨服务 Token 验证用户服务提供verifyTokenRPC 接口文章服务接收客户端 Token调用用户服务完成身份认证并将用户信息传递给业务逻辑。测试无 Token、非法 Token、合法 Token 场景验证 RPC 调用链中的身份传递与异常处理。一、环境准备约 20 分钟继续使用hyperf-app项目其中已包含用户服务端口 9502和文章服务端口 9501的 HTTP 服务。确保两个服务均已启动。进入容器cdswoole-coursedocker-composeexecswoolebashcd/var/www/hyperf-app确认昨天已实现用户服务UserService及 JSON-RPC 服务端。今天我们需要为用户服务增加verifyToken方法。创建业务异常类。配置 RPC 服务端异常处理器。在文章服务中注入用户服务 RPC 客户端并在控制器中调用验证。二、知识核心RPC 异常处理与 Token 传递模式约 1 小时1. RPC 异常传播问题在单体应用中异常可以直接抛出并由全局处理器捕获。在 RPC 调用中服务端抛出的异常需要经过网络传输客户端需要知道发生了什么错误。如果直接传递 PHP 异常对象不仅包含敏感信息文件路径、堆栈还可能无法反序列化。因此通常做法是服务端定义业务异常类如BusinessException在业务逻辑中抛出。服务端异常处理器捕获该异常将其转化为统一的 JSON 结构包含 code、message然后作为 RPC 响应返回HTTP 状态码通常为 200表示 RPC 调用成功但业务结果在 body 中。客户端RPC 客户端在接收到包含业务错误的响应后解析并抛出一个本地业务异常或直接返回错误结果由上层处理。Hyperf 的 JSON-RPC 组件本身不自动处理业务异常我们需要手动实现。2. 跨服务 Token 传递的两种方式通过 RPC 方法参数显式传递上游服务如网关或文章服务从自己的请求中提取 Token作为参数调用用户服务的verifyToken($token)方法。这是最简单直接的方式适合方法数量较少的情况。通过 RPC 上下文传递使用 Hyperf 的Context或自定义 Header在 RPC 请求中携带 Token服务端从上下文中读取。但 JSON-RPC over HTTP 本身支持 Header可以在客户端调用时设置 Header服务端通过Request对象获取。这种方式更透明但需要服务端和客户端约定好 Header 名称。今天我们采用方法参数显式传递因为它清晰且易于理解也符合 RPC 的接口契约。3. 用户服务增加 verifyToken 方法用户服务在登录时会签发 JWT现在需要提供一个 RPC 方法verifyToken(string $token)返回用户信息或抛出业务异常。该方法模拟验证 Token 的有效性例如解码 JWT 并查询用户。三、实战统一异常处理与 Token 验证约 2.5 小时步骤 1创建业务异常类新建app/Exception/BusinessException.php?phpnamespaceApp\Exception;useHyperf\Server\Exception\ServerException;classBusinessExceptionextendsServerException{publicfunction__construct(string$message,int$code400,\Throwable$previousnull){parent::__construct($message,$code,$previous);}}这里继承ServerException或RuntimeException携带业务错误码和消息。步骤 2为用户服务添加 verifyToken 方法修改app/JsonRpc/UserServiceInterface.php添加方法声明publicfunctionverifyToken(string$token):array;修改app/JsonRpc/UserService.php实现publicfunctionverifyToken(string$token):array{// 模拟验证实际上应解析 JWT 并查询用户// 为了演示假设合法 token 为 valid-token对应 user_id1if($tokenvalid-token){return[id1,usernameadmin,emailadminexample.com,];}// 无效 token抛出业务异常thrownew\App\Exception\BusinessException(无效的Token,401);}步骤 3创建 RPC 服务端异常处理器当用户服务抛出BusinessException时我们希望它不会导致 RPC 调用直接失败而是返回一个包含业务错误的 JSON 响应。我们可以通过异常处理器捕获所有异常并转换为 JSON-RPC 响应格式。Hyperf 的 JSON-RPC 服务端是基于 HTTP 服务器因此可以注册一个全局异常处理器类似 HTTP 异常处理器但需要区分是否为 RPC 请求。创建app/Exception/Handler/RpcExceptionHandler.php?phpnamespaceApp\Exception\Handler;useApp\Exception\BusinessException;useHyperf\ExceptionHandler\ExceptionHandler;useHyperf\HttpMessage\Stream\SwooleStream;usePsr\Http\Message\ResponseInterface;useThrowable;classRpcExceptionHandlerextendsExceptionHandler{publicfunctionhandle(Throwable$throwable,ResponseInterface$response){// 停止异常传播$this-stopPropagation();// 构造业务错误响应JSON-RPC 格式$bodyjson_encode([jsonrpc2.0,error[code$throwable-getCode(),message$throwable-getMessage(),],idnull,// 可以从上下文中获取但这里简单处理],JSON_UNESCAPED_UNICODE);// 返回 HTTP 200错误信息在 body 中return$response-withStatus(200)-withHeader(Content-Type,application/json)-withBody(newSwooleStream($body));}publicfunctionisValid(Throwable$throwable):bool{// 仅处理业务异常return$throwableinstanceofBusinessException;}}在config/autoload/exceptions.php中注册该处理器放在最前面?phpreturn[handler[http[\App\Exception\Handler\RpcExceptionHandler::class,// RPC 业务异常// 其他 HTTP 异常处理器...],],];注意RpcExceptionHandler应该只处理 RPC 请求但我们的服务既有 HTTP 普通接口又有 RPC 接口。为了区分可以在处理器中判断请求路径或上下文。但对于演示我们可以简单使用因为普通 HTTP 接口抛出的BusinessException也应该返回类似的结构但可能不符合 REST 规范。更好的做法是创建两个处理器分别处理 HTTP 和 RPC。这里为了简洁我们只处理 RPC 请求通过检查请求的 Header 或路径来区分。简单做法在处理器中检查$response的 URI 是否为 RPC 端点例如/且Content-Type包含application/json-rpc但这样复杂。今天我们就统一处理后续可以优化。步骤 4在文章服务中实现 Token 验证调用文章服务需要调用用户服务来验证 Token。我们已经在文章服务中注入了UserServiceInterface的 RPC 客户端之前用于获取用户信息。现在添加一个受保护的接口GET /articles/secure要求客户端携带Authorization: Bearer token文章服务提取 token调用verifyToken成功则返回用户信息和一些文章数据。修改app/Controller/ArticleController.php添加方法useApp\Exception\BusinessException;// 在 ArticleController 类中添加#[RequestMapping(path:secure,methods:get)]publicfunctionsecure(){$token$this-request-getHeaderLine(Authorization);if(empty($token)||!str_starts_with($token,Bearer )){thrownewBusinessException(缺少Token,401);}$tokensubstr($token,7);try{// 通过 RPC 调用用户服务验证 token$user$this-userService-verifyToken($token);}catch(\Throwable$e){// 捕获 RPC 客户端抛出的异常可能是 BusinessException 的序列化// Hyperf 的 RPC 客户端在接收到业务错误时会抛出 RuntimeException 或自定义异常// 我们可以包装为业务异常返回thrownewBusinessException($e-getMessage(),$e-getCode());}return[code200,message身份验证通过,user$user,articlesArticle::limit(5)-get(),];}注意需要在文章服务中注入UserServiceInterface之前已经注入过如果还没有参考第三周周二内容添加#[RpcClient]。这里$this-userService是 RPC 客户端代理调用verifyToken时如果服务端返回业务错误客户端会抛出异常吗Hyperf 的 JSON-RPC 客户端在收到错误响应时会抛出一个Hyperf\Rpc\Exception\RecvException或包含错误信息的异常。我们需要捕获并处理。实际测试中客户端可能抛出的异常 message 就是服务端设置的错误 messagecode 也是业务 code所以可以直接重新抛出 BusinessException。步骤 5测试 RPC 异常与 Token 验证重启hyperf-app服务确保异常处理器和 RPC 接口生效。测试 1直接调用用户服务 RPC verifyToken 无效 tokencurl-XPOST http://localhost:9502\-HContent-Type: application/json\-d{jsonrpc:2.0,method:user/verifyToken,params:[invalid-token],id:1}预期返回 HTTP 200body 为{jsonrpc:2.0,error:{code:401,message:无效的Token},id:null}可以看到服务端抛出的业务异常被正确处理没有泄露堆栈。测试 2调用文章服务受保护接口无 tokencurlhttp://localhost:9501/articles/secure预期返回 401 错误因为我们抛出了 BusinessException但还没配置 HTTP 异常处理器可能会返回 500 或默认错误。我们需要为 HTTP 也配置一个异常处理器或者直接返回 JSON 响应。但今天的重点是 RPC 异常所以这个测试可能不完美可以先忽略或手动处理。测试 3调用文章服务受保护接口携带无效 tokencurl-HAuthorization: Bearer invalid-tokenhttp://localhost:9501/articles/secure预期文章服务调用用户服务 verifyToken用户服务返回错误RPC 客户端抛出异常文章服务捕获后返回 401 错误。测试 4携带有效 tokencurl-HAuthorization: Bearer valid-tokenhttp://localhost:9501/articles/secure预期返回用户信息和文章列表状态 200。步骤 6完善异常处理可选由于我们的文章服务是普通 HTTP 服务BusinessException也需要被处理否则会返回 500。我们可以创建一个AppExceptionHandler类似第二周周三的通用异常处理器将BusinessException转换为 JSON 响应。这里快速添加一个处理器创建app/Exception/Handler/BusinessExceptionHandler.php?phpnamespaceApp\Exception\Handler;useApp\Exception\BusinessException;useHyperf\ExceptionHandler\ExceptionHandler;useHyperf\HttpMessage\Stream\SwooleStream;usePsr\Http\Message\ResponseInterface;useThrowable;classBusinessExceptionHandlerextendsExceptionHandler{publicfunctionhandle(Throwable$throwable,ResponseInterface$response){$this-stopPropagation();$bodyjson_encode([code$throwable-getCode(),message$throwable-getMessage(),],JSON_UNESCAPED_UNICODE);return$response-withStatus($throwable-getCode())-withHeader(Content-Type,application/json)-withBody(newSwooleStream($body));}publicfunctionisValid(Throwable$throwable):bool{return$throwableinstanceofBusinessException;}}在exceptions.php中注册放在 RpcExceptionHandler 之后注意顺序Rpc 处理器可能更早匹配。实际上两个处理器都会匹配 BusinessException我们需要确保 RpcExceptionHandler 只处理 RPC 请求否则会导致普通 HTTP 请求也返回 JSON-RPC 格式。更好的做法是让 RpcExceptionHandler 判断请求是否为 RPC可以通过$response的getAttribute或检查 URI 路径是否为 RPC 端点。但为了简化我们可以让两个处理器都注册但 RpcExceptionHandler 的isValid中增加判断条件仅当请求头包含X-Rpc或者请求路径是/RPC 默认端点时才处理。今天我们暂不细化直接使用 BusinessExceptionHandler 处理普通 HTTP 请求RpcExceptionHandler 处理 RPC 请求可通过检查$response的 Header 来区分如果无法区分可以暂时只保留 BusinessExceptionHandler它会同时影响 RPC 响应但这也没问题因为 JSON-RPC 客户端也能解析。但标准 JSON-RPC 响应要求jsonrpc字段所以最好分开。我们可以在 RpcExceptionHandler 中判断当前请求是否为 RPC通过Hyperf\Context\Context获取当前请求的Content-Type或路径。但为了避免复杂性我们采用折中在 RpcExceptionHandler 的handle中不设置jsonrpc字段而是返回与 HTTP 相同的 JSON 结构这样客户端也能识别错误但客户端可能期望错误在error字段。实际 Hyperf 的 RPC 客户端会解析响应体如果响应中包含error字段且非空就会抛出异常。所以我们可以在 RpcExceptionHandler 中返回{error:{code:401,message:无效的Token}}而 BusinessExceptionHandler 返回{code:401,message:无效的Token}。两者格式不同但客户端会尝试寻找error字段。因此RpcExceptionHandler 应优先处理 RPC 请求我们可以在其中检查请求是否含有jsonrpc参数或 header。但为了课程流畅我们简单认为 RpcExceptionHandler 只处理来自 RPC 端点的异常通过检查当前请求的路径是否为/且方法为 POST 来近似判断。今天我们先不深究重点是理解异常处理的原理。课后可以自行优化。四、成果测试与验证约 1 小时测试清单检验项方法通过标准服务端业务异常返回格式直接调用 RPC verifyToken 无效 token返回 JSON 包含error.code和error.messageHTTP 200客户端捕获 RPC 异常文章服务调用 verifyToken 无效 token文章服务返回 401且消息为“无效的Token”合法 Token 验证通过使用 valid-token 调用文章服务 secure 接口返回用户信息和文章列表状态 200缺少 Token 拦截不带 Authorization 头调用 secure返回 401消息“缺少Token”异常不泄露堆栈检查响应体确认没有文件路径或堆栈只包含 code 和 messageRPC 客户端降级模拟用户服务宕机调用 secure 接口文章服务返回 500 或降级数据可后续优化常见问题RPC 客户端抛出异常类型实际可能是Hyperf\Rpc\Exception\RecvException需要查看具体实现捕获Throwable即可。异常处理器优先级确保RpcExceptionHandler在BusinessExceptionHandler之前注册并且isValid正确区分请求类型。Token 传递安全在实际项目中JWT 应该由网关统一验证并传递用户 ID而不是每个服务都验证。今天只是演示 RPC 中传递验证。五、今日作业与学习产出提交代码业务异常类、用户服务verifyToken、RPC 异常处理器、文章服务 secure 接口。完善 Token 验证在用户服务中真正解析 JWT使用hyperf/jwt而不是硬编码 valid-token。文章服务将验证后的用户 ID 写入请求上下文方便后续使用。学习笔记对比单体应用和微服务中异常处理的异同总结 RPC 异常设计最佳实践。绘制跨服务 Token 验证的时序图客户端 → 文章服务 → 用户服务 → 文章服务 → 客户端。挑战任务实现RPC 上下文传递 Token在 RPC 客户端调用时自动附加 Header服务端从 Header 中读取 Token 并验证避免每个方法都传 token 参数。为 RPC 调用增加超时和重试如果还没做结合异常处理实现优雅降级。通过今天的学习你已掌握微服务中 RPC 通信的异常处理和身份传递能够构建健壮、安全的服务间调用。下周我们将进入服务注册发现与配置中心继续深化微服务治理能力。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进