轻松实现一个简单的外部化动态配置-干掉配置文件!

前言

本文主要讲解 手动实现外部化配置 的方法,目前未在生产环境进行验证,请谨慎使用,自己可以先在测试环境玩玩

为了干掉配置文件而生!

一、是什么

外部化配置:从字面意思来讲就是把项目中的配置进行外部化(放入项目之外的其他地方) 这样的话我们的配置就可以进行灵活的变动了

二、为什么

如果项目到了生产环境,可能有某个配置需要进行变动,根据原始方法的话你就要在配置文件中更改配置然后进行重新发布。这样无疑会影响我们的效率,而且也相当的麻烦

其实目前市面上已经有了很多很成熟的外部化配置框架,像:SpringCloud Config 、Nacos、Apollo 等... 但是会发现一个问题:他们都不适合在SpringBoot单体式应用中使用,他们都是为了SpringCloud 分布式应用进行开发的,所以我们的单体式项目不需要使用这些框架

所以我们今天就手动写实现一个基于MongoDB,(Redis也行) 的外部化配置功能。

三、开始

准备工作

名称 作用
SpringBoot 底层核心框架
MongoDB 存放我们的配置
Hutool 工具类库-方便开发

1.1 创建项目文件夹

创建一个名为 config-demo 的SpringBoot工程 使用IDEA创建 创建过程略过...

1.2 添加依赖

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.yufire</groupId>
    <artifactId>config-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <url>https://yufire.cn</url>
    <name>config-demo</name>
    <description>config-demo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <licenses>
        <license>
            <name>Apache 2.0</name>
            <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
        </license>
    </licenses>

    <developers>
        <developer>
            <id>[email protected]</id>
            <name>Yufire</name>
        </developer>
    </developers>


    <dependencies>
        <!--SpringBoot Web的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--MongoDB的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

        <!--Hutool工具类库-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.5.9</version>
        </dependency>

        <!--FastJSON-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!--SpringBoot多环境配置-->
    <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <profilesActive>dev</profilesActive>
            </properties>
            <!--默认激活DEV环境-->
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>pre</id>
            <properties>
                <profilesActive>pre</profilesActive>
            </properties>
        </profile>
        <profile>
            <id>pro</id>
            <properties>
                <profilesActive>pro</profilesActive>
            </properties>
        </profile>
    </profiles>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

复制代码

1.3 创建核心配置类

我们配置的核心就是这个配置类,这个配置类是全局唯一的,所以我们可以在项目中的任意一个地方都可以访问到内部的属性,也就是我们的配置

我们的这个类使用了懒汉式设计模式来保证类的全局唯一性

我们分别设置了四个属性:String、Integer、Boolean、Double 分别用来测试四种数据类型的赋值情况

注意: 配置类中的名称要和Properties配置文件中的名称保持一致!!!

类名:AppConfiguration

package cn.yufire.config.core;


import lombok.Data;

/**
 * @author Yufire
 * @date 2021/3/17 16:32
 * @description 全局唯一核心配置类
 */
@Data
public class AppConfiguration {

    /**
     * 用户名
     */
    private String userName;
    /**
     * 用户年龄
     */
    private Integer userAge;
    /**
     * 是否喜欢吃鸡蛋
     */
    private Boolean userIsEatEgg;
    /**
     * 分数
     */
    private Double score;


    /**
     * 全局唯一对象
     * 第二层锁,volatile关键字禁止指令重排
     */
    private volatile static AppConfiguration config = null;

    /**
     * 懒汉式
     */
    private AppConfiguration() {
    }

    public static AppConfiguration getInstance() {
        // 第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
        if (config == null) {
            // 第一层锁,保证只有一个线程进入
            // 双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
            // 当某一线程获得锁创建一个AppConfiguration对象时,即已有引用指向对象,config不为空,从而保证只会创建一个对象
            // 假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
            synchronized (AppConfiguration.class) {
                // 第二层检查
                if (config == null) {
                    // config = new AppConfiguration() 语句为非原子性,实际上会执行以下内容:
                    // (1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象
                    // volatile关键字可保证config = new AppConfiguration()语句执行顺序为123,
                    config = new AppConfiguration();
                }
            }
        }
        return config;
    }
}
复制代码

1.4 创建配置维护类

这个类的主要作用就是用来维护我们的核心配置类里的内容。这俩边分别包含了几个方法

refresh 这个方法用于刷新我们的配置

parsingProperties 这个方法用于解析我们的配置文件 (目前值实现了解析Properties格式的配置文件)

fillProperties 这个方法用于将我们解析好的数据填充到我们的核心配置类内

getProperties 这个方法用于将字符转换为 Properties 对象

类名:Refresh

package cn.yufire.config.core;

