Spring-Gateway与Nacos相关配置

一、Nacos Config 配置项动态刷新

1、介绍与环境引入

1.1 简介

Nacos官方手册:https://nacos.io/zh-cn/docs/quick-start.html

动态刷新通过线上的配置更新进行推送,不需要代码改动也不需要重启服务器,这样可以更快更方便的进行服务器配置文件修改(因为重启Tomcat往往会消耗大量的时间)。首先需要引入相关依赖,这里默认已经启动Nacos服务器了,可以通过http://localhost:8848/nacos/index.html查看

1.2 环境引入

  • Nacos 既能用作配置管理也能用作服务注册,如果你想要引入 Nacos 的服务发现功能,需要添加的是 nacos-discovery 包;

  • 而如果你想引入的是 Nacos 的配置管理功能,则需要添加 nacos-config 包。第二个依赖项是为了让程序在启动时能够加载本地的 bootstrap 配置文件,因为 Nacos 配置中心的连接信息需要配置在 bootstrap 文件,而**非 application.yml **文件中。

<!-- 添加Nacos Config配置项 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
 
<!-- 读取bootstrap文件 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

在 Spring Cloud 2020.0.0 版本之后,bootstrap 文件不会被自动加载,你需要主动添加 spring-cloud-starter-bootstrap 依赖项,来开启 bootstrap 的自动加载流程

**为什么集成 Nacos 配置中心必须用到 bootstrap 配置文件呢?**这就要说到 Nacos Config 在项目启动过程中的优先级了。如果你在 Nacos 配置中心里存放了访问 MySQL 数据库的 URL、用户名和密码,而这些数据库配置会被用于其它组件的初始化流程,比如数据库连接池的创建。为了保证应用能够正常启动,我们必须在其它组件初始化之前从 Nacos 读到所有配置项,之后再将获取到的配置项用于后续的初始化流程。

因此,在服务的启动阶段,你需要通过某种途径将 Nacos 配置项加载的优先级设置为最高。而在 Spring Boot 规范中,bootstrap 文件通常被用于应用程序的上下文引导,bootstrap.yml 文件的加载优先级是高于 application.yml 的。如果我们将 Nacos Config 的连接串和参数添加到 bootstrap 文件中,就能确保程序在启动阶段优先执行 Nacos Config 远程配置项的读取任务。这就是我们必须将 Nacos Config 连接串配置在 bootstrap 中的原因。

2、Nacos Config 本地配置项

2.1 配置项举例

首先创建bootstrap.yml文件,在 bootstrap.yml 文件中添加一些 Nacos Config 配置项

spring:
  profiles:
    active: dev
  # 必须把name属性从application.yml迁移过来,否则无法动态刷新
  application:
    name: coupon-customer-serv
  cloud:
    nacos:
      config:
        # nacos config服务器的地址
        server-addr: localhost:8848
        file-extension: yml
        # prefix: 文件名前缀,默认是spring.application.name
        # 如果没有指定命令空间,则默认命令空间为PUBLIC,如果指定需要写ID
        namespace: 
        # 如果没有配置Group,则默认值为DEFAULT_GROUP
        group: DEFAULT_GROUP
        # 从Nacos读取配置项的超时时间
        timeout: 5000
        # 长轮询超时时间
        config-long-poll-timeout: 10000        
        # 轮询的重试时间
        config-retry-time: 2000
        # 长轮询最大重试次数
        max-retry: 3
        # 开启监听和自动刷新
        refresh-enabled: true
        # Nacos的扩展配置项,数字越大优先级越高
        extension-configs:
          - dataId: redis-config.yml
            group: EXT_GROUP
            # 动态刷新
            refresh: true
          - dataId: rabbitmq-config.yml
            group: EXT_GROUP
            refresh: true

2.2 配置项详解

