基于Mina的配置中心(六)配置中心遗留问题
首先要解决的问题是无法配置数据库的问题。
我们要知其然也要知其所以然。为什么无法配置数据库呢?
这就要说一下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的状态? 如果可以动态刷新数据源,又牵扯到一系列问题,比如事务,数据库连接,如何平滑切换,结论就是还是重启最好用。
不过这也是一个问题,可以深入研究一下。
欢迎大家关注我的公众号,共同学习,一起进步。加油
本文使用 tech.souyunku.com 排版