SpringCloud与Nacos动态路由二

标签: Springboot 分类: Java 创建时间:2020-05-27 03:27:49 更新时间:2024-11-24 10:04:01

上一篇文章 SpringCloud与Nacos动态路由 中,探讨了如何通过服务名进行动态路由的实现方式。更近一步的需求,既然uri中的http可以配置变量,那么uri中lb能不能也使用变量的形式呢?比如我的路由/lsmm/bigdata,由lsmm微服务进行服务,路由lsmm2/bigdata由lsmm2进行服务。

在配置文件中有一个gateway.discovery.locator配置项中,有一个url-expression,这个是不是可以做一点文章呢?

(1) 经过我多方尝试,和参阅相关资料,还是没有找到动态服务名的方法。因为你在uri中添加变量,最后都会被转义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
application:
name: ${app-name}
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
ip: 127.0.0.1
gateway:
discovery:
locator:
enabled: true
routes:
- id: url-proxy-1
uri: lb://$\{segment}-bigdata
# uri: ${service-url.user-service}/{servername}_bigdata/**
predicates:
- Path=/lsmfhx/bigdata/**
filters:
- RewritePath=/lsmfhx(?<segment>/?.*), $\{segment}-bigdata

但是使用uri中使用http协议的时候,就可以使用变量的形式,比如上面的注释的地方。

(2) 经过查看错误地方,出错的地方主要是自定义了一个Filter,(RouteToRequestUrlFilter implements GlobalFilter, Ordered)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
if (route == null) {
return chain.filter(exchange);
}
log.trace("RouteToRequestUrlFilter start");
URI uri = exchange.getRequest().getURI();
boolean encoded = containsEncodedParts(uri);
URI routeUri = route.getUri();

if (hasAnotherScheme(routeUri)) {
// this is a special url, save scheme to special attribute
// replace routeUri with schemeSpecificPart
exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR,
routeUri.getScheme());
routeUri = URI.create(routeUri.getSchemeSpecificPart());
}

if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
// Load balanced URIs should always have a host. If the host is null it is
// most
// likely because the host name was invalid (for example included an
// underscore)
throw new IllegalStateException("Invalid host: " + routeUri.toString());
}

URI mergedUrl = UriComponentsBuilder.fromUri(uri)
// .uri(routeUri)
.scheme(routeUri.getScheme()).host(routeUri.getHost())
.port(routeUri.getPort()).build(encoded).toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);
return chain.filter(exchange);
}

(3) 经过苦心思索和资料查找,还是没有一个正经的可以在uri中填写参数的方案。然后我想到了另一个方案,就是动态路由的方法。多说的nacos动态路由,不都是通过使用配置中心的方式吗?那能不能进行结合一下呢,将配置中心填写动态路由,改为通过获取服务名称来获取和更新动态路由。也就是说,根据获取到的服务名,然后动态创建predicates和filters。

找来找去,还是不行啊。nacos没有提供监听服务注册和销毁的方法。根据github上的一个ISSUE:是否可以增加一个方法,可以监听任意一个ServiceInfo的变化事件?,这个2019年12月的Issue,发现也有人遇到了和我一样的需求,但是苦于没有对应的服务上下线的监听函数,所以有人使用轮询的方式,监听服务列表的变化,有人通过在上下线时,修改配置文件的方法,间接更新路由。

在目前严重的官方不支持的情况下,如何实现自己的业务需求呢?是重新更换服务注册中心,还是通过迂回的方式解决问题呢?

参考文章:
1.Nacos 服务注册 (这篇文章,有点类似于源码解析的味道)
2.Nacos服务注册与发现
3.是否可以增加一个方法,可以监听任意一个ServiceInfo的变化事件?
4.eureka监听各服务状态,下线、重连等,并做相应的处理
5.Support service change event
6.使用Nacos实现Spring Cloud Gateway的动态路由 (配置中心动态路由,刚开始采用了这个代码,写了自己的一套使用配置中心进行路由更新的操作,下载下来的代码可以动态监听配置的更新,但是改到我的项目中,就是起不来,我怀疑是版本的问题。我采用了spring-cloud-starter-alibaba-nacos-config,但是示例代码中,还是采用了nacos-client,而且是0.4.0版本。于是果断放弃了这篇文章和代码)
7.服务发现组件Eureka (这篇文章讲了如何从零开始一步步创建以eureka为注册和发现中心的微服务应用)

经过不断的尝试和探索,终于实现了通过配置中心实现动态路由的方法

1.pom.xml

我这里使用了模块的开发设计,所以有主模块的pom.xml,有子模块的pom.xml
(1) 主模块的pom.xml,主要就是dependencyManagement管理spring-cloud-dependencies的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.bibichuan</groupId>
<artifactId>SpringBootCould</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>lsmfhx</name>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/>
</parent>

<modules>
<module>gateway</module>
</modules>

<dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>0.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
</dependencies>

</project>

(2) 子模块的pom.xml,当然,主模块和子模块还是可以直接合并到一起的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringBootCould</artifactId>
<groupId>com.bibichuan</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../</relativePath> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>gateway</artifactId>
<packaging>jar</packaging>
<description>网关</description>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

2.编写三个类

(1) 应用程序启动时直接执行的事件注册以及路由注册的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.bibichuan;

import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.annotation.NacosInjected;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.annotation.NacosConfigurationProperties;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.Executor;

@Component
@NacosConfigurationProperties(dataId = "${nacos.config.data-id}", autoRefreshed = true, groupId = "${nacos.config.group}")
public class NacosGatewayDefineConfig implements CommandLineRunner {

private static final Logger log= LoggerFactory.getLogger(NacosGatewayDefineConfig.class);

@Autowired
NacosConfigProperties nacosConfigProperties;

@Value("${nacos.config.data-id}")
private String dataId;

@Value("${nacos.config.group}")
private String group;

@Autowired
NacosDynamicRouteService nacosDynamicRouteService;
/**
* Callback used to run the bean.
*
* @param args incoming main method arguments
* @throws Exception on error
*/
@Override
public void run(String... args) throws Exception {
addRouteNacosListen();
}