import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;

/**
 * @author Yufire
 * @date 2021/3/3 09:31
 * @description 刷新类
 */
@Slf4j
public class Refresh {

    /**
     * 刷新配置
     *
     * @param configurationStr 配置字符串
     * @param configName       配置名称
     */
    public static void refresh(String configurationStr, String configName) {
        System.out.printf("******** 正在同步:%s ********\n", configName);
        fillProperties(parsingProperties(configurationStr));
    }

    /**
     * 解析配置文件方法
     *
     * @param configurationStr 配置字符串 xxx.properties类型 格式要求严格
     */
    private static Map<String, Object> parsingProperties(String configurationStr) {
        try {
            Properties properties = getProperties(configurationStr);
            if (ObjectUtil.isNull(properties) || properties.size() <= 0) {
                log.error("配置文件读取失败!");
                return null;
            }
            // 存储解析后配置的Map  (key,val)
            Map<String, Object> propertiesMap = new HashMap<>(properties.size());
            // 2. 填充map
            for (String propertiesName : properties.stringPropertyNames()) {
                // 填充时的key名转小写(更方便匹配)
                propertiesMap.put(propertiesName.toLowerCase(), properties.getProperty(propertiesName));
            }
            return propertiesMap;
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("********同步失败********");
        }

        return null;
    }

    /**
     * 填充数据至配置类
     *
     * @param propertiesMap 配置集合
     */
    private static void fillProperties(Map<String, Object> propertiesMap) {
        if (CollUtil.isEmpty(propertiesMap)) {
            log.error("读取到的配置Map为空不加载配置!");
            return;
        }
        // 获取单例config对象
        AppConfiguration instance = AppConfiguration.getInstance();
        // 因为要调用该对象的setXXX方法 所以要获取该对象的所有方法列表
        Method[] methods = instance.getClass().getDeclaredMethods();
        // 循环每一个方法
        for (Method method : methods) {
            // 参数个数
            int parameterCount = method.getParameterCount();
            // set方法的参数只会有一个 添加方法形参数量为1的判断
            if (parameterCount == 1) {
                // 获取参数类型 用于执行setXXX方法是时的形参类型转换     // 只有一个参数 所有获取第0个下标
                Class<?> parameterType = method.getParameterTypes()[0];
                // 获取该字段的名称 Lombok生成的setXXX方法都是以驼峰的形式命名的 所以只需把set去掉剩下的就是该字段名 转小写和Map里的参数匹配上
                String filedName = method.getName().replace("set", "").toLowerCase();
                // 要给该字段赋的值
                Object val = propertiesMap.get(filedName);
                if (ObjectUtil.isNotNull(val)) {
                    // 类型转换 用于匹配不同类型的参数
                    if (parameterType.equals(String.class)) {
                        val = Convert.toStr(val);
                    } else if (parameterType.equals(Integer.class)) {
                        val = Convert.toInt(val);
                    } else if (parameterType.equals(Double.class)) {
                        val = Convert.toDouble(val);
                    } else if (parameterType.equals(Boolean.class)) {
                        val = Convert.toBool(val);
                    } else {
                        val = Convert.toStr(val);
                    }
                    // 执行setXXX方法
                    try {
                        method.invoke(instance, val);
                    } catch (Exception e) {
                        log.error("字段类型转换错误,或setXXX方法未匹配成功!");
                    }
                }
            }
        }
        System.out.println("******** 同步完成 ********");
    }


    /**
     * 获取Properties对象
     *
     * @param configurationStr 配置字符串
     * @return
     */
    private static Properties getProperties(String configurationStr) {
        if (StrUtil.isEmpty(configurationStr)) {
            log.error("配置文件为空不加载配置");
            return null;
        }
        // 1. 解析配置
        InputStream inputStream = new ByteArrayInputStream(configurationStr.getBytes());
        Properties properties = new Properties();
        try {
            // 使用 InputStreamReader 防止中文乱码
            properties.load(new InputStreamReader(inputStream));
        } catch (Exception e) {
            log.error("解析配置失败!");
            return null;
        }
        return properties;
    }

}
复制代码

1.5 配置文件添加配置

application.properties

都说了配置外部化,那么为什么还需要添加配置呢? 没有配置怎么连接MongoDB呢? 当然你也可以直接在JavaBean中定义

spring.data.mongodb.uri=mongodb://userName:passWord@host:port/collectionName
复制代码

1.6 创建刷新配置接口

接口名:ConfigService

package cn.yufire.config.service;

/**
 * @author Yufire
 * @date 2021/3/17 17:07
 * @description 配置服务接口
 */
public interface ConfigService {

    /**
     * 刷新配置
     */
    void refresh();


}
复制代码