文件定位配置项:主要用于匹配 Nacos 服务器上的配置文件

  • namespace:Nacos Config 的 namespace 和 Nacos 服务发现阶段配置的 namespace 是同一个概念和用法。我们可以使用 namespace 做多租户(multi-tenant)隔离方案,或者隔离不同环境。我指定了 namespace=dev,应用程序只会去获取 dev 这个命名空间下的配置文件;

  • group:概念和用法与 Nacos 服务发现中的 group 相同,如未指定则默认值为 DEFAULT_GROUP,应用程序只会加载相同 group 下的配置文件;

  • prefix:需要加载的文件名前缀,默认为当前应用的名称,即 spring.application.name,一般不需要特殊配置;

  • file-extension:需要加载的文件扩展名,默认为 properties,我改成了 yml(yaml),还可以选择 xml、json、html 等格式。

超时和重试配置项

  • timeout:从 Nacos 读取配置项的超时时间,单位是 ms,默认值 3000 毫秒

  • config-retry-time:获取配置项失败的重试时间;

  • config-long-poll-timeout:长轮询超时时间,单位为 ms;

  • max-retry:最大重试次数。

超时和重试配置里提到的长轮询机制的工作原理:当 Client 向 Nacos Config 服务端发起一个配置查询请求时,服务端并不会立即返回查询结果,而是会将这个请求 hold 一段时间。如果在这段时间内有配置项数据的变更,那么服务端会触发变更事件,客户端将会监听到该事件,并获取相关配置变更;如果这段时间内没有发生数据变更,那么在这段"hold 时间"结束后,服务端将释放请求。采用长轮询机制可以降低多次请求带来的网络开销,并降低更新配置项的延迟。

通用配置

  • server-addr:Nacos Config 服务器地址;

  • refresh-enabled: 是否开启监听远程配置项变更的事件,默认为 true。

扩展配置

  • extension-configs:如果你想要从多个配置文件中获取配置项,那么你可以使用 extension-configs 配置多源读取策略。extension-configs 是一个 List 的结构,每个节点都有 dataId、group 和 refresh 三个属性,分别代表了读取的文件名、所属分组、是否支持动态刷新

    在实际的应用中,我们经常需要将一个公共配置项分配给多个微服务使用,比如多个服务共享同一份 Redis、RabbitMQ 中间件连接信息。这时我们就可以在 Nacos Config 中添加一个配置文件,并通过 extension-configs 配置项将这个文件作为扩展配置源加到各个微服务中。这样一来,我们就不需要在每个微服务中单独管理通用配置了

3、Nacos Config Server文件配置

我们在本地启动 Nacos 服务器,打开配置管理模块下的"配置列表"页面,再切换到相应的命名空间下

在这里插入图片描述

然后创建对应的Config文件,主文件的命名规则为${spring.application.name}-${spring.profile.active}.${file-extension},如果没写active属性则默认是无;接下来,你就可以将原本配置在本地 application.yml 中的配置项转移到 Nacos Config 中了,由于 Data ID 后缀是 yml,所以在编辑配置项的时候,你需要在页面上选择"YAML"作为配置格式。

4、动态配置推送

声明一个布尔值的变量 disableCoupon,并使用 @Value 注解将 Nacos 配置中心里的disableCouponRequest 属性注入进来。我们给 disableCouponRequest 属性设置了一个默认值"false",这样做的目的是加一层容错机制。即便 Nacos Config 连接异常无法获取配置项,应用程序也可以使用默认值完成启动加载。

类头上添加一个 RefreshScope 注解,有了这个注解,Nacos Config 中的属性变动就会动态同步到当前类的变量中。如果不添加 RefreshScope 注解,即便应用程序监听到了外部属性变更,那么类变量的值也不会被刷新。当然Gateway网关可以自动监听到

@RefreshScope
public class TestBean {

  @Value("${disableCouponRequest:false}")
  private Boolean disableCoupon;

  ...
}

最后server修改配置文件,在springboot即可实现热更新

二、基于Nacos实现GateWay动态路由

1、前言

一般Gateway配置路由会在配置文件中写死,当我们想添加新路由的时候还得在routes中添加新路由然后重启服务。这样显然是不太合理的,所以动态路由的好处也随之而来

