外部化配置
前言
本文主要讲解 手动实现外部化配置 的方法,目前未在生产环境进行验证,请谨慎使用,自己可以先在测试环境玩玩
为了干掉配置文件而生!
一、是什么
外部化配置:从字面意思来讲就是把项目中的配置进行外部化(放入项目之外的其他地方) 这样的话我们的配置就可以进行灵活的变动了
二、为什么
如果项目到了生产环境,可能有某个配置需要进行变动,根据原始方法的话你就要在配置文件中更改配置然后进行重新发布。这样无疑会影响我们的效率,而且也相当的麻烦
其实目前市面上已经有了很多很成熟的外部化配置框架,像:SpringCloud Config 、Nacos、Apollo 等… 但是会发现一个问题:他们都不适合在SpringBoot单体式应用中使用,他们都是为了SpringCloud 分布式应用进行开发的,所以我们的单体式项目不需要使用这些框架
所以我们今天就手动写实现一个基于MongoDB,(Redis也行) 的外部化配置功能。
三、开始
准备工作
名称 | 作用 |
SpringBoot | 底层核心框架 |
MongoDB | 存放我们的配置 |
Hutool | 工具类库-方便开发 |
1.1 创建项目文件夹
创建一个名为 config-demo
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>yufirem@vip.qq.com</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
fillProperties
getProperties
类名: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中查看配置是否添加成功
我是用的工具是 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下载地址
三、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}") 即可获取这个值