简易但不简单的配置中心No.79

嘛小伙伴们都问我我是怎么抽那么多时间来看书的,其实说难也不难说简单其实也不简单,就是提高效率和挤时间嘛。你要相信在一天中,每个时间都有它自己应该待的位置,做好工作计划,提升工作效率,你会发现一天下来你会有稍微多个一两个小时的时间,不然就只是忙忙忙然之后到最后不知道自己在忙什么。

至于怎么看书,我看书的时间点大概就两个,一个是午饭后,第二个是睡觉前,都会看个一章或者两章,久而久之,你会发现你看的书比旁边吃鸡的同学看多了很多的书。当然呢,也别问我看什么书有用,我什么书都看。你看过的那些书,可能你会忘记,但会沉淀在你的骨头里,在你潜意识里。总有一天你会偶然看到一个东西,恍然大悟,咦这个小玩意我好像认识,虽然不知道在哪里见过但就是很眼熟。嗯。

接下来都是技术干货,非技术战斗人员请立刻左上角退出战场。

今天这个关于配置中心的小项目是早上起床抽空花了差不多两个小时写的~希望能帮大家理解理解配置中心实现的原理。

记得先启动ConfigurationCenter,再启动ConfigurationMiniServer,JDK用1.8,至于详细的内容嘛,容我细细道来。

原理就是这样,配置中心起一个 RPC 进程 ConfigurationCenterService ,用来提供注册的服务。服务器所有的配置项都从类的静态域里取,服务器本地起一个 RPC 进程 ConfigurationMiniService,用来接收来自配置中心的配置更新的 push ,取到之后替换掉静态域的值。那么下次配置项的使用方在使用的时候就能获取到新的值啦。

原理说完了,那我们看几个核心的东西。

首先定义了一个注解 Config ,这个注解的作用域是 FIELD 也就是每个类的属性。这个注解只有一个作用,就是把当前的属性标记为配置项识别出来而已,为什么要实现成注解呢?原因只有一个,就是对程序无入侵,如果想作为配置项,那就加上注解。如果某个值不想作为配置项,直接把注解去掉即可,装卸十分方便。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Config {
    public String desc() default "lazy";
}

这里我们定义了一个真正的配置类,也就是我们平时开发的时候使用到的类。所有定义为配置项的地方,用我们刚刚定义的 @Config 注解进行注册。我们需要使用配置项的时候,直接从这个类的静态域获取即可。最终呢,在配置变更的时候,服务器接收到配置变更的请求的时候会直接替换类里静态域的值。

public class Configuration {
    @Config(desc = "数字配置")
    public static Integer NUMBER_CONFIG  = 5;
    @Config(desc = "开关型配置")
    public static Boolean SWITCH_CONFIG = true;
}

ConfigurationCenterService 定义了三个行为。

第一个是 register 注册,给服务器注册自己的配置项用的。

第二个是 pushConfig 配置推送,给 client 或者 web 页面进行配置推送用的。第三个是 getAllConfig() ,给 client 或者 web 页面获取当前所有配置项用的。

public interface ConfigurationCenterService extends Remote {
    int register(ConfigDTO configDTO)  throws RemoteException;
    int pushConfig(ConfigDTO configDTO)  throws RemoteException;
    void getAllConfig() throws RemoteException;
}

ConfigurationMiniService 定义了三个行为。

第一个是 registerClass 注册,给本地的配置类注册到配置中心用的。

第二个是 changeConfig 配置变更,暴露给配置中心,配置中心有配置变更的请求就直接调用本地的 mini 服务器的ConfigurationMiniService 进行配置变更的才做。

第三个是 init() ,是一个普通的初始化方法。

public interface ConfigurationMiniService extends Remote {
    int changeConfig(ConfigDTO configDTO)  throws RemoteException;
    void registerClass(Class c) throws RemoteException;
    void init() throws RemoteException, AlreadyBoundException, MalformedURLException, NotBoundException;
}

ConfigurationMiniServer 是真正的本地 mini 服务器,首先定义了哪些类是配置类,这个我只是简单实现了,真正做的使用可以给类加一个注解,用对包进行扫描的形式发现配置类。然后实例化了一个本地 RPC 进程ConfigurationMiniService。接着把所有的类一个一个使用本地的 RPC 进程进行注册。

