专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

基于Mina的配置中心(五)

基于Mina的配置中心(五)

终于要开始编写客户端了。先处理一下Server端遗留的问题:依赖问题。

由于在mina-config父项目的pom.xml中写了一些依赖,导致mina-base引用了很多依赖,比如Swagger:只是需要用一下注解;Mybatis-Plus:只是用一下Model类和几个注解;就要引一大堆包,太浪费了。

所以我把Message这个类移到了mina-server中,然后在mina-base里面新建了一个类MessageDO,其实里面的属性和Message一模一样,只是少了一些注解和不继承Model类,这个类用来给Client使用,这个类只用了lombok的注解,再加上一些mina-base需要使用的依赖。 89_1.png

mina-client中引用的时候,依赖树就很简单了。

89_2.png 只有这两个依赖,剩下的是`Mina`的依赖。

具体的修改可以去Github查看。

下面开始Client端,开始之前先提出几个问题:

1、 在项目启动时,向 Server发送消息,从配置中心获取需要的配置。是如何取到那些需要从配置中心获取配置的属性呢?还有在启动时获取,要考虑 SpringBoot启动流程,要在 Environment初始化后才可以去获取这些配置。
2、 当在 Server端修改配置, Server端向客户端推送消息,客户端收到消息后,在运行状态下修改 Environment中的属性配置。可是 Environment是没有 set属性的方法的。
3、 开发过程中又会发现, Mina必须要先建立了一次连接之后,才能再自定义发送消息,有点像废话,第一次发送消息是在连接的时候,这时候是不知道有哪些配置需要从 Server端获取的,昨天看到一个名字形容这个消息很贴切,可以叫:回声消息。
4、 为了保证高可靠性,我还想要弄一个拉的模型,用一个定时任务,每30秒或者一分钟从 Server主动拉取一次配置信息。
5、 最终这个项目是要被打成 Jar包的,供第三方引用的,如何保证别人引用后,里面 SpringBoot配置相关的东西和定时任务还可以正常运行?

不用害怕,上面就是我们要一一解决的问题。

客户端我准备换种方式,其实写完Server端,Mina的东西就差不多了,我准备从SpringBoot的角度,按照解决上面问题的方法来讲一下客户端。如果要看源码的话,可以去GitHub查看。

1. 启动时获取Environment中的属性配置

如果想在启动时执行我们自己定义的方法,有以下四种方法

1、 实现 org.springframework.beans.factory.InitializingBean接口,复写 afterPropertiesSet方法。
2、 实现 org.springframework.boot.SpringApplicationRunListener接口,因为我们这个方法执行的时机是 context已经加载, environment也要加载并且属性值已经设置完毕,看这个接口里面的方法注释,貌似 started才适合。
3、 配置 init-method方法。
4、 使用 @PostConstruct注解。

鉴于之前我写过权限相关-SpringBoot 在启动时获取所有的请求路径url,所以我们使用第二种。当然第二种也更符合规范,他监听的是SpringBoot的启动流程。

我们要在resources目录下建立一个文件夹META-INF,然后创建spring.factories文件(这个文件里有SpringBoot能够自动配置的秘密),配置启动时要执行的方法的类。我是创建了一个类ConfStartCollectSendManager来处理。

所以配置是:

org.springframework.boot.SpringApplicationRunListener=com.lww.mina.manager.ConfStartCollectSendManager

感觉越来越有模有样了。

不写不知道啊,原来environment里面不止有application.properties配置东西,还有其他的。 放几张图看看 89_3.png

这里是application.properties里面的 89_4.png

这是Java相关的系统环境 89_5.png

这是系统的环境变量 89_6.png

所以叫Environment是名副其实啊。

2. 获取哪些是要从配置中心获取的

这个问题有点麻烦,网上很多说法是用自定义注解。Nacos也是自定义了一个注解@NacosValueSpringBoot的理念就是约定大于配置,既然如此,何不定义一个前缀呢?

所以有了这个mina.config,注入值还是使用@Value注解,原来怎么用还是怎么用(真正的无侵入啊), 如果是使用这个mina.config前缀配置的,都会认为是要从配置中心去拉取数据。

package com.lww.mina.manager;

