功能:
希望外部訪問 http://domain/acc/v1/endpoint 時, 可以轉發到 springcloud 中 ACC-V1 的實例,
並且將路由資訊儲存至 Redis 下次重啟 or 增加新的 gateway 不用重新配置路由資訊.
配置部分
application.yml
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: false
redis:
host: 127.0.0.1
port: 6379
password:
server:
port: ${port:9000}
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
management:
endpoints:
web:
exposure:
include: '*'
關閉由服務發現來建立路由
spring.cloud.gateway.discovery.locator.enabled = false
開放 gateway 的 endpoint
management.endpoints.web.exposure.include = "*"
程式部分
準備兩支
Redis 配置
RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
需要配置使用 Json 儲存到 Redis, 因為 org.springframework.cloud.gateway.route.RouteDefinition 並沒有 implements Serializable,
所以不轉成 Json 會出錯.
實作 RouteDefinitionRepository
RedisRouteDefinitionRepository.java
@Slf4j
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
public static final String GATEWAY_ROUTES = "geteway_routes";
@Autowired
private RedisTemplate redisTemplate;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteDefinition> routeDefinitions = new ArrayList<>();
redisTemplate.opsForHash().values(GATEWAY_ROUTES).stream().forEach(routeDefinition -> {
try {
routeDefinitions.add((RouteDefinition) routeDefinition);
} catch (Exception e) {
e.printStackTrace();
}
});
return Flux.fromIterable(routeDefinitions);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route
.flatMap(routeDefinition -> {
redisTemplate.opsForHash().put(GATEWAY_ROUTES, routeDefinition.getId(),
routeDefinition);
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
if (redisTemplate.opsForHash().hasKey(GATEWAY_ROUTES, id)) {
redisTemplate.opsForHash().delete(GATEWAY_ROUTES, id);
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new NotFoundException("RouteDefinition not found: " + routeId)));
});
}
}
gateway 預設是將路由放在記憶體中管理, 但當發現有額外提供 RouteDefinitionRepository 時則會採用你提供的,
這邊實作則是將路由寫到 Redis 做存放.
看一下 Eureka 上有哪些服務
跟 gateway 要路由表資訊看看
curl -X GET http://localhost:9000/actuator/gateway/routes
ResponseBody
[]
可以看到路由表是空的
新增路由
curl -X POST \
http://localhost:9000/actuator/gateway/routes/acc_v1 \
-H 'Content-Type: application/json' \
-d '{
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/ACC/V1/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"regexp": "/ACC/V1/(?<remaining>.*)",
"replacement": "/${remaining}"
}
}
],
"uri": "lb://ACC-V1",
"order": 0
}'
再次做查詢
curl -X GET http://localhost:9000/actuator/gateway/routes
ResponseBody
[
{
"route_id": "acc_v1",
"route_definition": {
"id": "acc_v1",
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/ACC/V1/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"regexp": "/ACC/V1/(?<remaining>.*)",
"replacement": "/${remaining}"
}
}
],
"uri": "lb://ACC-V1",
"order": 0
},
"order": 0
}
]
此時可以發現已經有路由表了, 也去 Redis 查一下
接下來去請求 ACC-V1 的資料
curl -X GET http://localhost:9000/ACC/V1/version
ResponseBody
{
"appName": "acc-v1"
}
可以正確拿到v1的資料
此時啟動新的服務版本是 v2
這時候去拿 v2 資料是拿不到
curl -X GET http://localhost:9000/ACC/V2/version
ResponseBody
{
"timestamp": "2018-11-07T09:11:10.323+0000",
"path": "/ACC/V2/version",
"status": 404,
"error": "Not Found",
"message": null
}
增加新服務路由 v2
curl -X POST \
http://localhost:9000/actuator/gateway/routes/acc_v2 \
-H 'Content-Type: application/json'
-d '{
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/ACC/V2/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"regexp": "/ACC/V2/(?<remaining>.*)",
"replacement": "/${remaining}"
}
}
],
"uri": "lb://ACC-V2",
"order": 0
}'
這時候去拿 v2 的資料就可以正確取得
curl -X GET http://localhost:9000/ACC/V2/version
ResponseBody
{
"appName": "acc-v2"
}
但服務 acc-v1 還是可以訪問到, v1 v2 服務同時提供服務, 並且透過 url 來指定就完成了
curl -X GET http://localhost:9000/ACC/V1/version
ResponseBody
{
"appName": "acc-v1"
}
當 v1 要廢棄的時候, 可以先透過 management.endpoints 來刪除
curl -X DELETE \
http://localhost:9000/actuator/gateway/routes/acc_v1
刪除之後確定沒問題就可以真正的把服務關掉囉
curl -X GET \
http://localhost:9000/actuator/gateway/routes
ResponseBody
[
{
"route_id": "acc_v2",
"route_definition": {
"id": "acc_v2",
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/ACC/V2/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"regexp": "/ACC/V2/(?<remaining>.*)",
"replacement": "/${remaining}"
}
}
],
"uri": "lb://ACC-V2",
"order": 0
},
"order": 0
}
]
當 gateway 重啟或是增加新的時候, 都會去 Redis 取得路由資訊, 這樣就不用一個一個去配置啦
如果手動增加或刪除 Redis 內的路由資訊, 其實也是可以同步到所有 Gateway 上, 這樣也不一定需要開 management.endpoints gateway.
參考資料
Spring Cloud Gateway的动态路由实现 | 许进沉思录-专注于互联网与中间件基础架构技术研究
Spring-Cloud-Gateway 源码解析 —— 路由(1.3)之 RouteDefinitionRepository 存储器 | 芋道源码 —— 纯源码解析博客
Spring Cloud Gateway运行时动态配置网关 - 学习记录 - 开源中国
原碼的話可以看看這幾支
GatewayAutoConfiguration.jav
GatewayControllerEndpoint.java
RouteDefinition.java
InMemoryRouteDefinitionRepository.java