转载

基于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 需要使用的依赖。 基于Mina的配置中心(五)

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

基于Mina的配置中心(五)

只有这两个依赖,剩下的是`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 配置东西,还有其他的。 放几张图看看 基于Mina的配置中心(五)

这里是 application.properties 里面的 基于Mina的配置中心(五)

这是 Java 相关的系统环境 基于Mina的配置中心(五)

这是系统的环境变量 基于Mina的配置中心(五)

所以叫 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 这个注解。所以都可以取到。

基于Mina的配置中心(五)

applicationContext.getBeansWithAnnotation 调用了这里

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

基于Mina的配置中心(五) 基于Mina的配置中心(五)

然后最关键的是 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配置类。

问题解决了吗?

:sob:没有

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

解决方法:

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

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

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

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

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

最后打包,发布Jar包。

如何使用

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

Server:

基于Mina的配置中心(五)

都有默认配置,只是自己测试的话,不需要写什么配置。

配一个端口吧: server.port=8080

初始化用户名和密码是为了以后增加用户登录预留的。

现在有两个接口用来新增和修改配置: 新增是不会触发消息的,因为新增没有客户端绑定信息。 基于Mina的配置中心(五)

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

基于Mina的配置中心(五)

写一个 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 配置,必须要配置项目名称,不配置会报错的。

基于Mina的配置中心(五) 基于Mina的配置中心(五)

测试本地环境 local

我配置的内容

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

数据库中的配置: 基于Mina的配置中心(五)

先启动 Server ,再启动 ClientDemo

可以看到,Client连接上了,并且服务器响应了客户端发送的消息。绑定了客户端连接。后面从数据库查询到配置,然后发送给了客户端。 基于Mina的配置中心(五)

5秒之后,收到心跳请求,并且响应。 基于Mina的配置中心(五)

客户端发送基本消息, 收到响应的基本消息。 基于Mina的配置中心(五)

客户端收到配置消息, Environment 中的值已经改变。 基于Mina的配置中心(五)

定时任务正常执行。 基于Mina的配置中心(五)

请求接口1,查看注入到对象中的值,已经改变。 基于Mina的配置中心(五)

请求接口2,直接从 Environment 中获取值,已经改变。 基于Mina的配置中心(五)

现在修改为灰度

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

可以正常取到配置的灰度的值 基于Mina的配置中心(五)

定时任务正常执行。 基于Mina的配置中心(五)

请求接口1,查看注入到对象中的值,已经改变。 基于Mina的配置中心(五)

请求接口2,直接从 Environment 中获取值,已经改变。 基于Mina的配置中心(五)

调接口修改配置 基于Mina的配置中心(五) 数据库: 基于Mina的配置中心(五)

修改成功,创建消息,发布事件,服务器发送消息成功。 基于Mina的配置中心(五)

客户端收到服务器消息,修改配置的值。 基于Mina的配置中心(五)

Server源码

Client源码

Client-Demo源码

总结一下

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

还有一个隐藏的功能。因为我的 configValueString ,你可以配置成JSON字符串,然后获取到配置再自己转为配置类,又是一个小技巧。 基于Mina的配置中心(五)

<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 容器中了,此时虽然修改了配置的值,但是容器中的对象是没有改变的,所以是无法生效的。

加油,继续努力!

基于Mina的配置中心(五)

本文使用 mdnice 排版

原文  https://juejin.im/post/5f074a29e51d4534911e0316
正文到此结束
Loading...