点击上方“猿芯”,选择“设为星标”
后台回复"1024",有份惊喜送给面试的你
本文将分四部分讲解:
- 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
缓存获取路由信息呢。
往期推荐
- 肝九千字长文 | MyBatis-Plus 码之重器 lambda 表达式使用指南,开发效率瞬间提升80%
- 用 MHA 做 MySQL 读写分离,频繁爆发线上生产事故后,泪奔分享 Druid 连接池参数优化实战
- 微服务架构下,解决数据库跨库查询的一些思路
- 一文读懂阿里大中台、小前台战略
作者简介:猿芯,一枚简单的北漂程序员。喜欢用简单的文字记录工作与生活中的点点滴滴,愿与你一起分享程序员灵魂深处真正的内心独白。我的微信号:WooolaDunzung,公众号【猿芯】输入 1024 ,有份面试惊喜送给你哦。
< END >
【猿芯】
微信扫描二维码,关注我的公众号。
原创不易,莫要干想,如果觉得有点用的话,动动你的发财之手,一键三连击:分享、点赞、在看,你们的鼓励是我写作更多优质文章的最强动力 ^_^
分享、点赞、在看,3连3连!