Spring Cloud Gateway集成Nacos实现动态配置加载

一、前言

网关作为整个微服务集群的入口,承担着对外部请求的认证和鉴权职责,同时作为网关,相关配置的频繁变动是必然的,这就要求网关具备高度的灵活性,不能每次配置变动都需要重启网关来实现配置加载。基于以上实际场景,本文引出接下来要介绍的重点——网关节点动态加载网关配置,基本原理是基于nacos的配置中心和Spring容器的事件发布监听机制,配置中心的网关配置变动后,触法配置变动事件,gateway重新加载配置到运行内存中。

二、Nacos配置

如下图所示,不同环境创建不同的命名空间。

Spring Cloud Gateway集成Nacos实现动态配置加载_第1张图片

这里需要注意的是,这里文件的名字需要和不同环境里配置的dataId的值保持一致,否则会加载失败。

Spring Cloud Gateway集成Nacos实现动态配置加载_第2张图片

本文使用以下可能在服务运行期间会发生变动的应用配置:

## 不需要认证的请求
auth.skip.urls=[/login]
## jwt生成key使用到的秘钥
jwt.secret.key=q3t6w9z$C&F)J@NcQfTjWnZr4u7x
## jwt的token过期时间
jwt.refresh.token.expireTime=1000

三、代码实现

网关集成动态加载配置需要基于Spring容器的事件发布监听机制,配置更改事件发布后,触发RouteDefinitionWriter更新内存中的配置值。其中RouteDefinitionWriter的默认实现为InMemoryRouteDefinitionRepository,该类的功能是持有网关运行期间使用到的各种应用配置,其底层数据结构是一个HashMap,属性名为key,RouteDefinition为包装了属性值的Bean,在下文中将详细介绍如何实现动态配置加载。
首选,需要定义属性配置类及相关配置:

@Data
@ConfigurationProperties(prefix="nacos")
@Configuration
public class NacosGatewayProperties {

    private String address;

    private String dataId;

    private String groupId;

    private Long timeout;

}

配置值如下所示,网关服务启动后,根据以下配置去nacos拉取不同环境的配置,此处需结合spring.profiles.active使用,本文不再展开,下文中以prod配置文件演示。

nacos.address=127.0.0.1
nacos.dataId=spring-cloud-gateway-prod.properties
nacos.groupId=DEFAULT_GROUP
nacos.timeout=10

nacos相关配置中加入命名空间区分不同环境的配置文件,该命名空间同样需要根据不同环境进行相关的配置。

spring.cloud.nacos.config.namespace=c842a7b3-672d-4e51-81f5-2af3b7573bc7

动态路由配置核心类如下:

@Component
@Slf4j
public class DynamicRouteConfig {


    @Autowired
    private DynamicRoutePublisher dynamicRoutePublisher;

    @Autowired
    private NacosGatewayProperties nacosGatewayProperties;



    @PostConstruct
    public void init(){
	// 在服务启动时,其拉取相关配置
        dynamicRouteByNacosListener();
    }

    /**
     * 监听Nacos Server下发的动态路由配置
     */
    public void dynamicRouteByNacosListener (){
        try {
            ConfigService configService= NacosFactory.createConfigService(nacosGatewayProperties.getAddress());
            String content = configService.getConfig(nacosGatewayProperties.getDataId(), nacosGatewayProperties.getGroupId(), nacosGatewayProperties.getTimeout());
            log.info("配置参数[{}]",content);
            configService.addListener(nacosGatewayProperties.getDataId(), nacosGatewayProperties.getGroupId(), new Listener()  {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    List<RouteDefinition> list = JSON.parseArray(configInfo, RouteDefinition.class);
                    list.forEach(definition->{
                        dynamicRoutePublisher.update(definition);
                    });
                }
                @Override
                public Executor getExecutor() {
                    return null;
                }
            });
        } catch (NacosException e) {
            e.printStackTrace();
        }
    }
}
@Component
public class DynamicRoutePublisher implements ApplicationEventPublisherAware {

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    private ApplicationEventPublisher publisher;

    /**
     * 增加路由
     * @param definition
     * @return
     */
    public String add(RouteDefinition definition) {
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }

    /**
     * 更新路由
     * @param definition
     * @return
     */
    public String update(RouteDefinition definition) {
        try {
            this.routeDefinitionWriter.delete(Mono.just(definition.getId()));

        } catch (Exception e) {
            return "update fail,not find route  routeId: "+definition.getId();
        }
        try {
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception e) {
            return "update route  fail";
        }

    }
    /**
     * 删除路由
     * @param id
     * @return
     */
    public String delete(String id) {
        try {
            this.routeDefinitionWriter.delete(Mono.just(id));
            return "delete success";
        } catch (Exception e) {
            e.printStackTrace();
            return "delete fail";
        }
    }
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }
}

以上两个类的核心功能是:当用户在nacos配置中心进行相关配置更改后,触发配置变更事件,将旧的配置删除,保存新的配置。

因为配置会发生变动,所以配置的使用与传统方式有所区别,本文以登录接口解释:

@Service
@Slf4j
public class LoginServiceImpl implements LoginService {

    private final String LOGIN_USER_KEY = "login:user:%s";

    private final long refreshTokenExpireTime=1000;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    @Autowired
    private EchoService echoService;
    @Autowired
    private ConfigurableApplicationContext applicationContext;
    @Override
    public LoginUser login(String userName, String password) {
        //生成JWT
        log.info("秘钥[{}]",applicationContext.getEnvironment().getProperty("jwt.secret.key"));
        String token = JwtTokenUtil.createToken(userName,applicationContext.getEnvironment().getProperty("jwt.secret.key"));
        //生成refreshToken
        String refreshToken = UUID.randomUUID().toString().replaceAll("-","");
        String refreshTokenKey = String.format(LOGIN_USER_KEY, refreshToken);
        redisTemplate.opsForHash().put(refreshTokenKey,
                "token", token);
        redisTemplate.opsForHash().put(refreshTokenKey,
                "userName", userName);
        //refreshToken设置过期时间
        String refreshTokenExpireTime=applicationContext.getEnvironment().getProperty("jwt.refresh.token.expireTime");
        if(StringUtils.isBlank(refreshTokenExpireTime)){
            return null;
        }
        redisTemplate.expire(refreshTokenKey,
                Long.parseLong(refreshTokenExpireTime), TimeUnit.SECONDS);
        return LoginUser.builder()
                .userName(userName)
                .userToken(token)
                .loginTime(new Date())
                .build();
    }
}

通过装载入ConfigurableApplicationContext,通过配置上下文拿到相关的配置key,进行获取配置值。

四、演示

如下图所示,在更改前jwt.refresh.token.expireTime的值为1000,更改后为10000。

Spring Cloud Gateway集成Nacos实现动态配置加载_第3张图片

查看网关日志,如图中红箭头所示,

在这里插入图片描述

使用Postman调用登录接口通过断点查看配置值是否真的发生变动。

  1. 变更前

在这里插入图片描述

  1. 变更后

Spring Cloud Gateway集成Nacos实现动态配置加载_第4张图片

可以很清晰地看到,配置值在不重新启动的条件下,已经发生变化,在有多个服务实例的情况下,所以节点同步更新。

五、总结

本文通过简单的演示,展现了通过动态加载配置这一功能,在统一的配置中心进行配置管理,与传统的配置文件相比,不仅极大地提高了服务变更的灵活性,而且通过Nacos提供的可视化界面,所以环境的配置管理变得更加简单,降低了运维同学的管理难度。

你可能感兴趣的:(JAVA,java,spring,cloud,gateway)