springboot配置项动态刷新

文章目录

  • 一,序言
  • 二,准备工作
    • 1. pom.xml引入组件
    • 2. 配置文件示例
  • 三,自定义配置项动态刷新编码实现
    • 1. 定义自定义配置项对象
    • 2. 添加注解实现启动时自动注入
    • 3. 实现yml文件监听以及文件变化处理
  • 四,yaml文件转换为java对象
    • 1. 无法使用前缀绑定的处理
    • 2. 实现yaml文件转换java对象
  • 五、完整代码
    • 1. 代码结构
    • 2. 完整代码备份
    • 3. 运行说明

一,序言

springboot 配置文件一般以yaml方式保存,除了系统配置项如spring、server等外,还有我们自定义的配置项,方便系统启动时自动注入。

自定义的配置项一般是动态配置项,在系统运行过程中,可能需要在线修改,来实现自定义的配置项不停服更新,也就是类似于spring-cloud-starter-config的动态刷新。

由于系统不重启,无法通过自动注入的方式自动更新自定义配置, 这儿便需要我们手动加载yaml文件,转换为java对象,将变化赋值到spring管理的对象中

二,准备工作

采用最常见的snakeyaml、YAMLMapper来实现yaml文件处理。

1. pom.xml引入组件

因 jackson-dataformat-yaml 已经包含snakeyaml ,只需引入前者。

<dependency>
	<groupId>com.fasterxml.jackson.dataformat</groupId>
	<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

2. 配置文件示例

sample.yml

spring:
  datasource:
    url: ${druid.url}
    username: ${druid.username}
    password: ${druid.password}
    driverClassName: ${druid.driverClassName}
    type: com.alibaba.druid.pool.DruidDataSource
    sqlScriptEncoding: utf-8
    schema: classpath:sql/schema.sql
    continue-on-error: true
    druid:
      initial-size: 5                                       # 初始化大小
      min-idle: 10                                          # 最小连接数
      max-active: 20                                        # 最大连接数
      max-wait: 60000                                       # 获取连接时的最大等待时间
      min-evictable-idle-time-millis: 300000                # 一个连接在池中最小生存的时间,单位是毫秒
      time-between-eviction-runs-millis: 60000              # 多久才进行一次检测需要关闭的空闲连接,单位是毫秒
      validation-query: SELECT 1                            # 检测连接是否有效的 SQL语句,为空时以下三个配置均无效
      test-on-borrow: true                                  # 申请连接时执行validationQuery检测连接是否有效,默认true,开启后会降低性能
      test-on-return: true                                  # 归还连接时执行validationQuery检测连接是否有效,默认false,开启后会降低性能
      test-while-idle: true                                 # 申请连接时如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效,默认false,建议开启,不影响性能
  devtools:
    restart:
      exclude: application-dev.yml,welcome.properties

person: 
  name: qinjiang
  age: 18
  happy: false
  birth: 2000-01-01
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - girl
    - music
  dog:
    name: 旺财
    age: 1

三,自定义配置项动态刷新编码实现

1. 定义自定义配置项对象

import java.util.Date;
import java.util.List;
import java.util.Map;

import lombok.Data;

@Data
public class Person
{
    private String name;
    
    private Integer age;
    
    private Boolean happy;
    
    private Date birth;
    
    private Map<String, Object> maps;
    
    private List<Object> lists;
    
    private Dog dog;
}
import lombok.Data;

@Data
public class Dog
{
    private String name;
    
    private Integer age;
}

2. 添加注解实现启动时自动注入

在Person类添加 @Component、@ConfigurationProperties(prefix = “person”) 实现自动注入,spring管理

@Data
@Component
@ConfigurationProperties(prefix = "person")
public class Person
{
    private String name;
    
    private Integer age;
    
    private Boolean happy;
    
    private Date birth;
    
    private Map<String, Object> maps;
    
    private List<Object> lists;
    
    private Dog dog;
}

3. 实现yml文件监听以及文件变化处理

/**
 * 监听文件变化(推荐)
 */
@Slf4j
@Component
public class ReloadByFileAlterationMonitor
{
    @Autowired
    private Welcome welcome;
    
    /**
     * thread-safe
     */
    YAMLMapper yamlMapper = new YAMLMapper();
    
    /**
     * 初始化yml文件监听器
     */
    @PostConstruct
    public void initYamlMonitor()
    {
        try
        {
            URL url = ResourceUtils.getURL(ResourceUtils.CLASSPATH_URL_PREFIX);
            if (ResourceUtils.isFileURL(url))
            {
                FileAlterationObserver observer = new FileAlterationObserver(url.getPath(), FileFilterUtils.suffixFileFilter(".yml"));
                observer.addListener(new FileAlterationListenerAdaptor()
                {
                    @Override
                    public void onFileChange(File file)
                    {
                        log.info(" {} changed.", file.getName());
                        if (StringUtils.equals("application-dev.yml", file.getName()))
                        {
                            try
                            {
                                // yaml to JavaBean
                                String text = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
                                JavaBean javaBean = yamlMapper.readValue(text, JavaBean.class);
                                if (javaBean != null && javaBean.getWelcome() != null)
                                {
                                    String value = javaBean.getWelcome().getMessage();
                                    log.info("#### autoRefresh to: {}", value);
                                    welcome.setMessage(value);
                                }
                            }
                            catch (IOException e)
                            {
                                log.error(e.getMessage(), e.getCause());
                            }
                        }
                    }
                });
                long interval = TimeUnit.SECONDS.toMillis(10);
                FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
                monitor.start();
            }
        }
        catch (Exception e)
        {
            log.error(e.getMessage(), e.getCause());
        }
    }
}

四,yaml文件转换为java对象

1. 无法使用前缀绑定的处理

定义Result 使用Person person绑定yaml中前缀为person的数据,为了避免报错,同时定义了Map<String, Object> spring 来保存spring节点数据。

import lombok.Data;

