Spring Cloud Gateway接入nacos动态路由刷新
简介
通常在Nacos接入了Spring Cloud的Gateway后还需自定义实现动态的路由配置来提供后续更为灵活的接口发布与维护,这里主要记录实现步骤。
接入Nacos做naming
添加mvn依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<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>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>添加配置
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
29spring:
application:
name: gateway
main:
allow-bean-definition-overriding: true
cloud:
nacos:
config:
server-addr: localhost:8848
namespace: dev
# bootstrap:
# enable: true
data-id: hellorobot-gateway
type: yaml
auto-refresh: true
discovery:
namespace: dev
register:
service-name: gateway
server-addr: localhost:8848
gateway:
enabled: true
routes:
- id: user
uri: http://localhost:8002
predicates:
- Path=/api/user/**
filters:
- RewritePath=/api/user, /user/admin
动态更新配置文件
指定路由配置文件,用于启动时创建Nacos Config文件监听
1 |
|
具体实现
- DynamicRouteServiceImpl
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308/**
* 动态路由服务实现
*
* 具体的路由信息变更刷新,因监听文件更新仅可拿到全量的路由配置,
* 为了减轻整体逻辑负担,使用merge逻辑更新definition并发出变
* 更通知。
*
* 虽然仅变更不通知,可以简易的通过全量删除并全量添加即可实现路由
* 更新,但并不保证后续是否存在对历史definition对象的引用,故此
* 处使用更保险的策略。
*
* 又因为merge策略,可能导致对部分definition更新后会影响默认的
* order,所以在添加注册时会填充未标记的order。
*/
4j
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
private RouteDefinitionRepository routeDefinitionRepository;
private ApplicationEventPublisher publisher;
/**
* merge更新路由
*
* 保证刷新逻辑不存在线程安全问题,刷新路由并没有很高的性能需求,此处锁住整个refresh方法。
* @param definitions 路由详情集合
*/
public synchronized void refresh(List<RouteDefinition> definitions) {
// 填充生成order
fillTargetRouteOrder(definitions);
// 目标routes id集合
List<String> targetDefIds = definitions.stream().map(RouteDefinition::getId).collect(Collectors.toList());
// 获取现存所有路由map
Map<String, RouteDefinition> aliveRouteMap = getAliveRouteMap();
// 删除失效的routes
removeDefinitions(targetDefIds, aliveRouteMap);
// 更新definitions
updateDefinitions(definitions, aliveRouteMap);
// 添加definitions
createDefinitions(definitions, aliveRouteMap);
// 发布路由已更新时间
publishRouteChangedEvent();
}
/**
* 填充目标路由order
*
* @param definitions 路由详情集合
*/
private void fillTargetRouteOrder(List<RouteDefinition> definitions) {
int order = 1;
for (RouteDefinition route : definitions) {
if (route.getOrder() == 0) {
route.setOrder(order++);
}
}
}
/**
* 发布路由已更新消息
*/
private void publishRouteChangedEvent() {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}
/**
* 添加routes
*
* @param definitions 目标routes
* @param aliveRouteMap 当前存活路由map
*/
private void createDefinitions(List<RouteDefinition> definitions, Map<String, RouteDefinition> aliveRouteMap) {
// 获取新添加的definitions
Set<String> aliveRouteIdSet = aliveRouteMap.keySet();
List<RouteDefinition> needCreateDefs =
definitions
.stream()
.filter(route -> !aliveRouteIdSet.contains(route.getId())) // 不存在与当前存活集合
.collect(Collectors.toList());
doCreateDefinitions(needCreateDefs);
}
/**
* 执行添加路由
*
* @param needCreateDefs 需要新增的路由集合
*/
private void doCreateDefinitions(List<RouteDefinition> needCreateDefs) {
needCreateDefs.forEach(createDef -> {
try {
this.routeDefinitionRepository.save(Mono.just(createDef)).subscribe();
log.info("created route: {}", createDef.getId());
} catch (Exception e) {
e.printStackTrace();
log.info("create route {} fail", createDef.getId());
}
});
}
/**
* 更新路由
*
* @param definitions 目标routes
* @param aliveRouteMap 当前存活路由map
*/
private void updateDefinitions(List<RouteDefinition> definitions, Map<String, RouteDefinition> aliveRouteMap) {
Set<String> aliveRouteIdSet = aliveRouteMap.keySet();
List<RouteDefinition> needUpdateDefs =
definitions
.stream()
.filter(route -> aliveRouteIdSet.contains(route.getId())
&& !route.equals(aliveRouteMap.get(route.getId()))) // 当前存活且与当前definition不同则为更新
.collect(Collectors.toList());
doUpdateDefinitions(needUpdateDefs);
}
/**
* 删除并重新创建路由实现更新
*
* route repo无updater的结局方法
* @param needUpdateDefs 需要更新route集合
*/
private void doUpdateDefinitions(List<RouteDefinition> needUpdateDefs) {
needUpdateDefs.forEach(updateDefinition -> {
try {
this.routeDefinitionRepository
.delete(Mono.just(updateDefinition.getId()))
.subscribe();
log.info("removed old route(will be recreate): {}", updateDefinition.getId());
} catch (Exception e) {
e.printStackTrace();
log.info("can't clean route(will be create): {}", updateDefinition.getId());
}
try {
this.routeDefinitionRepository.save(Mono.just(updateDefinition)).subscribe();
log.info("updated route: {}", updateDefinition.getId());
} catch (Exception e) {
e.printStackTrace();
log.info("updated route {} fail", updateDefinition.getId());
}
});
}
/**
* 获取当前存活的路由描述map
*
* @return 当前存活的路由描述map
*/
private Map<String, RouteDefinition> getAliveRouteMap() {
return routeDefinitionRepository
.getRouteDefinitions()
.toStream()
.collect(Collectors.toMap(RouteDefinition::getId, Function.identity()));
}
/**
* 删除剔除的routes
*
* @param targetDefIds 目标route id集合
* @param aliveRouteMap 当前存活的路由map
*/
private void removeDefinitions(List<String> targetDefIds, Map<String, RouteDefinition> aliveRouteMap) {
List<String> removedDefinitionIds =
aliveRouteMap
.keySet()
.stream()
.filter(routeId -> !targetDefIds.contains(routeId)) // 不存在于目标id集合判定为删除
.collect(Collectors.toList());
doRemoveDefinitions(removedDefinitionIds);
}
/**
* 删除剔除的routes
*
* @param removedDefinitionIds 需要被剔除的route id集合
*/
private void doRemoveDefinitions(List<String> removedDefinitionIds) {
removedDefinitionIds.forEach(removedId -> {
this.routeDefinitionRepository
.delete(Mono.just(removedId))
.subscribe();
log.info("removed route: {}", removedId);
});
}
/**
* 开启监听
*
* @param applicationEventPublisher publisher instance
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
}
```
* DynamicRouteServiceListener
```java
/**
* 动态路由监听
*
* 添加对对应配置文件更新时的监听,实现动态路由刷新。一般的,为了保证
* 仅在启动时注册指定的对应文件(通常这个文件也是动态配置,这里暂时没
* 有实现当更换路由配置文件时的刷新)更新时对正在运行的路由信息进行刷
* 新。
*
* 该类主要实现yaml解析,并构建definition对象,对于具体的刷新逻辑
* @see DynamicRouteServiceImpl
*/
4j
public class DynamicRouteServiceListener implements CommandLineRunner {
private DynamicRouteServiceImpl dynamicRouteService;
NacosConfigManager nacosConfigManager;
private DynamicRoutingFileProperties dynamicRoutingFileProperties;
/**
* 添加配置文件更新监听
*/
private void dynamicRouteListener () {
try {
ConfigService configService = nacosConfigManager.getConfigService();
// first process ed add listener
processConfigInfo(configService.getConfigAndSignListener(
dynamicRoutingFileProperties.getDataId(),
dynamicRoutingFileProperties.getGroupId(),
dynamicRoutingFileProperties.getTimeout(),
new Listener() {
public void receiveConfigInfo(String configInfo) {
processConfigInfo(configInfo);
}
public Executor getExecutor() {
return null;
}
}
));
} catch (NacosException e) {
log.error("add config listener fail !!!");
e.printStackTrace();
}
}
/**
* 处理配置信息
*
* @param configInfo 配置string
*/
private void processConfigInfo(String configInfo) {
if (Objects.isNull(configInfo)) return;
// 解析yaml文件获取路由定义
List<RouteDefinition> targetRouteDefinitions = getRouteDefinitionsByYaml(configInfo);
// 更新路由信息
dynamicRouteService.refresh(targetRouteDefinitions);
}
/**
* 通过yaml str解析出route定义
*
* @param configInfo yaml str
* @return RouteDefinition array
*/
private List<RouteDefinition> getRouteDefinitionsByYaml(String configInfo) {
Yaml yaml = new Yaml();
Map<Object, Object> document = yaml.load(configInfo);
List<Map<String, Object>> routeList = (List<Map<String, Object>>) document.get("routes");
List<RouteDefinition> targetRouteDefinitions = new ArrayList<>(routeList.size());
for (Map<String, Object> routeItem : routeList) {
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId((String) routeItem.get("id"));
routeDefinition.setUri(URI.create((String) routeItem.get("uri")));
List<String> predicateStrList = (List<String>) routeItem.get("predicates");
List<PredicateDefinition> predicateDefinitions = predicateStrList.stream().map(PredicateDefinition::new).collect(Collectors.toList());
routeDefinition.setPredicates(predicateDefinitions);
List<String> filterStrList = (List<String>) routeItem.get("filters");
if (CollectionUtils.isNotEmpty(filterStrList)) {
List<FilterDefinition> filterDefinitions = filterStrList.stream().map(FilterDefinition::new).collect(Collectors.toList());
routeDefinition.setFilters(filterDefinitions);
}
Object orderObj = routeItem.get("order");
int order = Objects.isNull(orderObj) ? 0 : (int) orderObj;
routeDefinition.setOrder(order);
targetRouteDefinitions.add(routeDefinition);
}
return targetRouteDefinitions;
}
public void run(String... args) {
Long startTime = System.currentTimeMillis();
dynamicRouteListener();
Long completeTime = System.currentTimeMillis();
log.info("dynamic router cost {}ms to initialization routes and registered configurer.", completeTime - startTime);
}
}
配置与填序
1 | routes: |
uri使用lb走的是SCA的nacos naming,后面跟服务名称就好了,具体还有一个order参数,是用来标识路由顺序的,默认做了列表的填序,如果自己配置了order的话还是以自定义的order为主。