🐛 还原MK Feign降级根因并返回可读错误
避免全局 BladeFeignFallback 将域名解析/超时等异常吞成 null,前端与日志可直接看到失败原因。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+30
-6
@@ -1,17 +1,41 @@
|
||||
package org.springblade.thirdparty.mk.config;
|
||||
|
||||
import feign.Request;
|
||||
import feign.RequestInterceptor;
|
||||
import org.springblade.thirdparty.mk.feign.MKClientFallbackFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* MK Feign 客户端配置
|
||||
* <p>
|
||||
* 仅通过 {@link org.springframework.cloud.openfeign.FeignClient#configuration} 引用,避免拦截器全局生效。
|
||||
*
|
||||
* @author bfhuange
|
||||
* @since 2024/12/18
|
||||
*/
|
||||
public class MKFeignClientConfig {
|
||||
@Bean
|
||||
public RequestInterceptor requestInterceptor() {
|
||||
return template -> {
|
||||
// 空实现屏蔽 全局拦截器 BladeFeignRequestInterceptor
|
||||
};
|
||||
}
|
||||
@Bean
|
||||
public RequestInterceptor requestInterceptor() {
|
||||
return template -> {
|
||||
// 空实现屏蔽 全局拦截器 BladeFeignRequestInterceptor
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MK 降级工厂:保留 Feign 根因(域名解析/超时/远程错误体),避免全局兜底吞成 null
|
||||
*/
|
||||
@Bean
|
||||
public MKClientFallbackFactory mkClientFallbackFactory() {
|
||||
return new MKClientFallbackFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* 超时:connect 10s / read 60s(与 HCS/BFI 对齐,避免默认约 5s 过早降级)
|
||||
*/
|
||||
@Bean
|
||||
public Request.Options options() {
|
||||
return new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-1
@@ -18,7 +18,8 @@ import java.util.List;
|
||||
* @author bfhuange
|
||||
* @date 2024/9/11
|
||||
*/
|
||||
@FeignClient(name = "MK", url = "${thirdParty.mk.baseUrl}", configuration = MKFeignClientConfig.class)
|
||||
@FeignClient(name = "MK", url = "${thirdParty.mk.baseUrl}", configuration = MKFeignClientConfig.class,
|
||||
fallbackFactory = MKClientFallbackFactory.class, primary = false)
|
||||
public interface IMKClient {
|
||||
|
||||
/**
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.thirdparty.mk.feign;
|
||||
|
||||
import feign.FeignException;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.thirdparty.mk.exception.MKException;
|
||||
import org.springblade.thirdparty.mk.pojo.vo.MKResultVO;
|
||||
import org.springblade.thirdparty.mk.pojo.vo.MKTokenVO;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* MK Feign 客户端降级工厂
|
||||
* <p>
|
||||
* Feign 调用失败(域名解析失败、连接超时、非 2xx 等)时,全局 BladeFeignFallback
|
||||
* 对非 R 返回值一律返回 null,导致调用侧只能看到“获取token异常/响应参数:null”。
|
||||
* 本工厂接管降级:{@link MKResultVO} 还原为带详细 msg 的失败结果;
|
||||
* {@link MKTokenVO}、附件下载等直接抛出带根因的 {@link MKException},供前端展示。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class MKClientFallbackFactory implements FallbackFactory<IMKClient> {
|
||||
|
||||
@Override
|
||||
public IMKClient create(Throwable cause) {
|
||||
return (IMKClient) Proxy.newProxyInstance(IMKClient.class.getClassLoader(), new Class<?>[]{IMKClient.class},
|
||||
(Object proxy, Method method, Object[] args) -> {
|
||||
if (method.getDeclaringClass() != IMKClient.class) {
|
||||
return method.invoke(this, args);
|
||||
}
|
||||
String detail = resolveFallbackMessage(cause);
|
||||
if (MKResultVO.class.isAssignableFrom(method.getReturnType())) {
|
||||
return buildFailResult(cause, detail);
|
||||
}
|
||||
// getToken / downloadFile 等无业务失败体可还原,直接抛出可读异常
|
||||
throw new MKException("调用MK接口[" + method.getName() + "]失败:" + detail, cause);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造失败的 MKResultVO,优先还原远程错误体中的 msg/traceId
|
||||
*/
|
||||
private MKResultVO<?> buildFailResult(Throwable cause, String detail) {
|
||||
MKResultVO<Object> result = new MKResultVO<>();
|
||||
result.setSuccess(false);
|
||||
if (cause instanceof FeignException feignException) {
|
||||
result.setCode("errors.http." + feignException.status());
|
||||
MKResultVO<?> remote = parseRemoteResult(feignException.contentUTF8());
|
||||
if (remote != null && StringUtil.isNotBlank(remote.getMsg())) {
|
||||
result.setMsg(remote.getMsg());
|
||||
result.setTraceId(remote.getTraceId());
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
result.setCode("errors.feignFallback");
|
||||
}
|
||||
result.setMsg(detail);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误体反序列化为 MKResultVO(非 JSON 或解析失败时返回 null)
|
||||
*/
|
||||
private MKResultVO<?> parseRemoteResult(String body) {
|
||||
if (StringUtil.isBlank(body) || !body.trim().startsWith("{")) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JsonUtil.parse(body, MKResultVO.class);
|
||||
} catch (Exception exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底消息:优先识别域名/超时等网络类根因,其次用异常摘要
|
||||
*/
|
||||
private String resolveFallbackMessage(Throwable cause) {
|
||||
Throwable current = cause;
|
||||
while (current != null) {
|
||||
if (current instanceof UnknownHostException) {
|
||||
return "MK域名解析失败(" + nullToEmpty(current.getMessage()) + "),请检查容器DNS或hosts";
|
||||
}
|
||||
if (current instanceof SocketTimeoutException) {
|
||||
return "MK接口连接/读取超时(" + nullToEmpty(current.getMessage()) + ")";
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
if (cause != null && StringUtil.isNotBlank(cause.getMessage())) {
|
||||
return cause.getMessage();
|
||||
}
|
||||
String type = cause == null ? "Unknown" : cause.getClass().getSimpleName();
|
||||
return "MK接口调用异常(" + type + ")";
|
||||
}
|
||||
|
||||
private String nullToEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}
|
||||
+20
-4
@@ -160,12 +160,12 @@ public class MKServiceImpl implements IMKService {
|
||||
}
|
||||
log.error("调用mk接口 {}异常响应参数:{}", type, JSONUtil.toJsonStr(result));
|
||||
String message = getMessage(result);
|
||||
throw new MKException("调用mk接口" + type + "异常 " + message);
|
||||
throw new MKException("调用mk接口" + type + "异常" + (StringUtil.isBlank(message) ? "" : ":" + message));
|
||||
} catch (MKException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("调用mk接口 {}异常", type, e);
|
||||
throw new MKException("调用mk接口" + type + "异常");
|
||||
throw new MKException("调用mk接口" + type + "异常:" + resolveExceptionMessage(e), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,12 +515,28 @@ public class MKServiceImpl implements IMKService {
|
||||
return accessToken;
|
||||
}
|
||||
log.error("调用mk接口 获取token 异常响应参数:{}", JSONUtil.toJsonStr(tokenResult));
|
||||
throw new MKException("调用mk接口获取token异常");
|
||||
throw new MKException(tokenResult == null
|
||||
? "调用mk接口获取token异常:无响应(Feign已降级)"
|
||||
: "调用mk接口获取token异常:errcode=" + tokenResult.getErrCode());
|
||||
} catch (MKException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("调用mk接口 获取token 异常", e);
|
||||
throw new MKException("调用mk接口获取token异常");
|
||||
throw new MKException("调用mk接口获取token异常:" + resolveExceptionMessage(e), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常摘要:优先 cause 链上的可读 message
|
||||
*/
|
||||
private String resolveExceptionMessage(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current != null) {
|
||||
if (StringUtil.isNotBlank(current.getMessage())) {
|
||||
return current.getMessage();
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
return throwable == null ? "未知错误" : throwable.getClass().getSimpleName();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user