1 搭建一个svn仓库,存放springboot客户端的配置文件

配置文件命名方式参考官网,这里取其中一种:{application}-{profile}.yml

2 服务端搭建

新建一个springboot项目,添加依赖:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>

bootstrap.yml文件配置如下:

server:
  port: 8087

spring:
  application:
    name: config-server-svn 
  profiles: 
    active: subversion     #使用svn作为配置仓库,必须显示声明profiles.active=subversion,不然还是用的git
  cloud: 
    config: 
      server: 
        svn: 
          uri: http://xxx.xx.xxx.xxx/springconfig/trunk/SpringCloudConfig    #配置svn仓库地址
          searchPaths: respo                                        #配置仓库路径
          username: xxxxxx                                          #访问svn仓库的用户名
          password: xxxxxx                                          #访问svn仓库的用户密码
          basedir: /data                                            #默认在系统临时目录下面,需要调整一下避免临时文件被系统自动清理
      label: trunk                                                  #配置svn的分支

  rabbitmq:
    host: xxx.xx.xxx.xxx
    port: 5672
    username: xxxxxxx
    password: xxxxxxxx

configServer的工作流程:
1 服务端注册客户端获取配置接口
2 健康检查时刷新svn仓库到本地
3 客户端启动和每次健康检查时调用该接口获取配置.

3 客户端搭建

另起一个客户端,依赖config,actuator以及bus-amqp

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
 
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
 
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

bootstrap.yml文件配置如下:

server:
  port: 8120

spring:
  application:
    name: microservice
  cloud:
    config:
      uri: http://config.xxxxxxxxx.com/          #指明配置服务中心的网址
      profile: test                                          #dev开发环境配置文件,test测试环境配置文件,prod生产环境配置文件
      label: trunk
      fail-fast: true                                        #拉取配置失败时快速退出,否则有时项目依旧会启动,造成某些依赖的组件为空

  rabbitmq:
    host: xxx.xxx.xx.xx
    port: 5672
    username: xxxx
    password: xxxxxxx

4.客户端配置热更新

至此仅实现客户端初始化时从配置中心拉取配置。修改配置需重启应用.如何实现客户端配置的热更新?主要是@RefreshScope这个注解,热更新redis示例:

/**
* 因为@RefreshScope注解的组件是懒加载的,所以如果有starter的话,可
* 能上下文中已经有了一个redisConnectionFactory,所以建议不要依赖自动* 装配的包,自己定义一个redis配置的组件
*/
@Configuration
@RefreshScope
public class RedisConnectionFactoryConf {

    @Value("${spring.redis.database}")
    private int database;

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.timeout:2000}")
    private long timeout;

    @Value("${spring.redis.lettuce.pool.max-idle:5}")
    private int maxIdle;

    @Value("${spring.redis.lettuce.pool.min-idle:0}")
    private int minIdle;

    @Value("${spring.redis.lettuce.pool.max-active:10}")
    private int maxActive;

    @Value("${spring.redis.lettuce.pool.max-wait:2000}")
    private long maxWait;

    private LettuceConnectionFactory lettuceConnFactory() {

        // 单机版配置
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setDatabase(database);
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        redisStandaloneConfiguration.setPassword(RedisPassword.of(password));

        // 集群版配置
//        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
//        String[] serverArray = clusterNodes.split(",");
//        Set<RedisNode> nodes = new HashSet<RedisNode>();
//        for (String ipPort : serverArray) {
//            String[] ipAndPort = ipPort.split(":");
//            nodes.add(new RedisNode(ipAndPort[0].trim(), Integer.valueOf(ipAndPort[1])));
//        }
//        redisClusterConfiguration.setPassword(RedisPassword.of(password));
//        redisClusterConfiguration.setClusterNodes(nodes);
//        redisClusterConfiguration.setMaxRedirects(maxRedirects);

        //连接池配置
        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxIdle(maxIdle);
        genericObjectPoolConfig.setMinIdle(minIdle);
        genericObjectPoolConfig.setMaxTotal(maxActive);
        genericObjectPoolConfig.setMaxWaitMillis(maxWait);

        LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
                .commandTimeout(Duration.ofMillis(timeout))
                .poolConfig(genericObjectPoolConfig)
                .build();

        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig);
        lettuceConnectionFactory.afterPropertiesSet();
        return lettuceConnectionFactory;
    }

    @RefreshScope
    @Bean("lettuceRedisTemplate")
    public RedisTemplate<String, String> myTemplate() {
        LettuceConnectionFactory factory = this.lettuceConnFactory();
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        return template;
    }

}

配置文件为bootstrap.yml,配置基本数据,主要包括config客户端配置。svn配置hooks到config的/actuator/bus-refresh这个接口。当有svn push事件发生时,会调用该接口,从而通过spring bus通知到客户端,客户端删除@RefreshScope的Bean缓存,以重新创建基于新配置的bean。

流程如下:
1.svn仓库push事件
2.hooks->POST configServer/actuator/bus-refresh
3.configServer publish>>Bus
4.Client subscribe>>Bus
5.refresh

这里需要注意,如果是svn配置的hooks,因其body的payload的原因,config服务端可能会解析失败,此时有如下解决方案,配置一个filter,使此request的body为空:

@WebFilter(urlPatterns = "/actuator/bus-refresh")
public class RefreshFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

        String url = httpServletRequest.getRequestURI();
        if (!url.endsWith("/actuator/bus-refresh")) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            EmptyRequestWrapper requestWrapper = new EmptyRequestWrapper(httpServletRequest);
            filterChain.doFilter(requestWrapper, servletResponse);
        }
        return;
    }

    @Override
    public void destroy() {

    }

    /**
     * 返回自定义的空request
     */
    private class EmptyRequestWrapper extends HttpServletRequestWrapper {
        public EmptyRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            byte[] bytes = new byte[0];
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return byteArrayInputStream.read() == -1 ? true : false;
                }

                @Override
                public boolean isReady() {
                    return false;
                }

                @Override
                public void setReadListener(ReadListener readListener) {

                }

                @Override
                public int read() throws IOException {
                    return byteArrayInputStream.read();
                }
            };
        }
    }
}