Spring Boot配置的分离与动态加载实践
需求
-
配置分离: 项目中不同环境下往往配置不一样,将与环境相关的配置提取到
war
包/jar
包外可以避免重复打包、配置文件管理混乱等问题。 -
配置动态加载: 随着业务增长,部署的
war
包/jar
包越来越大,重启服务也变得是一件很难的事情。生产环境下,修改配置文件能够不用重启服务就能生效,无疑是非常nice
的操作。
分析
配置分离
配置分离的方法有很多,从分离到的位置来看:可以分离到文件中、分离到数据库或reids
中、分离中间服务管理下。
我们当然希望有一个单独的服务帮我们管理这些配置,并且能提供一套完美的机制让第三方服务介入进来,这个需求演化成了配置中心。
但是一些old
系统或者小型系统,没成本再去部署一个单独的服务管理配置。而且需求也简单:希望自己的服务仅仅能做到配置分离,分离到war
包之外的文件即可。
幸运的是日常用的Spring MVC
或Spring Boot
支持这样的实现,我们这里只讲Spring Boot
如何实现。
一些常用配置中心的客户端也是基于Spring MVC或Spring Boot本身功能去实现的。
配置动态加载
一个优秀的配置中心当然也会提供配置动态加载的功能,如果没有配置中心,当然我们也可以手动让框架Spring MVC
或Srping Boot
实现动态加载。
解决
配置分离
Spring Boot有用一个非常特殊的PropertySource
顺序,该顺序旨在允许合理地覆盖属性值。(使用优先级由高到低)
-
$HOME/.config/spring-boot
当devtools
处于活动状态时,文件夹中的Devtools
全局设置属性。 -
@TestPropertySource
测试中的注释。 -
properties
测试中的属性。可用于测试应用程序的特定部分@SpringBootTest
的测试注释和注释。 -
命令行参数。
-
来自的属性
SPRING_APPLICATION_JSON
(嵌入在环境变量或系统属性中的嵌入式JSON)。 -
ServletConfig
初始化参数。 -
ServletContext
初始化参数。 -
JNDI
属性java:comp/env
。 -
Java
系统属性(System.getProperties()
)。 -
操作系统环境变量。
-
jar
外的application-{profile}.properties
和YAML
。 -
jar
内的application-{profile}.properties
和YAML
。 -
jar
外的application.properties
和YAML
。 -
jar
内的application.properties
和YAML
。 -
@PropertySource
修饰的@Configuration
的类。 -
Spring
默认属性值SpringApplication.setDefaultProperties
。
如果是jar
包部署,可以从命令行中指定配置文件:
java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties
如果是war包部署,可以使用ServletContext
指定参数。
约定好配置文件路径,然后写到启动代码中:
@SpringBootApplication
public class ApplicationStart extends SpringBootServletInitializer {
/**
* 配置文件路径, 以','分割的字符串. 配置采用覆盖式, 当有多个配置路径, 且包含相同配置属性时, 后者会覆盖前者. (windows环境下 /home/...以当前磁盘为根目录)
*/
public final static String CONFIG_FILES_PATH = "classpath:application.yml,file:/etc/conf/myproject/application.yml";
/**
* main方法启动
*/
public static void main(String[] args) {
SpringApplication.run(ApplicationStart.class, "--spring.config.location=" + CONFIG_FILES_PATH);
}
/**
* tomcat启动
*/
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.setInitParameter("spring.config.location", CONFIG_FILES_PATH);
super.onStartup(servletContext);
}
}
配置动态加载
配置文件(准确的说应该是上下文)的动态加载,是使用了Spring Cloud
,以及它的@RefreshScope
注解。
被@RefreshScope
修饰的注解才会被动态刷新属性值。
使用这样的方式去构建工程,确切的说你的工程已经从Spring Boot
变成了Spring Cloud
。
引入Spring Cloud依赖
spring-cloud.version
属性值
<properties>
<spring-cloud.version>Hoxton.SR1</spring-cloud.version>
</properties>
spring cloud pom
依赖
这里使用了
2.2.1
版本的spring cloud依赖,要求Spring Boot版本2.2.2+
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
引入spring-cloud-context
上下文
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
使用注解修饰要动态变化bean
@Service
@RefreshScope
public class TestServiceImpl {
@Value("${switch.option}")
private String switchOption;
}
或
@Configuration
public class TestConfiguration {
@Bean
@RefreshScope
public Object getBean(){
return new Object();
}
}
如何触发
- 可以添加
spring actuator
依赖,通过手动调接口触发刷新:
添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
手动触发
curl -X POST http://localhost:8080/actuator/refresh
- 也可以定时监控配置文件,发现修改就触发:
@Configuration
@Slf4j
public class ConfigRefreshConfigure implements InitializingBean {
/**
* refresher
*/
@Autowired
private ContextRefresher contextRefresher;
@Override
public void afterPropertiesSet() throws Exception {
DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
String[] configFilePaths = ApplicationStart.CONFIG_FILES_PATH.split(",");
if (ArrayUtils.isNotEmpty(configFilePaths)) {
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(() -> {
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
//absolute config file parent path
Set<String> hasRegisterDirs = new HashSet<>();
//absolute config path
Set<String> hasRegisterFiles = new HashSet<>();
//add config file parentDir to register
for (String configFilePath : configFilePaths) {
Resource configFileResource = defaultResourceLoader.getResource(configFilePath);
if (configFileResource.exists() && configFileResource.isFile()) {
hasRegisterFiles.add(configFileResource.getFile().getAbsolutePath());
File configFileDir = configFileResource.getFile().getParentFile();
if (!hasRegisterDirs.contains(configFileDir.getAbsolutePath())) {
Paths.get(configFileDir.toURI())
.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW);
hasRegisterDirs.add(configFileDir.getAbsolutePath());
}
}
}
//watch config file change
while (true) {
WatchKey key = watchService.take();//block wait
boolean hasChange = false;
for (WatchEvent<?> pollEvent : key.pollEvents()) {
Path changed = (Path) pollEvent.context();
if (hasRegisterFiles.stream().anyMatch(s -> s.equals(((Path)key.watchable()).resolve(changed).toString()))) {
hasChange = true;
break;
}
}
if (hasChange) {
log.info("refresh properties ok! changes: " + JSON.toJSONString(contextRefresher.refresh()));
}
if (!key.reset()) {
log.info("some confFiles has been unregistered in refresh");
}
}
} catch (Exception e) {
log.error("refresh happen error : " + e.getMessage(), e);
}
});
}
}
}