/**
* 添加动态路由监听器
*/
private void addRouteNacosListen() {
try {
ConfigService configService = nacosConfigProperties.configServiceInstance();
String configInfo = configService.getConfig(dataId, group, 5000);
log.info("从Nacos返回的配置:" + configInfo);
getNacosDataRoutes(configInfo);


//注册Nacos配置更新监听器,用于监听触发
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
log.info("Nacos更新了!");
log.info("接收到数据:"+configInfo);
getNacosDataRoutes(configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});

} catch (NacosException e) {
log.error("nacos-addListener-error", e);
e.printStackTrace();
}
}

/**
* 从Nacos返回的配置
* @param configInfo
*/
private void getNacosDataRoutes(String configInfo) {
List<RouteDefinition> list = JSON.parseArray(configInfo, RouteDefinition.class);
list.stream().forEach(definition -> {
log.info(""+JSON.toJSONString(definition));
nacosDynamicRouteService.update(definition);
});
}
}

(2) 路由更新的服务接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.bibichuan;

import org.springframework.cloud.gateway.route.RouteDefinition;

/**
* 更新内存中的路由信息
*/
public interface NacosDynamicRouteService {

/**
* 更新路由信息
* @param gatewayDefine
* @return
* @throws Exception
*/
String update(RouteDefinition gatewayDefine);
}

