1.前言

当我们在本地调试spring boot程序或者将应用部署到对应环境后,每当改变了程序的配置文件,都要重启以获取最新的配置。
针对应用集群,spring cloud已经有了配置中心来实现统一刷新,不需要重启集群中的应用。详见【spring cloud】新手从零搭建spring cloud

那么,有没有一种方法可以让单个应用也可以做到不需要重启就能获取最新的配置呢?

2.思路

配置文件自动刷新有多种方式,也有类似zookeeper,Apollo,spring cloud config,disconf等分布式配置中心的实现。配置中心适合大型项目,本文只提供小项目的一种思路。

整体思路:应用启动后,新建一个线程监控配置文件,当配置文件有变化时,想办法获取到最新的文件。

3.监控配置文件

3.1 WatchService

Java 提供了 WatchService 接口,这个接口是利用操作系统本身的文件监控器对目录和文件进行监控,当被监控对象发生变化时,会有信号通知,从而可以高效的发现变化。

这种方式大致的原理:先根据操作系统 new 一个监控器( WatchService ),然后选择要监控的配置文件所在目录或文件,然后订阅要监控的事件,例如创建、删除、编辑,最后向被监控位置注册这个监控器。一旦触发对应我们所订阅的事件时,执行相应的逻辑即可。

用watchservice修改配置文件方式仅适合于比较小的项目,例如只有一两台服务器,而且配置文件是可以直接修改的。例如 Spring mvc 以 war 包的形式部署,可以直接修改resources 中的配置文件。如果是 Spring boot 项目,还想用这种方式的话,就要引用一个外部可以编辑的文件,比如一个固定的目录,因为 spring boot 大多数以 jar 包部署,打到包里的配置文件没办法直接修改。

3.2 步骤

  • 创建新的监控器。
  • 获取监控文件的路径地址。
  • 订阅监控的事件,并注册到监控器。
  • 开始监控。
  • JVM关闭应用退出时关闭监控。

3.3 监控事件StandardWatchEventKinds

StandardWatchEventKinds类提供了三种监控事件,分别是:

  • ENTRY_CREATE 文件创建
  • ENTRY_DELETE 文件删除
  • ENTRY_MODIFY 文件变更

3.4 talk is cheep, show me the code

public class ConfigWatcher extends Thread {
    private static Logger logger = LoggerFactory.getLogger(ConfigWatcher.class);

    private volatile boolean isRun = true;
    private HttpUtil httpUtil;

    public ConfigWatcher(HttpUtil httpUtil) {
        this.httpUtil = httpUtil;
        this.start();
    }

    @Override
    public void run() {
        WatchService watchService;
        Path path;
        try {
            //1.创建新的监控器。
            watchService = FileSystems.getDefault().newWatchService();

            //2.获取监控文件的路径地址。
            String confDirString = System.getProperty("conf.dir", "src/main/resources");
            File confDir = new File(confDirString);
            if (!confDir.exists()) {
                confDirString = getClass().getResource("/").getFile().toString();
            }
            path = FileSystems.getDefault().getPath(confDirString);
            logger.info("confDirString = {}",confDirString);
            //3.获取监控文件的路径地址。
            path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
        } catch (Exception e) {
            logger.error("Exceptione caught when register the watch key", e.getCause());
            return;
        }

        while (isRun) {
            try {
                //4.监控
                WatchKey wk = watchService.take();
                for (WatchEvent<?> event : wk.pollEvents()) {
                    final Path changed = (Path)event.context();
                    if (changed.endsWith("application.properties")) {
                        //5.重新加载配置文件
                        httpUtil.reload();
                        logger.info("application.yml was modify and we reload it");
                    }
                }

                boolean valid = wk.reset();
                if (!valid) {
                    path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
                }
            } catch (InterruptedException e) {
                break;
            } catch (IOException e) {
                break;
            }
        }
    }

    public void close() {
        this.isRun = false;
        logger.error("关闭了");
        this.interrupt();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            logger.error("Exception caught when close the watchService thread.");
        }
    }
}

4. 刷新文件

在3.4中,我们已经实现了对配置文件的监控,下面就是最关键的一步,实现对配置文件的加载刷新。

