• 聊聊Spring Cloud Gateway 动态路由及通过Apollo的实现


    这里是weihubeats,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党

    背景

    在之前我们了解的Spring Cloud Gateway配置路由方式有两种方式

    1. 通过配置文件
    spring:
      cloud:
        gateway:
          routes:
            - id: test
              predicates:
                - Path=/ms/test/*
              filters:
                - StripPrefix=2
              uri: http://localhost:9000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 通过JavaBean
        @Bean
        public RouteLocator routeLocator(RouteLocatorBuilder builder) {
            return builder.routes()
                    .route(r -> r.path("/ms/test/**")
                    .filters(f -> f.stripPrefix(2))
                    .uri("http://localhost:9000"))
                    .build();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    但是遗憾的是这两种方式都不支持动态路由,都需要重启服务。
    所以我们需要对Spring Cloud Gateway进行改造,在改造的时候我们就需要看看源码了解下Spring Cloud Gateway的路由加载

    路由的加载

    我们之前分析了路由的加载主要在GatewayAutoConfigurationrouteDefinitionRouteLocator方法加载的

    在这里插入图片描述

    实际上最终获取的路由信息都是在GatewayProperties这个配置类中
    在这里插入图片描述

    在这里插入图片描述

    所以我们在动态路由的时候修改GatewayProperties中的属性即可,即

    1. List routes
    2. List defaultFilters

    恰巧Spring Cloud Gateway也提供了相应的getset方法

    在这里插入图片描述

    实际如果我们修改了该属性我们会发现并不会立即生效,因为我们会发现还有一个RouteLocator就是CachingRouteLocator,并且在配置Bean的时候加了注解@Primary,说明最后使用额RouteLocator实际是CachingRouteLocator
    在这里插入图片描述
    CachingRouteLocator最后还是使用RouteDefinitionRouteLocator类加载的,也是就我们上面分析的,看CachingRouteLocator就知道是缓存作用

    在这里插入图片描述
    这里引用网上一张加载图片

    在这里插入图片描述

    参考 https://www.jianshu.com/p/490739b183af

    所以看到这里我们知道我们还需要解决的一个问题就是更新缓存,如何刷新缓存呢,这里Spring Cloud Gateway利用spring的事件机制给我提供了扩展
    在这里插入图片描述

    在这里插入图片描述
    所以我们要做的事情就是这两件事:

    1. GatewayProperties
    2. 刷新缓存

    实现动态路由

    这里代码参考 https://github.com/apolloconfig/apollo-use-cases

    @Component
    @Slf4j
    public class GatewayPropertiesRefresher implements ApplicationContextAware, ApplicationEventPublisherAware {
    	
    	private static final String ID_PATTERN = "spring\\.cloud\\.gateway\\.routes\\[\\d+\\]\\.id";
    
    	private static final String DEFAULT_FILTER_PATTERN = "spring\\.cloud\\.gateway\\.default-filters\\[\\d+\\]\\.name";
    
    	private ApplicationContext applicationContext;
    
    	private ApplicationEventPublisher publisher;
    
    	@Autowired
    	private GatewayProperties gatewayProperties;
    
    	@Override
    	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    		this.applicationContext = applicationContext;
    	}
    
    
    	@Override
    	public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
    		this.publisher = applicationEventPublisher;
    	}
    
    	@ApolloConfigChangeListener(value = "route.yml",interestedKeyPrefixes = "spring.cloud.gateway.")
    	public void onChange(ConfigChangeEvent changeEvent) {
    		refreshGatewayProperties(changeEvent);
    	}
    
    	/***
    	 * 刷新org.springframework.cloud.gateway.config.PropertiesRouteDefinitionLocator中定义的routes
    	 *
    	 * @param changeEvent
    	 * @return void
    	 * @author ksewen
    	 * @date 2019/5/21 2:13 PM
    	 */
    	private void refreshGatewayProperties(ConfigChangeEvent changeEvent) {
    		log.info("Refreshing GatewayProperties!");
    		preDestroyGatewayProperties(changeEvent);
    		this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
    		refreshGatewayRouteDefinition();
    		log.info("GatewayProperties refreshed!");
    	}
    
    	/***
    	 * GatewayProperties没有@PreDestroy和destroy方法
    	 * org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#rebind(java.lang.String)中destroyBean时不会销毁当前对象
    	 * 如果把spring.cloud.gateway.前缀的配置项全部删除(例如需要动态删除最后一个路由的场景),initializeBean时也无法创建新的bean,则return当前bean
    	 * 若仍保留有spring.cloud.gateway.routes[n]或spring.cloud.gateway.default-filters[n]等配置,initializeBean时会注入新的属性替换已有的bean
    	 * 这个方法提供了类似@PreDestroy的操作,根据配置文件的实际情况把org.springframework.cloud.gateway.config.GatewayProperties#routes
    	 * 和org.springframework.cloud.gateway.config.GatewayProperties#defaultFilters两个集合清空
    	 *
    	 * @param
    	 * @return void
    	 * @author ksewen
    	 * @date 2019/5/21 2:13 PM
    	 */
    	private synchronized void preDestroyGatewayProperties(ConfigChangeEvent changeEvent) {
    		log.info("Pre Destroy GatewayProperties!");
    		final boolean needClearRoutes = this.checkNeedClear(changeEvent, ID_PATTERN, this.gatewayProperties.getRoutes()
    				.size());
    		if (needClearRoutes) {
    			this.gatewayProperties.setRoutes(new ArrayList<>());
    		}
    		final boolean needClearDefaultFilters = this.checkNeedClear(changeEvent, DEFAULT_FILTER_PATTERN, this.gatewayProperties.getDefaultFilters()
    				.size());
    		if (needClearDefaultFilters) {
    			this.gatewayProperties.setDefaultFilters(new ArrayList<>());
    		}
    		log.info("Pre Destroy GatewayProperties finished!");
    	}
    
    	private void refreshGatewayRouteDefinition() {
    		log.info("Refreshing Gateway RouteDefinition!");
    		this.publisher.publishEvent(new RefreshRoutesEvent(this));
    		log.info("Gateway RouteDefinition refreshed!");
    	}
    
    	/***
    	 * 根据changeEvent和定义的pattern匹配key,如果所有对应PropertyChangeType为DELETED则需要清空GatewayProperties里相关集合
    	 *
    	 * @param changeEvent
    	 * @param pattern
    	 * @param existSize
    	 * @return boolean
    	 * @author ksewen
    	 * @date 2019/5/23 2:18 PM
    	 */
    	private boolean checkNeedClear(ConfigChangeEvent changeEvent, String pattern, int existSize) {
    		return changeEvent.changedKeys().stream().filter(key -> key.matches(pattern))
    				.filter(key -> {
    					ConfigChange change = changeEvent.getChange(key);
    					return PropertyChangeType.DELETED.equals(change.getChangeType());
    				}).count() == existSize;
    	}
    }
    
    • 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

    然后我们在apollo添加namespace:route.yml

    配置内容如下:

    spring:
      cloud:
        gateway:
          routes:
            - id: test
              predicates:
                - Path=/ms/test/*
              filters:
                - StripPrefix=2
              uri: http://localhost:9000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    然后我们可以通过访问地址:
    http:localhost:8080/ms/test/health

    看删除后是否是404,加上后是否可以正常动态路由

    值得注意的是上面@ApolloConfigChangeListener中如果没有添加新的namespacevalue可以不用填写,如果配置文件是yml配置文件,在监听的时候需要指定文件后缀

  • 相关阅读:
    Polygon Miden:扩展以太坊功能集的ZK-optimized rollup
    css宽高自适应
    手把手教你深度学习和实战-----全连接神经网络
    阿里云CDN是什么意思?
    JVM 虚拟机系列:架构(二)一图看懂虚拟机架构:JNI
    IEEE出版社旗下期刊的投稿整理——(更新ing)
    大数据驱动业务增长:数据分析和洞察力的新纪元
    多线程抽象知识汇总
    C++ [](){}匿名函数
    转铁蛋白(TF)修饰紫杉醇(PTX)脂质体(TF-PTX-LP)|转铁蛋白(Tf)修饰姜黄素脂质体
  • 原文地址:https://blog.csdn.net/qq_42651904/article/details/127542815