前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微服务-如何捕获上游服务抛出异常?

微服务-如何捕获上游服务抛出异常?

作者头像
Blue_007
发布2023-10-21 12:40:44
3761
发布2023-10-21 12:40:44
举报
文章被收录于专栏:代码生涯代码生涯

一、原由/场景🍔

最近公司来了个新项目,业务功能线很多也比较繁琐,用户量也会不少,在跟同事商量后,决定搭个微服务架构来应对。便开始集成网关,上注册/服务中心,上分布式事务等等…

整体架构大概完善后,便开始了业务功能的编写,这个时候便遇到了问题:

  1. 用户添加订单时,向 订单服务 发送请求并携带用户ID(真实场景并不明文)购买金额等参数;
  2. 订单服务 接收到请求,拿到用户id,会使用 FeignClient 来调用 用户服务(上游服务) 去查询用户的基本信息(比如当前用户状态是否正常?是否允许交易等);
  3. 订单服务 拿到用户的基本信息后便创建对应的订单并保存到数据库中。

🍳问题便在 订单服务 去调用 用户服务 时,如果 用户服务 查询失败(如用户状态被冻结,用户不允许等)并抛出了带有提示信息的异常,而在我们 订单服务 是无法获取到异常信息的,它会抛出 FeignC 自带的FeignException异常,并不会携带用户服务本身抛出的异常订单服务 会显示一个网络为500的请求失败异常

如:服务A 调用 服务B

服务B 在运行时 抛出一个异常:

代码语言:javascript
复制
new RuntimeException("User does not exist or has been frozen");

而在 服务A 显示的异常信息为:


可能会有人问:用户服务 如果不抛出异常,而是查询失败后直接返回一个null,在 订单服务 调用完毕后,对其进行非空判断,然后在 订单服务 返回异常信息。 是的,想法可行,但是 订单服务 返回异常信息能否像 用户服务 那样详细,能够准确的知道用户到底是被冻结了,还是无法交易了呢? 显然是不能的,因为目前我们只知道查询用户失败了,反馈了一个空对象,到底失败的原因是什么我们并不清楚。

二、方案🍗

1. 自定义异常类🕶

这里的 服务端 指服务提供者,也叫 上游服务客户端 指 服务使用者,也叫下游服务

服务端在 处理具体业务各种服务之间的调用 时,会出现一些错误导致业务无法正常进行下去,例如:支付的时候余额不足,下单的时候库存不足等等,针对此种情况统一采用抛出一个自定义的业务异常 OkdFeignException,同时采用错误码来区分各种错误,具体代码如下:

代码语言:javascript
复制
import lombok.Data;

/**
 * 自定义异常类/采用错误码来区分各种Feign错误
 * @ClassName OkdFeignException
 * @Author Blue Email:2113438464@qq.com
 * @Date 2023/8/19
 */
@Data
public class OkdFeignException extends RuntimeException {
    /**
     * 错误码
     */
    private String code;

    /**
     * 错误信息
     */
    private String msg;

    /**
     * 数据
     */
    private Object data;

    public OkdFeignException(String msg) {
        super(msg);
        this.msg = msg;
    }

