一个可以代替冗长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的新特性,如流式编程和函数式编程,同时心里也产生了许多疑问。对于这些疑问,就让我们在以后的文章中慢慢道来吧。

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

原文地址:https://www.cnblogs.com/mozi-song/p/9150442.html