import com.lww.mina.dto.MessageDO;
import com.lww.mina.event.ConfSendEvent;
import com.lww.mina.util.Const;
import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplicationRunListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; /** * @author lww * @date 2020-07-09 11:33 */ @Slf4j public class ConfStartCollectSendManager implements SpringApplicationRunListener { public ConfStartCollectSendManager(SpringApplication application, String[] args) { super(); } public static Map<String, Object> configs = new ConcurrentHashMap<>(16); 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"; @Override public void started(ConfigurableApplicationContext context) { ConfigurableEnvironment environment = context.getEnvironment(); MutablePropertySources propertySources = environment.getPropertySources(); //遍历 Environment for (Object property : propertySources) { 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)); } } } } } //发消息 for (Entry<String, Object> entry : configs.entrySet()) { MessageDO message = new MessageDO(); message.setProjectName(environment.getProperty(PROJECT_NAME)); message.setPropertyValue(entry.getValue().toString()); message.setEnvValue(StringUtils.isNotBlank(environment.getProperty(ENV_KEY)) ? environment.getProperty(ENV_KEY) : "local"); //发送消息 context.publishEvent(new ConfSendEvent(message)); } } } 

对,又用到了SpringBoot事件发布与订阅,可以看我之前的文章:SpringBoot事件发布与订阅

Listener这里就很简单了,组装消息然后发到Server就好了

@EventListener
public void onApplicationEvent(ConfSendEvent event) {
    MessageDO message = event.getMessage();
    log.info("ConfSendMessageListener_onApplicationEvent_message:{}", JSONObject.toJSONString(message));
    MessagePack pack = new MessagePack(Const.CONFIG_MANAGE, JSONObject.toJSONString(message));
 IoSession session = SessionManager.getSession(); if (session != null) { session.write(pack); } else { log.error("ConfSendMessageListener_onApplicationEvent_session is null"); } } 

3. 运行中修改Environment中的值

现在我们可以从application.properties中获取到需要从配置中心拉取的配置了,也发送了消息,问题是服务端响应了消息,我们怎么去修改Environment中的值呢?

首先还是使用事件,监听响应消息,我定义了一个Listener监听到接收消息事件。

  • com.lww.mina.listener.ConfChangeReceiveEventListener
@EventListener
public void onApplicationEvent(ConfChangeEvent event) throws Exception {
    log.info("接收到事件 ConfChangeReceiveEventListener_onApplicationEvent_event:{}", event.getClass());
    MessageDO message = event.getMessage();
    Map<String, Object> componentBeans = applicationContext.getBeansWithAnnotation(Component.class);
 changeValue(componentBeans, message); } 

这里为什么只获取被这个注解@Component标注的类呢? 因为applicationContext.getBeansWithAnnotation这个方法很强大,它不仅能获取当前类上的注解,还能获取注解上的注解,而在SpringBoot中,@Controller@Service@Repository@Configuration等等,这些注解都组合了@Component这个注解。所以都可以取到。

89_7.png

applicationContext.getBeansWithAnnotation调用了这里

org.springframework.beans.factory.support.DefaultListableBeanFactory#findMergedAnnotationOnBean

89_8.png 89_9.png

然后最关键的是changeValue

private void changeValue(Map<String, Object> beans, MessageDO message) throws IllegalAccessException {
    log.info("ConfChangeReceiveEventListener_changeValue_message:{}", JSONObject.toJSONString(message));
    //获取当前环境
    ConfigurableEnvironment environment = (ConfigurableEnvironment) applicationContext.getEnvironment();
    //循环bean
 for (Object value : beans.values()) { Class<?> clazz = value.getClass(); //获取所有字段 Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { //设置访问权限 field.setAccessible(true); //获取注解 Value value1 = field.getAnnotation(Value.class); if (value1 != null) { //获取注解的value String value2 = value1.value(); //去掉 ${} String replace = value2.replace(Const.PLACEHOLDER_PREFIX, "").replace(Const.PLACEHOLDER_SUFFIX, "").trim(); //是否是 mina.config 开头,mina.config.* 的字段是要从配置服务器获取的 if (replace.contains(CONF)) { String property = environment.getProperty(replace); //根据此值能从 environment 中取到 并且有配置 if (StringUtils.isNotBlank(property) && StringUtils.isNotBlank(message.getConfigValue())) { log.info("原始值 ConfChangeReceiveEventListener_changeValue_replace:{}, property:{}", replace, property); //反射修改已经注入到对象中的值 field.set(value, message.getConfigValue()); Properties props = new Properties(); props.put(replace, message.getConfigValue()); //修改 Environment 中的值,否则从 Environment 中获取,还是原来的值 environment.getPropertySources().addFirst(new PropertiesPropertySource(CONF, props)); } } } } } Map<String, Object> configs = ConfStartCollectSendManager.configs; for (Entry<String, Object> entry : configs.entrySet()) { String nowValue = environment.getProperty(entry.getKey()); log.info("Environment 中 ConfChangeReceiveEventListener_changeValue_propertity:{}, nowValue:{}", entry.getKey(), nowValue); } } 

主要做了两件事:

1、 获取所有注入的地方,使用反射修改已经注入到对象中的值。
2、 修改 Environment中的值,因为有时候用户可能不通过注入获取值,而是通过 context.getEnvironment().getProperty("mina.config.name")这个方法。

最后下面循环是可以不要的,主要是为了展示Environment中的值是否改变。

Map<String, Object> configs = ConfStartCollectSendManager.configs;
for (Entry<String, Object> entry : configs.entrySet()) {
    String nowValue = environment.getProperty(entry.getKey());
    log.info("Environment 中 ConfChangeReceiveEventListener_changeValue_propertity:{}, nowValue:{}", entry.getKey(), nowValue);
}

问题解决。

4. 客户端启动发送消息建立连接

客户端在启动时,需要向服务器发送一次消息,建立连接后才能发送自定义的消息,姑且称之为回声消息吧,不知道是不是很准确。

com.lww.mina.config.MinaClientConfig#ioConnector

/**
 * 开启mina的client服务,并设置对应的参数
 */
@Bean
public IoConnector ioConnector(DefaultIoFilterChainBuilder filterChainBuilder, InetSocketAddress inetSocketAddress) {
 Assert.isTrue(StringUtils.isNotBlank(config.getProjectName()), "项目名称不能为空!"); //1、创建客户端IoService 非阻塞的客户端 IoConnector connector = new NioSocketConnector(); //客户端链接超时时间 设置超时时间 connector.setConnectTimeoutMillis(config.getTimeout()); //2、客户端过滤器 设置编码解码器 connector.setFilterChainBuilder(filterChainBuilder); connector.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, config.getIdelTimeOut()); //第一次连接 在服务端校验这个值,不做处理,原样返回,在客户端为了绑定session MessageDO message = new MessageDO(); message.setProjectName(config.getProjectName()); message.setPropertyValue(Const.CONF); message.setEnvValue(config.getEnv()); MessagePack pack = new MessagePack(Const.BASE, JSONObject.toJSONString(message)); //设置handler 发送消息 connector.setHandler(new ConfigClientHandler(pack)); //连接服务端 connector.connect(inetSocketAddress); return connector; } 

这是客户端的Mina配置类,可以看出我们没有主动发送消息,只是建立连接,可是就会发出一条消息,我是建立了一个基本的消息,发送的内容就是mina.config,这条消息会在服务端单独处理。

5. 定时任务拉取配置信息

其实定时任务还是很简单的,使用@EnableScheduling注解,写一个job,每分钟去拉一次就好了,关键问题是,这个定时任务打包到Jar包中,如何还能运行呢?

@Slf4j
@Component
public class CheckAndPullJob {

    @Resource
 private ApplicationContext context; @Resource private MinaClientProperty config; @Scheduled(cron = "0 * * * * ?") public void checkAndPull() { long now = System.currentTimeMillis(); log.info("CheckAndPullJob_checkAndPull_start_time:{}", CommonUtil.getNowTimeString()); Map<String, Object> configs = ConfStartCollectSendManager.configs; for (Entry<String, Object> entry : configs.entrySet()) { log.info("发布事件 CheckAndPullJob_checkAndPull_entry:{}", entry.getValue().toString()); MessageDO message = new MessageDO(); message.setPropertyValue(entry.getValue().toString()); message.setProjectName(config.getProjectName()); message.setEnvValue(config.getEnv()); context.publishEvent(new ConfSendEvent(message)); } log.info("CheckAndPullJob_checkAndPull_end_time:{}", CommonUtil.getNowTimeString()); log.info("CheckAndPullJob_checkAndPull_耗时:{}", (System.currentTimeMillis() - now) / 1000); } } 

6. 打包为第三方Jar包

SpringBoot有过了解的人都知道,解决方案就是使用自动配置。

恭喜你,答对了!

/resources/META-INF/spring.factories这个文件中,添加一行 org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.lww.mina.config.MinaClientConfig开启自动配置。

MinaClientConfig这个类就是我们的Mina配置类。

问题解决了吗?
没有

为什么会这样? 我们只是把MinaClientConfig这个类加入了自动配置,其实就是把这个类注入到了SpringBoot的容器中,但是我们这个项目中有定时任务,有好几个用@Component注解修饰的类,它们是没有被注入到容器中的。

解决方法:

我们的目的很简单,就是把这个项目里使用@Component注解修饰的类,注入到SpringBoot容器中。

黑科技:@ComponentScan(basePackages = "com.lww.mina")

之前遇到过一次,一个多模块项目,SpringBoot无法扫描到一个子模块,加了这个注解就好了。这在Jar包中也是可以使用的。

以后我们在写第三方Jar包时,用到了SpringBoot相关的东西,只要使用这个注解就可以让SpringBoot也扫描到第三方Jar包。

还有@EnableScheduling这个注解也加到MinaClientConfig这个类上。

最后打包,发布Jar包。

如何使用

至此Server端和Client端都完成了,也都打包发布了,如何使用呢?

Server:

89_10.png

都有默认配置,只是自己测试的话,不需要写什么配置。
配一个端口吧:server.port=8080
初始化用户名和密码是为了以后增加用户登录预留的。

现在有两个接口用来新增和修改配置: 新增是不会触发消息的,因为新增没有客户端绑定信息。 89_11.png

现在写一个测试项目吧,新建一个client-demo项目,添加依赖

89_12.png

写一个Controller

public class ValueController {

    @Value("${mina.config.name}")
    private String name;

 @Resource private ApplicationContext context; @GetMapping(value = "/name1", name = "获取注入的配置") public HttpResult name1() { return HttpResult.success(name); } @GetMapping(value = "/name2", name = "直接从Environment获取配置") public HttpResult name2() { String property = context.getEnvironment().getProperty("mina.config.name"); return HttpResult.success(property); } } 

重点

重点是application.properties配置,必须要配置项目名称,不配置会报错的。

89_13.png 89_14.png

测试本地环境 local

我配置的内容

#端口号
server.port=8081
#项目名称
mina.client.project-name=ClientDemo
#要从配置中心拉取的配置,以 mina.config 开头
mina.config.name=data1 

数据库中的配置: 89_15.png

先启动Server,再启动ClientDemo

可以看到,Client连接上了,并且服务器响应了客户端发送的消息。绑定了客户端连接。后面从数据库查询到配置,然后发送给了客户端。 89_16.png

5秒之后,收到心跳请求,并且响应。 89_17.png

客户端发送基本消息, 收到响应的基本消息。 89_18.png

客户端收到配置消息,Environment中的值已经改变。 89_19.png

定时任务正常执行。 89_20.png

请求接口1,查看注入到对象中的值,已经改变。 89_21.png

请求接口2,直接从Environment中获取值,已经改变。 89_22.png

现在修改为灰度

#端口号
server.port=8081
#项目名称
mina.client.project-name=ClientDemo
#修改为灰度
mina.client.env=gray #要从配置中心拉取的配置 mina.config.name=data1 

可以正常取到配置的灰度的值 89_23.png

定时任务正常执行。 89_24.png

请求接口1,查看注入到对象中的值,已经改变。 89_25.png

请求接口2,直接从Environment中获取值,已经改变。 89_26.png

调接口修改配置 89_27.png 数据库: 89_28.png

修改成功,创建消息,发布事件,服务器发送消息成功。 89_29.png

客户端收到服务器消息,修改配置的值。 89_30.png

Server源码

Client源码

Client-Demo源码

总结一下

这个项目终于写完了。虽然遇到了很多问题,但是都解决了,整体上还不错。使用非常简单, 只要引入下面的依赖,配置好服务器地址和端口,只要是mina.config.开头的配置,都会自动从服务器获取。 真正的无侵入。而且天生支持多环境,只要配置好不同环境,会自动获取不同环境配置, 不用再写application-dev.propertiesapplication-gray.propertiesapplication-online.properties了, 代码一下子干净了很多。

还有一个隐藏的功能。因为我的configValueString,你可以配置成JSON字符串,然后获取到配置再自己转为配置类,又是一个小技巧。 89_31.png

<dependency>
    <groupId>com.lww</groupId>
    <artifactId>mina-client</artifactId>
    <version>1.0.0</version>
</dependency>

虽然说项目做完了,其实还有很多地方优化:

1、 Server端没有前端页面
2、 Server端没有用户登录管理
3、 Server端没有权限管理
4、 Server端没有把配置信息加密

最后再说两句,不看注册中心的功能,只看配置管理这一块,是不是比Nacos简答好用?虽然还有很多不足的地方,不过大家可以共同来贡献一份力量。

最后一篇,写了很多,本来想分几篇写的,最后想想还是一起发吧。

欢迎大家关注我的公众号,共同学习,一起进步。加油

本来这篇文章是最后一篇。可是发现了一些问题,无法配置数据库,因为数据库的配置注入还要早一点。要在org.springframework.boot.SpringApplication#prepareContext中执行,而started方法已经是启动完成了。后面有时间会继续修复这个,还有动态刷新数据库配置,这也是个麻烦的地方。因为数据库的配置是注入到dataSource对象中的,而对象已经保存到SpringBoot容器中了,此时虽然修改了配置的值,但是容器中的对象是没有改变的,所以是无法生效的。

加油,继续努力!

89_32.png

本文使用 tech.souyunku.com 排版

文章永久链接:https://tech.souyunku.com/33742

未经允许不得转载:搜云库技术团队 » 基于Mina的配置中心(五)

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们