美文网首页
Spring Boot配置的分离与动态加载实践

Spring Boot配置的分离与动态加载实践

作者: 新签名 | 来源:发表于2020-01-17 13:38 被阅读0次

    需求

    • 配置分离: 项目中不同环境下往往配置不一样,将与环境相关的配置提取到war包/jar包外可以避免重复打包、配置文件管理混乱等问题。
    • 配置动态加载: 随着业务增长,部署的war包/jar包越来越大,重启服务也变得是一件很难的事情。生产环境下,修改配置文件能够不用重启服务就能生效,无疑是非常nice的操作。

    分析

    配置分离

    配置分离的方法有很多,从分离到的位置来看:可以分离到文件中、分离到数据库或reids中、分离中间服务管理下。

    我们当然希望有一个单独的服务帮我们管理这些配置,并且能提供一套完美的机制让第三方服务介入进来,这个需求演化成了配置中心

    但是一些old系统或者小型系统,没成本再去部署一个单独的服务管理配置。而且需求也简单:希望自己的服务仅仅能做到配置分离,分离到war包之外的文件即可。

    幸运的是日常用的Spring MVCSpring Boot支持这样的实现,我们这里只讲Spring Boot如何实现。

    一些常用配置中心的客户端也是基于Spring MVC或Spring Boot本身功能去实现的。

    配置动态加载

    一个优秀的配置中心当然也会提供配置动态加载的功能,如果没有配置中心,当然我们也可以手动让框架Spring MVCSrping Boot实现动态加载。

    解决

    配置分离

    Spring Boot有用一个非常特殊的PropertySource顺序,该顺序旨在允许合理地覆盖属性值。(使用优先级由高到低)

    • $HOME/.config/spring-bootdevtools处于活动状态时,文件夹中的Devtools全局设置属性。

    • @TestPropertySource 测试中的注释。

    • properties测试中的属性。可用于测试应用程序的特定部分@SpringBootTest的测试注释和注释。

    • 命令行参数。

    • 来自的属性SPRING_APPLICATION_JSON(嵌入在环境变量或系统属性中的嵌入式JSON)。

    • ServletConfig 初始化参数。

    • ServletContext 初始化参数。

    • JNDI属性java:comp/env

    • Java系统属性(System.getProperties())。

    • 操作系统环境变量。

    • jar外的application-{profile}.propertiesYAML

    • jar内的application-{profile}.propertiesYAML

    • jar外的application.propertiesYAML

    • jar内的application.propertiesYAML

    • @PropertySource修饰的@Configuration的类。

    • Spring默认属性值SpringApplication.setDefaultProperties

    如果是jar包部署,可以从命令行中指定配置文件:

    java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties
    

    如果是war包部署,可以使用ServletContext指定参数。

    约定好配置文件路径,然后写到启动代码中:

    @SpringBootApplication
    public class ApplicationStart extends SpringBootServletInitializer {
    
        /**
         * 配置文件路径, 以','分割的字符串. 配置采用覆盖式, 当有多个配置路径, 且包含相同配置属性时, 后者会覆盖前者. (windows环境下 /home/...以当前磁盘为根目录)
         */
        public final static String CONFIG_FILES_PATH = "classpath:application.yml,file:/etc/conf/myproject/application.yml";
    
        /**
         * main方法启动
         */
        public static void main(String[] args) {
            SpringApplication.run(ApplicationStart.class, "--spring.config.location=" + CONFIG_FILES_PATH);
        }
    
        /**
         * tomcat启动
         */
        @Override
        public void onStartup(ServletContext servletContext) throws ServletException {
            servletContext.setInitParameter("spring.config.location", CONFIG_FILES_PATH);
            super.onStartup(servletContext);
        }
    }
    

    配置动态加载

    配置文件(准确的说应该是上下文)的动态加载,是使用了Spring Cloud,以及它的@RefreshScope注解。

    @RefreshScope修饰的注解才会被动态刷新属性值。

    使用这样的方式去构建工程,确切的说你的工程已经从Spring Boot变成了Spring Cloud

    引入Spring Cloud依赖

    spring-cloud.version属性值

    <properties>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties> 
    

    spring cloud pom依赖

    这里使用了2.2.1版本的spring cloud依赖,要求Spring Boot版本 2.2.2+

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
        </dependency>
        </dependencies>
    </dependencyManagement>
    

    引入spring-cloud-context上下文

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-context</artifactId>
    </dependency>
    
    使用注解修饰要动态变化bean
    @Service
    @RefreshScope
    public class TestServiceImpl {
    
        @Value("${switch.option}")
        private String switchOption;
    }
    

    @Configuration
    public class TestConfiguration {
    
        @Bean
        @RefreshScope
        public Object getBean(){
            return new Object();
        }
    }
    
    如何触发
    1. 可以添加spring actuator依赖,通过手动调接口触发刷新:

    添加依赖

    <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
    </dependencies>
    

    手动触发

    curl -X POST http://localhost:8080/actuator/refresh
    
    1. 也可以定时监控配置文件,发现修改就触发:
    @Configuration
    @Slf4j
    public class ConfigRefreshConfigure implements InitializingBean {
        /**
         * refresher
         */
        @Autowired
        private ContextRefresher contextRefresher;
    
        @Override
        public void afterPropertiesSet() throws Exception {
            DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
            String[] configFilePaths = ApplicationStart.CONFIG_FILES_PATH.split(",");
            if (ArrayUtils.isNotEmpty(configFilePaths)) {
                ExecutorService executorService = Executors.newFixedThreadPool(1);
                executorService.submit(() -> {
                    try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
                        //absolute config file parent path
                        Set<String> hasRegisterDirs = new HashSet<>();
                        //absolute config path
                        Set<String> hasRegisterFiles = new HashSet<>();
    
                        //add config file parentDir to register
                        for (String configFilePath : configFilePaths) {
                            Resource configFileResource = defaultResourceLoader.getResource(configFilePath);
                            if (configFileResource.exists() && configFileResource.isFile()) {
                                hasRegisterFiles.add(configFileResource.getFile().getAbsolutePath());
                                File configFileDir = configFileResource.getFile().getParentFile();
                                if (!hasRegisterDirs.contains(configFileDir.getAbsolutePath())) {
                                    Paths.get(configFileDir.toURI())
                                            .register(watchService, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW);
                                    hasRegisterDirs.add(configFileDir.getAbsolutePath());
                                }
                            }
                        }
    
                        //watch config file change
                        while (true) {
                            WatchKey key = watchService.take();//block wait
                            boolean hasChange = false;
                            for (WatchEvent<?> pollEvent : key.pollEvents()) {
                                Path changed = (Path) pollEvent.context();
                                if (hasRegisterFiles.stream().anyMatch(s -> s.equals(((Path)key.watchable()).resolve(changed).toString()))) {
                                    hasChange = true;
                                    break;
                                }
                            }
                            if (hasChange) {
                                log.info("refresh properties ok! changes: " + JSON.toJSONString(contextRefresher.refresh()));
                            }
    
                            if (!key.reset()) {
                                log.info("some confFiles has been unregistered in refresh");
                            }
                        }
                    } catch (Exception e) {
                        log.error("refresh happen error : " + e.getMessage(), e);
                    }
                });
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:Spring Boot配置的分离与动态加载实践

          本文链接:https://www.haomeiwen.com/subject/wrmczctx.html