/**
 * 定义Result实体绑定Person<br>
 * 与下面的Spring配置等价<br>
 * @Component<br>
 * @ConfigurationProperties(prefix = "person")<br>
 * public class Person { ... }
 */
@Data
public class Result
{
    private Person person;
    
    private Map<String, Object> spring;
}

2. 实现yaml文件转换java对象

注意: org.yaml.snakeyaml.Yaml 非线程安全,建议使用 YAMLMapper

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import org.apache.commons.io.IOUtils;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.yaml.snakeyaml.Yaml;

import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.fly.refresh.entity.Result;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SampleTest
{
    static String yamlText;
    
    YAMLMapper yamlMapper = new YAMLMapper();
    
    @BeforeClass
    public static void init()
    {
        try
        {
            yamlText = IOUtils.toString(new ClassPathResource("yaml/sample.yml").getURL(), StandardCharsets.UTF_8);
            log.info("yamlText => {}", yamlText);
        }
        catch (IOException e)
        {
            log.error(e.getMessage(), e.getCause());
        }
    }
    
    /**
     * 解析带prefix的yaml
     */
    @Test
    public void test()
        throws IOException
    {
        Result result = new Yaml().loadAs(yamlText, Result.class);
        log.info("snakeyaml  toJavaBean: {}", result);
        
        result = yamlMapper.readValue(yamlText, Result.class);
        log.info("yamlMapper toJavaBean: {}", result);
    } 
}

五、完整代码

1. 代码结构

在这里插入图片描述

2. 完整代码备份

如何使用下面的备份文件恢复成原始的项目代码,请移步查阅:神奇代码恢复工具

//goto dockerdocker-compose.yml
version: '3'
services:
  hello:
    image: registry.cn-shanghai.aliyuncs.com/00fly/spring-config-refresh:1.0.0
    container_name: config-refresh
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 300M
        reservations:
          cpus: '0.05'
          memory: 200M
    ports:
    - 8080:8080
    environment:
      JAVA_OPTS: -server -Xms200m -Xmx200m -Djava.security.egd=file:/dev/./urandom
    restart: on-failure
    logging:
      driver: json-file
      options:
        max-size: 5m
        max-file: '1'


//goto docker
estart.sh
#!/bin/bash
docker-compose down && docker system prune -f && docker-compose up -d && docker stats
//goto dockerstop.sh
#!/bin/bash
docker-compose down
//goto Dockerfile
FROM openjdk:8-jre-alpine

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

COPY target/*.jar  /app.jar

EXPOSE 8080

CMD ["--server.port=8080"]

ENTRYPOINT ["java","-jar","/app.jar"]
//goto pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.fly</groupId>
	<artifactId>spring-config-refresh</artifactId>
	<version>1.0.0</version>
	<packaging>jar</packaging>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.build.timestamp.format>yyyyMMdd-HH</maven.build.timestamp.format>
		<docker.hub>registry.cn-shanghai.aliyuncs.com</docker.hub>
		<java.version>1.8</java.version>
		<skipTests>true</skipTests>
	</properties>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.4.RELEASE</version>
	</parent>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-log4j2</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.apache.tomcat</groupId>
					<artifactId>tomcat-jdbc</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>1.2.16</version>
		</dependency>
		<dependency>
			<groupId>com.github.xiaoymin</groupId>
			<artifactId>knife4j-spring-boot-starter</artifactId>
			<version>2.0.5</version>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>commons-configuration</groupId>
			<artifactId>commons-configuration</artifactId>
			<version>1.10</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.dataformat</groupId>
			<artifactId>jackson-dataformat-yaml</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.dataformat</groupId>
			<artifactId>jackson-dataformat-properties</artifactId>
		</dependency>
		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>2.6</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>provided</scope>
		</dependency>

		<!-- Test -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-configuration2</artifactId>
			<version>2.8.0</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>commons-beanutils</groupId>
			<artifactId>commons-beanutils</artifactId>
			<version>1.9.4</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<finalName>${project.artifactId}-${project.version}</finalName>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>

			<!-- 添加docker-maven插件 -->
			<plugin>
				<groupId>io.fabric8</groupId>
				<artifactId>docker-maven-plugin</artifactId>
				<version>0.41.0</version>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>build</goal>
							<goal>push</goal>
							<goal>remove</goal>
						</goals>
					</execution>
				</executions>
				<configuration>
					<!-- 连接到带docker环境的linux服务器编译image -->
					<!--<dockerHost>http://192.168.182.10:2375</dockerHost>-->

					<!-- Docker 推送镜像仓库地址 -->
					<pushRegistry>${docker.hub}</pushRegistry>
					<images>
						<image>
							<!--推送到私有镜像仓库,镜像名需要添加仓库地址 -->
							<name>
								${docker.hub}/00fly/${project.artifactId}:${project.version}-UTC-${maven.build.timestamp}</name>
							<!--定义镜像构建行为 -->
							<build>
								<dockerFileDir>${project.basedir}</dockerFileDir>
							</build>
						</image>
						<image>
							<name>
								${docker.hub}/00fly/${project.artifactId}:${project.version}</name>
							<build>
								<dockerFileDir>${project.basedir}</dockerFileDir>
							</build>
						</image>
					</images>
				</configuration>
			</plugin>
		</plugins>
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<excludes>
					<exclude>**/*.java</exclude>
				</excludes>
			</resource>
			<resource>
				<directory>src/main/resources</directory>
				<includes>
					<include>**/**</include>
				</includes>
			</resource>
		</resources>
	</build>
</project>
//goto srcmainjavacomflyBootApplication.java

package com.fly;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.SystemUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.scheduling.annotation.EnableScheduling;

import com.fly.core.utils.SpringContextUtils;

import lombok.extern.slf4j.Slf4j;

/**
 * 
 * SpringBoot 启动入口
 * 
 * @author 00fly
 * @version [版本号, 2018年7月20日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Slf4j
@EnableScheduling
@SpringBootApplication
@PropertySource("classpath:jdbc-h2.properties")
public class BootApplication
{
    public static void main(String[] args)
    {
        // args = new String[] {"--noweb"};
        boolean web = !ArrayUtils.contains(args, "--noweb");
        log.info("############### with Web Configuration: {} #############", web);
        new SpringApplicationBuilder(BootApplication.class).web(web ? WebApplicationType.SERVLET : WebApplicationType.NONE).run(args);
    }
    
    @Bean
    @ConditionalOnWebApplication
    CommandLineRunner init()
    {
        return args -> {
            if (SystemUtils.IS_OS_WINDOWS)
            {
                log.info("  now open Browser  ");
                String url = SpringContextUtils.getServerBaseURL();
                Runtime.getRuntime().exec("cmd /c start /min " + url + "/doc.html");
                Runtime.getRuntime().exec("cmd /c start /min " + url + "/h2-console");
            }
        };
    }
}
//goto srcmainjavacomflycoreconfigKnife4jConfig.java
package com.fly.core.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.swagger.annotations.ApiOperation;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * knife4j
 *
 * @author jack
 */
