diff --git a/yudao-framework/yudao-spring-boot-starter-env/pom.xml b/yudao-framework/yudao-spring-boot-starter-env/pom.xml index c502a7267..3b5ca0771 100644 --- a/yudao-framework/yudao-spring-boot-starter-env/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-env/pom.xml @@ -46,6 +46,21 @@ jakarta.servlet-api + + + org.springframework.cloud + spring-cloud-loadbalancer + + + io.github.openfeign + feign-core + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + diff --git a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/config/YudaoEnvRpcAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/config/YudaoEnvRpcAutoConfiguration.java new file mode 100644 index 000000000..28e7148b3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/config/YudaoEnvRpcAutoConfiguration.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.framework.env.config; + +import cn.iocoder.yudao.framework.env.core.fegin.EnvLoadBalancerClientFactory; +import cn.iocoder.yudao.framework.env.core.fegin.EnvRequestInterceptor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; +import java.util.List; + +/** + * 多环境的 RPC 组件的自动配置 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoEnvRpcAutoConfiguration { + + // ========== Feign 相关 ========== + + // TODO @芋艿:由于 loadBalancerClientFactoryBeanPostProcessor 拦截不到 LoadBalancerClientFactory,所以采用 loadBalancerClientFactory 实现 +// @Bean +// public BeanPostProcessor loadBalancerClientFactoryBeanPostProcessor(LoadBalancerClientsProperties properties) { +// return new BeanPostProcessor() { +// @Override +// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { +// if (!(bean instanceof LoadBalancerClientFactory)) { +// return bean; +// } +// return bean; +// } +// }; +// } + + /** + * 创建 {@link EnvLoadBalancerClientFactory} Bean + * + * 参考 {@link org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration#loadBalancerClientFactory(LoadBalancerClientsProperties)} 方法 + */ + @Bean + public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties, + ObjectProvider> configurations) { + EnvLoadBalancerClientFactory clientFactory = new EnvLoadBalancerClientFactory(properties); + clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList)); + return clientFactory; + } + + @Bean + public EnvRequestInterceptor envRequestInterceptor() { + return new EnvRequestInterceptor(); + } + + // ========== Dubbo 相关 ========== + +} diff --git a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/config/YudaoEnvWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/config/YudaoEnvWebAutoConfiguration.java index ad21b80b5..a730a6851 100644 --- a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/config/YudaoEnvWebAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/config/YudaoEnvWebAutoConfiguration.java @@ -7,6 +7,11 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * 多环境的 Web 组件的自动配置 + * + * @author 芋道源码 + */ @Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class YudaoEnvWebAutoConfiguration { diff --git a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClient.java b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClient.java new file mode 100644 index 000000000..950aa8223 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClient.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.framework.env.core.fegin; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.env.core.context.EnvContextHolder; +import cn.iocoder.yudao.framework.env.core.util.EnvUtils; +import com.alibaba.cloud.nacos.balancer.NacosBalancer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.DefaultResponse; +import org.springframework.cloud.client.loadbalancer.EmptyResponse; +import org.springframework.cloud.client.loadbalancer.Request; +import org.springframework.cloud.client.loadbalancer.Response; +import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; +import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * 多环境的 {@link org.springframework.cloud.client.loadbalancer.LoadBalancerClient} 实现类 + * 在从服务实例列表选择时,优先选择 tag 匹配的服务实例 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class EnvLoadBalancerClient implements ReactorServiceInstanceLoadBalancer { + + /** + * 用于获取 serviceId 对应的服务实例的列表 + */ + private final ObjectProvider serviceInstanceListSupplierProvider; + /** + * 需要获取的服务实例名 + * + * 暂时用于打印 logger 日志 + */ + private final String serviceId; + /** + * 被代理的 ReactiveLoadBalancer 对象 + */ + private final ReactiveLoadBalancer reactiveLoadBalancer; + + @Override + public Mono> choose(Request request) { + String tag = EnvContextHolder.getTag(); + if (StrUtil.isEmpty(tag)) { + return Mono.from(reactiveLoadBalancer.choose(request)); + } + // 选择实例 + ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new); + return supplier.get(request).next().map(list -> getInstanceResponse(list, tag)); + } + + private Response getInstanceResponse(List instances, String tag) { + // 如果服务实例为空,则直接返回 + if (CollUtil.isEmpty(instances)) { + log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId); + return new EmptyResponse(); + } + + // 筛选满足条件的实例列表 + List chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance))); + if (CollUtil.isEmpty(chooseInstances)) { + log.warn("[getInstanceResponse][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag); + chooseInstances = instances; + } + + // TODO 芋艿:https://juejin.cn/post/7056770721858469896 想通网段 + + // 随机 + 权重获取实例列表 TODO 芋艿:目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法 + return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClientFactory.java b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClientFactory.java new file mode 100644 index 000000000..ebaeca97e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClientFactory.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.framework.env.core.fegin; + + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; +import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; + +/** + * 多环境的 {@link LoadBalancerClientFactory} 实现类 + * 目的:在创建 {@link ReactiveLoadBalancer} 时,会额外增加 {@link EnvLoadBalancerClient} 代理,用于 tag 过滤服务实例 + * + * @author 芋道源码 + */ +public class EnvLoadBalancerClientFactory extends LoadBalancerClientFactory { + + public EnvLoadBalancerClientFactory(LoadBalancerClientsProperties properties) { + super(properties); + } + + @Override + public ReactiveLoadBalancer getInstance(String serviceId) { + ReactiveLoadBalancer reactiveLoadBalancer = super.getInstance(serviceId); + // 参考 {@link com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancerClientConfiguration#nacosLoadBalancer(Environment, LoadBalancerClientFactory, NacosDiscoveryProperties)} 方法 + return new EnvLoadBalancerClient(super.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), + serviceId, reactiveLoadBalancer); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvRequestInterceptor.java b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvRequestInterceptor.java new file mode 100644 index 000000000..4c60eb556 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvRequestInterceptor.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.framework.env.core.fegin; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.env.core.context.EnvContextHolder; +import cn.iocoder.yudao.framework.env.core.util.EnvUtils; +import feign.RequestInterceptor; +import feign.RequestTemplate; + +/** + * 多环境的 {@link RequestInterceptor} 实现类:Feign 请求时,将 tag 设置到 header 中,继续透传给被调用的服务 + * + * @author 芋道源码 + */ +public class EnvRequestInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate requestTemplate) { + String tag = EnvContextHolder.getTag(); + if (StrUtil.isNotEmpty(tag)) { + EnvUtils.setTag(requestTemplate, tag); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/util/EnvUtils.java b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/util/EnvUtils.java index e7fb36c91..f867b9ab3 100644 --- a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/util/EnvUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/util/EnvUtils.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.framework.env.core.util; import cn.hutool.core.net.NetUtil; +import feign.RequestTemplate; +import org.springframework.cloud.client.ServiceInstance; import javax.servlet.http.HttpServletRequest; import java.util.Objects; @@ -12,15 +14,23 @@ import java.util.Objects; */ public class EnvUtils { - private static final String HEADER_DUBBO_TAG = "tag"; + private static final String HEADER_TAG = "tag"; private static final String HOST_NAME_VALUE = "${HOSTNAME}"; public static String getTag(HttpServletRequest request) { - String tag = request.getHeader(HEADER_DUBBO_TAG); + String tag = request.getHeader(HEADER_TAG); // 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名 // 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做 return Objects.equals(tag, HOST_NAME_VALUE) ? NetUtil.getLocalHostName() : tag; } + public static String getTag(ServiceInstance instance) { + return instance.getMetadata().get(HEADER_TAG); + } + + public static void setTag(RequestTemplate requestTemplate, String tag) { + requestTemplate.header(HEADER_TAG, tag); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-env/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-env/src/main/resources/META-INF/spring.factories index c5751ebde..ffb232030 100644 --- a/yudao-framework/yudao-spring-boot-starter-env/src/main/resources/META-INF/spring.factories +++ b/yudao-framework/yudao-spring-boot-starter-env/src/main/resources/META-INF/spring.factories @@ -1,2 +1,3 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - cn.iocoder.yudao.framework.env.config.YudaoEnvWebAutoConfiguration + cn.iocoder.yudao.framework.env.config.YudaoEnvWebAutoConfiguration,\ + cn.iocoder.yudao.framework.env.config.YudaoEnvRpcAutoConfiguration diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/grey/GrayLoadBalancer.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/grey/GrayLoadBalancer.java index a0902cef7..1ef2d01ab 100644 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/grey/GrayLoadBalancer.java +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/grey/GrayLoadBalancer.java @@ -35,8 +35,16 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer { private static final String VERSION = "version"; - private final ObjectProvider serviceInstanceListSupplierProvider; // 用于获取 serviceId 对应的服务实例的列表 - private final String serviceId; // 服务名,暂时用于打印 logger 日志 + /** + * 用于获取 serviceId 对应的服务实例的列表 + */ + private final ObjectProvider serviceInstanceListSupplierProvider; + /** + * 需要获取的服务实例名 + * + * 暂时用于打印 logger 日志 + */ + private final String serviceId; @Override public Mono> choose(Request request) { @@ -50,9 +58,7 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer { private Response getInstanceResponse(List instances, HttpHeaders headers) { // 如果服务实例为空,则直接返回 if (CollUtil.isEmpty(instances)) { - if (log.isWarnEnabled()) { - log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId); - } + log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId); return new EmptyResponse(); }