Set<Class> classesToRegister = new HashSet<>();
classesToRegister.add(Configuration.class);
ConfigurationMiniService service = new ConfigurationMiniServiceImpl("127.0.0.1","8000");
service.init();

for(Class currentClass : classesToRegister){
    service.registerClass(currentClass);
}

那么是怎么注册的呢?其实也不难。先在服务本地记录一下配置类,准备开始注册。获得目标类的所有的 Field,然后判断这个 Field 是不是有 @Config 注解,如果有,那么获得当前的类名,属性,值,描述,服务器信息等,调用配置中心的 ConfigurationCenterService 进行注册。

public void registerClass(Class currentClass) throws RemoteException {
    this.configClasses.put(currentClass.getName(),currentClass);
    Field[] fields = currentClass.getDeclaredFields();
    for(Field field : fields){
        field.setAccessible(true);
        if(field.isAnnotationPresent(Config.class)){
            ConfigDTO configDTO = new ConfigDTO();
            configDTO.setServer(serverUri);
            configDTO.setClassName(currentClass.getName());
            configDTO.setFiled(field.getName());
            configDTO.setDesc(field.getAnnotation(Config.class).desc());
            try {
                configDTO.setValueType(field.getType().getName());
                Object value = field.get(null);
                configDTO.setValue(String.valueOf(value));

            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
           configurationCenterService.register(configDTO);

        }
    }
}

数据结构长这样,因为 RPC 要经过网络传输,所以一定要实现序列化。

public class ConfigDTO implements Serializable{
    private String server;
    private String className;
    private String filed;
    private String desc;
    private String valueType;
    private String value;
}

ConfigurationCenterService 配置中心接收到消息之后呢,就在本地记录一下,顺便把目标 mini 服务器的 RPC 调用进行初始化。

@Override
public int register(ConfigDTO configDTO) throws RemoteException {
    configs.add(configDTO);
    getOrCreateBundle(configDTO.getServer());
    Logger.log(configDTO);
    return 200;
}

到这里,一个配置项的注册就算完成了,那么如何进行配置变更呢?下面这些代码很长,但是目的只有一个,就是封装出目标服务器,目标类,目标 Field ,要变更的值,以及值的类型,然后 push 给 mini服务器就好了。

Scanner scanner = new Scanner(System.in);
System.out.println("input\n" +
        "get //to get all configs \n" +
        "push 127.0.0.1:8000/cfg_miniserver config.Configuration NUMBER_CONFIG 6 java.lang.Integer \r\n");
while (scanner.hasNext()){
    String command = scanner.nextLine();
    String[] commanArray = command.split(" ");

    String cmd = commanArray[0];

    if(cmd.equals("get")){
        service.getAllConfig();
    }
    else if(cmd.equals("push")){
        String server = commanArray[1];
        String className = commanArray[2];
        String field = commanArray[3];
        String value = commanArray[4];
        String valueType = commanArray[5];

        ConfigDTO configDTO = new ConfigDTO();
        configDTO.setFiled(field);
        configDTO.setClassName(className);
        configDTO.setServer(server);
        configDTO.setValue(value);
        configDTO.setValueType(valueType);

        int result = service.pushConfig(configDTO);
        if(result == 200){
            Logger.log("[success]推送配置 "+field+" ,值为 "+value+" 到服务器"+server+" 成功");
        }else{
            Logger.log("[error]推送配置 "+field+" ,值为 "+value+" 到服务器"+server+" 失败");
        }
    }
}

喏,简单的直接 push 给 mini 服务器。

@Override
public int pushConfig(ConfigDTO configDTO) throws RemoteException {
    ConfigurationMiniService currentService =  getOrCreateBundle(configDTO.getServer());

    return currentService.changeConfig(configDTO);
}

当 mini 服务器接收到来自配置中心的请求的是时候,会进行本地值的替换,我们在传输的时候都是序列化的字符串,所以要转一下。原理也很简单,就是利用反射识别出目标类的目标 Field,将值变更为新的值。

@Override
public int changeConfig(ConfigDTO configDTO) throws RemoteException {
    Class targetClass = this.configClasses.get(configDTO.getClassName());
    if(targetClass == null){
        return 500;
    }

    try {
        Field field = targetClass.getField(configDTO.getFiled());
        field.setAccessible(true);
        switch (configDTO.getValueType()){

            case "java.lang.Integer":
            field.set(null,Integer.valueOf(configDTO.getValue()));
                break;

            case "java.lang.Boolean":
                field.set(null,Boolean.valueOf(configDTO.getValue()));
                break;

        }

    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }


    return 200;
}

有人说我怎么知道它变更了呢?喇喇喇。早就给你想好了,每3秒输出一次当前的值,这样子值一变更就可以肉眼看到了。当然实际在使用的时候基本可以实现配置中心推完,就实时更新,这个要看网络延迟了。

Runnable check = new Runnable() {
    @Override
    public void run() {
        Logger.log(Configuration.NUMBER_CONFIG);
        Logger.log(Configuration.SWITCH_CONFIG);
    }
};

ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleAtFixedRate(check,0,3, TimeUnit.SECONDS);

看,注册成功会显示这个,给配置中心发送请求 get 也可以获取到。

我们试试看 push 一下值,如果成功会看到推送成功。

也能看到值会从 5 变更我们推过去的 6 了。

好啦,今天的配置中心就讲到这里,大家有什么想法都可以留言,也欢迎大家跟我一起边玩代码边学习。代码我已经放到 github上了,

github 的地址: https://github.com/CallMeDJ/BananaConfigurationCenter.git 喜欢的小伙伴可以下载下来自己玩玩。

原文发布于微信公众号 - 一名叫大蕉的程序员(DaBananaTalk)

原文发表时间:2018-01-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JMCui

Netty 系列五(单元测试).

    Netty 的单元测试,主要是对业务逻辑的 ChannelHandler 做测试(毕竟对 Bootstrap、EventLoop 这些做测试着实没有多大...

12530
来自专栏Java技术栈

Java 虚拟机对锁优化所做的努力

作为一款公用平台,JDK 本身也为并发程序的性能绞尽脑汁,在 JDK 内部也想尽一切办法提供并发时的系统吞吐量。这里,我将向大家简单介绍几种 JDK 内部的 "...

8120
来自专栏精讲JAVA

java面试线程必备知识点,怼死面试官,从我做起

内存屏障:限制命令操作顺序,有LoadLoad、LoadStore、LoadStore、StroreStreo四种屏障

13820
来自专栏Java面试通关手册

值得保存的 synchronized 关键字总结

本文是对 synchronized 关键字使用、底层原理、JDK1.6之后的底层优化以及和ReenTrantLock对比做的总结。如果没有学过 synchron...

6400
来自专栏Albert陈凯

Apache Avro是什么干什么用的(RPC/序列化)

Avro总结(RPC/序列化) Avro(读音类似于[ævrə])是Hadoop的一个子项目, 由Hadoop的创始人Doug Cutting(也是Lucene...

42840
来自专栏JavaEdge

LDAP认证模式简介1. 目录服务2. LDAP特点3. LDAP组织数据的方式4. 基本概念

目录是一个为查询、浏览和搜索而优化的专业分布式数据库,它呈树状结构组织数据,就好像Linux/Unix系统中的文件目录一样 目录数据库和关系数据库不同,它有优...

26430
来自专栏Java3y

Activiti就是这么简单

Activiti介绍 什么是Activiti? Activiti5是由Alfresco软件在2010年5月17日发布的业务流程管理(BPM)框架,它是覆盖了业务...

78980
来自专栏小灰灰

Spring定时任务高级使用篇

前面一篇博文 《Spring之定时任务基本使用篇》 介绍了Spring环境下,定时任务的简单使用姿势,也留了一些问题,这一篇则希望能针对这些问题给个答案

14220
来自专栏扎心了老铁

分布式锁的实现(redis)

1、单机锁 考虑在并发场景并且存在竞态的状况下,我们就要实现同步机制了,最简单的同步机制就是加锁。 加锁可以帮我们锁住资源,如内存中的变量,或者锁住临界区(线程...

42360
来自专栏mini188

聊聊从web session的共享到可扩展缓存设计

先从web session的共享说起 许多系统需要提供7*24小时服务,这类系统肯定需要考虑灾备问题,单台服务器如果宕机可能无法立马恢复使用,这必定影响到服务。...

22060

扫码关注云+社区

领取腾讯云代金券