@Configuration
@EnableSwagger2
public class Knife4jConfig
{
    @Value("${knife4j.enable: true}")
    private boolean enable;
    
    @Bean
    Docket api()
    {
        return new Docket(DocumentationType.SWAGGER_2).enable(enable)
            .apiInfo(apiInfo())
            .groupName("Rest API")
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.fly.refresh.web"))
            .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
            .paths(PathSelectors.any())
            .build();
    }
    
    private ApiInfo apiInfo()
    {
        return new ApiInfoBuilder().title("接口API").description("接口文档").termsOfServiceUrl("http://00fly.online/").version("1.0.0").build();
    }
}
//goto srcmainjavacomflycoreconfigScheduleThreadPoolConfig.java
package com.fly.core.config;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

/**
 * 
 * Schedule线程池配置
 * 
 * @author 00fly
 * @version [版本号, 2023年10月22日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Configuration
public class ScheduleThreadPoolConfig implements SchedulingConfigurer
{
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar)
    {
        ScheduledExecutorService service = new ScheduledThreadPoolExecutor(8, new CustomizableThreadFactory("schedule-pool-"));
        taskRegistrar.setScheduler(service);
    }
}
//goto srcmainjavacomflycoreconfigSysDataBaseConfig.java
package com.fly.core.config;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
 * 
 * 数据库配置信息加载类
 * 
 * @author 00fly
 * @version [版本号, 2021年10月24日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Slf4j
@Configuration
public class SysDataBaseConfig
{
    @Autowired
    JdbcTemplate jdbcTemplate;
    
    @Autowired
    ConfigurableEnvironment environment;
    
    @PostConstruct
    public void initDatabasePropertySource()
    {
        // 取配置信息列表并过滤空值
        List<SysConfig> data = jdbcTemplate.query("SELECT `key`, `value` FROM sys_config WHERE `status` = '1'", new BeanPropertyRowMapper<>(SysConfig.class));
        Map<String, Object> collect = data.stream().filter(p -> StringUtils.isNoneEmpty(p.getKey(), p.getValue())).collect(Collectors.toMap(SysConfig::getKey, SysConfig::getValue));
        log.info("====== init from database ===== {}", collect);
        
        // 追加配置到系统变量中,name取值随意
        environment.getPropertySources().addLast(new MapPropertySource("sys_config", collect));
    }
}

/**
 * 
 * 配置信息实体对象
 * 
 * @author 00fly
 * @version [版本号, 2021年10月24日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Data
class SysConfig
{
    private String key;
    
    private String value;
}
//goto srcmainjavacomflycoreJsonResult.java
package com.fly.core;

import lombok.Data;

/**
 * 
 * 结果对象
 * 
 * @author 00fly
 * @version [版本号, 2021年5月2日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Data
public class JsonResult<T>
{
    private T data;
    
    private boolean success;
    
    private String errorCode;
    
    private String message;
    
    public JsonResult()
    {
        super();
    }
    
    public static <T> JsonResult<T> success(T data)
    {
        JsonResult<T> r = new JsonResult<>();
        r.setData(data);
        r.setSuccess(true);
        return r;
    }
    
    public static JsonResult<?> success()
    {
        JsonResult<Object> r = new JsonResult<>();
        r.setSuccess(true);
        return r;
    }
    
    public static JsonResult<Object> error(String code, String msg)
    {
        JsonResult<Object> r = new JsonResult<>();
        r.setSuccess(false);
        r.setErrorCode(code);
        r.setMessage(msg);
        return r;
    }
    
    public static JsonResult<Object> error(String msg)
    {
        return error("500", msg);
    }
}
//goto srcmainjavacomflycoreutilsSpringContextUtils.java
package com.fly.core.utils;

import java.net.InetAddress;
import java.net.UnknownHostException;

import javax.servlet.ServletContext;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import lombok.extern.slf4j.Slf4j;

/**
 * Spring Context 工具类
 * 
 * @author 00fly
 *
 */
@Slf4j
@Component
public class SpringContextUtils implements ApplicationContextAware
{
    private static ApplicationContext applicationContext;
    
    /**
     * web服务器基准URL
     */
    private static String SERVER_BASE_URL = null;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
        throws BeansException
    {
        log.info("###### execute setApplicationContext ######");
        SpringContextUtils.applicationContext = applicationContext;
    }
    
    public static ApplicationContext getApplicationContext()
    {
        return applicationContext;
    }
    
    public static <T> T getBean(Class<T> clazz)
    {
        Assert.notNull(applicationContext, "applicationContext is null");
        return applicationContext.getBean(clazz);
    }
    
    /**
     * execute @PostConstruct May be SpringContextUtils not inited, throw NullPointerException
     * 
     * @return
     */
    public static String getActiveProfile()
    {
        Assert.notNull(applicationContext, "applicationContext is null");
        String[] profiles = applicationContext.getEnvironment().getActiveProfiles();
        return StringUtils.join(profiles, ",");
    }
    
