Spring Cloud Gateway運行時動態配置網關

Spring Cloud Gateway官方教程講的都是提早在配置文件中配置網關,實際項目中,Spring Cloud Gateway做爲微服務的入口,須要儘可能避免重啓,因此咱們須要在Spring Cloud Gateway運行時動態配置網關。html

Spring Cloud Gateway自帶接口

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.htmlapp

二、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。

自定義RouteDefinitionWriter

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)));
        });
    }

}
相關文章
相關標籤/搜索