4.1 spring传统方法

在传统方法中,我们可以使用Properties类,利用Properties.load(InputStream in)方法将配置文件加载到类里,在使用配置的地方,通过Properties.getProperty()方法获取对应的配置。
每次监控到配置文件变更时,都重新加载一遍配置文件。这样就可以实现配置文件的自动刷新了。

加载配置文件:

public void loadProperties() {
        InputStream in = null;
        try {
            //1.获取配置文件
            String confDirString = System.getProperty("conf.dir", "src/main/resources");
            File confDir = new File(confDirString);
            if (!confDir.exists()) {
                confDirString = getClass().getResource("/").getFile().toString();
            }
            in = new FileInputStream(confDirString + File.separator + "application.properties");
            //加载配置文件
            Properties newProps = new Properties();
            newProps.load(in);
            props = newProps;
        } catch (Exception e) {
            logger.error("fail to load the application.properties file", e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                }
            }
        }
    }

获取配置属性:

public static String getString(String key) {
        return props.getProperty(key);
}

使用时,将loadProperties方法替换3.4中的第5步的方法。

4.2 利用spring cloud config

利用传统spring方法肃然也能够实现配置文件自动刷新,但是不够优雅,spring boot更倾向于使用注解的方式来实现功能。

在前面的博文中,我已经简单的介绍了spring cloud config配置中心的相关实现。spring cloud config一般用在集群应用中,在单个应用中,我们不太可能再单独为应用开发一个server端用于获取配置文件,那么,我们能不能将server端和client 端都集中写在我们的应用中呢?

4.2.1 引入依赖

引入spring cloud config相关配置。

<dependencies>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-config-client</artifactId>
	<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>Finchley.RELEASE</version>
		        <type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

4.2.2

启动类加上**@EnableConfigServer**注解

4.2.3 bootstrap.yml配置

server:
  port: 8092

spring:
  application:
    name: config_test
  profiles:
      active: native
  cloud:
    config:
      server:
        native:
          search-locations: file:D:\\Documents and Settings\\Workspaces\\new workSpace\\consumer\\src\\main\\resources
      name: application                                      #需要加载的配置名称
      profile: default                                           #加载的环境,默认default
      enabled: true                                             #默认true,从配置中心获取配置信息,false则表示读取本地配置
      uri: http://127.0.0.1:${server.port}             #获取config server地址,也就是当前应用地址      
management:
  endpoints:
    web:
      exposure:
        include: "*"                                              #开启actuator相关接口

如上,将配置中心改为本地,client端获取server端的配置文件时,server端地址即为当前应用地址

4.2.4 使用配置文件

我们在application.properties配置文件中增加一个配置项

config.test=22222

在controller中使用该配置,注意加上**@RefreshScope**注解

@RestController
@RefreshScope
public class ConsumerController {

    @Value("${config.test}")
    private String configMsg;

    @RequestMapping(value = "/getMessage", method = RequestMethod.GET)
    public String message(){
        return configMsg;
    }
}

在监控到配置文件后,我们需要新建一个工具类,用来发送**/actuator/refresh**命令实现刷新。并且将该类注入ioc容器中。

@Component
public class HttpUtil {
    private Logger logger = LoggerFactory.getLogger(HttpUtil.class);

    public void reload() {
        try {
            String url = "http://127.0.0.1:8092/actuator/refresh";
            HttpPost httpPost = new HttpPost(url);

            CloseableHttpResponse response = new DefaultHttpClient().execute(httpPost);
            HttpEntity responseEntity = response.getEntity();
            if (responseEntity != null) {
                String recvData = EntityUtils.toString(responseEntity, "utf-8");
                logger.info(recvData);
            } else {
                logger.info("adfasdfasdfasdfasdfasdf");
            }
        } catch (Exception e) {
            logger.error("Exception Caught when reload.", e);
        }
    }
}

为了让应用启动时,就开始运行监控线程,我们将3.4的线程类注入ioc容器中。

@Configuration
public class CommonConfig {

    @Bean(destroyMethod = "close")
    @Autowired
    public ConfigWatcher initConfigWatcher(HttpUtil httpUtil) {
        return new ConfigWatcher(httpUtil);
    }
}

