Spring Cloud Gateway官方教程講的都是提早在配置文件中配置網關,實際項目中,Spring Cloud Gateway做爲微服務的入口,須要儘可能避免重啓,因此咱們須要在Spring Cloud Gateway運行時動態配置網關。html
Spring Cloud Gateway在2018年6月份發佈了2.0第一個release版本,官方文檔並無講如何動態配置,翻看Spring Cloud Gateway源碼,發現類org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint中提供了網關配置Restful接口,默認沒有啓用。在類org.springframework.cloud.gateway.config.GatewayAutoConfiguration中配置了GatewayControllerEndpointjava
@Configuration @ConditionalOnClass(Health.class) protected static class GatewayActuatorConfiguration { @Bean @ConditionalOnEnabledEndpoint public GatewayControllerEndpoint gatewayControllerEndpoint(RouteDefinitionLocator routeDefinitionLocator, List<GlobalFilter> globalFilters, List<GatewayFilterFactory> GatewayFilters, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { return new GatewayControllerEndpoint(routeDefinitionLocator, globalFilters, GatewayFilters, routeDefinitionWriter, routeLocator); } }
存在org.springframework.boot.actuate.health.Health時啓用,添加actuator依賴:react
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
在gateway項目的application.yml中啓用gateway api:web
#開啓actuator管理api,後面要關閉 management: endpoints: web: exposure: include: "*"
訪問 http://localhost:網關端口/actuator/gateway/routes,返回了當前網關的路由信息。redis
不過這裏咱們不打算用自帶的Restful接口,一來官方文檔也沒說新增網關參數怎麼傳,再者咱們也不但願在網關暴露這些接口。參照org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint,咱們本身編程來動態改變網關。spring
直接上代碼:編程
import java.net.URI; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; @Service public class TestGatewayService implements ApplicationEventPublisherAware { @Autowired private RouteDefinitionWriter routeDefinitionWriter; private ApplicationEventPublisher publisher; public String save() { RouteDefinition definition = new RouteDefinition(); PredicateDefinition predicate = new PredicateDefinition(); Map<String, String> predicateParams = new HashMap<>(8); definition.setId("baiduRoute"); predicate.setName("Path"); //請替換成本地可訪問的路徑 predicateParams.put("pattern", "/baidu"); //請替換成本地可訪問的路徑 predicateParams.put("pathPattern", "/baidu"); predicate.setArgs(predicateParams); definition.setPredicates(Arrays.asList(predicate)); //請替換成本地可訪問的域名 URI uri = UriComponentsBuilder.fromHttpUrl("http://www.baidu.com").build().toUri(); definition.setUri(uri); routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); return "success"; } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } }
代碼說明:json
一、predicate.setName("Path"); 設置predicat名稱,這個名稱不是亂起的,Spring會根據名稱去查找對應的FilterFactory,目前支持的名稱有:After、Before、Between、Cookie、Header、Host、Method、Path、Query、RemoteAddr。api
對應官方文檔的Route Predicate Factories(http://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.0.0.RELEASE/single/spring-cloud-gateway.html)app
二、definition.setId("baiduRoute"); 設置definition的id,須要全局惟一,默認使用UUID。
三、predicateParams.put("pattern", "/baidu"); 每一個Route Predicate的參數不一樣,詳情在官網文檔查看對應的Route Predicate配置示例,然而官方文檔也很坑,好比Path Route的- Path=/foo/{segment},把參數給省略了。仍是得看源碼,在包org.springframework.cloud.gateway.handler.predicate裏有Spring Cloud Gateway全部的Predicate,打開對應的RoutePredicateFactory,內部類Config就是該Predicate支持的參數。
四、routeDefinitionWriter.save(Mono.just(definition)).subscribe(); 默認的RouteDefinitionWriter實現類是org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository。注意最後必定要調用subscribe(),不然不執行。
至此已編碼動態配置了一個基本的網關。
帶Filter的配置代碼:
RouteDefinition routeDefinition = new RouteDefinition(); PredicateDefinition predicateDefinition = new PredicateDefinition(); Map<String, String> predicateParams = new HashMap<>(8); Map<String, String> filterParams = new HashMap<>(8); FilterDefinition filterDefinition = new FilterDefinition(); URI uri = UriComponentsBuilder.fromUriString("lb://HELLO-SERVICE").build().toUri(); routeDefinition.setId("rateLimitTest"); // 名稱是固定的,spring gateway會根據名稱找對應的PredicateFactory predicateDefinition.setName("Path"); predicateParams.put("pattern", "/rate/**"); predicateDefinition.setArgs(predicateParams); // 名稱是固定的,spring gateway會根據名稱找對應的FilterFactory filterDefinition.setName("RequestRateLimiter"); // 每秒最大訪問次數 filterParams.put("redis-rate-limiter.replenishRate", "2"); // 令牌桶最大容量 filterParams.put("redis-rate-limiter.burstCapacity", "3"); // 限流策略(#{@BeanName}) filterParams.put("key-resolver", "#{@hostAddressKeyResolver}"); // 自定義限流器(#{@BeanName}) //filterParams.put("rate-limiter", "#{@redisRateLimiter}"); filterDefinition.setArgs(filterParams); routeDefinition.setPredicates(Arrays.asList(predicateDefinition)); routeDefinition.setFilters(Arrays.asList(filterDefinition)); routeDefinition.setUri(uri); routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe(); publisher.publishEvent(new RefreshRoutesEvent(this));
須要注意的是filterParams.put("key-resolver", "#{@hostAddressKeyResolver}"); hostAddressKeyResolver是我自定義的Spring Bean,#{@BeanName}是Spring的表達式,用來注入Bean。
Spring Cloud Gateway默認的RouteDefinitionWriter實現類是org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository,Route信息保存在當前實例的內存中,這在集羣環境中會存在同步問題。咱們能夠自定義一個基於Redis的RouteDefinitionWriter。
import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionRepository; import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import com.alibaba.fastjson.JSON; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** * 使用Redis保存自定義路由配置(代替默認的InMemoryRouteDefinitionRepository) * <p/> * 存在問題:每次請求都會調用getRouteDefinitions,當網關較多時,會影響請求速度,考慮放到本地Map中,使用消息通知Map更新。 * * @since 2018年7月9日 下午2:39:02 */ @Component public class RedisRouteDefinitionRepository implements RouteDefinitionRepository { public static final String GATEWAY_ROUTES = "geteway_routes"; @Autowired private StringRedisTemplate redisTemplate; @Override public Flux<RouteDefinition> getRouteDefinitions() { List<RouteDefinition> routeDefinitions = new ArrayList<>(); redisTemplate.opsForHash().values(GATEWAY_ROUTES).stream().forEach(routeDefinition -> { routeDefinitions.add(JSON.parseObject(routeDefinition.toString(), RouteDefinition.class)); }); return Flux.fromIterable(routeDefinitions); } @Override public Mono<Void> save(Mono<RouteDefinition> route) { return route .flatMap(routeDefinition -> { redisTemplate.opsForHash().put(GATEWAY_ROUTES, routeDefinition.getId(), JSON.toJSONString(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))); }); } }