    /**
     * can use in @PostConstruct
     * 
     * @param context
     * @return
     */
    public static String getActiveProfile(ApplicationContext context)
    {
        Assert.notNull(context, "context is null");
        String[] profiles = context.getEnvironment().getActiveProfiles();
        return StringUtils.join(profiles, ",");
    }
    
    /**
     * get web服务基准地址,一般为 http://${ip}:${port}/${contentPath}
     * 
     * @return
     * @throws UnknownHostException
     * @see [类、类#方法、类#成员]
     */
    public static String getServerBaseURL()
        throws UnknownHostException
    {
        if (SERVER_BASE_URL == null)
        {
            ServletContext servletContext = getBean(ServletContext.class);
            Assert.notNull(servletContext, "servletContext is null");
            String ip = InetAddress.getLocalHost().getHostAddress();
            SERVER_BASE_URL = "http://" + ip + ":" + getProperty("server.port") + servletContext.getContextPath();
        }
        return SERVER_BASE_URL;
    }
    
    /**
     * getProperty
     * 
     * @param key eg:server.port
     * @return
     * @see [类、类#方法、类#成员]
     */
    public static String getProperty(String key)
    {
        return applicationContext.getEnvironment().getProperty(key, "");
    }
}
//goto srcmainjavacomflycoreutilsYamlUtils.java
package com.fly.core.utils;

import java.io.IOException;
import java.util.Map;
import java.util.Properties;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;

/**
 * 
 * yaml转换工具
 * 
 * @author 00fly
 * @version [版本号, 2023年4月25日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public final class YamlUtils
{
    private static YAMLMapper yamlMapper = new YAMLMapper();
    
    private static JavaPropsMapper javaPropsMapper = new JavaPropsMapper();
    
    /**
     * yaml转Json字符串
     * 
     * @param yamlContent
     * @return
     * @throws IOException
     */
    public static String yamlToJson(String yamlContent)
        throws IOException
    {
        JsonNode jsonNode = yamlMapper.readTree(yamlContent);
        return jsonNode.toPrettyString();
    }
    
    /**
     * yaml转Map<String, String>
     * 
     * @param yamlContent
     * @return
     * @throws IOException
     */
    public static Map<String, String> yamlToMap(String yamlContent)
        throws IOException
    {
        JsonNode jsonNode = yamlMapper.readTree(yamlContent);
        return javaPropsMapper.writeValueAsMap(jsonNode);
    }
    
    /**
     * yaml转properties
     * 
     * @param yamlContent
     * @return
     * @throws IOException
     */
    public static Properties yamlToProperties(String yamlContent)
        throws IOException
    {
        JsonNode jsonNode = yamlMapper.readTree(yamlContent);
        return javaPropsMapper.writeValueAsProperties(jsonNode);
    }
    
    /**
     * yaml转properties字符串
     * 
     * @param yamlContent
     * @return
     * @throws IOException
     */
    public static String yamlToPropText(String yamlContent)
        throws IOException
    {
        JsonNode jsonNode = yamlMapper.readTree(yamlContent);
        return javaPropsMapper.writeValueAsString(jsonNode);
    }
    
    private YamlUtils()
    {
        super();
    }
}
//goto srcmainjavacomfly
efreshackReloadByDataBase.java
package com.fly.refresh.back;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import com.fly.refresh.entity.Welcome;

import lombok.extern.slf4j.Slf4j;

/**
 * 数据库配置表手动刷新
 */
@Slf4j
@Service
public class ReloadByDataBase
{
    @Autowired
    Welcome welcome;
    
    @Autowired
    JdbcTemplate jdbcTemplate;
    
    /**
     * 更新到数据库
     * 
     * @param message
     * @return
     */
    public int update(String message)
    {
        int count = jdbcTemplate.update("UPDATE sys_config SET `value`=? WHERE `key` = 'welcome.message'", message);
        if (count > 0)
        {
            log.info("#### autoRefresh to: {}", message);
            welcome.setMessage(message);
        }
        return count;
    }
}
//goto srcmainjavacomfly
efreshackReloadByFileAlterationMonitor.java
package com.fly.refresh.back;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;

import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.fly.refresh.entity.Person;
import com.fly.refresh.entity.Result;
import com.fly.refresh.entity.Welcome;

import lombok.extern.slf4j.Slf4j;

/**
 * 监听文件变化(推荐)
 */
@Slf4j
@Component
public class ReloadByFileAlterationMonitor
{
    @Autowired
    private Person person;
    
    @Autowired
    private Welcome welcome;
    
    /**
     * thread-safe
     */
    YAMLMapper yamlMapper = new YAMLMapper();
    
    /**
     * thread-safe
     */
    JavaPropsMapper javaPropsMapper = new JavaPropsMapper();
    
    /**
     * 初始化yml文件监听器
     */
    @PostConstruct
    public void initYamlMonitor()
    {
        try
        {
            URL url = ResourceUtils.getURL(ResourceUtils.CLASSPATH_URL_PREFIX);
            if (ResourceUtils.isFileURL(url))
            {
                FileAlterationObserver observer = new FileAlterationObserver(url.getPath(), FileFilterUtils.suffixFileFilter(".yml"));
                observer.addListener(new FileAlterationListenerAdaptor()
                {
                    @Override
                    public void onFileChange(File file)
                    {
                        log.info(" {} changed.", file.getName());
                        if (StringUtils.equals("application-dev.yml", file.getName()))
                        {
                            try
                            {
                                // yaml to JavaBean
                                String text = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
                                Result javaBean = yamlMapper.readValue(text, Result.class);
                                
                                // Welcome属性拷贝
                                if (javaBean != null && javaBean.getWelcome() != null)
                                {
                                    Welcome from = javaBean.getWelcome();
                                    BeanUtils.copyProperties(from, welcome);
                                    log.info("#### autoRefresh to: {}", welcome);
                                }
                                // Person属性拷贝
                                if (javaBean != null && javaBean.getPerson() != null)
                                {
                                    Person from = javaBean.getPerson();
                                    BeanUtils.copyProperties(from, person);
                                    log.info("#### autoRefresh to: {}", person);
                                }
                            }
                            catch (IOException e)
                            {
                                log.error(e.getMessage(), e.getCause());
                            }
                        }
                    }
                });
                long interval = TimeUnit.SECONDS.toMillis(10);
                FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
                monitor.start();
            }
        }
        catch (Exception e)
        {
            log.error(e.getMessage(), e.getCause());
        }
    }
    
