完善 yudao-spring-boot-starter-env 组件,完成 feign 组件

This commit is contained in:
YunaiV 2022-06-25 20:58:07 +08:00
parent f879c4aa2b
commit d0ce24a2f6
9 changed files with 239 additions and 8 deletions

View File

@ -46,6 +46,21 @@
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<!-- RPC 相关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<!-- Registry 注册中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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<List<LoadBalancerClientSpecification>> configurations) {
EnvLoadBalancerClientFactory clientFactory = new EnvLoadBalancerClientFactory(properties);
clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList));
return clientFactory;
}
@Bean
public EnvRequestInterceptor envRequestInterceptor() {
return new EnvRequestInterceptor();
}
// ========== Dubbo 相关 ==========
}

View File

@ -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 {

View File

@ -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<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
/**
* 需要获取的服务实例名
*
* 暂时用于打印 logger 日志
*/
private final String serviceId;
/**
* 被代理的 ReactiveLoadBalancer 对象
*/
private final ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer;
@Override
public Mono<Response<ServiceInstance>> 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<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, String tag) {
// 如果服务实例为空则直接返回
if (CollUtil.isEmpty(instances)) {
log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
return new EmptyResponse();
}
// 筛选满足条件的实例列表
List<ServiceInstance> 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));
}
}

View File

@ -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<ServiceInstance> getInstance(String serviceId) {
ReactiveLoadBalancer<ServiceInstance> 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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -35,8 +35,16 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final String VERSION = "version";
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; // 用于获取 serviceId 对应的服务实例的列表
private final String serviceId; // 服务名暂时用于打印 logger 日志
/**
* 用于获取 serviceId 对应的服务实例的列表
*/
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
/**
* 需要获取的服务实例名
*
* 暂时用于打印 logger 日志
*/
private final String serviceId;
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
@ -50,9 +58,7 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, HttpHeaders headers) {
// 如果服务实例为空则直接返回
if (CollUtil.isEmpty(instances)) {
if (log.isWarnEnabled()) {
log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
}
log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
return new EmptyResponse();
}