SpringBoot运行时更改配置信息
前言
对于很多项目而言 都需要配置来进行一些服务的设定 如果每次更改配置文件并重启 那么有些太浪费时间 我们可不可以通过请求修改配置 并直接看到更改呢?
在SpringCloud中 我们可以使用各种配置中心来完成这件事情 但是对于大部分项目而言 我们都是在一个较小的体量下进行的 如果只有SpringBoot 在不引入配置中心的情况下 我们可以实现动态更新配置信息吗
答案是可以的 我们的实现方式将不依赖其他第三方库 而是只采用SpringBoot本身的特质以及数据库完成 来保证配置的实时性和持久性
准备
一个普通的SpringBoot项目即可
Maven依赖参考如下
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>im-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--springboot web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</project>
由于需要使用数据库 所以要引入MySql驱动以及MybatisPlus用来简化数据库操作
思路
对于大部分配置类而言 它实际上的作用就是提供一个类 用于保存配置信息的字段 所以我们只需要在需要修改的时候 将类的成员值以及数据库字段值行修改即可 至于数据库更改 由于是单条数据 性能开销几乎可以不计 同时为了适配多种配置 如(安全配置 连接配置等不同种类的 又有可能需要经常调整的配置)
我们要抽象出一个类用于进行流程固定操作 也就是所谓的模板方法模式
有了修改的思路 调用起来就很简单了 只需要在Controller中设置修改和查询的接口(前提是做好权限管理 小型项目可以使用AOP或者Spring Security框架)
编码
测试用数据库配置表
思路在上面已经分析过 那么现在不再废话 首先给出用于测试的数据库表
create table test_config
(
id int auto_increment
primary key,
value_one int null,
value_two double null,
value_str varchar(255) null
);
为什么需要设置id值呢 一是为了方便管理 二是为了不同的版本控制 如果之后需要多个版本的配置来回切换 则可以通过枚举id值来获取到对应的记录 需要哪个版本的 可以直接使用对应的id值来查询
基础配置抽象类
用来定义通用属性以及模板方法
package org.demo.im.config;
import com.baomidou.mybatisplus.annotation.TableField;
import org.demo.im.util.BeanUtil;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.concurrent.locks.ReentrantLock;
/**
* @description: 基础配置类
* @date: 2024/6/18 11:43
* @version: 1.0
*/
@Component
public abstract class BaseConfig {
@TableField(exist = false)
//标注此成员不是表字段
private ReentrantLock lock=new ReentrantLock();
public void setConfigValue(String key,Object value){
if (lock.isLocked()) {
return;
}
lock.lock();
Field field=null;
try{
field = BeanUtil.getFieldByName(this.getClass(),key);
if(field==null){
throw new IllegalArgumentException("配置类中不存在此配置项");
}
field.setAccessible(true);
final Class<?> type = field.getType();
final Object trueValue = convertToWrapper(value.toString(), type);
field.set(this,trueValue);
setConfigDBValue(this);
}catch (Exception e){
throw new IllegalArgumentException("不合法的配置");
}
finally {
if(field!=null) {
field.setAccessible(false);
}
lock.unlock();
}
}
//转换类型 因为配置中可能有其他类型
protected Object convertToWrapper(String value, Class<?> primitiveType) {
if (primitiveType == Integer.class) {
return Integer.valueOf(value);
} else if (primitiveType == Double.class) {
return Double.valueOf(value);
}else if(primitiveType==String.class){
return value;
}
// 其他类型可以添加到这里...
throw new IllegalArgumentException("不支持的数据类型: " + primitiveType);
}
protected abstract void setConfigDBValue(Object instance) throws RuntimeException;
}
这里的锁是为了防止同一个配置被并发修改 而 setConfigValue 是提供给外界的调用用来修改的方法 setConfigDBValue 是子类需要实现的更新数据库表的方法
而BeanUtil是自己定义的类 完成获取指定类的成员属性的操作
package org.demo.im.util;
import org.springframework.stereotype.Service;
import java.lang.reflect.Field;
/**
* @description: 操作bean的工具类
* @date: 2024/8/18 11:40
* @version: 1.0
*/
public class BeanUtil {
public static Field getFieldByName(Class<?> clazz,String name){
try {
return clazz.getDeclaredField(name);
} catch (NoSuchFieldException e) {
return null;
}
}
}
测试配置实现类
首先我们需要定义数据库表的结构
package org.demo.im.config;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.demo.im.config.po.TestConfigMapper;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.Serializable;
/**
* @description: 测试修改配置
* @date: 2024/8/17 23:29
* @version: 1.0
*/
@Component
@TableName(value ="test_config")
public class TestConfig extends BaseConfig implements Serializable {
private Integer id;
private Integer valueOne;
private Double valueTwo;
private String valueStr;
public Integer getValueOne() {
return valueOne;
}
public void setValueOne(Integer valueOne) {
this.valueOne = valueOne;
}
public Double getValueTwo() {
return valueTwo;
}
public void setValueTwo(Double valueTwo) {
this.valueTwo = valueTwo;
}
public String getValueStr() {
return valueStr;
}
public void setValueStr(String valueStr) {
this.valueStr = valueStr;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
TestConfig other = (TestConfig) that;
return (this.getValueOne() == null ? other.getValueOne() == null : this.getValueOne().equals(other.getValueOne()))
&& (this.getValueTwo() == null ? other.getValueTwo() == null : this.getValueTwo().equals(other.getValueTwo()))
&& (this.getValueStr() == null ? other.getValueStr() == null : this.getValueStr().equals(other.getValueStr()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getValueOne() == null) ? 0 : getValueOne().hashCode());
result = prime * result + ((getValueTwo() == null) ? 0 : getValueTwo().hashCode());
result = prime * result + ((getValueStr() == null) ? 0 : getValueStr().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", valueOne=").append(valueOne);
sb.append(", valueTwo=").append(valueTwo);
sb.append(", valueStr=").append(valueStr);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
然后我们需要定义MybatisPlus用于操作数据库的Mapper接口
package org.demo.im.config;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.demo.im.config.TestConfig;
/**
* @description: mapper层
* @date: 2024/8/20 13:03
* @version: 1.0
*/
@Mapper
public interface TestConfigMapper extends BaseMapper<TestConfig> {
}
然后完善我们的实现类
package org.demo.im.config;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.demo.im.config.po.TestConfigMapper;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.Serializable;
/**
* @description: 测试修改配置
* @date: 2024/8/17 23:29
* @version: 1.0
*/
@Component
@TableName(value ="test_config")
public class TestConfig extends BaseConfig implements Serializable {
private Integer id;
private Integer valueOne;
private Double valueTwo;
private String valueStr;
@Resource
@TableField(exist = false)
//非数据库字段
private TestConfigMapper testConfigMapper;
@Override
protected void setConfigDBValue(Object instance) throws RuntimeException {
//进行数据库读写操作
if(instance instanceof TestConfig) {
TestConfig config=(TestConfig)instance;
testConfigMapper.update(config,new LambdaUpdateWrapper<TestConfig>()
.set(!ObjectUtils.isEmpty(config.getValueOne()),TestConfig::getValueOne,config.getValueOne())
.set(!ObjectUtils.isEmpty(config.getValueTwo()),TestConfig::getValueTwo,config.getValueTwo())
.set(!ObjectUtils.isEmpty(config.getValueStr()),TestConfig::getValueStr,config.getValueStr()));
}else{
throw new RuntimeException("无法同步到数据库 请检查传递的配置数据格式");
}
}
@PostConstruct
//在启动时初始化
public void configInject(){
//可以根据ID选择或者设置版本
final TestConfig config = testConfigMapper.selectOne(new LambdaQueryWrapper<TestConfig>()
.eq(TestConfig::getId,1));
if(config==null){
throw new RuntimeException("重要配置加载失败: testConfig");
}
this.valueOne=config.getValueOne();
this.valueTwo=config.getValueTwo();
this.valueStr=config.getValueStr();
}
public Integer getValueOne() {
return valueOne;
}
public void setValueOne(Integer valueOne) {
this.valueOne = valueOne;
}
public Double getValueTwo() {
return valueTwo;
}
public void setValueTwo(Double valueTwo) {
this.valueTwo = valueTwo;
}
public String getValueStr() {
return valueStr;
}
public void setValueStr(String valueStr) {
this.valueStr = valueStr;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
TestConfig other = (TestConfig) that;
return (this.getValueOne() == null ? other.getValueOne() == null : this.getValueOne().equals(other.getValueOne()))
&& (this.getValueTwo() == null ? other.getValueTwo() == null : this.getValueTwo().equals(other.getValueTwo()))
&& (this.getValueStr() == null ? other.getValueStr() == null : this.getValueStr().equals(other.getValueStr()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getValueOne() == null) ? 0 : getValueOne().hashCode());
result = prime * result + ((getValueTwo() == null) ? 0 : getValueTwo().hashCode());
result = prime * result + ((getValueStr() == null) ? 0 : getValueStr().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", valueOne=").append(valueOne);
sb.append(", valueTwo=").append(valueTwo);
sb.append(", valueStr=").append(valueStr);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
定义Controller接收
为了验证配置是否对不同组件生效 我们将查询和更改放在两个Controller中
package org.demo.im.controller;
import org.demo.im.config.TestConfig;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @description: 配置检查控制器
* @date: 2024/8/18 0:12
* @version: 1.0
*/
@RestController
@RequestMapping("/fuys/config")
public class ConfigSelectController {
@Resource
private TestConfig testConfig;
@GetMapping("/select")
public String selectConfig(){
return testConfig.toString();
}
}
用于修改的Controller如下
package org.demo.im.controller;
import org.demo.im.config.TestConfig;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @description: 配置测试控制器
* @date: 2024/8/17 23:39
* @version: 1.0
*/
@RestController
@RequestMapping("/fuys/config")
public class ConfigTestController {
@Resource
private TestConfig testConfig;
@GetMapping("/update")
public String updateConfig(String key,String value){
System.out.println(value+" "+key);
testConfig.setConfigValue(key,value);
return "ok";
}
}
测试
在测试之前我们需要在数据库中为配置表赋值 这个就随便赋值即可
现在我们启动SpringBoot项目 并通过PostMan来进行测试
可以看到数据是一样的 有些不需要的可以在ToString方法中去掉 或是优化接口 通过key查询 这也需要反射 获取到Field之后利用get方法即可获取值 如果不明白 可以评论或私信 我会修改文章补充
现在我们尝试修改
发现返回ok之后 再次尝试查询
数据库中数据也更改了
最后
经过文章的操作之后 我们就可以完成对于SpringBoot进行动态更新配置 可以看到测试中 配置的更新既完成了即时更新 又做到了持久性 还可以进行简单的版本管理 并且只依赖数据库和SpringBoot本身的初始化拓展注解完成 依赖性弱
最后 如果觉得文章对你有帮助 点个赞和关注吧 我会持续更新后端的实用小知识 以及 前端遇到的问题还有新奇的样式~