一、背景

最近前端反应开发环境有时候调接口会很慢,原因是有开发图方便将本地服务注册到开发环境,请求路由到开发本地导致,

为了解决该问题想到可以通过标签路由的方式避免该问题,实现前端联调和开发自测互不干扰。

该方案除了用于本地调试,还可以用于用户灰度发布。

二、实现方案

关于负载均衡,低版本的SpringCloud用的是Spring Cloud Ribbon,高版本用Spring Cloud LoadBalancer替代了,

Ribbon可以通过实现IRlue接口实现,这里只介绍高版本的实现方案。

实现方案:

  1. idea在环境变量中设置tag,本地服务启动时读取环境变量将tag注册到nacos的元数据

  2. 重写网关的负载均衡算法,从请求头中获取到的request-tag和服务实例的元数据进行匹配,如果匹配到则返回对应的

    服务实例,否则提示服务未找到。

三、编码实现

3.1 order服务

新建一个SpringCloud服务order-service,注册元数据很简单,只需要排除掉NacosDiscoveryClientConfiguration,再写一个自己的NacosDiscoveryClientConfiguration配置类即可。

创建MyNacosDiscoveryClientConfiguration

/**
* @Author: Ship
* @Description:
* @Date: Created in 2025/2/12
*/
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnDiscoveryEnabled
@ConditionalOnBlockingDiscoveryEnabled
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class})
@AutoConfigureAfter({NacosDiscoveryAutoConfiguration.class})
public class MyNacosDiscoveryClientConfiguration { @Bean
public DiscoveryClient nacosDiscoveryClient(NacosServiceDiscovery nacosServiceDiscovery) {
return new NacosDiscoveryClient(nacosServiceDiscovery);
} @Bean
@ConditionalOnProperty(
value = {"spring.cloud.nacos.discovery.watch.enabled"},
matchIfMissing = true
)
public NacosWatch nacosWatch(NacosServiceManager nacosServiceManager, NacosDiscoveryProperties nacosDiscoveryProperties,
ObjectProvider<ThreadPoolTaskScheduler> taskExecutorObjectProvider, Environment environment) {
// 环境变量读取标签
String tag = environment.getProperty("tag");
nacosDiscoveryProperties.getMetadata().put("request-tag", tag);
return new NacosWatch(nacosServiceManager, nacosDiscoveryProperties, taskExecutorObjectProvider);
}
}

这里代码基本与NacosDiscoveryClientConfiguration一致,只是加上了设置元数据的逻辑。

@SpringBootApplication(exclude = NacosDiscoveryClientConfiguration.class)
public class OrderApplication { public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
} }

启动类上需要排除默认的NacosDiscoveryClientConfiguration,不然启动会报bean重复注册的错误,或者配置添加spring.main.allow-bean-definition-overriding=true允许重复注册也行。

写一个测试接口,方便后面测试

/**
* @Author: Ship
* @Description:
* @Date: Created in 2025/2/12
*/
@RequestMapping("test")
@RestController
public class TestController { @GetMapping("")
public String sayHello(){
return "hello";
}
}

3.2 gateway服务

新建一个网关服务,pom文件如下:

 <properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
<spring-boot.version>2.5.1</spring-boot.version>
<maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency> <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>${spring-cloud-alibaba.version}</version>
</dependency> <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${spring-cloud-alibaba.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency> <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
</dependencies> </dependencyManagement> <build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

Spring-Cloud-loadBalancer默认使用轮询的算法,即org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer类实现,因此可以参考RoundRobinLoadBalancer实现一个TagLoadBalancer,代码如下:

/**
* @Author: Ship
* @Description:
* @Date: Created in 2025/2/12
*/
public class TagLoadBalancer implements ReactorServiceInstanceLoadBalancer { private static final String TAG_HEADER = "request-tag"; private static final Log log = LogFactory.getLog(TagLoadBalancer.class);
final AtomicInteger position;
final String serviceId;
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; public TagLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this(serviceInstanceListSupplierProvider, serviceId, (new Random()).nextInt(1000));
} public TagLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, int seedPosition) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.position = new AtomicInteger(seedPosition);
} @Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier) this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map((serviceInstances) -> {
return this.processInstanceResponse(supplier, serviceInstances, request);
});
} private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances, Request request) {
Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances, request);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback) supplier).selectedServiceInstance((ServiceInstance) serviceInstanceResponse.getServer());
} return serviceInstanceResponse;
} private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + this.serviceId);
}
return new EmptyResponse();
}
if (request instanceof DefaultRequest) {
DefaultRequest<RequestDataContext> defaultRequest = (DefaultRequest) request;
// 上下文获取请求头
HttpHeaders headers = defaultRequest.getContext().getClientRequest().getHeaders();
List<String> list = headers.get(TAG_HEADER);
if (!CollectionUtils.isEmpty(list)) {
String requestTag = list.get(0);
for (ServiceInstance instance : instances) {
String str = instance.getMetadata().getOrDefault(TAG_HEADER, "");
if (requestTag.equals(str)) {
return new DefaultResponse(instance);
}
}
log.error(String.format("No servers available for service:%s,tag:%s ", this.serviceId, requestTag));
return new EmptyResponse();
}
} int pos = Math.abs(this.position.incrementAndGet());
ServiceInstance instance = instances.get(pos % instances.size());
return new DefaultResponse(instance);
}
}