4.2.5 测试

启动应用后,进行测试

springboot app更新接口 springboot不停服更新_spring


在不停止应用的情况下,更新配置值为111

2019-12-25 17:04:46.829 | INFO | http-nio-8092-exec-3 | s.c.a.AnnotationConfigApplicationContext | 588 | Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@40a1c632: startup date [Wed Dec 25 17:04:46 CST 2019]; root of context hierarchy
2019-12-25 17:04:46.890 | INFO | http-nio-8092-exec-3 | f.a.AutowiredAnnotationBeanPostProcessor | 153 | JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2019-12-25 17:04:46.919 | INFO | http-nio-8092-exec-3 | trationDelegate$BeanPostProcessorChecker | 326 | Bean 'configurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$7acf867b] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-12-25 17:04:47.012 | INFO | http-nio-8092-exec-3 | c.c.c.ConfigServicePropertySourceLocator | 205 | Fetching config from server at : http://127.0.0.1:8092
2019-12-25 17:04:47.205 | INFO | http-nio-8092-exec-2 | s.c.a.AnnotationConfigApplicationContext | 588 | Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@61f52f8e: startup date [Wed Dec 25 17:04:47 CST 2019]; root of context hierarchy
2019-12-25 17:04:47.207 | INFO | http-nio-8092-exec-2 | f.a.AutowiredAnnotationBeanPostProcessor | 153 | JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2019-12-25 17:04:47.212 | INFO | http-nio-8092-exec-2 | o.s.c.c.s.e.NativeEnvironmentRepository | 264 | Adding property source: file:D://Documents and Settings//Workspaces//new workSpace//consumer//src//main//resources/application.properties
2019-12-25 17:04:47.213 | INFO | http-nio-8092-exec-2 | s.c.a.AnnotationConfigApplicationContext | 991 | Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@61f52f8e: startup date [Wed Dec 25 17:04:47 CST 2019]; root of context hierarchy
2019-12-25 17:04:47.318 | INFO | http-nio-8092-exec-3 | c.c.c.ConfigServicePropertySourceLocator | 149 | Located environment: name=application, profiles=[native], label=null, version=null, state=null
2019-12-25 17:04:47.318 | INFO | http-nio-8092-exec-3 | b.c.PropertySourceBootstrapConfiguration | 98 | Located property source: CompositePropertySource {name='configService', propertySources=[MapPropertySource {name='file:D://Documents and Settings//Workspaces//new workSpace//consumer//src//main//resources/application.properties'}]}
2019-12-25 17:04:47.321 | INFO | http-nio-8092-exec-3 | o.s.boot.SpringApplication | 658 | The following profiles are active: native
2019-12-25 17:04:47.325 | INFO | http-nio-8092-exec-3 | s.c.a.AnnotationConfigApplicationContext | 588 | Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5caeab59: startup date [Wed Dec 25 17:04:47 CST 2019]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@40a1c632
2019-12-25 17:04:47.328 | INFO | http-nio-8092-exec-3 | f.a.AutowiredAnnotationBeanPostProcessor | 153 | JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2019-12-25 17:04:47.339 | INFO | http-nio-8092-exec-3 | o.s.boot.SpringApplication | 59 | Started application in 0.568 seconds (JVM running for 283.409)
2019-12-25 17:04:47.341 | INFO | http-nio-8092-exec-3 | s.c.a.AnnotationConfigApplicationContext | 991 | Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5caeab59: startup date [Wed Dec 25 17:04:47 CST 2019]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@40a1c632
2019-12-25 17:04:47.342 | INFO | http-nio-8092-exec-3 | s.c.a.AnnotationConfigApplicationContext | 991 | Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@40a1c632: startup date [Wed Dec 25 17:04:46 CST 2019]; root of context hierarchy
2019-12-25 17:04:47.471 | INFO | Thread-14 | com.euraka.center1.config.HttpUtil | 71 | ["config.test"]
2019-12-25 17:04:47.471 | INFO | Thread-14 | com.euraka.center1.config.ConfigWatcher | 59 | application.yml was modify and we reload it

可以从日志中看到,配置文件已经刷新。

springboot app更新接口 springboot不停服更新_spring_02

值也变为新的配置值。