• 一个可以代替冗长switch-case的消息分发小框架


    在项目中,我需要维护一个应用层的字节流协议。这个协议的每条报文都是一个字节数组,数组的头两个字节表示消息的传送方向,第三、四个字节表示消息ID,也就是消息种类,再往后是消息内容、时间戳、校验码等……整个消息看起来差不多长这样:

    Message Head Message ID Content  Timestamp Checksum
    2 bytes 2 bytes n bytes  8 bytes 1 byte

    Message ID指定了消息类型,根据不同的消息类型,对Content有不同的解析方法。在处理报文的类中,我们不得不用一个switch-case结构去处理不同类型的报文:

    1   switch(messageID){
    2           case 0x01: doSomething();  break;
    3           case 0x02: doAnotherThing(); break;
    4           case 0x03: doSomeOtherThing(); break;
    5           ...
    6 }

    在某些报文的消息内容中还存在一个子消息ID,这个子ID会定义具体要实现的操作分类。于是在switch-case中,我们还要再加入嵌套的switch-case去处理对应的子ID:

     1 switch(messageID){
     2   case 0x01: doSomething();  break;
     3   case 0x02: doAnotherThing(); break;
     4   case 0x03: 
     5     byte subID = getSubID();
     6     switch(subID)
     7     case 0x01: process1In3(); break;
     8     case 0x02: process2In3(); break;
     9     ...
    10     break;
    11   ...
    12 }

    由于需求一直在变化,我们自身的功能也在演变,协议中经常会增减消息,对同一个消息的处理也常常会变化,渐渐地这个switch-case达到了300多行,而我经常要从这300多行中找到对应的消息去修改它的处理代码,稍不注意就会出错,真是苦不堪言。

    一个大神同事提示我,可以用注解解决这个问题。定义一个Handler接口作为消息处理器,再定义一个注解去标示该处理器要处理的消息ID,然后提供一个Util类,利用反射机制读取注解,自动将不同的消息ID和对应的Handler加载到一个Map中,形成一个微型的消息分发处理框架。在这个框架的作用下,上面的switch-case语句变成了这样:

     1     //存放消息ID和对应的MessageHandler
     2     private Map<Byte, MessageHandler> handlersMap = null;
     3     void init() {
     4         //读取注解,将下列定义的HANDLER_1,2,3加载到handlersMap中
     5         handlersMap = MessageHandlerUtil.loadMessageHandlers(this);
     6     }
     7 
     8     //接收消息的回调函数
     9     public void onDataReceived(CommData command) {
    10         byte commandID = getCommandId(command);
    11         handlersMap.get(commandID).process(command);//调用对应的Handler处理
    12     }
    13 
    14     //消息ID为01的Handler
    15     @Handle(messageID=0x01)
    16     private final MessageHandler HANDLER_1 = (command) -> {    doSomething(); };
    17     //消息ID为02的Handler
    18     @Handle(messageID=0x02)
    19     private final MessageHandler HANDLER_2 = (command) -> { doAnotherThing(); };
    20     //消息ID为03的Handler
    21     @Handle(messageID=0x03)
    22     private final MessageHandler HANDLER_3 = (command) -> { doSomeOtherThing(); };

    在这个微型框架之下,要实现报文解析,只需要准备一个HashMap,在初始化时调用MessageHandlerUtil.loadMessageHandlers(this), MessageHandlerUtil这个辅助类会自动将类中所有带@Handle注解的MessageHandler加载到一个Map中并返回,用户在收到消息时,只需要从Map中拿到消息对应的Handler,调用Handler的process方法即可。在增减消息时,只需要增减带@Handle注解的MessageHandler;就算要修改某一个消息的处理,也能比较快速地定位到对应的Handler,在这个独立的Handler里面修改即可,比起之前的switch-case,可以说是更好地遵守了开放-封闭原则(对扩展开放,对更改封闭)。

    不得不说,这个框架还是有一点小问题,那就是不能支持子消息的分类。在我负责的业务中,只有一个消息(记为A消息)有子消息,于是我偷懒定义了一个@SubHandle注解,在加载时将@SubHandler注解的MessageHandler加载到另一个Map中,当收到的消息为A消息时,再手动将对应的MessageHandler从第二个Map中取出。这种做法可以说是十分hard code了,还好我负责的业务比较固定,暂时还能用。另外,返回的HashMap是裸露的,用户可以随意修改,看起来似乎也不是很妥当。

    周一来上班时,惊喜地发现大神把我的微型框架重构并加入了项目的通用类库中。重构后的框架加强了对数据结构的封装,并且实现了很强的通用性,可以支持三层以上的子消息结构,却只用了两个Map。下面我们就一起来看看他是怎么做的。

    首先,还是通过客户代码来看一下这个框架的使用场景。(对于下面的分析,我们都假设消息是一个String类型以使叙述简洁。如果要处理byte数组或其他类型的消息,可以在客户代码中进行转型,或者像大神一样在框架中加入对byte数组的支持,在此就不赘述了)

     1   private HandlerDispatcher<String> handlerDispatcher;//消息分发器
     2   
     3   void init(){
     4         handlerDispatcher = new HandlerDispatcherImpl<>();
     5         handlerDispatcher.load(this); //将注解的Handler加载到分发器
     6   }
     7   
     8   public void onReceivedMessage(String data) {
     9       //第一个char为消息ID
    10       handlerDispatcher.getHandler(data.charAt(0)+"").process(data); 
    11   }
    12 
    13   final Supplier<HandlerDispatcher<String>> supplier = () -> handlerDispatcher;
    14 
    15   @Mapped("1") //第一层,处理消息ID为1的消息
    16   final CustomizedHandler A = (data) -> { log("A"); };
    17   
    18   @Mapped("2") //第一层,处理消息ID为2的消息
    19   final ParentHandler<String> B = ParentHandler.build(supplier, (data) -> (data.charAt(1)+""));
    20   
    21   @Parent("B")
    22   @Mapped("1") //第二层,处理消息ID为2,子消息ID为1的消息
    23   final ParentHandler<String> B1 = ParentHandler.build(supplier, (data) -> (data.charAt(2)+""));
    24   
    25   @Parent("A")
    26   @Mapped("1") //第二层,处理消息ID为1,子消息ID也为1的消息
    27   final CustomizedHandler A1 = (data) -> { log("A1"); };
    28   
    29   @Parent("B1")
    30   @Mapped("1") //第三层,处理消息ID为1,子消息ID为2,子子消息ID为1的消息
    31   final CustomizedHandler B11 = (data) -> { log("B11"); };

    示例中定义了三层消息的解析器,用@Mapped(value = messageID)注解定义对应的消息ID,只有该注解的是第一层消息的解析器,同时定义了@Mapped和@Parent注解的是第二层及以下的解析器,其中@Parent(value = parentName)定义了其父消息的解析器名称。

    除了能支持多层子消息以外,新的设计将消息分发这个过程封装到了HandlerDispatcher类中,更好地符合了单一指责原则,这样业务模块就能更专心地处理业务逻辑了,而HandlerDispatcher的实现类也可以随时根据业务来调整消息分发的逻辑。

    至于上文中的Supplier和ParentHandler.build(),此处可能暂时看不懂,但它们并不是很重要,可以先跳过。

    既然说到了HandlerDispatcher,我们就来说一下这个小框架的类结构,其实十分简单:

    图1:消息分发小框架类图

    各个类的关系图中已经比较清楚了,核心类HandlerDispatcher负责加载和分发消息到对应的Handler。框架提供了一个ParentHandler接口供用户在有子消息的场景使用,该接口对上层接口Handler中的process()方法做了默认实现:

    1     @Override
    2     default void process(T data) {
    3         getHandlerDispatcher().getSubHandler(this, resolve(data)).process(data);
    4     }

     这是整个框架中我觉得比较聪明的地方之一,也或许是面向对象和面向过程的程序员的思维区别之一。在多级消息分发器的结构中,消息分发到下层的逻辑是由上层消息分发器定义的,而不是在用户模块中或者HandlerDispatcher中定义的。也就是说在整个框架中并没有一个全能选手统揽全局,每个对象都像流水线上的工人,只要把产品加工后传给下一个工人,就完成了自己的责任。

    图2:消息分发结构示例

    如果我们有一个上图所示的消息分发结构,那么顶层和中间层的消息处理器可以使用框架提供的ParentHandler,也可以使用自己定义的处理逻辑。如果使用ParentHandler,则process()方法会从HandlerDispatcher拿到当前Handle的子Handler进行处理,即自动实现了到下层的消息分发;如果用户需要一些特殊处理,比如分发到下层之前先打印出一些信息,或者要分发给多个子消息处理器,则可以继承ParentHandler并重写其中的process()方法,或者定义自己的Handler。

    看到这里,我们应该已经基本熟悉了消息分发小框架的结构。下面我们来看一下HandlerDispatcher是如何加载和找到一个对应的Handler的。

    在设计加载算法之前,再来复习一下使用场景:对于顶层消息,我们用handlerDispatcher.getHandler(getMessageIDSomehow()).process(data)来处理;顶层以下的消息,按照ParentHandler的默认实现,通过getHandlerDispatcher().getSubHandler(this, resolve(data)).process(data)处理。也就是说,HandlerDispatcher加载所有Handler之后需要达到两个效果, 一是通过messageID拿到顶层Handler,二是通过上层Handler和subID拿到对应的下层Handler。

    对于第一点,只要用一个HashMap<String, Handler>就可以实现。第二点有点难办,考虑到消息的层级结构,我们先用一个树来保存不同层级的Handler。在加载时,读取@Parent来获取上下层级的对应关系。TreeNode类的定义如下:

     1 public class TreeNode<T> {
     2     T data;
     3     List<TreeNode<T>> children = new ArrayList<>();
     4     TreeNode<T> parent;
     5     
     6     public TreeNode(TreeNode<T> parent, T data){
     7         Objects.requireNonNull(parent);
     8         this.parent = parent;
     9         this.data = data;
    10         parent.children.add(this);
    11     }
    12     
    13     public TreeNode(T data){
    14         this.data = data;
    15     }
    16 
    17     public List<TreeNode<T>> getChildren() {
    18         return children;
    19     }
    20 
    21     public T getData() {
    22         return data;
    23     }
    24 }

    图2示意的框架如果用TreeNode装载,可以表示如下:

    以上只是简略图,实际上TreeNode<T>是一个泛型类,泛型T代表树节点中存储的数据。为了表示messageID与Handler的对应关系,我们还会设计一个NodeData类把两者都存放起来,而这个NodeData就成为TreeNode<T>中对应的T。

     1     class NodeData{
     2         Handler<V> handler;
     3         String key;
     4 
     5         public NodeData(String key, Handler<V> handler) {
     6             Objects.requireNonNull(key);
     7             this.key = key;
     8             this.handler = handler;
     9         }
    10         
    11         public boolean accept(String anotherKey){
    12             return anotherKey != null && key.equals(anotherKey);
    13         }
    14 
    15         public Handler<V> getHandler() {
    16             return handler;
    17         }
    18     }

    按照TreeNode去存放后,我们就可以找到某一个父消息的子消息处理器了。但这样要求我们先遍历树,找到对应的父消息处理器;再遍历父消息处理器的孩子节点,找到子消息处理器。为了节省第一步的时间,我们把所有的父消息处理器和它们对应的树节点存在Map中,这样就可以直接按照父消息的id拿到父消息处理器了。

    总结起来,我们一共设计了一个树,两个Map。将它们定义在HandlerDispatcherImpl类中作为私有域:

    1     private Map<String, Handler<V>> topMap;
    2     private Map<Handler<V>, TreeNode<NodeData>> nodeMap;
    3     private TreeNode<NodeData> root;

    下面我们来看一下HandlerDispatcherImpl中的load方法是怎么把Handler加载到这三个数据结构的。

    首先,我们按照刚才的设计,将带注释的域分为两类:带@Parent的和不带@Parent的,分别放入两个数组中。

     1         //Scan all fields
     2         Class<?> instanceClass = instance.getClass();
     3         Field[] fields = instanceClass.getDeclaredFields();
     4         List<Field> topHandlerFields = new LinkedList<>();
     5         List<Field> subHandlerFields = new LinkedList<>();
     6         
     7         //Find fields with "@Mapped" annotation, insert them into 
     8         //topHandlerFields and subHandlerFields
     9         for(Field field : fields){
    10             Mapped mapped = field.getAnnotation(Mapped.class);
    11             if(mapped == null) continue;
    12             Class<?> fieldType = field.getType();
    13             if(!Handler.class.isAssignableFrom(fieldType))
    14                 continue;
    15             Parent parent = field.getAnnotation(Parent.class);
    16             if(parent == null) topHandlerFields.add(field);
    17             else subHandlerFields.add(field);
    18         }

    注意,在这里我们对域类型做了一个判断,只有域是Handler的实现类时才会被加载,否则会被无视。

    将所有域加载到两个数组中以后,首先处理顶层消息对应的域,把它们放到topMap中,同时也把对应的树节点存在nodeMap中。

     1         //insert topHandlerFields into topMap; link them with tree
     2         topHandlerFields.forEach(field -> {
     3             String key = extractKeyFromField(field);
     4             try {
     5                 field.setAccessible(true);
     6                 Handler<V> handler = (Handler<V>) field.get(instance);
     7                 topMap.put(key, handler);
     8                 NodeData nodeData = new NodeData(key, handler);
     9                 TreeNode<NodeData> treeNode = new TreeNode<NodeData>(root, nodeData);
    10                 nodeMap.put(handler, treeNode);
    11             } catch (IllegalArgumentException | IllegalAccessException e) {
    12                 e.printStackTrace(out);
    13             }
    14         });

    然后处理非顶层消息的处理器域。这里,考虑到用户不一定是按由顶向下的顺序定义的,加载时可能先读取到子处理器,后读取父处理器,这样就无法有效地形成一个完整的树结构。于是我们采用一种循环加载的方式,先加载父节点已在树中的处理器,并将已加载的域放在一个叫processed的链表中做记录;父节点还没有被加载的那些处理器留待下一次循环时再尝试加载。(不得不佩服大神同事的考虑十分周到……)

    1         //link subHandlerFields with tree
    2         List<Field> processed = new LinkedList<>();
    3         while(processed.size() < subHandlerFields.size()){
    4             subHandlerFields.stream().filter(field -> !processed.contains(field)).forEach(field -> {
    5                 linkWithParent(field, processed, instance);
    6             });
    7         }
     1     private void linkWithParent(Field field, List<Field> processed, Object instance) {
     2         field.setAccessible(true);
     3         Class<?> instanceClass = instance.getClass();
     4         //Firstly, see if we could find parent field
     5         String key = extractKeyFromField(field);
     6         String parentName = field.getAnnotation(Parent.class).value();
     7         Field parentHandlerField = null;
     8         try {
     9             parentHandlerField = instanceClass.getDeclaredField(parentName);
    10         } catch (NoSuchFieldException | SecurityException e) {
    11             log("Dude, your parent " + parentName + " doesn't exist.");
    12             processed.add(field);
    13             e.printStackTrace(); return;
    14         }
    15         //Try to get parentHandler, and its corresponding value in nodeMap
    16         try {
    17             parentHandlerField.setAccessible(true);
    18             Handler<V> parentHandler = (Handler<V>) parentHandlerField.get(instance);
    19             TreeNode<NodeData> parentNode = nodeMap.get(parentHandler);
    20             if(parentNode == null){
    21                 log("Parent " + parentName + " of field " + field + " not processed yet. Will wait a while.");
    22                 return;
    23             }
    24             //Add subHandler as a child to parentHandler
    25             Handler<V> subHandler = null;
    26             subHandler = (Handler<V>) field.get(instance);
    27             NodeData subHandlerNodeData = new NodeData(key, subHandler);
    28             TreeNode<NodeData> childNode = new TreeNode<>(parentNode, subHandlerNodeData);
    29             nodeMap.put(subHandler, childNode);
    30             processed.add(field);
    31             log("Attached " + field);
    32         }catch (IllegalArgumentException | IllegalAccessException e) {
    33             //should not happen
    34             e.printStackTrace(out);
    35             processed.add(field);
    36         }
    37     }

    搞定了加载,我们再来看看获取。顶层Handler的获取十分容易,只要从Map拿一下就好了:

    1     public Handler<V> getHandler(String key) {
    2         return topMap.get(key);
    3     }

    获取子Handler也不难,先从nodeMap中找到父节点对应的树节点,然后遍历其子节点就可以了。

    1     public Handler<V> getSubHandler(Handler<V> parentHandler, String subKey) {
    2         TreeNode<NodeData> parentNode = nodeMap.get(parentHandler);
    3         final Predicate<TreeNode<NodeData>> SUBHANDLER_ACCEPTS_KEY = (treeNode) -> (
    4                 ((NodeData)treeNode.getData()).accept(subKey)
    5         );
    6         Result<TreeNode<NodeData>> result = new Result<>();
    7         parentNode.getChildren().stream().filter(SUBHANDLER_ACCEPTS_KEY).findFirst().ifPresent(result::set);
    8         return result.isNULL() ? null : result.get().getData().getHandler();
    9     }

    至此,我们基本完成了消息加载的小框架的核心代码啦。现在再看看开头的的示例程序中,我们看不懂的那一部分:

    1   final Supplier<HandlerDispatcher<String>> supplier = () -> handlerDispatcher;
    2 
    3   @Mapped("2") //第一层,处理消息ID为2的消息
    4   final ParentHandler<String> B = ParentHandler.build(supplier, (data) -> (data.charAt(1)+""));

    这里有两个问题。第一是ParentHandler.build。其实这是一个静态Util方法,用于快速生成一个ParentHandler的实现类。看一下这个方法的定义:

     1     static <T> ParentHandler<T> build(final Supplier<HandlerDispatcher<T>> supplier, Resolver<T> resolver){
     2         ParentHandler<T> parentHandler = new ParentHandler<T>(){
     3 
     4             @Override
     5             public String resolve(T data) {
     6                 return resolver.apply(data);
     7             }
     8 
     9             @Override
    10             public HandlerDispatcher<T> getHandlerDispatcher() {
    11                 return supplier.get();
    12             }
    13             
    14         };
    15         return parentHandler;
    16     }

    这里的Resolver是一个简单的接口,它的定义如下:

    1 public interface Resolver<T> extends Function<T, String>{}

    其实它就是一个Function接口,功能是输入消息类型T返回String,即定义一个返回子消息ID的方法。在build方法定义的ParentHandler中,通过resolver.apply(data)即可返回子消息的ID。

    第二个问题是supplier。Supplier<T>类型是Java 1.8引进的一个接口,它的功能与Function类似,返回一个T类型,在这里我们定义它为HandlerDispatcher的供应者,它返回的用户定义的一个HandlerDispatcher对象。那么为什么不能直接用HandlerDispatcher对象呢?

    我们先来复习一下Java类初始化的顺序:

    1. 当用户创建某类的对象或使用该类的静态变量/方法时,Java解释器会在classpath中寻找.class文件并加载。
    2. 进行所有的静态初始化。即某一个类的静态初始化只进行一次。
    3. 在内存堆中按需分配该对象的内存。
    4. 初始化该类的所有非静态变量至0或null。
    5. 按照类中变量的定义对变量进行初始化。
    6. 调用构造函数。

    注意步骤5和6,JVM是先对类中的域进行初始化,再调用构造函数的。在我们的用户模块中,所有的Handler都定义为对象中的域,所以它们会先被初始化。而

    1 handlerDispatcher = new HandlerDispatcherImpl<>();

    是在构造函数中才被调用的。也就是说,如果我们不使用Supplier,我们必须要这么写:

     @Mapped("2") //第一层,处理消息ID为2的消息
    final ParentHandler<String> B = ParentHandler.build(handlerDispatcher, (data) -> (data.charAt(1)+""));

    可是这里的handlerDispatcher是一个空指针。那么后续再通过handlerDispatcher查找子处理器的时候,会抛出空指针错误。

    supplier定义了一个拿到handlerDispatcher对象的方法。虽然在定义supplier时,handlerDispatcher也是空的,但是由于我们传进去的是一个方法,所以没关系。

    通过学习大神的代码,除了学到了一点大神的设计思路,还了解了一些Java 1.8的新特性,如流式编程和函数式编程,同时心里也产生了许多疑问。对于这些疑问,就让我们在以后的文章中慢慢道来吧。

    消息分发小框架源码:戳这里

  • 相关阅读:
    Threading in C# Learining
    win7 设置IIS
    Aforge视频采集
    C# 删除文件夹下的所有文件
    C# Timer实现实时监听
    Java中堆内存和栈内存详解
    彻底掌握 AQS
    四种线程同步/互斥方式小结
    CyclicBarrier可重用的循环栅栏
    答疑解惑之ExecutorService——shutdown方法和awaitTermination方法使用
  • 原文地址:https://www.cnblogs.com/mozi-song/p/9150442.html
Copyright © 2020-2023  润新知