终于要开始编写客户端了。先处理一下		Server
端遗留的问题:依赖问题。	
由于在		mina-config
父项目的		pom.xml
中写了一些依赖,导致		mina-base
引用了很多依赖,比如		Swagger
:只是需要用一下注解;		Mybatis-Plus
:只是用一下		Model
类和几个注解;就要引一大堆包,太浪费了。	
所以我把		Message
这个类移到了		mina-server
中,然后在		mina-base
里面新建了一个类		MessageDO
,其实里面的属性和		Message
一模一样,只是少了一些注解和不继承		Model
类,这个类用来给		Client
使用,这个类只用了		lombok
的注解,再加上一些		mina-base
需要使用的依赖。		 
	
在		mina-client
中引用的时候,依赖树就很简单了。	
 
	只有这两个依赖,剩下的是`Mina`的依赖。
具体的修改可以去Github查看。
下面开始		Client
端,开始之前先提出几个问题:	
Server SpringBoot Environment
Server Server Environment Environment set
Mina
必须要先建立了一次连接之后,才能再自定义发送消息,有点像废话,第一次发送消息是在连接的时候,这时候是不知道有哪些配置需要从				Server
端获取的,昨天看到一个名字形容这个消息很贴切,可以叫:回声消息。			Server
主动拉取一次配置信息。			Jar
包的,供第三方引用的,如何保证别人引用后,里面				SpringBoot
配置相关的东西和定时任务还可以正常运行?			不用害怕,上面就是我们要一一解决的问题。
客户端我准备换种方式,其实写完		Server
端,		Mina
的东西就差不多了,我准备从		SpringBoot
的角度,按照解决上面问题的方法来讲一下客户端。如果要看源码的话,可以去		
			GitHub
		
查看。	
Environment
中的属性配置		
	如果想在启动时执行我们自己定义的方法,有以下四种方法
org.springframework.beans.factory.InitializingBean
接口,复写				afterPropertiesSet
方法。			org.springframework.boot.SpringApplicationRunListener context environment started
init-method
方法。			@PostConstruct
注解。			鉴于之前我写过 权限相关-SpringBoot 在启动时获取所有的请求路径url ,所以我们使用第二种。当然第二种也更符合规范,他监听的是SpringBoot的启动流程。
我们要在resources目录下建立一个文件夹		META-INF
,然后创建		spring.factories
文件(这个文件里有		SpringBoot
能够自动配置的秘密),配置启动时要执行的方法的类。我是创建了一个类		ConfStartCollectSendManager
来处理。	
所以配置是:
		org.springframework.boot.SpringApplicationRunListener=com.lww.mina.manager.ConfStartCollectSendManager
	
感觉越来越有模有样了。
不写不知道啊,原来		environment
里面不止有		application.properties
配置东西,还有其他的。
放几张图看看		 
	
这里是		application.properties
里面的		 
	
这是		Java
相关的系统环境		 
	
这是系统的环境变量		 
	
所以叫		Environment
是名副其实啊。	
这个问题有点麻烦,网上很多说法是用自定义注解。		Nacos
也是自定义了一个注解		@NacosValue
。		SpringBoot
的理念就是约定大于配置,既然如此,何不定义一个前缀呢?	
所以有了这个		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");
    }
}
复制代码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
这个注解。所以都可以取到。	
 
	
		applicationContext.getBeansWithAnnotation
调用了这里	
		org.springframework.beans.factory.support.DefaultListableBeanFactory#findMergedAnnotationOnBean
	
 
	 
	
然后最关键的是		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);
    }
}
复制代码主要做了两件事:
获取所有注入的地方,使用反射修改已经注入到对象中的值。
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);
}
复制代码问题解决。
客户端在启动时,需要向服务器发送一次消息,建立连接后才能发送自定义的消息,姑且称之为回声消息吧,不知道是不是很准确。
		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