(3) 路由更新的服务接口实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.bibichuan;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
public class NacosDynamicRouteServiceImpl implements NacosDynamicRouteService {

@Autowired
private RouteDefinitionWriter routeDefinitionWriter;

@Autowired
private ApplicationEventPublisher publisher;
/**
* 更新路由
* 只能是先删除在添加,由于没有提供更新路由的方法
*
* @param definition
* @return
*/
@Override
public String update(RouteDefinition definition) {
try {
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
} catch (Exception e) {
return "删除路由失败: RouteId:" + definition.getId();
}
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "更新路由成功";
} catch (Exception e) {
return "更新路由失败";
}


}
}
参考文章:
1.Spring Cloud Gateway的动态路由怎样做?集成Nacos实现很简单 (这篇文章,后台代码,链接了一个非常强大的后台管理项目,这个倒是可以参考和学习)
2.基于Nacos配置中心实现Spring Cloud Gateway的动态路由管理 (我参考了这篇文章写了自己的动态路由代码)

3.配置文件

(1) application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app-name: gateway

server:
port: 7000
spring:
application:
name: ${app-name}
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
ip: 127.0.0.1

nacos:
config:
server-addr: 127.0.0.1:8848 #nacos的serverAdd配置
group: DEFAULT_GROUP #分组的配置
file-extension: json
data-id: route-list #data-id的配置

(2) bootstrap.properties

1
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

4.打开nacos编辑配置

打开localhost:8848/nacos,编辑路由配置文件route-list.DEFAULT_GROUP

1
2
3
4
5
6
7
8
9
10
11
12
[{
"filters": [],
"id": "jd_route",
"order": 0,
"predicates": [{
"args": {
"pattern": "/jd"
},
"name": "Path"
}],
"uri": "http://www.baidu.com"
}]

这里有一点需要说明的就是,如果刚开始使用这个json进行路由配置,可能会对filter传参产生疑问,比如如何添加

1
2
filters:
- StripPrefix=1

经过反复的尝试和参考相关文章,我找打了方法:

1
2
3
4
5
6
 "filters":[{
"name":"StripPrefix",
"args":{
"_genkey_0":1
}
}]

如果有多个参数,那就是”_genkey_1”、”_genkey_2”

5.启动应用程序

启动应用程序,就可以看到从nacos配置中心中获取到了路由配置,然后在配置中心中修改了配置,路由也会相应的更新了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.bibichuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
public class Application{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);

}
}

总结
(1) 需要使用spring-cloud-starter-alibaba-nacos-config依赖
(2) 在应用程序启动时监听配置改变configService.addListener
(3) ApplicationEventPublisher发布路由更新事件

问题

(1) @NacosInjected 注入Nacos 的NamingService实例为null

解决方法就是使用:ConfigService configService = nacosConfigProperties.configServiceInstance(); 代替@NacosInjected注入

1
2
3
4
5
6
7
8
@Autowired
NacosDiscoveryProperties nacosDiscoveryProperties;

@Autowired
NacosConfigProperties nacosConfigProperties;

ConfigService configService = nacosConfigProperties.configServiceInstance();
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();

还有种方法就是使用NacosFactory构造工厂,通过服务地址生成配置实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
String content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
} catch (NacosException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
小额赞助
本人提供免费与付费咨询服务,感谢您的支持!赞助请发邮件通知,方便公布您的善意!
**光 3.01 元
Sun 3.00 元
bibichuan 3.00 元
微信公众号
广告位
诚心邀请广大金主爸爸洽谈合作
每日一省
isNaN 和 Number.isNaN 函数的区别?

1.函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

2.函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

每日二省
为什么0.1+0.2 ! == 0.3,如何让其相等?

一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3。

每日三省
== 操作符的强制类型转换规则?

1.首先会判断两者类型是否**相同,**相同的话就比较两者的大小。

2.类型不相同的话,就会进行类型转换。

3.会先判断是否在对比 null 和 undefined,是的话就会返回 true。

4.判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number。

5.判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断。

6.判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断。

每日英语
Happiness is time precipitation, smile is the lonely sad.
幸福是年华的沉淀,微笑是寂寞的悲伤。