    public OkdFeignException(String code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public OkdFeignException(String code, String msg, Object data) {
        super(msg);
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

}

这个Exception类可以放在上游服务,也可以放在通用模块

2. 服务端统一处理Exception🎣

全局异常处理类:通过 @ExceptionHandler 统一处理被抛出的 OkdFeignException,代码如下

代码语言:javascript
复制
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.qhzx.okd.constant.FeignHttpStatus;
import com.qhzx.okd.exception.err.OkdFeignException;
import com.qhzx.okd.untils.web.Result;
import com.sun.mail.smtp.SMTPAddressFailedException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailSendException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.mail.SendFailedException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.MissingResourceException;

/**
 * 全局异常处理程序
 * @ClassName GlobalExceptionHandler
 * @Author Blue Email:2113438464@qq.com
 * @Date 2023/8/21
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 自定义验证异常处理
     * 自定义异常类/采用错误码来区分各种错误
     */
    @ExceptionHandler(OkdFeignException.class)
    public void handleOkdFeignException(HttpServletResponse response, OkdFeignException e) {
        log.error(e.getMessage(), e);
        response.setStatus(FeignHttpStatus.OKD_ERROR.getVal());
        if (StrUtil.isNotBlank(e.getCode())) {
            response.addHeader("code",e.getCode());
        }

        // post请求头中的数据如果有中文必须进行编码,接收端要进行编码,例如:URLDecoder.decode(msg,"uft-8")
        String msg = "";
        String data = "";
        try {
            if (StrUtil.isNotBlank(e.getMsg())) {
                msg = URLEncoder.encode(e.getMsg(), "utf-8");
            }
            if (ObjectUtil.isNotEmpty(e.getData())) {
                data = URLEncoder.encode(JSON.toJSONString(e.getData()),"utf-8");
            }
        } catch (Exception e2) {}
        response.addHeader("msg",msg);
        response.addHeader("data",data);
    }
    
    //....其他异常处理

    /**
     * 所有异常处理
     * @param e
     * @return
     */
    @ExceptionHandler
    public Object handlerException(Exception e) {
        log.error(e.getMessage(), e);
        return Result.fail(e.getMessage());
    }

}

错误码类:明确服务内部调用的错误码,代码如下

代码语言:javascript
复制
/**
 * 服务内部调用Feign Status
 * @ClassName FeignHttpStatus
 * @Author Blue Email:2113438464@qq.com
 * @Date 2023/8/21
 */
public enum FeignHttpStatus {
    OKD_ERROR(600,"Feign errors");

    private final Integer val;
    private final String res;

    private FeignHttpStatus(Integer val,String res) {
        this.val = val;
        this.res = res;
    }

    public Integer getVal() {
        return this.val;
    }

    public String getRes() {
        return this.res;
    }

}

上面两个类可以放在下游服务,也可以放在通用模块

3、客户端统一处理 status==600 的 Response 🛒

ErrorDecoder类:

代码语言:javascript
复制
import cn.hutool.core.util.StrUtil;
import com.qhzx.okd.constant.FeignHttpStatus;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.URLDecoder;
import java.util.Collection;
import java.util.Map;

/**
 * Feign调用内部Exception转换
 * @ClassName GlobalExceptionHandler
 * @Author Blue Email:2113438464@qq.com
 * @Date 2023/8/22
 */
@Slf4j
@Component
public class FeignErrorDecoder implements ErrorDecoder {
    private final ErrorDecoder defaultErrorDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        // 获取Feign返回的原始异常信息
        int status = response.status();
        String feignErrorMessage = defaultErrorDecoder.decode(methodKey, response).getMessage();
        // 将 status == FeignHttpStatus.OKD_ERROR 的 Response 转化
        if (FeignHttpStatus.OKD_ERROR.getVal() == status) {
            Map<String, Collection<String>> headers = response.headers();
            String msg = headers.get("msg").iterator().next();
//            String code = headers.get("code").iterator().next();
//            String data = headers.get("data").iterator().next();

            if (StrUtil.isNotBlank(methodKey)) {
                try {
                    feignErrorMessage = URLDecoder.decode(msg, "utf-8");
                } catch (Exception e) {}
            }
        }
        log.error("feignError: " + feignErrorMessage);

        // 构造包含信息的异常对象并抛出
        return new RuntimeException(feignErrorMessage);
    }

}

4. 归总

这个时候流程便成为了这样:

如:服务A 调用 服务B

服务B 在运行时 抛出一个异常:

代码语言:javascript
复制
new OkdFeignException("User does not exist or has been frozen");

在 服务A 显示的异常信息为:

代码语言:javascript
复制
new RuntimeException("User does not exist or has been frozen");

好,记录完毕!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 代码生涯 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、原由/场景🍔
  • 二、方案🍗
    • 1. 自定义异常类🕶
      • 2. 服务端统一处理Exception🎣
        • 3、客户端统一处理 status==600 的 Response 🛒
          • 4. 归总
          相关产品与服务
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档