Browse Source

feat:核心服务支持动态FeignClient构建

hidewnd 2 weeks ago
parent
commit
5e2f1d5147

+ 43 - 0
yt-agent/agent-service/src/main/java/com/ytpm/config/feign/FeignBuilderConfig.java

@@ -0,0 +1,43 @@
+package com.ytpm.config.feign;
+
+import feign.Feign;
+import feign.Request;
+import feign.Retryer;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
+import org.springframework.cloud.openfeign.support.SpringDecoder;
+import org.springframework.cloud.openfeign.support.SpringEncoder;
+import org.springframework.cloud.openfeign.support.SpringMvcContract;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 自定义FeignClient 构造
+ * @author lih
+ * @date 2025-10-24 09:41
+ */
+@Configuration
+public class FeignBuilderConfig {
+    @Value("${feign.client.config.default.connectTimeout:3000}")
+    private Integer connectTimeoutMillis;
+
+    @Value("${feign.client.config.default.readTimeout:3000}")
+    private Integer readTimeoutMillis;
+
+    @Bean
+    @Scope("prototype")
+    public Feign.Builder feignBuilder(ObjectFactory<HttpMessageConverters> messageConverters) {
+        return Feign.builder()
+                .contract(new SpringMvcContract())
+                .encoder(new SpringEncoder(messageConverters))
+                .decoder(new SpringDecoder(messageConverters))
+                .retryer(new Retryer.Default(100, TimeUnit.SECONDS.toMicros(1), 3))
+                .options(new Request.Options(connectTimeoutMillis, readTimeoutMillis))
+                .requestInterceptor(new FeignConfiguration());
+
+    }
+}

+ 52 - 0
yt-agent/agent-service/src/main/java/com/ytpm/handle/DynamicFeignClientFactory.java

@@ -0,0 +1,52 @@
+package com.ytpm.handle;
+
+import feign.Feign;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
+import org.springframework.stereotype.Component;
+
+/**
+ * 动态FeignClient 构造工厂
+ * @author lih
+ * @date 2025-10-24 09:45
+ */
+@Slf4j
+@Component
+public class DynamicFeignClientFactory {
+
+    private final Feign.Builder feignBuilder;
+    private final LoadBalancerClient loadBalancerClient;
+
+
+    public DynamicFeignClientFactory(Feign.Builder feignBuilder,
+                                     LoadBalancerClient loadBalancerClient) {
+        this.feignBuilder = feignBuilder;
+        this.loadBalancerClient = loadBalancerClient;
+    }
+
+    public <T> T getClient(String serviceName, Class<T> clazz) {
+        int maxRetry = 3;
+        int retryCount = 0;
+        Exception lastException = null;
+
+        while (retryCount < maxRetry) {
+            try {
+                ServiceInstance instance = loadBalancerClient.choose(serviceName);
+                if (instance == null) {
+                    throw new RuntimeException("未找到可用的服务实例:" + serviceName);
+                }
+                String url = instance.getUri().toString();
+                log.info("为服务{}构建FeignClient,目标地址为:{}", serviceName, url);
+                return feignBuilder.target(clazz, url);
+            } catch (Exception e) {
+                lastException = e;
+                log.warn("第 {} 次尝试获取FeignClient失败,服务名:{},错误信息:{}", retryCount + 1, serviceName, e.getMessage());
+                retryCount++;
+
+            }
+        }
+        throw new RuntimeException("创建FeignClient失败,服务名:" + serviceName, lastException);
+    }
+
+}

+ 32 - 9
yt-agent/agent-service/src/main/java/com/ytpm/utils/FeignClientInvoker.java

@@ -1,9 +1,12 @@
 package com.ytpm.utils;
 
+import com.ytpm.handle.DynamicFeignClientFactory;
+import com.ytpm.question.base.BaseFeign;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.context.ApplicationContext;
 import org.springframework.stereotype.Component;
 
+import javax.annotation.Resource;
 import java.lang.reflect.Method;
 import java.util.HashMap;
 import java.util.Map;
