需求
由于每次增减服务路由,网关服务都需要重启。由于网关服务作为所有服务的http接口的入口,我们更想让它热更新,每次增减服务路由,修改网关的路由配置信息即可,无需重启网关服务。
用户更加无感知,切换路由更新顺滑。
实现思路
将服务路由配置信息放置单独的一个配置data中「之前我们是放在网关服务data」
然后往Nacos注册监听器,监听服务路由配置信息,一旦服务路由配置信息变动,就基于Gateway的API进行路由的CRUD操作
相关环境
JDK :1.8
Spring Boot: 2.3.2.RELEASE
Spring Cloud:Hoxton.SR8
Spring Cloud Alibaba: 2.2.3.RELEASE
相关代码
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
</parent>
<groupId>com.xxx</groupId>
<artifactId>xxx-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>xxx-gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring.boot.version>2.3.2.RELEASE</spring.boot.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
<!-- dubbo hession 协议依赖 -->
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- spring cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- REDIS -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--鉴权-->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</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>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
网关相关配置类
/**
* 网关相关配置
* @author: cyw
* @CreateTime: 2021/8/18 11:59
**/
@Data
@NoArgsConstructor
@Accessors(chain = true)
@Configuration
public class GatewayConfig implements Serializable {
private static final long serialVersionUID = -745598752826462768L;
/** 读取配置的超时时间 */
public static final long DEFAULT_TIMEOUT = 30000;
/** Nacos 服务器地址 */
public static String NACOS_SERVER_ADDR;
/** 命名空间 */
public static String NACOS_NAMESPACE;
/** data-id */
public static String NACOS_ROUTE_DATA_ID;
/** 分组 id */
public static String NACOS_ROUTE_GROUP;
@Value("${spring.cloud.nacos.discovery.server-addr}")
public void setNacosServerAddr(String nacosServerAddr) {
NACOS_SERVER_ADDR = nacosServerAddr;
}
@Value("${spring.cloud.nacos.discovery.namespace}")
public void setNacosNamespace(String nacosNamespace) {
NACOS_NAMESPACE = nacosNamespace;
}
@Value("${nacos.gateway.route.config.data-id:gateway-router}")
public void setNacosRouteDataId(String nacosRouteDataId) {
NACOS_ROUTE_DATA_ID = nacosRouteDataId;
}
@Value("${nacos.gateway.route.config.group:xxx-gateway}")
public void setNacosRouteGroup(String nacosRouteGroup) {
NACOS_ROUTE_GROUP = nacosRouteGroup;
}
}
网关路由相关操作类
/**
* 网关路由相关操作
*
* @author: cyw
* @CreateTime: 2021/8/18 13:35
**/
@Slf4j
@Component
public class DynamicRouteManger implements ApplicationEventPublisherAware {
@Resource
private RouteDefinitionWriter routeDefinitionWriter;
@Resource
private RouteDefinitionLocator routeDefinitionLocator;
@Resource
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public String addRouteDefinition(RouteDefinition definition) {
log.info("[网关服务]-[添加路由]: [{}]", definition);
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
public List<RouteDefinition> list() {
return routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
}
public String updateList(List<RouteDefinition> definitions) {
log.info("[网关服务]-[刷新路由]: [{}]", definitions);
List<RouteDefinition> routeDefinitionsExits = list();
if (isNotEmpty(routeDefinitionsExits)) {
routeDefinitionsExits.forEach(this::deleteById);
}
Map<String, RouteDefinition> existMap = StreamUtils.propMap(routeDefinitionsExits, RouteDefinition::getId, Function.identity());
for (RouteDefinition definition : definitions) {
updateByRouteDefinition(definition, !existMap.containsKey(definition.getId()));
}
return "success";
}
private String deleteById(RouteDefinition definition) {
try {
if (definition.getId().startsWith("ReactiveCompositeDiscoveryClient")) {
log.info("[网关服务]-[删除路由]: id包含ReactiveCompositeDiscoveryClient跳过:{}", definition.getId());
return "success";
}
this.routeDefinitionWriter.delete(Mono.just(definition.getId())).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
} catch (Exception ex) {
log.error("[网关服务]-[删除路由失败]: id:{}, ex:[{}]", definition.getId(), ex.getMessage(), ex);
}
log.info("[网关服务]-[删除路由成功]: [{}]", definition);
return "success";
}
private String updateByRouteDefinition(RouteDefinition definition, Boolean skipDelete) {
if (skipDelete != null && !skipDelete) {
try {
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
log.info("[网关服务]-[删除路由成功]: [{}]", definition);
} catch (Exception ex) {
log.error("[网关服务]-[删除路由失败]: id:{}, ex:[{}]", definition.getId(), ex.getMessage(), ex);
}
}
addRouteDefinition(definition);
return "success";
}
}
通过Nacos 动态配置刷新路由
/**
* 通过 nacos 动态配置刷新路由
*
* @author: cyw
* @CreateTime: 2021/8/18 14:35
**/
@Slf4j
@Component
@DependsOn({"gatewayConfig"})
public class DynamicRouteComponent {
private ConfigService configService;
@Resource
private DynamicRouteManger dynamicRouteService;
@PostConstruct
public void init() {
log.info("[网关服务]-[初始化路由]");
try {
configService = initConfigService();
if (null == configService) {
log.info("[网关服务]-[初始化路由失败]-获取Nacos客户端为空");
return;
}
String configInfo = configService.getConfig(
GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_ROUTE_GROUP,
GatewayConfig.DEFAULT_TIMEOUT
);
List<RouteDefinition> definitionList = JsonUtils.toListIfPresent(configInfo, RouteDefinition.class);
if (isNotEmpty(definitionList)) {
for (RouteDefinition definition : definitionList) {
dynamicRouteService.addRouteDefinition(definition);
}
}
} catch (Exception ex) {
log.error("[网关服务]-[初始化路由失败]-相关报错信息: {}", ex.getMessage(), ex);
}
dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_ROUTE_GROUP);
}
/**
* <h2>初始化 Nacos Config</h2>
*/
private ConfigService initConfigService() {
try {
Properties properties = new Properties();
properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);
return configService = NacosFactory.createConfigService(properties);
} catch (Exception ex) {
log.info("[网关服务]-[初始化路由失败]-获取Nacos客户端失败");
return null;
}
}
/**
* <h2>监听 Nacos 动态路由配置</h2>
*/
private void dynamicRouteByNacosListener(String dataId, String group) {
try {
configService.addListener(dataId, group, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
List<RouteDefinition> definitionList = JsonUtils.toListIfPresent(configInfo, RouteDefinition.class);
dynamicRouteService.updateList(definitionList);
}
});
} catch (NacosException ex) {
log.error("[网关服务]-[动态刷新路由失败]-error: [{}]", ex.getMessage(), ex);
}
}
}
在Nacos上增加 路由配置文件
配置格式改成json
具体的 服务路由配置信息 ,根据大家的项目进行配置
[
{
"id":"xx",
"uri":"lb://xxx",
"predicates":[{
"name":"Path",
"args": {
}
}],
"filters": [
{
}
]
}
]
controller测试下
@Slf4j
@RestController
@NotOnProduction
@RequestMapping(value = "/config")
public class DynamicController {
@Resource
private GatewayConfig config;
@Resource
private DynamicRouteManger dynamicRouteManger;
@PostMapping
public Mono<String> config() {
log.info(
"config, address= {}, namespace= {}, dataId= {}, group= {}",
GatewayConfig.NACOS_SERVER_ADDR, GatewayConfig.NACOS_NAMESPACE,
GatewayConfig.NACOS_ROUTE_DATA_ID, GatewayConfig.NACOS_ROUTE_GROUP
);
return Mono.just("config");
}
@PostMapping("/list")
public Mono<String> list() {
List<RouteDefinition> list = dynamicRouteManger.list();
log.info("list : {}", JsonUtils.toJsonIfPresent(list));
return Mono.just("list");
}
}
相关发布方案
最好在开发环境测了没问题之后,再发布到测试环境,测试环境跑了一段时间之后,再上预发、生产环境…
服务路由一多之后,单独抽出 路由配置文件 还有有点风险了的…尤其是没有制定相关的路由规范…当时我们这边30多个服务,由于我们没有制定相关的路由规范…所以我迁移的时候 挺费劲的…在测试环境跑了段时间,也发现了路由修改错误,好在热更新 直接修改就生效了
要是线上网关不是集群的话…那其实也只能直接发了
我们这边网关服务线上是两节点,所以以下是按两节点的方式发布「其实多节点也一样- - 」
1. 增加 路由配置文件 启动一台网关初始化成功之后
2. 再删除配置中心中路由相关的信息,再启动另一台网关服务
3. 启动成功之后,再重启第一台网关服务「超过两个节点,后面网关服务直接重启就好了」