    /**
     * 初始化Properties文件监听器
     */
    @PostConstruct
    public void initPropsMonitor()
    {
        try
        {
            URL url = ResourceUtils.getURL(ResourceUtils.CLASSPATH_URL_PREFIX);
            if (ResourceUtils.isFileURL(url))
            {
                FileAlterationObserver observer = new FileAlterationObserver(url.getPath(), FileFilterUtils.suffixFileFilter(".properties"));
                observer.addListener(new FileAlterationListenerAdaptor()
                {
                    @Override
                    public void onFileChange(File file)
                    {
                        log.info(" {} changed.", file.getName());
                        if (StringUtils.equals("welcome.properties", file.getName()))
                        {
                            try
                            {
                                // Properties to JavaBean
                                Properties prop = PropertiesLoaderUtils.loadProperties(new ClassPathResource(file.getName()));
                                Result javaBean = javaPropsMapper.readPropertiesAs(prop, Result.class);
                                if (javaBean != null && javaBean.getWelcome() != null)
                                {
                                    String value = javaBean.getWelcome().getMessage();
                                    log.info("#### autoRefresh to: {}", value);
                                    welcome.setMessage(value);
                                }
                            }
                            catch (IOException e)
                            {
                                log.error(e.getMessage(), e.getCause());
                            }
                        }
                    }
                });
                long interval = TimeUnit.SECONDS.toMillis(10);
                FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
                monitor.start();
            }
        }
        catch (Exception e)
        {
            log.error(e.getMessage(), e.getCause());
        }
    }
    
}
//goto srcmainjavacomfly
efreshackReloadByReloadingStrategy.java
package com.fly.refresh.back;

import javax.annotation.PostConstruct;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.FileConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.fly.refresh.entity.Welcome;

import lombok.extern.slf4j.Slf4j;

/**
 * 文件重加载策略(不推荐)
 */
@Slf4j
@Component
public class ReloadByReloadingStrategy
{
    String lastMsg;
    
    @Autowired
    Welcome welcome;
    
    FileConfiguration propConfig;
    
    /**
     * 初始化properties文件重加载策略
     */
    @PostConstruct
    public void initReloadingStrategy()
    {
        try
        {
            // 只支持properties
            propConfig = new PropertiesConfiguration("welcome.properties");
            FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
            strategy.setRefreshDelay(10000L);
            propConfig.setReloadingStrategy(strategy);
            lastMsg = propConfig.getString("welcome.message");
        }
        catch (ConfigurationException e)
        {
            log.error(e.getMessage(), e.getCause());
        }
    }
    
    /**
     * 配置变更时刷新
     */
    @Scheduled(initialDelay = 30000L, fixedRate = 10000L)
    public void autoRefresh()
    {
        // 是否变更,何时刷新逻辑实现
        String message = propConfig.getString("welcome.message");
        if (!StringUtils.equals(message, lastMsg))
        {
            log.info("#### autoRefresh to: {}, after properties Changed", message);
            welcome.setMessage(message);
            lastMsg = message;
        }
    }
}
//goto srcmainjavacomfly
efreshentityDog.java
package com.fly.refresh.entity;

import lombok.Data;

@Data
public class Dog
{
    private String name;
    
    private Integer age;
}
//goto srcmainjavacomfly
efreshentityPerson.java
package com.fly.refresh.entity;

import java.util.Date;
import java.util.List;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Component;

import lombok.Data;

@Data
@Component
@ConfigurationProperties(prefix = "person")
public class Person
{
    private String name;
    
    private Integer age;
    
    private Boolean happy;
    
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birth;
    
    private Map<String, Object> maps;
    
    private List<Object> lists;
    
    private Dog dog;
}
//goto srcmainjavacomfly
efreshentityResult.java
package com.fly.refresh.entity;

import java.util.Map;

import lombok.Data;

/**
 * 定义Result实体绑定Person、Welcome<br>
 * 与下面的Spring配置等价<br>
 * <br>
 * @Component<br>
 * @ConfigurationProperties(prefix = "person")<br>
 * public class Person { ... }<br>
 * <br>
 * @Component<br>
 * @ConfigurationProperties(prefix = "welcome")<br>
 * public class Welcome { ... }
 */
@Data
public class Result
{
    private Person person;
    
    private Welcome welcome;
    
    private Map<String, Object> spring;
}
//goto srcmainjavacomfly
efreshentityWelcome.java
package com.fly.refresh.entity;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import lombok.Data;

/**
 * 
 * Welcome配置文件实体<br>
 * 使用@Lazy待SysDataBaseConfig方法initDatabasePropertySource执行完再注入<br>
 * 否则仅使用数据库初始化时开发环境和Jar运行message值不一致
 * 
 * @author 00fly
 * @version [版本号, 2023年11月3日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Data
@Lazy
@Component
@ConfigurationProperties(prefix = "welcome")
public class Welcome
{
    /**
     * message赋值方式:<br>
     * 1. Configuration注解在SysDataBaseConfig<br>
     * 2. spring.profiles.active指定dev即application-dev.yml<br>
     * 3. welcome.properties内容变更时触发<br>
     * 4. /show/refresh接口被调用时触发<br>
     * 方式1、2有竞争,不能严格区分先后
     */
    private String message = "hello, 00fly in java!";
}
//goto srcmainjavacomfly
efreshjobSimpleJob.java
package com.fly.refresh.job;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.fly.refresh.entity.Person;
import com.fly.refresh.entity.Welcome;