这里需要实现ReactorServiceInstanceLoadBalancer接口,如果请求头带有标签则根据标签路由,否则使用默认的轮询算法。

还要把TagLoadBalancer用起来,所以需要定义一个配置类TagLoadBalancerConfig,并通过@LoadBalancerClients注解添加默认配置,代码如下:

/**
* @Author: Ship
* @Description:
* @Date: Created in 2025/2/12
*/
public class TagLoadBalancerConfig { @Bean
public ReactorLoadBalancer reactorTagLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new TagLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
} @LoadBalancerClients(defaultConfiguration = {TagLoadBalancerConfig.class})
@SpringBootApplication
public class GatewayApplication { public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
} }

最后在application.yml文件添加网关路由配置

spring:
application:
name: gateway
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: dev
group: DEFAULT_GROUP
discovery:
server-addr: 127.0.0.1:8848
namespace: dev
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- StripPrefix=1
server:
port: 9000

3.3 代码测试

  • 本地启动nacos后启动order(注意需要在idea设置环境变量tag=ship)和gateway服务,可以看到order服务已经成功注册了元数据

  • 然后用Postman请求网关http://localhost:9000/order/test

可以看到请求成功路由到了order服务,说明根据tag路由成功了。

  • 去掉环境变量tag后重新启动Order服务,再次请求响应报文如下:

    {
    "timestamp": "2025-02-14T12:10:44.294+00:00",
    "path": "/order/test",
    "status": 503,
    "error": "Service Unavailable",
    "requestId": "41651188-4"
    }

说明根据requst-tag找不到对应的服务实例,代码逻辑生效了。

四、总结

聪明的人已经发现了,本文只实现了网关路由到下游服务这部分的标签路由,下游服务A调服务B的标签路由并未实现,其实现方案也不难,只需要通过上下文传递+feign拦截器就可以做到全链路的标签路由,有兴趣的可以自己试试。

  本文代码已上传github,顺便推广下前段时间写的idea插件CodeFaster(快速生成常用流操作的代码,Marketplace搜索下载即可体验)。

