diff --git a/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/config/MKFeignClientConfig.java b/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/config/MKFeignClientConfig.java index 309cc4fa..99a65496 100644 --- a/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/config/MKFeignClientConfig.java +++ b/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/config/MKFeignClientConfig.java @@ -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 客户端配置 + *

+ * 仅通过 {@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); + } } diff --git a/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/feign/IMKClient.java b/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/feign/IMKClient.java index 64e38615..398a46b8 100644 --- a/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/feign/IMKClient.java +++ b/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/feign/IMKClient.java @@ -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 { /** diff --git a/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/feign/MKClientFallbackFactory.java b/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/feign/MKClientFallbackFactory.java new file mode 100644 index 00000000..d16458e7 --- /dev/null +++ b/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/feign/MKClientFallbackFactory.java @@ -0,0 +1,128 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * 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 客户端降级工厂 + *

+ * Feign 调用失败(域名解析失败、连接超时、非 2xx 等)时,全局 BladeFeignFallback + * 对非 R 返回值一律返回 null,导致调用侧只能看到“获取token异常/响应参数:null”。 + * 本工厂接管降级:{@link MKResultVO} 还原为带详细 msg 的失败结果; + * {@link MKTokenVO}、附件下载等直接抛出带根因的 {@link MKException},供前端展示。 + * + * @author Chill + */ +public class MKClientFallbackFactory implements FallbackFactory { + + @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 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; + } +} diff --git a/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/service/impl/MKServiceImpl.java b/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/service/impl/MKServiceImpl.java index eb5b1877..e42de9c7 100644 --- a/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/service/impl/MKServiceImpl.java +++ b/blade-third-party-api/blade-mk-api/src/main/java/org/springblade/thirdparty/mk/service/impl/MKServiceImpl.java @@ -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(); + } }