129

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

 6 years ago
source link: http://mp.weixin.qq.com/s/0oAPZcHgCDesqq6wmrSIBQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

Original 大蕉 一名叫大蕉的程序员 2018-01-05 12:17 Posted on

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

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

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



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

记得先启动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 也可以获取到。

Image

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

Image

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

Image

好啦,今天的配置中心就讲到这里,大家有什么想法都可以留言,也欢迎大家跟我一起边玩代码边学习。代码我已经放到 github上了,github 的地址发送"配置中心"获取喔,喜欢的小伙伴可以下载下来自己玩玩。

码了这么多字这么多代码,你不转发赞赏一下吗?一毛钱也是认可是不?

一毛钱也是认可

Image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK