在使用 Cloud Gateway 的时候,官方文档提供的方案总是基于配置文件配置的方式
- @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}")) - .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();
- }
- }
- spring:
- jmx:
- enabled: false
- cloud:
- gateway:
- default-filters:
- - PrefixPath=/httpbin
- - AddResponseHeader=X-Response-Default-Foo, Default-Bar
-
- routes:
- # =====================================
- # to run server
- # $ wscat --listen 9000
- # to run client
- # $ wscat --connect ws://localhost:8080/echo
- - id: websocket_test
- uri: ws://localhost:9000
- order: 9000
- predicates:
- - Path=/echo
- # =====================================
- - id: default_path_to_httpbin
- uri: ${test.uri}
- order: 10000
- predicates:
- - Path=/**
Spring Cloud Gateway作为微服务的入口,需要尽量避免重启,而现在配置更改需要重启服务不能满足实际生产过程中的动态刷新、实时变更的业务需求,所以我们需要在Spring Cloud Gateway运行时动态配置网关。
我们明确了目标需要实现动态路由,那么实现动态路由的方案有很多种,这里拿三种常见的方案来说明下:
前两种方案本质上是一种方案,只是数据存储方式不同,大体实现思路是这样,我们通过接口定义路由的增上改查接口,通过接口来修改路由信息,将修改后的数据存储到mysql或redis中,并刷新路由,达到动态更新的目的。
第三种方案相对前两种相对简单,我们使用nacos的配置中心,将路由配置放在nacos上,写个监听器监听nacos上配置的变化,将变化后的配置更新到GateWay应用的进程内。
我们下面采用第三种方案,因为网关未连接mysql,使用redis还有开发相应的api和对应的web,来配置路由信息,而我们目前没有开发web的需求,所以我们采用第三种方案。
RouteOperator
类,用来删除和增加gateway进程内的路由;RouteOperatorConfig
,可以将RouteOperator
作为bean对象注册到Spring环境中;整体架构图如下:
代码目录结构:
app-server-a、app-server-b 为测试服务,gateway-server为网关服务。
这里我们重点看下网关服务的实现;
代码非常简单,主要配置类、监听器、路由更新机制。
动态路由更新服务主要提供网关进程内删除、添加等操作。
该类主要有路由清除clear
、路由添加add
、路由发布到进程publish
和更新全部refreshAll
方法。其中clear
、add
、publish
为private
方法,对外提供的为refreshAll
方法。
实现思路:先清空路由->添加全部路由->发布路由更新事件->完成。
具体内容我们看下面代码:
- package com.july.gateway.service;
-
- import com.fasterxml.jackson.core.JsonProcessingException;
- import com.fasterxml.jackson.core.type.TypeReference;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
- import org.springframework.cloud.gateway.route.RouteDefinition;
- import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
- import org.springframework.context.ApplicationEventPublisher;
- import org.springframework.util.StringUtils;
- import reactor.core.publisher.Mono;
-
- import java.util.ArrayList;
- import java.util.List;
-
- /**
- * 动态路由更新服务
- *
- * @author wanghongjie
- */
- @Slf4j
- public class RouteOperator {
- private ObjectMapper objectMapper;
-
- private RouteDefinitionWriter routeDefinitionWriter;
-
- private ApplicationEventPublisher applicationEventPublisher;
-
- private static final List<String> routeList = new ArrayList<>();
-
- public RouteOperator(ObjectMapper objectMapper, RouteDefinitionWriter routeDefinitionWriter, ApplicationEventPublisher applicationEventPublisher) {
- this.objectMapper = objectMapper;
- this.routeDefinitionWriter = routeDefinitionWriter;
- this.applicationEventPublisher = applicationEventPublisher;
- }
-
- /**
- * 清理集合中的所有路由,并清空集合
- */
- private void clear() {
- // 全部调用API清理掉
- try {
- routeList.forEach(id -> routeDefinitionWriter.delete(Mono.just(id)).subscribe());
- } catch (Exception e) {
- log.error("clear Route is error !");
- }
- // 清空集合
- routeList.clear();
- }
-
- /**
- * 新增路由
- *
- * @param routeDefinitions
- */
- private void add(List<RouteDefinition> routeDefinitions) {
-
- try {
- routeDefinitions.forEach(routeDefinition -> {
- routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
- routeList.add(routeDefinition.getId());
- });
- } catch (Exception exception) {
- log.error("add route is error", exception);
- }
- }
-
- /**
- * 发布进程内通知,更新路由
- */
- private void publish() {
- applicationEventPublisher.publishEvent(new RefreshRoutesEvent(routeDefinitionWriter));
- }
-
- /**
- * 更新所有路由信息
- *
- * @param configStr
- */
- public void refreshAll(String configStr) {
- log.info("start refreshAll : {}", configStr);
- // 无效字符串不处理
- if (!StringUtils.hasText(configStr)) {
- log.error("invalid string for route config");
- return;
- }
- // 用Jackson反序列化
- List<RouteDefinition> routeDefinitions = null;
- try {
- routeDefinitions = objectMapper.readValue(configStr, new TypeReference<>() {
- });
- } catch (JsonProcessingException e) {
- log.error("get route definition from nacos string error", e);
- }
- // 如果等于null,表示反序列化失败,立即返回
- if (null == routeDefinitions) {
- return;
- }
- // 清理掉当前所有路由
- clear();
- // 添加最新路由
- add(routeDefinitions);
-
- // 通过应用内消息的方式发布
- publish();
-
- log.info("finish refreshAll");
- }
- }
监听器的主要作用监听nacos路由配置信息,获取配置信息后刷新进程内路由信息。
该配置类通过@PostConstruct
注解,启动时加载dynamicRouteByNacosListener
方法,通过nacos的host、namespace、group等信息,读取nacos配置信息。addListener
接口获取到配置信息后,将配置信息交给routeOperator.refreshAll
处理。
这里指定了数据ID为:gateway-json-routes
;
- package com.july.gateway.listener;
-
- import com.alibaba.nacos.api.NacosFactory;
- import com.alibaba.nacos.api.PropertyKeyConst;
- import com.alibaba.nacos.api.config.ConfigService;
- import com.alibaba.nacos.api.config.listener.Listener;
- import com.alibaba.nacos.api.exception.NacosException;
- import com.july.gateway.service.RouteOperator;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.stereotype.Component;
-
- import javax.annotation.PostConstruct;
- import java.util.Properties;
- import java.util.concurrent.Executor;
-
- /**
- * nacos监听器
- *
- * @author wanghongjie
- */
- @Component
- @Slf4j
- public class RouteConfigListener {
-
- private String dataId = "gateway-json-routes";
-
- @Value("${spring.cloud.nacos.config.server-addr}")
- private String serverAddr;
- @Value("${spring.cloud.nacos.config.namespace}")
- private String namespace;
- @Value("${spring.cloud.nacos.config.group}")
- private String group;
-
- @Autowired
- RouteOperator routeOperator;
-
- @PostConstruct
- public void dynamicRouteByNacosListener() throws NacosException {
- log.info("gateway-json-routes dynamicRouteByNacosListener config serverAddr is {} namespace is {} group is {}", serverAddr, namespace, group);
- Properties properties = new Properties();
- properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
- properties.put(PropertyKeyConst.NAMESPACE, namespace);
- ConfigService configService = NacosFactory.createConfigService(properties);
- // 添加监听,nacos上的配置变更后会执行
- configService.addListener(dataId, group, new Listener() {
- @Override
- public void receiveConfigInfo(String configInfo) {
- // 解析和处理都交给RouteOperator完成
- routeOperator.refreshAll(configInfo);
- }
-
- @Override
- public Executor getExecutor() {
- return null;
- }
- });
-
- // 获取当前的配置
- String initConfig = configService.getConfig(dataId, group, 5000);
-
- // 立即更新
- routeOperator.refreshAll(initConfig);
- }
- }
配置类非常简单,熟悉SpringBoot的都能理解;
- package com.july.gateway.config;
-
-
- import com.fasterxml.jackson.databind.ObjectMapper;
- import com.july.gateway.service.RouteOperator;
- import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
- import org.springframework.context.ApplicationEventPublisher;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
-
- /**
- * 路由配置类
- *
- * @author wanghongjie
- */
- @Configuration
- public class RouteOperatorConfig {
- @Bean
- public RouteOperator routeOperator(ObjectMapper objectMapper,
- RouteDefinitionWriter routeDefinitionWriter,
- ApplicationEventPublisher applicationEventPublisher) {
-
- return new RouteOperator(objectMapper,
- routeDefinitionWriter,
- applicationEventPublisher);
- }
- }
启动nacos,这里使用本机测试;
在nacos中增加以下配置:
- [
- {
- "id": "app-server-a",
- "uri": "lb://app-server-a",
- "predicates": [
- {
- "name": "Path",
- "args": {
- "pattern": "/a/**"
- }
- }
- ],
- "filters": [
- {
- "name": "StripPrefix",
- "args": {
- "parts": "1"
- }
- }
- ]
- }
- ]
这里我们先将app-server-a
添加到网关中。
我们启动app-server-a
、app-server-b
和gateway-server
;
我们启动网关可以看到正常拉去到配置信息:
我们测试下服务A能否正常访问,这里网关的端口是8080;
我们访问:127.0.0.1:8080/a/server-a
可以看到访问成功:
我们不停止服务,新增路由访问服务B:
nacos配置如下:
- [
- {
- "id": "app-server-a",
- "uri": "lb://app-center-a",
- "predicates": [
- {
- "name": "Path",
- "args": {
- "pattern": "/a/**"
- }
- }
- ],
- "filters": [
- {
- "name": "StripPrefix",
- "args": {
- "parts": "1"
- }
- }
- ]
- },
- {
- "id": "app-server-b",
- "uri": "lb://app-center-b",
- "predicates": [
- {
- "name": "Path",
- "args": {
- "pattern": "/b/**"
- }
- }
- ],
- "filters": [
- {
- "name": "StripPrefix",
- "args": {
- "parts": "1"
- }
- }
- ]
- }
- ]
我们在浏览器中访问:127.0.0.1:8080/b/server-b
我们把/b/改成c在测试下;
可以看到到使用c可以访问成功啦,在使用b访问,会出现404;
我们使用127.0.0.1:8080/actuator/gateway/routes查看下当前路由。
- [
- {
- "predicate": "Paths: [/a/**], match trailing slash: true",
- "route_id": "app-server-a",
- "filters": [
- "[[StripPrefix parts = 1], order = 1]"
- ],
- "uri": "lb://app-center-a",
- "order": 0
- },
- {
- "predicate": "Paths: [/c/**], match trailing slash: true",
- "route_id": "app-server-b",
- "filters": [
- "[[StripPrefix parts = 1], order = 1]"
- ],
- "uri": "lb://app-center-b",
- "order": 0
- }
- ]
至此,网关动态路由研发测试完成。
有些公司会在网关中增加限流,使用RequestRateLimiter
组件,正常配置信息如下:
WX20220906-175454@2x
那么动态路由中json应该这样配置:
- [
- {
- "id": "server",
- "uri": "lb://jdd-server",
- "predicates":[
- {
- "name": "Path",
- "args": {
- "pattern": "/server/**"
- }
- }
- ],
- "filters":[
- {
- "name":"StripPrefix",
- "args":{
- "parts": "1"
- }
- },
- {
- "name":"RequestRateLimiter",
- "args":{
- "redis-rate-limiter.replenishRate":"1000",
- "redis-rate-limiter.burstCapacity":"1000",
- "key-resolver":"#{@remoteAddrKeyResolver}"
- }
- }
- ]
- }
- ]
over!
关注公众号:杰子学编程 ,回复《动态路由》获取源码。