import lombok.extern.slf4j.Slf4j;

/**
 * 
 * SimpleJob
 * 
 * @author 00fly
 * @version [版本号, 2022年11月30日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Slf4j
@Component
public class SimpleJob
{
    @Autowired
    private Person person;
    
    @Autowired
    private Welcome welcome;
    
    /**
     * 不能实时刷新
     */
    @Value("#{welcome.message}")
    private String message;
    
    @Scheduled(cron = "*/10 * * * * ?")
    public void run()
    {
        log.info("---- autoRefresh: {} | fixed: {}", welcome.getMessage(), message);
        log.info("**** {}, {}", welcome, person);
    }
}
//goto srcmainjavacomfly
efreshResourceReloadConfig.java
package com.fly.refresh;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.ResourceBundle;
import java.util.concurrent.ScheduledThreadPoolExecutor;

import javax.annotation.PostConstruct;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.math.RandomUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
 * 配置文件实时刷新
 * 
 * @author 00fly
 * @version [版本号, 2017年4月25日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Slf4j
@Component
@ConditionalOnNotWebApplication
public class ResourceReloadConfig implements SchedulingConfigurer
{
    PropertiesConfiguration jobConfig;
    
    Resource cron = new ClassPathResource("test/cron.properties");
    
    ResourceBundle job = ResourceBundle.getBundle("test/job");
    
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar)
    {
        // 配置公共Schedule线程池
        taskRegistrar.setScheduler(new ScheduledThreadPoolExecutor(8, new CustomizableThreadFactory("schedule-pool-")));
        
        // 配置TriggerTask
        taskRegistrar.addTriggerTask(new Runnable()
        {
            @Override
            public void run()
            {
                // 任务逻辑
                log.info(" {} run ", getClass().getName());
            }
        }, new Trigger()
        {
            @Override
            public Date nextExecutionTime(TriggerContext triggerContext)
            {
                String cron = readCronText();
                return new CronTrigger(cron).nextExecutionTime(triggerContext);
            }
        });
    }
    
    /**
     * 初始化
     */
    @PostConstruct
    public void init()
    {
        try
        {
            jobConfig = new PropertiesConfiguration("test/job.properties");
            FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
            strategy.setRefreshDelay(60000L);// 刷新周期1分钟
            jobConfig.setReloadingStrategy(strategy);
        }
        catch (ConfigurationException e)
        {
            log.error(e.getMessage(), e.getCause());
        }
    }
    
    /**
     * 3种方式读取CronText
     * 
     * @return
     */
    private String readCronText()
    {
        String cronText = "*/10 * * * * ?";
        Integer key = RandomUtils.nextInt(3);
        switch (key)
        {
            case 0:
                cronText = jobConfig.getString("schedule.myjob.cron");
                break;
            
            case 1:
                try
                {
                    cronText = IOUtils.toString(cron.getURL(), StandardCharsets.UTF_8);
                }
                catch (IOException e)
                {
                    log.error(e.getMessage(), e.getCause());
                }
                break;
            
            case 2:
                ResourceBundle.clearCache();
                cronText = job.getString("schedule.myjob.cron");
                break;
            
            default:
                break;
        }
        log.info("**** key: {} ==> {}", key, cronText);
        return cronText;
    }
}
//goto srcmainjavacomfly
efreshwebShowController.java
package com.fly.refresh.web;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.fly.core.JsonResult;
import com.fly.refresh.back.ReloadByDataBase;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;

@RestController
@Api(tags = "演示接口")
@RequestMapping("/show")
public class ShowController
{
    @Autowired
    ReloadByDataBase reloadByDataBase;
    
    @ApiOperation("刷新欢迎语")
    @PostMapping("/refresh")
    @ApiImplicitParam(name = "message", value = "欢迎语", example = "热烈欢迎活捉洪真英,生擒李知恩! ", required = true)
    public JsonResult<?> refresh(String message)
    {
        if (StringUtils.isBlank(message))
        {
            return JsonResult.error("message不能为空");
        }
        boolean success = reloadByDataBase.update(message) > 0;
        return success ? JsonResult.success(message) : JsonResult.error("刷新欢迎语失败");
    }
}
//goto srcmain
esourcesapplication-dev.yml
person: 
  name: qinjiang
  age: 18
  happy: false
  birth: 2000-01-01
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - girl
    - music
  dog:
    name: 旺财
    age: 1

welcome:
  message: Hello 00fly in application-dev.yml
//goto srcmain
esourcesapplication-prod.yml
//goto srcmain
esourcesapplication-test.yml
//goto srcmain
esourcesapplication.yml
server:
  port: 8080
  servlet:
    context-path: /
    session:
      timeout: 1800
spring:
  datasource:
    url: ${druid.url}
    username: ${druid.username}
    password: ${druid.password}
    driverClassName: ${druid.driverClassName}
    type: com.alibaba.druid.pool.DruidDataSource
    sqlScriptEncoding: utf-8
    schema: classpath:sql/schema.sql
    continue-on-error: true
    druid:
      initial-size: 5                                       # 初始化大小
      min-idle: 10                                          # 最小连接数
      max-active: 20                                        # 最大连接数
      max-wait: 60000                                       # 获取连接时的最大等待时间
      min-evictable-idle-time-millis: 300000                # 一个连接在池中最小生存的时间,单位是毫秒
      time-between-eviction-runs-millis: 60000              # 多久才进行一次检测需要关闭的空闲连接,单位是毫秒
      validation-query: SELECT 1                            # 检测连接是否有效的 SQL语句,为空时以下三个配置均无效
      test-on-borrow: true                                  # 申请连接时执行validationQuery检测连接是否有效,默认true,开启后会降低性能
      test-on-return: true                                  # 归还连接时执行validationQuery检测连接是否有效,默认false,开启后会降低性能
      test-while-idle: true                                 # 申请连接时如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效,默认false,建议开启,不影响性能
  devtools:
    restart:
      exclude: application-dev.yml,welcome.properties
  h2:
    console:
      enabled: true
      path: /h2-console
      settings:
        web-allow-others: true
  profiles:
    active:
    - dev
