目录
一、前言
pom依赖版本信息:
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.7.RELEASE</spring-cloud-alibaba.version>
在当前版本的springcloud中,部分 reactive 包下的类已经弃用。
所以,对于有代码洁癖喜好的看起来会非常不爽,尤其是一个方法中有多个,如下图:
虽然可以使用@SuppressWarnings("deprecation") 注解来使其显示正常,心里还是有阴影。
二、效果演示
上图中演示了根据不同版本进行路由, 代码示例较多,本篇文章重点在于去掉了之前被spring弃用的部分,部分代码进行改造。
三、代码演示
1、网关配置
链接nacos的配置及完整pom依赖信息可以参考之前的文章。
application.properties 灰度路由配置:
#服务端口
server.port=9999
#服务名称
spring.application.name=zhufeng-gateway-loadbalancer
# 灰度测试
spring.cloud.gateway.routes[0].id=zhufeng-gray
spring.cloud.gateway.routes[0].uri=grayLb://zhufeng-gray
spring.cloud.gateway.routes[0].predicates[0]=Path=/gray/version
2、灰度服务配置
新建两个用于灰度路由的微服务,zhufeng-gray-higher、zhufeng-gray-lower。
zhufeng-gray-higher:
application.properties配置:
#服务端口
server.port=8101
#服务名称
spring.application.name=zhufeng-gray
bootstrap.properties配置:
#nacos注册中心地址
spring.cloud.nacos.discovery.server-addr=10.211.55.9:8848,10.211.55.10:8848,10.211.55.11:8848
#nacos元数据配置
spring.cloud.nacos.discovery.metadata.version=higher
spring.cloud.nacos.discovery.metadata.weight=30
#nacos配置中心地址
spring.cloud.nacos.config.server-addr=${spring.cloud.nacos.discovery.server-addr}
controller:
@Slf4j
@RestController
@RequestMapping("gray")
public class HigherController {
@RequestMapping("version")
public String version() {
log.info("访问higher版本...");
return "这里是higher版本";
}
}
zhufeng-gray-lower:
application.properties配置:
#服务端口
server.port=8102
#服务名称
spring.application.name=zhufeng-gray
bootstrap.properties配置:
#nacos注册中心地址
spring.cloud.nacos.discovery.server-addr=10.211.55.9:8848,10.211.55.10:8848,10.211.55.11:8848
#nacos元数据配置
spring.cloud.nacos.discovery.metadata.version=lower
spring.cloud.nacos.discovery.metadata.weight=60
#nacos配置中心地址
spring.cloud.nacos.config.server-addr=${spring.cloud.nacos.discovery.server-addr}
3、过滤器
@Slf4j
@Configuration
public class GrayGatewayReactiveLoadBalancerClientAutoConfiguration {
public GrayGatewayReactiveLoadBalancerClientAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean({GrayReactiveLoadBalancerClientFilter.class})
public GrayReactiveLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
log.info("初始化完成....");
return new GrayReactiveLoadBalancerClientFilter(clientFactory, properties);
}
}
/**
* @ClassName: GrayLoadBalancer
* @Description TODO
* @author 月夜烛峰
* @date 2022/10/8 15:35
*/
@Slf4j
public class GrayLoadBalancer {
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private String serviceId;
public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
public Mono<Response<ServiceInstance>> choose(Request request) {
HttpHeaders headers = (HttpHeaders) request.getContext();
if (this.serviceInstanceListSupplierProvider != null) {
ServiceInstanceListSupplier supplier = this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return ((Flux)supplier.get()).next().map(list->getInstanceResponse((List<ServiceInstance>)list,headers));
}
return null;
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,HttpHeaders headers) {
if (instances.isEmpty()) {
log.info("实例为空....");
return getServiceInstanceEmptyResponse();
} else {
//return getServiceInstanceResponseWithWeight(instances);
return getServiceInstanceResponseByVersion(instances, headers);
}
}
/**
* 根据版本进行分发
* @param instances
* @param headers
* @return
*/
private Response<ServiceInstance> getServiceInstanceResponseByVersion(List<ServiceInstance> instances, HttpHeaders headers) {
String versionNo = headers.getFirst("version");
log.info("查看版本:{}",versionNo);
Map<String,String> versionMap = new HashMap<>(16);
versionMap.put("version",versionNo);
final Set<Map.Entry<String,String>> attributes =
Collections.unmodifiableSet(versionMap.entrySet());
ServiceInstance serviceInstance = null;
for (ServiceInstance instance : instances) {
log.info("开始获取实例instances...{}", JSONObject.toJSONString(instances));
Map<String,String> metadata = instance.getMetadata();
if(metadata.entrySet().containsAll(attributes)){
log.info("开始获取实例...");
serviceInstance = instance;
break;
}
}
if(ObjectUtils.isEmpty(serviceInstance)){
log.info("实例为空...");
return getServiceInstanceEmptyResponse();
}
return new DefaultResponse(serviceInstance);
}
/**
*
* 根据在nacos中配置的权重值,进行分发
* @param instances
*
* @return
*/
private Response<ServiceInstance> getServiceInstanceResponseWithWeight(List<ServiceInstance> instances) {
Map<ServiceInstance,Integer> weightMap = new HashMap<>();
for (ServiceInstance instance : instances) {
log.info("获取所有实例:{}",JSONObject.toJSONString(instance));
Map<String,String> metadata = instance.getMetadata();
log.info("版本信息筛选:{} -->weight:",metadata.get("version"),metadata.get("weight"));
if(metadata.containsKey("weight")){
weightMap.put(instance,Integer.valueOf(metadata.get("weight")));
}
}
WeightMeta<ServiceInstance> weightMeta = WeightRandomUtils.buildWeightMeta(weightMap);
if(ObjectUtils.isEmpty(weightMeta)){
return getServiceInstanceEmptyResponse();
}
ServiceInstance serviceInstance = weightMeta.random();
if(ObjectUtils.isEmpty(serviceInstance)){
return getServiceInstanceEmptyResponse();
}
log.info(serviceInstance.getMetadata().get("version"));
return new DefaultResponse(serviceInstance);
}
private Response<ServiceInstance> getServiceInstanceEmptyResponse() {
log.info("No servers available for service: {}",this.serviceId);
return new EmptyResponse();
}
}
/**
* @ClassName: GrayReactiveLoadBalancerClientFilter
* @Description TODO
* @author 月夜烛峰
* @date 2022/10/8 15:40
*/
@Slf4j
public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {
/**读取网关请求报文体缓存key*/
public static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
private final LoadBalancerClientFactory clientFactory;
private LoadBalancerProperties properties;
public GrayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
this.clientFactory = clientFactory;
this.properties = properties;
}
@Override
public int getOrder() {
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
Object cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
log.info("请求报文体:{}",cachedBody);
String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
if (url != null && ("grayLb".equals(url.getScheme()) || "grayLb".equals(schemePrefix)|| "lb1".equals(url.getScheme()))) {
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
log.info("负载均衡选择:{}", ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
return this.choose(exchange).doOnNext((response) -> {
if (!response.hasServer()) {
throw NotFoundException.create(this.properties.isUse404() , "Unable to find instance for " + url.getHost());
} else {
URI uri = exchange.getRequest().getURI();
String overrideScheme = null;
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance((ServiceInstance)response.getServer(), overrideScheme);
URI requestUrl = this.reconstructURI(serviceInstance, uri);
log.info("LoadBalancerClientFilter url chosen: {}" , requestUrl);
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
}
}).then(chain.filter(exchange));
} else {
return chain.filter(exchange);
}
}
protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
}
private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
URI uri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
GrayLoadBalancer loadBalancer = new GrayLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class), uri.getHost());
if (loadBalancer == null) {
throw new NotFoundException("No loadbalancer available for " + uri.getHost());
} else {
return loadBalancer.choose(this.createRequest(exchange));
}
}
private Request createRequest(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
Request<HttpHeaders> request = new DefaultRequest<>(headers);
return request;
}
}
/**
* @ClassName: WeightMeta
* @Description TODO
* @author 月夜烛峰
* @date 2022/10/8 15:35
*/
public class WeightMeta<T> {
private final Random ran = new Random();
private final T[] nodes;
private final int[] weights;
private final int maxW;
public WeightMeta(T[] nodes, int[] weights) {
this.nodes = nodes;
this.weights = weights;
this.maxW = weights[weights.length - 1];
}
/**
* 该方法返回权重随机对象
* @return
*/
public T random() {
int index = Arrays.binarySearch(weights, ran.nextInt(maxW) + 1);
if (index < 0) {
index = -1 - index;
}
return nodes[index];
}
public T random(int ranInt) {
if (ranInt > maxW) {
ranInt = maxW;
} else if(ranInt < 0){
ranInt = 1;
} else {
ranInt ++;
}
int index = Arrays.binarySearch(weights, ranInt);
if (index < 0) {
index = -1 - index;
}
return nodes[index];
}
@Override
public String toString() {
StringBuilder l1 = new StringBuilder();
StringBuilder l2 = new StringBuilder("[random]\t");
StringBuilder l3 = new StringBuilder("[node]\t\t");
l1.append(this.getClass().getName()).append(":").append(this.hashCode()).append(":\n").append("[index]\t\t");
for (int i = 0; i < weights.length; i++) {
l1.append(i).append("\t");
l2.append(weights[i]).append("\t");
l3.append(nodes[i]).append("\t");
}
l1.append("\n");
l2.append("\n");
l3.append("\n");
return l1.append(l2).append(l3).toString();
}
}
/**
* @ClassName: WeightRandomUtils
* @Description TODO
* @author 月夜烛峰
* @date 2022/10/8 15:35
*/
public class WeightRandomUtils {
public static <T> WeightMeta<T> buildWeightMeta(final Map<T, Integer> weightMap) {
if(weightMap.isEmpty()){
return null;
}
final int size = weightMap.size();
Object[] nodes = new Object[size];
int[] weights = new int[size];
int index = 0;
int weightAdder = 0;
for (Map.Entry<T, Integer> each : weightMap.entrySet()) {
nodes[index] = each.getKey();
weights[index++] = (weightAdder = weightAdder + each.getValue());
}
return new WeightMeta<T>((T[]) nodes, weights);
}
public static void main(String[] args) {
Map<String,Integer> map = new HashMap<>();
map.put("v1",1);
map.put("v2",2);
WeightMeta<String> nodes = WeightRandomUtils.buildWeightMeta(map);
for(int i = 0; i < 10; i++){
new Thread(()->{
System.out.println(nodes.random());
}).start();
}
}
}