SpringBoot不使用配置中心实现本地配置文件定时动态刷新

由于还没有接入nacos配置中心,所以需要出一个版本更新本地动态刷新配置的功能,实现配置的动态更新。经过研究发现SpringCloud已经提供了手动接口/actuator/refresh方式刷新的功能,这里再进行处理下实现定时任务动态刷新配置。

使用版本

SpringBoot版本2.3.10.RELEASE

1
2
3
4
5
6
7
8
9
10
11
12
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/>
</parent>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<version>2.2.8.RELEASE</version>
</dependency>

首先集成SpringCloud中的refresh刷新功能

上面已经添加了所有的依赖包。

依赖spring-cloud-starter-config来实现配置更新,所以必须要有

yml配置

application.yml配置文件添加配置test.version, 必须要添加endpoints refresh对外暴露接口

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8011
undertow:
threads:
io: 10
worker: 40
management:
endpoints:
web:
exposure:
include: health,info,prometheus,refresh
test:
version: 1.0.0

@RefreshScope注解

在配置获取参数的地方加上@RefreshScope注解,必须要加这个注解才能够实现配置刷新。(@Data是Lombok注解)

1
2
3
4
5
6
7
8
@Data
@RefreshScope
@Component("propertyConfig")
public class PropertyConfig {

@Value("${test.version:false}")
private String testVersion;
}

添加controller

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@RestController
public class VersionController {

@Resource
private PropertyConfig propertyConfig;

@RequestMapping(value = {"/test/version"}, method = RequestMethod.GET)
public String testVersion() {
return propertyConfig.getTestVersion();
}
}

刷新步骤和现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 浏览器或着postman调用 http://localhost:8011/test/version接口。
返回结果:1.0.0

2. application.yml配置文件修改
test:
version: 2.0.0

3. post方式 调用 http://localhost:8011/actuator/refresh, 注意这里一定是post。
请求会返回已经被刷新的配置key列表:
[
"test.version"
]

4. 重复1的逻辑,调用 http://localhost:8011/test/version接口。
返回结果:2.0.0

注意问题

  1. http://localhost:8011/actuator/refresh 接口必须要用post方式调用。
  2. 比如在idea中运行,修改的配置文件一定是编译目录下的yml配置文件,一般在idea中是橘黄色标识 target/classes下的application.yml

分析refresh的实现

找到/actuator/refresh接口的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Endpoint(id = "refresh")
public class RefreshEndpoint {

private ContextRefresher contextRefresher;

public RefreshEndpoint(ContextRefresher contextRefresher) {
this.contextRefresher = contextRefresher;
}

@WriteOperation
public Collection<String> refresh() {
Set<String> keys = this.contextRefresher.refresh();
return keys;
}

}
  1. 发现就是使用 contextRefresher.refresh() 方法实现刷新的。所以我们可以利用这个方法来自己控制什么时候动态刷新配置。
  2. 感兴趣可以深入看一下源码内部实现。

集成schedule任务定时刷新

开启schedule

Application上加上@EnableScheduling注解

1
2
3
4
5
@EnableScheduling
@SpringBootApplication
public class Application {
// ......
}

定时刷新配置任务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.File;
import java.util.Set;

@RefreshScope
@Component
public class PropertiesRefreshTask {
private static final Logger log = LoggerFactory.getLogger(PropertiesRefreshTask.class);
/**
* 文件更新时间
*/
private long fileUpdateTime = 0L;
/**
* 这里是配置文件的目录
*/
@Value("${yml.path:/usr/local/lark}")
private String ymlPath;

@Resource
private ContextRefresher contextRefresher;

/**
* 30s检查一次
*/
@WriteOperation
@Scheduled(fixedDelay = 30000L)
public void propertiesRefreshTask() {
try {
File file = new File(ymlPath + "/application.yml");
// 检测文件是否存在
if (!file.exists()) {
log.warn("application.yml not exist");
return;
}
long lastModifiedTime = file.lastModified();
// 文件更新时间和记录对比
if (fileUpdateTime == 0 || fileUpdateTime >= lastModifiedTime) {
fileUpdateTime = lastModifiedTime;
return;
}
Set<String> keys = contextRefresher.refresh();
log.info("propertiesRefresh keys={}", keys);
} catch (Exception e) {
log.error("propertiesRefresh exception={}", e.getMessage(), e);
}
}

}

刷新步骤和现象

1
2
3
4
5
6
7
8
9
10
11
1. 浏览器或着postman调用 http://localhost:8011/test/version接口
返回结果:1.0.0

2. application.yml配置文件修改
test:
version: 2.0.0

3. 等待30s时间

4. 重复1的逻辑,调用 http://localhost:8011/test/version接口
返回结果:2.0.0

总结

  1. 这个是没有接入配置中心导致这么使用的原因,还是建议使用配置中心来实现配置动态加载。
  2. 后续会从源码角度分析下SrpingCloud的refresh的实现逻辑及作用范围。