一個可以代替冗長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的新特性,如流式編程和函數式編程,同時心里也產生了許多疑問。對於這些疑問,就讓我們在以后的文章中慢慢道來吧。

消息分發小框架源碼:戳這里

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM