springboot配置项动态刷新

文章目录

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

一,序言

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

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

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

二,准备工作

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

1. pom.xml引入组件

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

xml 复制代码
<dependency>
	<groupId>com.fasterxml.jackson.dataformat</groupId>
	<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

2. 配置文件示例

sample.yml

yaml 复制代码
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. 定义自定义配置项对象

java 复制代码
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;
}
java 复制代码
import lombok.Data;

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

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

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

java 复制代码
@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文件监听以及文件变化处理

java 复制代码
/**
 * 监听文件变化(推荐)
 */
@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节点数据。

java 复制代码
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

java 复制代码
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. 完整代码备份

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

java 复制代码
//goto docker\docker-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\restart.sh
#!/bin/bash
docker-compose down && docker system prune -f && docker-compose up -d && docker stats
//goto docker\stop.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 src\main\java\com\fly\BootApplication.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 src\main\java\com\fly\core\config\Knife4jConfig.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 src\main\java\com\fly\core\config\ScheduleThreadPoolConfig.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 src\main\java\com\fly\core\config\SysDataBaseConfig.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 src\main\java\com\fly\core\JsonResult.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 src\main\java\com\fly\core\utils\SpringContextUtils.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 src\main\java\com\fly\core\utils\YamlUtils.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 src\main\java\com\fly\refresh\back\ReloadByDataBase.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 src\main\java\com\fly\refresh\back\ReloadByFileAlterationMonitor.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 src\main\java\com\fly\refresh\back\ReloadByReloadingStrategy.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 src\main\java\com\fly\refresh\entity\Dog.java
package com.fly.refresh.entity;

import lombok.Data;

@Data
public class Dog
{
    private String name;
    
    private Integer age;
}
//goto src\main\java\com\fly\refresh\entity\Person.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 src\main\java\com\fly\refresh\entity\Result.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 src\main\java\com\fly\refresh\entity\Welcome.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 src\main\java\com\fly\refresh\job\SimpleJob.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 src\main\java\com\fly\refresh\ResourceReloadConfig.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 src\main\java\com\fly\refresh\web\ShowController.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 src\main\resources\application-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 src\main\resources\application-prod.yml
//goto src\main\resources\application-test.yml
//goto src\main\resources\application.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 src\main\resources\jdbc-h2.properties
druid.username=sa
druid.password=
druid.url=jdbc:h2:mem:reload;database_to_upper=false
druid.driverClassName=org.h2.Driver
//goto src\main\resources\sql\schema.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 src\main\resources\test\cron.properties
*/5 * * * * ?
//goto src\main\resources\test\job.properties
schedule.myjob.cron = */5 * * * * ?
//goto src\main\resources\welcome.properties
welcome.message = Hello 00fly in welcome.properties
//goto src\test\java\com\fly\refresh\config2\ResourceReloadConfigTest.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\test\java\com\fly\refresh\prop\JavaPropsMapperTest.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\test\java\com\fly\refresh\yaml\SampleTest.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\test\java\com\fly\refresh\yaml\SnakeYamlTest.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\test\java\com\fly\refresh\yaml\YAMLMapperTest.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\test\resources\job.properties
schedule.myjob.cron = */5 * * * * ?
//goto src\test\resources\log4j2.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\test\resources\prop\complex.properties
welcome.message=Hello 00fly in complex
//goto src\test\resources\yaml\complex.yml
welcome:
  message: Hello 00fly in test2.yml
//goto src\test\resources\yaml\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

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-

相关推荐
javaTodo7 分钟前
Claude Code 记忆机制详解:从 CLAUDE.md 到 Auto Memory,六层体系全拆解
后端
LSTM9728 分钟前
使用 C# 和 Spire.PDF 从 HTML 模板生成 PDF 的实用指南
后端
JaguarJack40 分钟前
为什么 PHP 闭包要加 static?
后端·php·服务端
BingoGo1 小时前
为什么 PHP 闭包要加 static?
后端
是糖糖啊1 小时前
OpenClaw 从零到一实战指南(飞书接入)
前端·人工智能·后端
百度Geek说1 小时前
基于Spark的配置化离线反作弊系统
后端
后端AI实验室2 小时前
用AI写代码,我差点把漏洞发上线:血泪总结的10个教训
java·ai
Java编程爱好者2 小时前
虚拟线程深度解析:轻量并发编程的未来趋势
后端
苏三说技术2 小时前
Spring AI 和 LangChain4j ,哪个更好?
后端