Nacos 实现 gateway 路由动态更新

通过Nacos配置spring-cloud-gateway的路由规则,实现路由规则的动态更新,代码在托管gateway-nacos

依赖

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
<properties>
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>

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

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

spring-cloud-starter-alibaba-nacos-config 的原理是将配置信息注入到Springenvironment中,并在配置更新时自动触发context refresh事件,从而将environment环境中的配置变更为最新配置

application.yml

1
2
server:
port: 8080

bootstrap.properties

1
2
3
4
5
spring.application.name=nacos-spring-cloud-gateway-example
spring.cloud.nacos.config.server-addr=192.168.56.101:8848

spring.cloud.nacos.config.ext-config[0].data-id=gateway.yaml
spring.cloud.nacos.config.ext-config[0].refresh=true

refresh要配置为true,否则不能动态更新 gateway.yaml 的配置项

App.java

1
2
3
4
5
6
7
8
9
10
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}

在 Nacos 控制台新建 gateway.yaml 配置

Data ID: gateway.yaml

Group: DEFAULT_GROUP

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: baidu
uri: https://www.baidu.com
predicates:
- Path=/s
- id: taobao
uri: https://www.taobao.com/
predicates:
- Path=/markets/3c/tbdc

配置完成后,执行 App.java 启动网关项目

浏览器访问

分别访问http://localhost:8080/shttp://localhost:8080/markets/3c/tbdc,能正常转发到百度、淘宝网站

修改 gateway.yaml 配置

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: baidu
uri: https://www.baidu.com
predicates:
- Path=/s

控制台打印以下日志,说明配置修改已经被监听到

1
2
org.springframework.cloud.endpoint.event.RefreshEventListener.handle(37) | Refresh keys changed: [spring.cloud
.gateway.routes.xxx]

再次访问http://localhost:8080/markets/3c/tbdc,会返回404页面

说明修改的配置已经动态更新了

动态更新原理

  1. Nacos 配置更新的时候,spring-cloud-starter-alibaba-nacos-configpublish 一个 RefreshEvent 事件,从而使 spring-cloud-commonsRefreshEventListener 监听到并触发 ContextRefresher.refresh() 方法。
  2. spring-cloud-gatewayRouteRefreshListener 监听了 ApplicationEvent 事件,当 Nacos 触发 ContextRefresher.refresh()后,会监听到 RefreshScopeRefreshedEvent事件并调用RouteRefreshListener.reset() 方法 publish 一个 RefreshRoutesEvent 路由更新事件,达到路由动态更新的目的。

spring-cloud-alibaba-nacos-config

spring-cloud-alibaba-nacos-config中,会默认监听配置的更新,并publish refresh事件

  • NacosRefreshProperties.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class NacosRefreshProperties {

@Value("${spring.cloud.nacos.config.refresh.enabled:true}")
private boolean enabled = true;

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

...refresh.enabled:true默认开启配置更新事件推送

  • NacosContextRefresher.java
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
93
94
95
public class NacosContextRefresher
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {

private final static Logger LOGGER = LoggerFactory
.getLogger(NacosContextRefresher.class);

public static final AtomicLong loadCount = new AtomicLong(0);

private final NacosRefreshProperties refreshProperties;

private final NacosRefreshHistory refreshHistory;

private final ConfigService configService;

private ApplicationContext applicationContext;

private AtomicBoolean ready = new AtomicBoolean(false);

private Map<String, Listener> listenerMap = new ConcurrentHashMap<>(16);

public NacosContextRefresher(NacosRefreshProperties refreshProperties,
NacosRefreshHistory refreshHistory, ConfigService configService) {
this.refreshProperties = refreshProperties;
this.refreshHistory = refreshHistory;
this.configService = configService;
}

@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}

private void registerNacosListenersForApplications() {
if (refreshProperties.isEnabled()) {
for (NacosPropertySource nacosPropertySource : NacosPropertySourceRepository
.getAll()) {

if (!nacosPropertySource.isRefreshable()) {
continue;
}

String dataId = nacosPropertySource.getDataId();
registerNacosListener(nacosPropertySource.getGroup(), dataId);
}
}
}

private void registerNacosListener(final String group, final String dataId) {

Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
loadCount.incrementAndGet();
String md5 = "";
if (!StringUtils.isEmpty(configInfo)) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
.toString(16);
}
catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
LOGGER.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
}
}
refreshHistory.add(dataId, md5);
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Refresh Nacos config group{},dataId{}", group, dataId);
}
}

@Override
public Executor getExecutor() {
return null;
}
});

try {
configService.addListener(dataId, group, listener);
}
catch (NacosException e) {
e.printStackTrace();
}
}

}

spring-cloud-gateway

  • RefreshEventListener.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RefreshEventListener {
private static Log log = LogFactory.getLog(RefreshEventListener.class);
private ContextRefresher refresh;
private AtomicBoolean ready = new AtomicBoolean(false);

public RefreshEventListener(ContextRefresher refresh) {
this.refresh = refresh;
}

@EventListener
public void handle(ApplicationReadyEvent event) {
this.ready.compareAndSet(false, true);
}

@EventListener
public void handle(RefreshEvent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
}
  • RouteRefreshListener.java
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
public class RouteRefreshListener
implements ApplicationListener<ApplicationEvent> {

private HeartbeatMonitor monitor = new HeartbeatMonitor();
private final ApplicationEventPublisher publisher;

public RouteRefreshListener(ApplicationEventPublisher publisher) {
Assert.notNull(publisher, "publisher may not be null");
this.publisher = publisher;
}

@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent
|| event instanceof RefreshScopeRefreshedEvent
|| event instanceof InstanceRegisteredEvent) {
reset();
}
else if (event instanceof ParentHeartbeatEvent) {
ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
resetIfNeeded(e.getValue());
}
else if (event instanceof HeartbeatEvent) {
HeartbeatEvent e = (HeartbeatEvent) event;
resetIfNeeded(e.getValue());
}
}

private void resetIfNeeded(Object value) {
if (this.monitor.update(value)) {
reset();
}
}

private void reset() {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}

}
  • 本文作者: forever杨
  • 本文链接: https://blog.yl-online.top/posts/da547d1d.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。如果文章内容对你有用,请记录到你的笔记中。本博客站点随时会停止服务,请不要收藏、转载!