@@ -19,6 +22,8 @@ public class FeignClientInvoker {
     private final ApplicationContext applicationContext;
     private final Map<String, Object> feignClientCache = new HashMap<>();
     private final Map<String, Method> methodCache = new HashMap<>();
+    @Resource
+    private DynamicFeignClientFactory feignClientFactory;
 
     public FeignClientInvoker(ApplicationContext applicationContext) {
         this.applicationContext = applicationContext;
@@ -51,19 +56,37 @@ public class FeignClientInvoker {
         if (feignClientCache.containsKey(serviceName)) {
             return feignClientCache.get(serviceName);
         }
-        
-        // 动态查找标注@FeignClient的Bean
-        Map<String, Object> feignClients = applicationContext.getBeansWithAnnotation(FeignClient.class);
-        for (Object bean : feignClients.values()) {
-            FeignClient annotation = bean.getClass().getInterfaces()[0].getAnnotation(FeignClient.class);
-            if (annotation != null && serviceName.equals(annotation.name())) {
-                feignClientCache.put(serviceName, bean);
-                return bean;
+        Object feignClient = null;
+        try {
+            // 默认查询声明实例
+            // 动态查找标注@FeignClient的Bean
+            Map<String, Object> feignClients = applicationContext.getBeansWithAnnotation(FeignClient.class);
+            for (Object bean : feignClients.values()) {
+                FeignClient annotation = bean.getClass().getInterfaces()[0].getAnnotation(FeignClient.class);
+                if (annotation != null && serviceName.equals(annotation.name())) {
+                    feignClientCache.put(serviceName, bean);
+                    feignClient = bean;
+                }
+            }
+            // 构建动态FeignClient
+            if (feignClient == null) {
+                Class<?> clazz = com.ytpm.question.base.BaseFeign.class;
+                if (serviceName.contains("ios")) {
+                    clazz = com.ytpm.lemonios.feign.base.BaseFeign.class;
+                }
+                feignClient = feignClientFactory.getClient(serviceName, clazz);
             }
+            feignClientCache.put(serviceName, feignClient);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("未找到服务: " + serviceName);
+        }
+        if (feignClient == null) {
+            throw new IllegalArgumentException("未找到服务: " + serviceName);
         }
-        throw new IllegalArgumentException("未找到服务: " + serviceName);
+        return feignClient;
     }
 
+
     private Method getTargetMethod(Object feignClient, String methodName, Object[] args) 
         throws NoSuchMethodException {
         

+ 44 - 0
yt-middle/middle-platform/src/main/java/com/ytpm/middle/config/FeignBuilderConfig.java

@@ -0,0 +1,44 @@
+package com.ytpm.middle.config;
+
+import feign.Feign;
+import feign.Logger;
+import feign.Request;
+import feign.Retryer;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
+import org.springframework.cloud.openfeign.support.SpringDecoder;
+import org.springframework.cloud.openfeign.support.SpringEncoder;
+import org.springframework.cloud.openfeign.support.SpringMvcContract;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 自定义FeignClient 构造
+ * @author lih
+ * @date 2025-10-24 09:41
+ */
+@Configuration
+public class FeignBuilderConfig {
+
+    @Value("${feign.client.config.default.connectTimeout:3000}")
+    private Integer connectTimeoutMillis;
+
+    @Value("${feign.client.config.default.readTimeout:3000}")
+    private Integer readTimeoutMillis;
+
+    @Bean
+    @Scope("prototype")
+    public Feign.Builder feignBuilder(ObjectFactory<HttpMessageConverters> messageConverters) {
+        return Feign.builder()
+                .contract(new SpringMvcContract())
+                .encoder(new SpringEncoder(messageConverters))
+                .decoder(new SpringDecoder(messageConverters))
+                .retryer(new Retryer.Default(100, TimeUnit.SECONDS.toMicros(1), 3))
+                .options(new Request.Options(connectTimeoutMillis, readTimeoutMillis));
+
+    }
+}

+ 52 - 0
yt-middle/middle-platform/src/main/java/com/ytpm/middle/handle/DynamicFeignClientFactory.java

@@ -0,0 +1,52 @@
+package com.ytpm.middle.handle;
+
+import feign.Feign;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
+import org.springframework.stereotype.Component;
+
+/**
+ * 动态FeignClient 构造工厂
+ * @author lih
+ * @date 2025-10-24 09:45
+ */
+@Slf4j
+@Component
+public class DynamicFeignClientFactory {
+
+    private final Feign.Builder feignBuilder;
+    private final LoadBalancerClient loadBalancerClient;
+
+
+    public DynamicFeignClientFactory(Feign.Builder feignBuilder,
+                                     LoadBalancerClient loadBalancerClient) {
+        this.feignBuilder = feignBuilder;
+        this.loadBalancerClient = loadBalancerClient;
+    }
+
+    public <T> T getClient(String serviceName, Class<T> clazz) {
+        int maxRetry = 3;
+        int retryCount = 0;
+        Exception lastException = null;
+
+        while (retryCount < maxRetry) {
+            try {
+                ServiceInstance instance = loadBalancerClient.choose(serviceName);
+                if (instance == null) {
+                    throw new RuntimeException("未找到可用的服务实例:" + serviceName);
+                }
+                String url = instance.getUri().toString();
+                log.info("为服务{}构建FeignClient,目标地址为:{}", serviceName, url);
+                return feignBuilder.target(clazz, url);
+            } catch (Exception e) {
+                lastException = e;
+                log.warn("第 {} 次尝试获取FeignClient失败,服务名:{},错误信息:{}", retryCount + 1, serviceName, e.getMessage());
+                retryCount++;
+
+            }
+        }
+        throw new RuntimeException("创建FeignClient失败,服务名:" + serviceName, lastException);
+    }
+
+}

+ 30 - 9
yt-middle/middle-platform/src/main/java/com/ytpm/middle/util/FeignClientInvoker.java

@@ -1,9 +1,11 @@
 package com.ytpm.middle.util;
 
+import com.ytpm.middle.handle.DynamicFeignClientFactory;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.context.ApplicationContext;
 import org.springframework.stereotype.Component;
 
+import javax.annotation.Resource;
 import java.lang.reflect.Method;
 import java.util.HashMap;
 import java.util.Map;
@@ -19,6 +21,8 @@ public class FeignClientInvoker {
     private final ApplicationContext applicationContext;
     private final Map<String, Object> feignClientCache = new HashMap<>();
     private final Map<String, Method> methodCache = new HashMap<>();
+    @Resource
+    private DynamicFeignClientFactory feignClientFactory;
 
     public FeignClientInvoker(ApplicationContext applicationContext) {
         this.applicationContext = applicationContext;
@@ -51,17 +55,34 @@ public class FeignClientInvoker {
         if (feignClientCache.containsKey(serviceName)) {
             return feignClientCache.get(serviceName);
         }
-        
-        // 动态查找标注@FeignClient的Bean
-        Map<String, Object> feignClients = applicationContext.getBeansWithAnnotation(FeignClient.class);
-        for (Object bean : feignClients.values()) {
-            FeignClient annotation = bean.getClass().getInterfaces()[0].getAnnotation(FeignClient.class);
-            if (annotation != null && serviceName.equals(annotation.name())) {
-                feignClientCache.put(serviceName, bean);
-                return bean;
+        Object feignClient = null;
+        try {
+            // 默认查询声明实例
+            // 动态查找标注@FeignClient的Bean
+            Map<String, Object> feignClients = applicationContext.getBeansWithAnnotation(FeignClient.class);
+            for (Object bean : feignClients.values()) {
+                FeignClient annotation = bean.getClass().getInterfaces()[0].getAnnotation(FeignClient.class);
+                if (annotation != null && serviceName.equals(annotation.name())) {
+                    feignClientCache.put(serviceName, bean);
+                    feignClient = bean;
+                }
+            }
+            // 构建动态FeignClient
+            if (feignClient == null) {
+                Class<?> clazz = com.ytpm.question.base.BaseFeign.class;
+                if (serviceName.contains("ios")) {
+                    clazz = com.ytpm.lemonios.feign.base.BaseFeign.class;
+                }
+                feignClient = feignClientFactory.getClient(serviceName, clazz);
             }
+            feignClientCache.put(serviceName, feignClient);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("未找到服务: " + serviceName);
+        }
+        if (feignClient == null) {
+            throw new IllegalArgumentException("未找到服务: " + serviceName);
         }
-        throw new IllegalArgumentException("未找到服务: " + serviceName);
+        return feignClient;
     }
 
     private Method getTargetMethod(Object feignClient, String methodName, Object[] args) 

+ 44 - 0
yt-risk/risk-manage/src/main/java/com/ytpm/config/feign/FeignBuilderConfig.java

@@ -0,0 +1,44 @@
+package com.ytpm.config.feign;
+
+import feign.Feign;
+import feign.Request;
+import feign.Retryer;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
+import org.springframework.cloud.openfeign.support.SpringDecoder;
+import org.springframework.cloud.openfeign.support.SpringEncoder;
+import org.springframework.cloud.openfeign.support.SpringMvcContract;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 自定义FeignClient 构造
+ * @author lih
+ * @date 2025-10-24 09:41
+ */
+@Configuration
+public class FeignBuilderConfig {
+
+    @Value("${feign.client.config.default.connectTimeout:3000}")
+    private Integer connectTimeoutMillis;
+
+    @Value("${feign.client.config.default.readTimeout:3000}")
+    private Integer readTimeoutMillis;
+
+    @Bean
+    @Scope("prototype")
+    public Feign.Builder feignBuilder(ObjectFactory<HttpMessageConverters> messageConverters) {
+        return Feign.builder()
+                .contract(new SpringMvcContract())
+                .encoder(new SpringEncoder(messageConverters))
+                .decoder(new SpringDecoder(messageConverters))
+                .retryer(new Retryer.Default(100, TimeUnit.SECONDS.toMicros(1), 3))
+                .options(new Request.Options(connectTimeoutMillis, readTimeoutMillis))
+                .requestInterceptor(new FeignConfiguration());
+
+    }
+}

+ 52 - 0
yt-risk/risk-manage/src/main/java/com/ytpm/handle/DynamicFeignClientFactory.java

@@ -0,0 +1,52 @@
+package com.ytpm.handle;
+
+import feign.Feign;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
+import org.springframework.stereotype.Component;
+
+/**
+ * 动态FeignClient 构造工厂
+ * @author lih
+ * @date 2025-10-24 09:45
+ */
+@Slf4j
+@Component
+public class DynamicFeignClientFactory {
+
+    private final Feign.Builder feignBuilder;
+    private final LoadBalancerClient loadBalancerClient;
+
+
+    public DynamicFeignClientFactory(Feign.Builder feignBuilder,
+                                     LoadBalancerClient loadBalancerClient) {
+        this.feignBuilder = feignBuilder;
+        this.loadBalancerClient = loadBalancerClient;
+    }
+
+    public <T> T getClient(String serviceName, Class<T> clazz) {
+        int maxRetry = 3;
+        int retryCount = 0;
+        Exception lastException = null;
+
+        while (retryCount < maxRetry) {
+            try {
+                ServiceInstance instance = loadBalancerClient.choose(serviceName);
+                if (instance == null) {
+                    throw new RuntimeException("未找到可用的服务实例:" + serviceName);
+                }
+                String url = instance.getUri().toString();
+                log.info("为服务{}构建FeignClient,目标地址为:{}", serviceName, url);
+                return feignBuilder.target(clazz, url);
+            } catch (Exception e) {
+                lastException = e;
+                log.warn("第 {} 次尝试获取FeignClient失败,服务名:{},错误信息:{}", retryCount + 1, serviceName, e.getMessage());
+                retryCount++;
+
+            }
+        }
+        throw new RuntimeException("创建FeignClient失败,服务名:" + serviceName, lastException);
+    }
+
+}

+ 32 - 5
yt-risk/risk-manage/src/main/java/com/ytpm/util/FeignClientInvoker.java

@@ -1,5 +1,7 @@
 package com.ytpm.util;
 
+import com.ytpm.handle.DynamicFeignClientFactory;
+import com.ytpm.question.base.BaseFeign;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.context.ApplicationContext;
 import org.springframework.stereotype.Component;
@@ -33,6 +35,8 @@ public class FeignClientInvoker {
     private Semaphore bulkhead;
     @Resource(name = "riskScheduledExecutorService")
     private ScheduledExecutorService scheduledExecutorService;
+    @Resource
+    private DynamicFeignClientFactory feignClientFactory;
 
     public FeignClientInvoker(ApplicationContext applicationContext) {
         this.applicationContext = applicationContext;
@@ -78,7 +82,12 @@ public class FeignClientInvoker {
     }
 
     private Object getFeignClient(String serviceName) {
-        return feignClientCache.computeIfAbsent(serviceName, name -> {
+        if (feignClientCache.containsKey(serviceName)) {
+            return feignClientCache.get(serviceName);
+        }
+        Object feignClient = null;
+        try {
+            // 默认查询声明实例
             Map<String, Object> feignClients = applicationContext.getBeansWithAnnotation(FeignClient.class);
             for (Object bean : feignClients.values()) {
                 Class<?>[] interfaces = bean.getClass().getInterfaces();
@@ -86,14 +95,32 @@ public class FeignClientInvoker {
                     FeignClient annotation = iface.getAnnotation(FeignClient.class);
                     if (annotation != null) {
                         String annName = annotation.name().isEmpty() ? annotation.value() : annotation.name();
-                        if (name.equals(annName)) {
-                            return bean;
+                        if (serviceName.equals(annName)) {
+                            feignClient = bean;
+                            break;
                         }
                     }
                 }
+                if (feignClient != null) {
+                    break;
+                }
             }
-            throw new IllegalArgumentException("未找到服务: " + name);
-        });
+            // 构建动态FeignClient
+            if (feignClient == null) {
+                Class<?> clazz = com.ytpm.question.base.BaseFeign.class;
+                if (serviceName.contains("ios")) {
+                    clazz = com.ytpm.lemonios.feign.base.BaseFeign.class;
+                }
+                feignClient = feignClientFactory.getClient(serviceName, clazz);
+            }
+            feignClientCache.put(serviceName, feignClient);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("未找到服务: " + serviceName);
+        }
+        if (feignClient == null) {
+            throw new IllegalArgumentException("未找到服务: " + serviceName);
+        }
+        return feignClient;
     }
 
     private Method getTargetMethod(Object feignClient, String methodName, Object[] args)