首先要解决的问题是无法配置数据库的问题。
我们要知其然也要知其所以然。为什么无法配置数据库呢?
这就要说一下 SpringBoot
的启动流程了。
如果要说 SpringBoot
的启动流程,那就少不了这个方法 org.springframework.boot.SpringApplication#run(java.lang.String...)
最核心就是上面框的三个方法。
我们使用的是 org.springframework.boot.SpringApplicationRunListener=...
而这个 SpringBootRunListener
是在 org.springframework.boot.SpringApplicationRunListeners#started
这个方法里去调用的。
可见已经都要启动结束了。这个时候数据库配置信息没有的话,直接启动失败。
我把配置的注入放到 prepareContext
中,这时还没有创建 dataSource
对象,只是准备上下文。
在 spring.factories
里把 org.springframework.boot.SpringApplicationRunListener=...
改为 org.springframework.context.ApplicationContextInitializer=...
实现的接口也要改,改为 ApplicationContextInitializer<ConfigurableApplicationContext>
,然后改一下方法,下面有完整例子。
这样就会在 prepareContext
中执行。
经过测试还是不行,因为我们使用到了 SpringBoot
的事件发布与订阅。
而这个Listener是在下面的 refreshContext
里 org.springframework.context.support.AbstractApplicationContext#refresh
这里绑定的。
org.springframework.context.support.AbstractApplicationContext#registerListeners
所以会报一个错 ApplicationEventMulticaster not initialized
。
后来我修改为不使用事件发布与订阅,还是不行,因为这时,客户端是没有与服务端建立连接的,所以也就没有与服务端连接的 Session
,也就无法通信。仿佛进入了死胡同。
Nacos
查找解决办法
万幸的是,我们还有 Nacos
可以去学习一下,看她是如何解决的。
在启动时初始化
com.alibaba.boot.nacos.config.autoconfigure.NacosConfigApplicationContextInitializer#initialize
获取配置信息
com.alibaba.boot.nacos.config.autoconfigure.NacosConfigApplicationContextInitializer#reqGlobalNacosConfig
com.alibaba.nacos.spring.util.config.NacosConfigLoader#load(java.lang.String, java.lang.String, java.util.Properties)
com.alibaba.nacos.spring.util.NacosUtils#getContent
com.alibaba.nacos.client.config.NacosConfigService#getConfig
com.alibaba.nacos.client.config.NacosConfigService#getConfigInner
com.alibaba.nacos.client.config.impl.ClientWorker#getServerConfig
通过发送http请求,从服务端获取配置
com.alibaba.nacos.client.config.http.MetricsHttpAgent#httpGet
在项目启动时, Nacos
是通过 http
请求,从服务端获取配置,所以我在 Server
端增加了一个接口,客户端可以通过这个接口获取配置信息。
@ApiOperation("获取单个配置信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "projectName", value = "项目名称", required = true),
@ApiImplicitParam(name = "env", value = "环境", required = true),
@ApiImplicitParam(name = "propertyValue", value = "application.properties 配置的值", required = true),
})
@GetMapping(value = "/conf", name = "获取配置信息")
public Message conf(String projectName, String env, String propertyValue) {
Message message = messageService.getOne(new QueryWrapper<Message>().lambda()
.eq(Message::getProjectName, projectName)
.eq(Message::getEnvValue, env)
.eq(Message::getPropertyValue, propertyValue)
.eq(Message::getIsDeleted, 0));
Assert.isTrue(message != null, "查询结果为空!");
return message;
}
复制代码
然后在 Client
中调整了配置属性,增加了 http
端口。
/**
* 服务器监听端口,默认 9123 Mian监听端口
*/
private Integer minaPort = 9123;
/**
* http端口,默认8080
*/
private Integer port = 8080;
复制代码
删除了原来的 ConfStartCollectSendManager
,使用了 MinaInitializer
在启动时获取配置。
package com.lww.mina.init;
import com.alibaba.fastjson.JSONObject;
import com.lww.mina.dto.MessageDO;
import com.lww.mina.util.Const;
import com.lww.mina.util.HttpUtils;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.util.Assert;
/**
* 在 org.springframework.boot.SpringApplication#prepareContext 中执行,
* 在bean创建注入之前,从服务器获取配置信息,如数据库等配置信息
*
* @author lww
* @date 2020-07-11 16:50
*/
public class MinaInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final String PROPERTY_SOURCE_NAME = "applicationConfig";
private static final String ENV_KEY = "mina.client.env";
private static final String PROJECT_NAME = "mina.client.project-name";
private static final String PORT = "mina.client.port";
private static final String SERVER_ADDRESS = "mina.client.server-address";
public static Map<String, Object> configs = new ConcurrentHashMap<>(16);
@Override
public void initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment();
MutablePropertySources sources = environment.getPropertySources();
//遍历 Environment
for (Object property : sources) {
if (property instanceof MapPropertySource) {
MapPropertySource propertySource = (MapPropertySource) property;
//取到 applicationConfig 这个配置对象
if (propertySource.getName().contains(PROPERTY_SOURCE_NAME)) {
String[] properties = propertySource.getPropertyNames();
for (String s : properties) {
//如果是以 mina.config 开头的,保存到 configs map中
if (s.startsWith(Const.CONF)) {
configs.put(s, propertySource.getProperty(s));
}
}
}
}
}
final String env = StringUtils.isNotBlank(environment.getProperty(ENV_KEY)) ? environment.getProperty(ENV_KEY) : "local";
final String port = StringUtils.isNotBlank(environment.getProperty(PORT)) ? environment.getProperty(PORT) : "8080";
final String address = StringUtils.isNotBlank(environment.getProperty(SERVER_ADDRESS)) ? environment.getProperty(SERVER_ADDRESS) : "127.0.0.1";
final String projectName = environment.getProperty(PROJECT_NAME);
final String remoteAddr = address.trim() + ":" + port.trim();
//通过http请求获取配置,修改配置的值
for (Entry<String, Object> entry : configs.entrySet()) {
String value = entry.getValue().toString();
String param = "projectName=" + projectName + "&env=" + env + "&propertyValue=" + value;
String result = HttpUtils.sendGetHttp("http://" + remoteAddr + "/message/conf", param, null);
if (StringUtils.isNotBlank(result)) {
MessageDO messageDO = JSONObject.parseObject(result, MessageDO.class);
Properties props = new Properties();
props.put(entry.getKey(), messageDO.getConfigValue());
//修改 Environment 中的值,否则从 Environment 中获取,还是原来的值
environment.getPropertySources().addFirst(new PropertiesPropertySource(Const.CONF, props));
} else {
Assert.isTrue(false, "获取配置信息失败!");
}
}
}
}
复制代码
数据库中配置的信息
可以看到,数据库配置信息已经可以取到,并且注入到了Mybatis-Plus配置类中
当我在修改配置时,
服务端发出消息
客户端接收到消息,并且修改了值
但是数据库连接是没有改变的,查询还是可以正常查询。不过重启的时候会报错。 重启报错
因为,数据库连接属性已经注入了 dataSource
对象,这个对象保存在 SpringBoot
容器中,我们单纯的修改配置的值是无法影响 SpringBoot
容器的。
最近在思考,如何不重启就可以刷新数据源配置。最近看到一篇文章 配置热更新,不想重启,如何更新Bean的状态? 如果可以动态刷新数据源,又牵扯到一系列问题,比如事务,数据库连接,如何平滑切换,结论就是还是重启最好用 。
不过这也是一个问题,可以深入研究一下。
Server最新源码
Client最新源码
Client-Demo最新源码
欢迎大家关注我的公众号,共同学习,一起进步。加油
本文使用 mdnice 排版