🐛 还原MK Feign降级根因并返回可读错误
避免全局 BladeFeignFallback 将域名解析/超时等异常吞成 null,前端与日志可直接看到失败原因。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+24
@@ -1,9 +1,17 @@
|
|||||||
package org.springblade.thirdparty.mk.config;
|
package org.springblade.thirdparty.mk.config;
|
||||||
|
|
||||||
|
import feign.Request;
|
||||||
import feign.RequestInterceptor;
|
import feign.RequestInterceptor;
|
||||||
|
import org.springblade.thirdparty.mk.feign.MKClientFallbackFactory;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* MK Feign 客户端配置
|
||||||
|
* <p>
|
||||||
|
* 仅通过 {@link org.springframework.cloud.openfeign.FeignClient#configuration} 引用,避免拦截器全局生效。
|
||||||
|
*
|
||||||
* @author bfhuange
|
* @author bfhuange
|
||||||
* @since 2024/12/18
|
* @since 2024/12/18
|
||||||
*/
|
*/
|
||||||
@@ -14,4 +22,20 @@ public class MKFeignClientConfig {
|
|||||||
// 空实现屏蔽 全局拦截器 BladeFeignRequestInterceptor
|
// 空实现屏蔽 全局拦截器 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
|
* @author bfhuange
|
||||||
* @date 2024/9/11
|
* @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 {
|
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));
|
log.error("调用mk接口 {}异常响应参数:{}", type, JSONUtil.toJsonStr(result));
|
||||||
String message = getMessage(result);
|
String message = getMessage(result);
|
||||||
throw new MKException("调用mk接口" + type + "异常 " + message);
|
throw new MKException("调用mk接口" + type + "异常" + (StringUtil.isBlank(message) ? "" : ":" + message));
|
||||||
} catch (MKException e) {
|
} catch (MKException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("调用mk接口 {}异常", type, 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;
|
return accessToken;
|
||||||
}
|
}
|
||||||
log.error("调用mk接口 获取token 异常响应参数:{}", JSONUtil.toJsonStr(tokenResult));
|
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) {
|
} catch (MKException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("调用mk接口 获取token 异常", 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