本文将分四部分讲解:
- SpringCloud Gateway 实现动态路由必要性
- SpringCloud Gateway 动态路由源码解析
- SpringCloud Gateway 动态路由配置实现方式
- SpringCloud Gateway 动态路由配置注意的事项
SpringCloud Gateway 实现动态路由必要性
在实际的生产环境中,如果采用了微服务架构,每次功能迭代发版上线,经常会遇到需要在网关,添加路由配置,如 zuul
。
zuul:
ignored-services: '*'
routes:
ddc:
path: /ddc/**
serviceId: portal-ddc
pcm:
path: /pcm/**
serviceId: portal-pcm
由于采用的是 yml
所以我们需要实现在不重启网关服务的前提下,实现添加服务路由零配置升级。
SpringCloud Gateway 动态路由源码解析
查看 Spring Cloud Gateway
官网,不幸的是 Gateway
并没有提供类似于 Nacos
控制台配置管理页面给开发者来管理服务的路由信息。
于是笔者翻阅
Gateway
路由相关源码,其内部是提供了路由
CRUD
相关
API
接口的。
GatewayControllerEndpoint 端点
Gateway
通过 GatewayControllerEndpoint
暴露路由 Endpoint
端点进行 CRUD
接下来利用 Postman
(据说还有个 Postwomen
)进行路由 CRUD
- 添加路由:
actuator/gateway/routes/{id}
- 删除路由:
actuator/gateway/routes/{id}
- 查询单条路由:
actuator/gateway/routes/{id}
- 查询所有路由:
actuator/gateway/routes
另外,如果想访问
GatewayControllerEndpoint
端点中的方法,需要在
Gateway
添加
spring-boot-starter-actuator
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
并在 yml
management:
endpoints:
web:
exposure:
include: "*"
打开浏览器输入 actuator
地址:http://localhost:8080/actuator/,如果找到 Gateway
端点信息:http://localhost:8080/actuator/gateway,说明可以通过 GatewayControllerEndpoint
进行 CRUD
操作了。
SpringCloud Gateway 动态路由配置实现方式
除了使用 GatewayControllerEndpoint
可以配置路由之外,还可以利用 RouteLocatorBuilder
@SpringBootApplication
public class DemogatewayApplication {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.uri("http://httpbin.org"))
.route("host_route", r -> r.host("*.myhost.org")
.uri("http://httpbin.org"))
.route("rewrite_route", r -> r.host("*.rewrite.org")
.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
.uri("http://httpbin.org"))
.route("hystrix_route", r -> r.host("*.hystrix.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
.uri("http://httpbin.org"))
.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
.uri("http://httpbin.org"))
.route("limit_route", r -> r
.host("*.limited.org").and().path("/anything/**")
.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
.uri("http://httpbin.org"))
.build();
}
}
另外,如果不嫌麻烦,可以利用 RouteDefinitionWriter
public interface RouteDefinitionWriter {
Mono<Void> save(Mono<RouteDefinition> route);
Mono<Void> delete(Mono<String> routeId);
}
默认情况下,Spring Cloud Gateway 使用内存方式(HashMap
)存储路由信息。
其实现逻辑在 InMemoryRouteDefinitionRepository
通过查看类图,我们知道 InMemoryRouteDefinitionRepository
是 RouteDefinitionWriter
这里给我们一个很大启发,是否可以利用 RouteDefinitionWriter
自定义实现类,把路由信息存储到 mysql
、redis
或者 mongo
答案是可以的。
例如,我们利用 Redis
缓存路由信息,只需在 RouteDefinitionWriter
实现类 RedisRouteDefinitionRepository
中添加 redisTemplate
注解,进行路由信息的 CRUD
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
public static final String GW_ROUTES = "apis_gateway_routes";
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteDefinition> routeDefinitions = new ArrayList<>();
redisTemplate.opsForHash().values(GW_ROUTES).stream()
.forEach(routeDefinition -> routeDefinitions.add(JSON.parseObject(routeDefinition.toString(), RouteDefinition.class)));
return Flux.fromIterable(routeDefinitions);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
RouteDefinition definition = new RouteDefinition();
definition.setId("id");
URI uri = UriComponentsBuilder.fromHttpUrl("lb://consumer-service").build().toUri();
definition.setUri(uri);
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName("Path");
Map<String, String> predicateArgs = new HashMap<>();
predicateArgs.put("pattern", "/consumer/**");
predicate.setArgs(predicateArgs);
definition.setPredicates(Arrays.asList(predicate));
FilterDefinition filter = new FilterDefinition();
filter.setName("StripPrefix");
Map<String, String> filterArgs = new HashMap<>();
filterArgs.put("_genkey_0", "1");
filter.setArgs(filterArgs);
definition.setFilters(Arrays.asList(filter));
redisTemplate.opsForHash().put(GW_ROUTES, "routeKey", JSON.toJSONString(definition));
return null;
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return null;
}
}
提供 REST
对外接口,对路由进行 CRUD
操作,最后,每次完成 save
或者 delete
删除,然后发一个 RefreshRoutesEvent
事件,通知 Gateway
@RestController
@RequestMapping("/routes")
public class RouteController implements ApplicationEventPublisherAware {
@Autowired
private RedisRouteDefinitionRepository routeDefinitionWriter;
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@PostMapping
public String addRoute(@RequestBody RouteDefinition definition) {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "0";
}
@GetMapping("/{id}")
public String delete(@PathVariable String id) {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "0";
}
}
如果自定义 RouteDefinitionWriter
的实现类,就会替换 InMemoryRouteDefinitionRepository
,从而当 rest 接口发送 RefreshRoutesEvent
刷新路由事件后, CachingRouteDefinitionLocator
刷新 Gateway
SpringCloud Gateway 动态路由配置注意的事项
在实际的生产环境中,Gateway
网关一般是多实例部署,那么基于 InMemoryRouteDefinitionRepository
因为每次通过 Gateway
的 rest
接口只会更新某个 Gateway
这就解释为什么要用 redis
这样当 Gateway
节点灰度重启或者在 Gateway
内置定时 job
刷新时,就可以通过 RedisRouteDefinitionRepository
的 getRouteDefinitions
方法 从 redis