spring:
  application:
    name: gateway
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: true 
          lower-case-service-id: true #是将请求路径上的服务名配置为小写
      routes:
        - id: provider
          uri: lb://nacos-provider
          predicates:
            - Path=/provider/**
          filters:
            # StripPrefix 数字表示要截断的路径的数量
            - StripPrefix=1
        - id: consumer
          uri: lb://nacos-consumer
          predicates:
            - Path=/consumer/**
          filters:
            - StripPrefix=1

2、实现思路

2.1 思路分析

  • 在Gateway启动的时候,读取nacos的路由信息配置,然后刷到Gateway路由

  • 监听nacos信息变化,如果配置修改了,重新调用Gateway刷新事件,刷新最新路由信息

  • Gateway的路由信息,默认是保存在内存中的,可查看GatewayAutoConfiguration源码

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
    @EnableConfigurationProperties
    @AutoConfigureBefore({ HttpHandlerAutoConfiguration.class,
        WebFluxAutoConfiguration.class })
    @AutoConfigureAfter({ GatewayLoadBalancerClientAutoConfiguration.class,
        GatewayClassPathWarningAutoConfiguration.class })
    @ConditionalOnClass(DispatcherHandler.class)
    public class GatewayAutoConfiguration {
    
    
      @Bean
      @ConditionalOnMissingBean(RouteDefinitionRepository.class)
      public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
        return new InMemoryRouteDefinitionRepository();
      }
    
      @Bean
      @Primary
      public RouteDefinitionLocator routeDefinitionLocator(
          List<RouteDefinitionLocator> routeDefinitionLocators) {
        return new CompositeRouteDefinitionLocator(
            Flux.fromIterable(routeDefinitionLocators));
      }
      //...
    }
    
    • 默认就是InMemoryRouteDefinitionRepository,实现了RouteDefinitionRepository,底层是一个线程安全的Map:SynchronizedMap

    • 其中注解@ConditionalOnMissingBean(RouteDefinitionRepository.class) 表示RouteDefinitionRepository没有实现类时,使用InMemoryRouteDefinitionRepository

2.2 实现流程

  • 既然知道了 路由信息 是保存在内存中,那我们可以自定义路由保存的位置,如:redis 等,只需要继承 RouteDefinitionRepository 接口,重写其中的三个方法逻辑即可;他们的save和delete都是 RouteDefinitionWriter 接口的方法
// 获取所有路由信息
Flux<RouteDefinition> getRouteDefinitions();

// 添加路由信息
Mono<Void> save(Mono<RouteDefinition> route);

// 删除路由信息
Mono<Void> delete(Mono<String> routeId);
  • 监听Nacos的路由配置变化,Nacos变化了,我们这边收到数据;收到数据之后,发布刷新路由事件,通知所有存储路由的组件更新路由即可

3、动态路由实战

3.1 自定义动态路由(法一)

重写RouteDefinitionRepository接口实现类

@Slf4j
@Component
public class MyInMemoryRouteDefinitionRepository implements RouteDefinitionRepository {

    private final Map<String, RouteDefinition> routes = Collections.synchronizedMap(new LinkedHashMap<>());

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        Map<String, RouteDefinition> routesSafeCopy = new LinkedHashMap(this.routes);
        return Flux.fromIterable(routesSafeCopy.values());
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap((r) -> {
            if (ObjectUtils.isEmpty(r.getId())) {
                return Mono.error(new IllegalArgumentException("id may not be empty"));
            } else {
                this.routes.put(r.getId(), r);
                return Mono.empty();
            }
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap((id) -> {
            if (this.routes.containsKey(id)) {
                this.routes.remove(id);
            } else {
                log.warn("RouteDefinition not found: " + routeId);
            }
            return Mono.empty();
        });
    }
}

配置动态路由工具类

@Slf4j
@Component
public class DynamicRouteUtil implements ApplicationEventPublisherAware {

    @Resource
    private MyInMemoryRouteDefinitionRepository routeDefinitionRepository;

    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void deleteRoute(String id) {
        try {
            log.info("gateway delete route id {}", id);
            this.routeDefinitionRepository.delete(Mono.just(id)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
        } catch (Exception e) {
            log.error("{}:删除路由失败", id);
        }
    }

    public void updateRoutes(List<RouteDefinition> definitions) {
        log.info("gateway update routes {}", definitions);
        // 获取存在路由列表
        List<RouteDefinition> routeDefinitionsExits = this.routeDefinitionRepository
                .getRouteDefinitions()
                .buffer()
                .blockFirst();

        // 删除路由
        if (CollectionUtils.isNotEmpty(routeDefinitionsExits)) {
            for (RouteDefinition routeDefinitionsExit : routeDefinitionsExits) {
                deleteRoute(routeDefinitionsExit.getId());
            }
        }
        // 更新路由
        definitions.forEach(this::updateRoute);
    }

    public void updateRoute(RouteDefinition definition) {
        // 先删
        log.info("gateway delete route {}", definition);
        this.routeDefinitionRepository.delete(Mono.just(definition.getId()));

        // 后增
        this.routeDefinitionRepository.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
    }

    public void addRoutes(List<RouteDefinition> definitions) {
        if (CollectionUtils.isEmpty(definitions)) {
            return;
        }
        for (RouteDefinition definition : definitions) {
            log.info("add route:{}", definition.getId());
            this.routeDefinitionRepository.save(Mono.just(definition)).subscribe();
        }
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
    }
}

创建处理类

@Component
@Slf4j
public class DynamicRouteHandle {

    @Resource
    private DynamicRouteUtil dynamicRouteUtil;

    @Resource
    private NacosConfigProperties nacosConfigProperties;

    private ConfigService configService;

    public static final String ROUTE_DATA_ID = "gateway_route";

    public static final long DEFAULT_TIMEOUT = 30000;

    @PostConstruct
    public void init() {
        log.info("gateway route init...");
        try {
            configService = initConfigService();
            if (configService == null) {
                log.warn("initConfigService fail");
                return;
            }

            String configInfo = configService.getConfig(ROUTE_DATA_ID, nacosConfigProperties.getGroup(), DEFAULT_TIMEOUT);
            log.info("获取网关当前配置:{}", configInfo);
            List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
            log.info("获取网关数量:{}", definitionList.size());

            dynamicRouteUtil.addRoutes(definitionList);
        } catch (Exception e) {
            log.error("初始化网关路由时发生错误", e);
        }
        // 添加监听
        dynamicRouteByNacosListener(ROUTE_DATA_ID, nacosConfigProperties.getGroup());
    }

    public void dynamicRouteByNacosListener(String dataId, String group) {
        try {
            configService.addListener(dataId, group, new Listener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    log.info("进行网关更新:\n\r{}", configInfo);
                    List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
                    log.info("update route : {}", definitionList.toString());
                    dynamicRouteUtil.updateRoutes(definitionList);
                }

                @Override
                public Executor getExecutor() {
                    log.info("getExecutor");
                    return null;
                }
            });
        } catch (NacosException e) {
            log.error("从nacos接收动态路由配置出错!!!", e);
        }
    }

    private ConfigService initConfigService() {
        try {
            Properties properties = new Properties();
            properties.setProperty("serverAddr", nacosConfigProperties.getServerAddr());
            properties.setProperty("namespace", nacosConfigProperties.getNamespace());
//            properties.setProperty("username", nacosConfigProperties.getUsername());
//            properties.setProperty("password", nacosConfigProperties.getPassword());
            return NacosFactory.createConfigService(properties);
        } catch (Exception e) {
            log.error("初始化网关路由时发生错误", e);
            return null;
        }
    }
}


3.2 自定义动态路由(法二)

@Slf4j
@Component
@ConditionalOnProperty(name = "route.dynamic.enabled", matchIfMissing = true)
public class DynamicGatewayRouteConfig implements ApplicationEventPublisherAware {

    @Value("${route.dynamic.enabled}")
    private Boolean enabled =Boolean.FALSE;

    @Value("${route.dynamic.dataId}")
    private String dataId;

    @Value("${route.dynamic.namespace}")
    private String namespace;

    @Value("${route.dynamic.group}")
    private String group;

    @Value("${spring.cloud.nacos.config.server-addr}")
    private String serverAddr;

    @Value("${spring.cloud.nacos.config.username}")
    private String username;

    @Value("${spring.cloud.nacos.config.password}")
    private String password;

    private RouteDefinitionWriter routeDefinitionWriter;

    private final long timeoutMs=5000;

    @Autowired
    public void setRouteDefinitionWriter(RouteDefinitionWriter routeDefinitionWriter) {
        this.routeDefinitionWriter = routeDefinitionWriter;
    }

    private ApplicationEventPublisher applicationEventPublisher;

    private static final List<String> ROUTES = new ArrayList<String>();

    @PostConstruct
    public void dynamicRouteByNacosListener() {
        if(enabled){
            try {
                Properties properties = new Properties();
                properties.put("serverAddr", serverAddr);
                properties.put("namespace", namespace);
//                properties.put("username", username);
//                properties.put("password", password);
                // 参考官网:https://nacos.io/zh-cn/docs/sdk.html
                ConfigService configService = NacosFactory.createConfigService(properties);
                // 程序首次启动, 并加载初始化路由配置
                String initConfigInfo = configService.getConfig(dataId, group, timeoutMs);
                batchAddOrUpdateRouteAndPublish(initConfigInfo);
                configService.addListener(dataId, group, new Listener() {
                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        batchAddOrUpdateRouteAndPublish(configInfo);
                    }

                    @Override
                    public Executor getExecutor() {
                        return null;
                    }
                });
            } catch (NacosException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 清空所有路由
     */
    private void clearRoute() {
        for(String id : ROUTES) {
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
        }
        ROUTES.clear();
    }

    /**
     * 添加单条路由信息
     * @param definition RouteDefinition
     */
    private void addRoute(RouteDefinition definition) {
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        ROUTES.add(definition.getId());
    }

    /**
     * 批量添加或更新路由,及发布 路由
     * @param configInfo 配置文件字符串, 必须为json array格式
     */
    private void batchAddOrUpdateRouteAndPublish(String configInfo) {
        try {
            clearRoute();
            List<RouteDefinition> gatewayRouteDefinitions = JSONObject.parseArray(configInfo, RouteDefinition.class);
            for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
                addRoute(routeDefinition);
            }
            publish();
            log.info("添加路由信息. {}", JSON.toJSONString(gatewayRouteDefinitions));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void publish() {
        this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
}

3.3 配置创建与修改

注: 如果服务设置了context-path并且与服务名称相同会有问题, 详情: 描述;因为GatewayDiscoveryClientAutoConfiguration.java会为每一个服务创建一个默认路由, 此路由有一个RewritePathGatewayFilter, 会将context-pathserviceId(服务名称)相同的进行置空。

本地配置

ip: localhost
server:
  port: 8080
spring:
  application:
    name: deepsoft-gateway-server
  cloud:
    nacos:
      discovery:
        server-addr: ${ip}:8848
      config:
        server-addr: ${ip}:8848 #nacos中心地址
        file-extension: yaml # 配置文件格式
        group: DEFAULT_GROUP
        namespace:
    gateway:
      discovery:
        locator:
          enabled: true  #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: import #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://deepsoft-import-server
          predicates:
            - Path=/import/**   #断言,路径相匹配的进行路由
          filters:
            - StripPrefix=1
#        - id: login #路由的ID,没有固定规则但要求唯一,建议配合服务名
#          uri: lb://deepsoft-login-consumer
#          predicates:
#            - Path=/login/**   #断言,路径相匹配的进行路由
#          filters:
#            - StripPrefix=1
      enabled: true
  profiles:
    active: dev
    
# 通过 网关服务:ip/actuator/gateway/routes, 可以查看具体的路由信息, 前提是要开启配置
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

logging:
  file:
    name: ./log/${spring.application.name}/${spring.application.name}.log
# 动态路由配置
route:
  dynamic:
    enabled: true
    # 注意如果不写就是默认的,填其他命名空间的话需要填写Id而不是名称
    namespace:
    dataId: gateway_route
    group: DEFAULT_GROUP

远程在server config创建gateway_route文件,选择json类型,即可实现动态路由

[{
    "id":"login",
    "predicates":[
      {
        "args":{
          "pattern": "/login/**"
        },
        "name": "Path"
      }
    ],
    "filters": [
      {
        "name": "StripPrefix",
        "args": {
          "parts": "1"
        }
      }
    ],
    "uri": "lb://deepsoft-login-consumer"
}]
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值