SpringCloud自定义loadbalancer实现标签路由的更多相关文章

  1. springboot+zuul(一)------实现自定义过滤器、动态路由、动态负载。

    参考:https://blog.csdn.net/u014091123/article/details/75433656 https://blog.csdn.net/u013815546/articl ...

  2. SpringCloud学习系列之七 ----- Zuul路由网关的过滤器和异常处理

    前言 在上篇中介绍了SpringCloud Zuul路由网关的基本使用版本,本篇则介绍基于SpringCloud(基于SpringBoot2.x,.SpringCloud Finchley版)中的路由 ...

  3. Android自定义控件之自定义ViewGroup实现标签云

    前言: 前面几篇讲了自定义控件绘制原理Android自定义控件之基本原理(一),自定义属性Android自定义控件之自定义属性(二),自定义组合控件Android自定义控件之自定义组合控件(三),常言 ...

  4. jsp如何自定义tag的标签库?

    虽然和上一次的使用自定义的tld标签简化jsp的繁琐操作的有点不同,但是目的也是一致的.自定义tag比较简单. 1.新建tag标签 在WEB-INF目录下新建一个tags的文件夹,是自定义tag标签的 ...

  5. 一、变量.二、过滤器(filter).三、标签(tag).四、条件分支tag.五、迭代器tag.六、自定义过滤器与标签.七、全系统过滤器(了解)

    一.变量 ''' 1.视图函数可以通过两种方式将变量传递给模板页面 -- render(request, 'test_page.html', {'变量key1': '变量值1', ..., '变量ke ...

  6. 工作总结 Rezor 里面的一些小知识----自定义类型 放在标签值中 会直接跳过去

    0 的时候不报错 1 的时候 报错了 原因 是 imagesname[i]  索引超出了 为什么在 上面 报错呢?  不在这里报错呢? 说明了  Rezor 对于 自定义的变量 放在标签值里的时候,调 ...

  7. 使用原生js自定义内置标签

    使用原生js自定义内置标签 效果图 代码 <!DOCTYPE html> <html lang="en"> <head> <meta ch ...

  8. JSP自定义tld方法标签

    卧槽 我们可以通过tld文件,自定义一个方法标签,以便在页面中使用,目录通常放在WEB-INF下面的tlds文件夹: 引入方式示例,直接在jsp上引入tld标签文件: <%@ taglib pr ...

  9. 基于SSM3框架FreeMarker自定义指令(标签)实现

    通过之前的Spring MVC 3.0.5+Spring 3.0.5+MyBatis3.0.4全注解实例详解系列文章,我们已经成功的整合到了一起,这次大象将在此基础上对框架中的FreeMarker模板 ...

  10. django自定义过滤器和标签

    1.自定义过滤器和标签的流程: 1.在某个app下创建一个名为templatetags(必需,且包名不可变)的包.假设我们在名为app01的app下创建了一个templatetags的包,并在该包下创 ...

随机推荐

  1. 使用 fiddler 进行抓包处理

    1.概述 fiddler是一个抓包工具,有时候方便我们在访问网页上,看看网页的参数和返回结果.其中很重要的一条是,可以查看网页的响应速度,在对于调优方面提供一些依据. 2.软件安装 我们可以通过360 ...

  2. 中电金信:数字经济时代,AI+金融技术应用与未来发展

  3. Yakit靶场-高级前端加解密与验签实战-全关卡通关教程

    一.前端验签-SHA256 本文作者为CVE-柠檬i CSDN:https://blog.csdn.net/weixin_49125123 博客园:https://www.cnblogs.com/CV ...

  4. Spring boot 2.0 之优雅停机

    spring boot 框架在生产环境使用的有一段时间了,它"约定大于配置"的特性,体现了优雅流畅的开发过程,它的部署启动方式(java -jar xxx.jar)也很优雅.但是我 ...

  5. Qt/C++控件设计器/属性栏/组态/可导入导出/中文属性/串口网络/拖曳开发

    一.功能特点 自动加载插件文件中的所有控件生成列表,默认自带的控件超过120个. 拖曳到画布自动生成对应的控件,所见即所得. 右侧中文属性栏,改变对应的属性立即应用到对应选中控件,直观简洁,非常适合小 ...

  6. Qt编写地图综合应用34-生成区域轮廓图

    一.前言 区域轮廓图的前提是,如何拿到这些轮廓的js文件,网络上其实能够找到各省市的轮廓的json数据,这些json数据对应内容是各种边界的一些类似 @@CGIUCACAAAAA@Q@ 字符的东西,每 ...

  7. Qt开源作品4-网络调试助手

    一.前言 网络调试助手和串口调试助手是一对的,用Qt开发项目与硬件通信绝大部分都是要么串口通信(RS232 RS485 Modbus等),要么就是网络通信(TCP UDP HTTP等),所以一旦涉及到 ...

  8. Android Studio4.1.2中,修改了gradle后,如何在不关闭AS IDE的情况下使gradle进行sync

    Android Studio4.1.2中,修改了gradle后,如何在不关闭AS IDE的情况下使gradle进行sync: 方法1: 修改了gradle后,上面自然就弹出了一个提示框 你点击上面的S ...

  9. GitHub Workflow 和 Action 的一些注意事项

    GitHub 的 workflow 和 action 存在一些注意事项,总结如下,以供参考 Workflow on.issues.types 如果需要判断 label,不需要指定 opened,只需要 ...

  10. NVM及NODE开发环境搭建

    NVM及NODE开发环境搭建 1. 安装NVM 1.1 下载安装包 下载地址 1.2 安装 双击安装包,一路下一步即可.安装完成后在终端输入nvm version,能查到版本号说明安装成功了. 2. ...