最近公司来了个新项目,业务功能线很多也比较繁琐,用户量也会不少,在跟同事商量后,决定搭个微服务架构来应对。便开始集成网关,上注册/服务中心,上分布式事务等等…
整体架构大概完善后,便开始了业务功能的编写,这个时候便遇到了问题:
用户ID(真实场景并不明文)
、购买金额
等参数;FeignClient
来调用 用户服务(上游服务) 去查询用户的基本信息(比如当前用户状态是否正常?是否允许交易等);🍳问题便在 订单服务 去调用 用户服务 时,如果 用户服务 查询失败(如用户状态被冻结,用户不允许等)并抛出了带有提示信息的异常,而在我们 订单服务 是无法获取到异常信息的,它会抛出 FeignC
自带的FeignException
异常,并不会携带用户服务本身抛出的异常,订单服务 会显示一个网络为500的请求失败异常
如:服务A 调用 服务B
服务B 在运行时 抛出一个异常:
new RuntimeException("User does not exist or has been frozen");
而在 服务A 显示的异常信息为:
可能会有人问:用户服务 如果不抛出异常,而是查询失败后直接返回一个null,在 订单服务 调用完毕后,对其进行非空判断,然后在 订单服务 返回异常信息。 是的,想法可行,但是 订单服务 返回异常信息能否像 用户服务 那样详细,能够准确的知道用户到底是被冻结了,还是无法交易了呢? 显然是不能的,因为目前我们只知道查询用户失败了,反馈了一个空对象,到底失败的原因是什么我们并不清楚。
这里的 服务端
指服务提供者,也叫 上游服务;客户端
指 服务使用者,也叫下游服务。
服务端在 处理具体业务 和 各种服务之间的调用 时,会出现一些错误导致业务无法正常进行下去,例如:支付的时候余额不足,下单的时候库存不足等等,针对此种情况统一采用抛出一个自定义的业务异常 OkdFeignException
,同时采用错误码来区分各种错误,具体代码如下:
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
类可以放在上游服务,也可以放在通用模块
全局异常处理类:通过 @ExceptionHandler 统一处理被抛出的 OkdFeignException
,代码如下
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());
}
}
错误码类:明确服务内部调用的错误码,代码如下
/**
* 服务内部调用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;
}
}
上面两个类可以放在下游服务,也可以放在通用模块
ErrorDecoder类:
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);
}
}
这个时候流程便成为了这样:
如:服务A 调用 服务B
服务B 在运行时 抛出一个异常:
new OkdFeignException("User does not exist or has been frozen");
在 服务A 显示的异常信息为:
new RuntimeException("User does not exist or has been frozen");
好,记录完毕!