,这条消息会在服务端单独处理。	
其实定时任务还是很简单的,使用		@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);
    }
}
复制代码
对		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:
 
	都有默认配置,只是自己测试的话,不需要写什么配置。
配一个端口吧:server.port=8080
		初始化用户名和密码是为了以后增加用户登录预留的。
现在有两个接口用来新增和修改配置:
新增是不会触发消息的,因为新增没有客户端绑定信息。		 
	
现在写一个测试项目吧,新建一个		client-demo
项目,添加依赖	
 
	
写一个		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
配置,必须要配置项目名称,不配置会报错的。	
 
	 
	我配置的内容
#端口号
server.port=8081
#项目名称
mina.client.project-name=ClientDemo
#要从配置中心拉取的配置,以 mina.config 开头
mina.config.name=data1
复制代码
数据库中的配置:		 
	
先启动		Server
,再启动		ClientDemo
	
可以看到,Client连接上了,并且服务器响应了客户端发送的消息。绑定了客户端连接。后面从数据库查询到配置,然后发送给了客户端。		 
	
5秒之后,收到心跳请求,并且响应。		 
	
客户端发送基本消息, 收到响应的基本消息。		 
	
客户端收到配置消息,		Environment
中的值已经改变。		 
	
定时任务正常执行。		 
	
请求接口1,查看注入到对象中的值,已经改变。		 
	
请求接口2,直接从		Environment
中获取值,已经改变。		 
	
#端口号
server.port=8081
#项目名称
mina.client.project-name=ClientDemo
#修改为灰度
mina.client.env=gray
#要从配置中心拉取的配置
mina.config.name=data1
复制代码
可以正常取到配置的灰度的值		 
	
定时任务正常执行。		 
	
请求接口1,查看注入到对象中的值,已经改变。		 
	
请求接口2,直接从		Environment
中获取值,已经改变。		 
	
调接口修改配置		 数据库:
数据库:		 
	
修改成功,创建消息,发布事件,服务器发送消息成功。		 
	
客户端收到服务器消息,修改配置的值。		 
	
Server源码
Client源码
Client-Demo源码
这个项目终于写完了。虽然遇到了很多问题,但是都解决了,整体上还不错。使用非常简单,
只要引入下面的依赖,配置好服务器地址和端口,只要是		mina.config.
开头的配置,都会自动从服务器获取。
真正的无侵入。而且天生支持多环境,只要配置好不同环境,会自动获取不同环境配置,
不用再写		application-dev.properties
,		application-gray.properties
,		application-online.properties
了,
代码一下子干净了很多。	
还有一个隐藏的功能。因为我的		configValue
是		String
,你可以配置成JSON字符串,然后获取到配置再自己转为配置类,又是一个小技巧。		 
	
<dependency>
    <groupId>com.lww</groupId>
    <artifactId>mina-client</artifactId>
    <version>1.0.0</version>
</dependency>
复制代码虽然说项目做完了,其实还有很多地方优化:
Server
端没有前端页面			Server
端没有用户登录管理			Server
端没有权限管理			Server
端没有把配置信息加密			
最后再说两句,不看注册中心的功能,只看配置管理这一块,是不是比		Nacos
简答好用?虽然还有很多不足的地方  ,不过大家可以共同来贡献一份力量。	
最后一篇,写了很多,本来想分几篇写的,最后想想还是一起发吧。
欢迎大家关注我的公众号,共同学习,一起进步。加油
本来这篇文章是最后一篇。可是发现了一些问题,无法配置数据库,因为数据库的配置注入还要早一点。要在		org.springframework.boot.SpringApplication#prepareContext
中执行,而started方法已经是启动完成了。后面有时间会继续修复这个,还有动态刷新数据库配置,这也是个麻烦的地方。因为数据库的配置是注入到dataSource对象中的,而对象已经保存到		SpringBoot
容器中了,此时虽然修改了配置的值,但是容器中的对象是没有改变的,所以是无法生效的。	
加油,继续努力!
 
	本文使用 mdnice 排版