//goto srcmain
esourcesjdbc-h2.properties
druid.username=sa
druid.password=
druid.url=jdbc:h2:mem:reload;database_to_upper=false
druid.driverClassName=org.h2.Driver
//goto srcmain
esourcessqlschema.sql
CREATE TABLE IF NOT EXISTS `sys_config` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `key` varchar(100),
  `value` varchar(200),
  `description` varchar(200),
  `status` varchar(20),
  `version` bigint,
  `creater` varchar(50),
  `create_time` datetime,
  `modifier` varchar(50),
  `modify_time` datetime,
  PRIMARY KEY (`id`)
);

INSERT INTO `sys_config` VALUES ('1', 'welcome.message',  CONCAT('hello from db, rand ' ,CAST(RAND()*65536 AS INT)), '系统提示语', '1', '0', 'admin', now(), 'admin', now());
//goto srcmain
esources	estcron.properties
*/5 * * * * ?
//goto srcmain
esources	estjob.properties
schedule.myjob.cron = */5 * * * * ?
//goto srcmain
esourceswelcome.properties
welcome.message = Hello 00fly in welcome.properties
//goto src	estjavacomfly
efreshconfig2ResourceReloadConfigTest.java
package com.fly.refresh.config2;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.ReloadingFileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Configurations;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.builder.fluent.PropertiesBuilderParameters;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.io.ClasspathLocationStrategy;
import org.apache.commons.configuration2.io.FileLocationStrategy;
import org.apache.commons.configuration2.reloading.PeriodicReloadingTrigger;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;

import lombok.extern.slf4j.Slf4j;

/**
 * Configuration2配置文件实时刷新 https://www.geek-share.com/detail/2727072209.html
 * 
 * @author 00fly
 * @version [版本号, 2017年4月25日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Slf4j
public class ResourceReloadConfigTest
{
    ReloadingFileBasedConfigurationBuilder<PropertiesConfiguration> builder;
    
    /**
     * 初始化
     */
    @Before
    public void init()
    {
        // 文件扫描策略
        // FileLocationStrategy strategy = new CombinedLocationStrategy(Arrays.asList(new ClasspathLocationStrategy(), new FileSystemLocationStrategy()));
        FileLocationStrategy strategy = new ClasspathLocationStrategy();
        PropertiesBuilderParameters propertiesBuilderParameters = new Parameters().properties()
            .setEncoding(StandardCharsets.UTF_8.name())
            .setPath(new ClassPathResource("job.properties").getPath())
            .setLocationStrategy(strategy)
            .setListDelimiterHandler(new DefaultListDelimiterHandler(','))
            .setReloadingRefreshDelay(2000L)
            .setThrowExceptionOnMissing(true);
        builder = new ReloadingFileBasedConfigurationBuilder<PropertiesConfiguration>(PropertiesConfiguration.class).configure(propertiesBuilderParameters);
        PeriodicReloadingTrigger trigger = new PeriodicReloadingTrigger(builder.getReloadingController(), null, 60, TimeUnit.SECONDS);
        trigger.start();
    }
    
    @Test
    public void read()
        throws ConfigurationException
    {
        // 直接读取
        Configurations configs = new Configurations();
        FileBasedConfigurationBuilder.setDefaultEncoding(PropertiesConfiguration.class, StandardCharsets.UTF_8.name());
        PropertiesConfiguration propConfig = configs.properties(new ClassPathResource("job.properties").getPath());
        log.info("propConfig:{}", propConfig.getString("schedule.myjob.cron"));
    }
    
    /**
     * https://cloud.tencent.com/developer/article/1600688
     * 
     * @throws ConfigurationException
     */
    @Test
    public void test()
        throws ConfigurationException
    {
        PropertiesConfiguration configuration = builder.getConfiguration();
        log.info("{}", configuration.getString("schedule.myjob.cron"));
    }
}
//goto src	estjavacomfly
efreshpropJavaPropsMapperTest.java
package com.fly.refresh.prop;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;

import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper;
import com.fly.core.utils.YamlUtils;
import com.fly.refresh.entity.Result;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class JavaPropsMapperTest
{
    /**
     * thread-safe
     */
    JavaPropsMapper javaPropsMapper = new JavaPropsMapper();
    
    /**
     * Properties to Bean
     * 
     * @throws IOException
     */
    @Test
    public void testPropToBean()
        throws IOException
    {
        Properties complex = PropertiesLoaderUtils.loadProperties(new ClassPathResource("prop/complex.properties"));
        
        // 多层结构转换成了嵌套Map
        Map<?, ?> map = javaPropsMapper.readPropertiesAs(complex, Map.class);
        log.info("***** PropToBean:{} => {}", complex, map);
        
        Result javaBean = javaPropsMapper.readPropertiesAs(complex, Result.class);
        log.info("***** PropToBean:{} => {}", complex, javaBean);
    }
    
    /**
     * Properties to Bean
     * 
     * @throws IOException
     */
    @Test
    public void testPropToBean2()
        throws IOException
    {
        String text = IOUtils.toString(new ClassPathResource("yaml/sample.yml").getURL(), StandardCharsets.UTF_8);
        Properties props = YamlUtils.yamlToProperties(text);
        props.keySet().forEach(key -> {
            log.info("{} => {}", key, props.get(key));
        });
        
        Result result = javaPropsMapper.readPropertiesAs(props, Result.class);
        log.info("***** PropToBean:{}", result);
    }
}
//goto src	estjavacomfly
efreshyamlSampleTest.java
package com.fly.refresh.yaml;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;

import org.apache.commons.io.IOUtils;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.yaml.snakeyaml.Yaml;

import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.fly.core.utils.YamlUtils;
import com.fly.refresh.entity.Result;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SampleTest
{
    static String yamlText;
    
    /**
     * thread-safe
     */
    YAMLMapper yamlMapper = new YAMLMapper();
    
    @BeforeClass
    public static void init()
    {
        try
        {
            yamlText = IOUtils.toString(new ClassPathResource("yaml/sample.yml").getURL(), StandardCharsets.UTF_8);
            log.info("yamlText => {}", yamlText);
        }
        catch (IOException e)
        {
            log.error(e.getMessage(), e.getCause());
        }
    }
    
    /**
     * 解析带prefix的yaml
     */
    @Test
    public void test()
        throws IOException
    {
        Result result = new Yaml().loadAs(yamlText, Result.class);
        log.info("snakeyaml  toJavaBean: {}", result);
        
        result = yamlMapper.readValue(yamlText, Result.class);
        log.info("yamlMapper toJavaBean: {}", result);
    }
    
    @Test
    public void test2()
        throws IOException
    {
        // TODO: yamlText截取person内容转换为Person对象
        Properties props = YamlUtils.yamlToProperties(yamlText);
        log.info("Properties: {}", props);
        
        Result result = new JavaPropsMapper().readPropertiesAs(props, Result.class);
        log.info("***** PropToBean:{}", result);
    }
}
//goto src	estjavacomfly
efreshyamlSnakeYamlTest.java
package com.fly.refresh.yaml;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;

import org.apache.commons.io.IOUtils;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.yaml.snakeyaml.Yaml;

import com.fly.core.utils.YamlUtils;
import com.fly.refresh.entity.Result;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SnakeYamlTest
{
    private static String text;
    
    @BeforeClass
    public static void init()
    {
        try
        {
            Resource resource = new ClassPathResource("yaml/complex.yml");
            text = IOUtils.toString(resource.getURL(), StandardCharsets.UTF_8);
            log.info("yamlText => {}", text);
        }
        catch (IOException e)
        {
            log.error(e.getMessage(), e.getCause());
        }
    }
    
    @Test
    public void test()
    {
        Yaml yaml = new Yaml();
        Result javaBean = yaml.loadAs(text, Result.class);
        log.info("***** toJavaBean => {}", javaBean);
    }
    
    /**
     * 注意区别
     */
    @Test
    public void testPk()
        throws IOException
    {
        Yaml yaml = new Yaml();
        Properties prop1 = yaml.loadAs(text, Properties.class);
        Properties prop2 = YamlUtils.yamlToProperties(text);
        log.info("** PK ** {} <=> {}", prop1, prop2);
        // {welcome={message=Hello 00fly in test2.yml}} <=> {welcome.message=Hello 00fly in test2.yml}
    }
}
//goto src	estjavacomfly
efreshyamlYAMLMapperTest.java
package com.fly.refresh.yaml;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.fly.refresh.entity.Result;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class YAMLMapperTest
{
    /**
     * thread-safe
     */
    YAMLMapper yamlMapper = new YAMLMapper();
    
    @Test
    public void test()
        throws IOException
    {
        Resource resource = new ClassPathResource("yaml/complex.yml");
        String text = IOUtils.toString(resource.getURL(), StandardCharsets.UTF_8);
        log.info("***** complex.yml yamlText => {}", text);
        
        Result javaBean = yamlMapper.readValue(text, Result.class);
        log.info("***** toJavaBean => {}", javaBean);
        
        // 报错com.fasterxml.jackson.databind.exc.MismatchedInputException
        // Properties prop = yamlMapper.readValue(text, Properties.class);
        // log.info("***** toJavaBean => {}", prop);
    }
}
//goto src	est
esourcesjob.properties
schedule.myjob.cron = */5 * * * * ?
//goto src	est
esourceslog4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="off" monitorInterval="0">
	<appenders>
		<console name="Console" target="system_out">
			<patternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %c - %msg%n" />
		</console>
	</appenders>
	<loggers>
		<root level="INFO">
			<appender-ref ref="Console" />
		</root>
	</loggers>
</configuration>
//goto src	est
esourcespropcomplex.properties
welcome.message=Hello 00fly in complex
//goto src	est
esourcesyamlcomplex.yml
welcome:
  message: Hello 00fly in test2.yml
//goto src	est
esourcesyamlsample.yml
spring:
  datasource:
    url: ${druid.url}
    username: ${druid.username}
    password: ${druid.password}
    driverClassName: ${druid.driverClassName}
    type: com.alibaba.druid.pool.DruidDataSource
    sqlScriptEncoding: utf-8
    schema: classpath:sql/schema.sql
    continue-on-error: true
    druid:
      initial-size: 5                                       # 初始化大小
      min-idle: 10                                          # 最小连接数
      max-active: 20                                        # 最大连接数
      max-wait: 60000                                       # 获取连接时的最大等待时间
      min-evictable-idle-time-millis: 300000                # 一个连接在池中最小生存的时间,单位是毫秒
      time-between-eviction-runs-millis: 60000              # 多久才进行一次检测需要关闭的空闲连接,单位是毫秒
      validation-query: SELECT 1                            # 检测连接是否有效的 SQL语句,为空时以下三个配置均无效
      test-on-borrow: true                                  # 申请连接时执行validationQuery检测连接是否有效,默认true,开启后会降低性能
      test-on-return: true                                  # 归还连接时执行validationQuery检测连接是否有效,默认false,开启后会降低性能
      test-while-idle: true                                 # 申请连接时如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效,默认false,建议开启,不影响性能
  devtools:
    restart:
      exclude: application-dev.yml,welcome.properties

person: 
  name: qinjiang
  age: 18
  happy: false
  birth: 2000-01-01
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - girl
    - music
  dog:
    name: 旺财
    age: 1

3. 运行说明

  1. 系统启动后从内存数据库h2表sys_config加载配置,从application-dev.yml加载配置
  2. 修改application-dev.yml、welcome.properties可以看到配置被动态刷新
  3. /show/refresh 接口调用刷新配置
  4. 如需要测试数据库配置,请在application.yml设置spring.profiles.active为test或者prod

有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!

-over-