实现类名:ConfigServiceImpl

package cn.yufire.config.service.impl;

import cn.yufire.config.core.Refresh;
import cn.yufire.config.mongo.ConfigPo;
import cn.yufire.config.service.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author Yufire
 * @date 2021/3/17 17:09
 * @description
 */
@Service
public class ConfigServiceImpl implements ConfigService {

    @Autowired
    private MongoTemplate mongoTemplate;

    /**
     * 刷新方法
     */
    @Override
    public void refresh() {
        // 从mongo中读取配置
        List<ConfigPo> configs = mongoTemplate.findAll(ConfigPo.class);
        // 因为我们mongo里目前就一个配置 所以就直接获取 configs下标0的数据了
        // 读取到的配置 真实情况下并不会这样写! 请根据情况而来
        ConfigPo configPo = configs.get(0);
        String config = configPo.getConfig();
        Refresh.refresh(config, "核心配置");
    }
}
复制代码

ConfigPo : MongoDB的实体类

package cn.yufire.config.mongo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

/**
 * @author Yufire
 * @date 2021/3/15 11:10
 * @description
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
@Document(collection = "Config_Test")
public class ConfigPo {

    @Id
    private String id;

    /**
     * 配置文件本体
     */
    @Field
    private String config;
}
复制代码

1.7 在MongoDB中添加数据

创建一个测试类 并在Mongo中添加一条测试的数据

package cn.yufire.config;


import cn.yufire.config.mongo.ConfigPo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.mongodb.core.MongoTemplate;

@SpringBootTest
class ConfigDemoApplicationTests {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Test
    void contextLoads() {

        // 一定要注意Properties的格式
        // 并且配置文件中的配置名称要与配置类中的配置名称保持一致
        String configStr = "userName=张三\n" +
                "userAge=18\n" +
                "userIsEatEgg=false\n" +
                "score=88.6";
        ConfigPo configPo = ConfigPo.builder().config(configStr).build();
        mongoTemplate.save(configPo);
    }
}
复制代码

去MongoDB中查看配置是否添加成功

Mongo确定配置

我是用的工具是 Navicat 在工具里看到我们的配置是没有换行的,其实是有换行的但是工具没有展示出来而已

1.8 创建开机启动类

这个类的作用就是为了让SpringBoot在启动的时候执行某些操作 可以设置为第一优先级

类名:AppRunner

package cn.yufire.config.core;

import cn.yufire.config.service.ConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

import javax.annotation.PostConstruct;

/**
 * @author Yufire
 * @date 2021/3/17 17:04
 * @description
 */
@Configuration
@Order(1)
@Slf4j
public class AppRunner {


    @Autowired
    private ConfigService configService;

    /**
     * 优先级最高的方法
     * 类在构造的时候执行的方法
     */
    @PostConstruct
    public void init() {
        // 调用我们的刷新方法
        configService.refresh();
    }
}
复制代码

1.8 创建测试类进行测试

package cn.yufire.config;


import cn.yufire.config.core.AppConfiguration;
import com.alibaba.fastjson.JSON;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ConfigDemoApplicationTests {
    @Test
    void testLoadConfig() {
        System.out.println(JSON.toJSONString(AppConfiguration.getInstance()));
    }
}

复制代码

输出

可以看到我们的数据已经加载成功了!

{"score":88.6,"userAge":18,"userIsEatEgg":false,"userName":"张三"}		
复制代码

其他的玩法请大家自行扩展,挖掘 ~

附录

一、流程图

配置读取流程图

二、完成的demo下载地址

阿里云OSS下载

三、SpringBoot多环境如何实现

方式一 :

创建多个SpringBoot配置文件 如:dev、pre、pro 每个配置文件里配置不同的MongoDB数据源、打包时使用不同的配置文件、从而可以读取不同的配置。

不知道这一块的同学请参考 SpringBoot多环境配置

方式二 :

在代码内获取当前打包环境、代码控制读取哪个配置

  • 实现方式:在pom.xml中添加配置
<!--SpringBoot多环境配置-->
    <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <profilesActive>dev</profilesActive>
            </properties>
            <!--默认激活DEV环境-->
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>pre</id>
            <properties>
                <profilesActive>pre</profilesActive>
            </properties>
        </profile>
        <profile>
            <id>pro</id>
            <properties>
                <profilesActive>pro</profilesActive>
            </properties>
        </profile>
    </profiles>
复制代码
  • 在application.properties中添加配置
spring.profiles.active=@profilesActive@
复制代码
  • 在项目中使用 @Value("${spring.profiles.active}") 即可获取这个值

完结撒花 By. Yufire

公众号: Yufire学习地

猜你喜欢

转载自juejin.im/post/7036907431238041614