在項目中,我需要維護一個應用層的字節流協議。這個協議的每條報文都是一個字節數組,數組的頭兩個字節表示消息的傳送方向,第三、四個字節表示消息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類初始化的順序:
- 當用戶創建某類的對象或使用該類的靜態變量/方法時,Java解釋器會在classpath中尋找.class文件並加載。
- 進行所有的靜態初始化。即某一個類的靜態初始化只進行一次。
- 在內存堆中按需分配該對象的內存。
- 初始化該類的所有非靜態變量至0或null。
- 按照類中變量的定義對變量進行初始化。
- 調用構造函數。
注意步驟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的新特性,如流式編程和函數式編程,同時心里也產生了許多疑問。對於這些疑問,就讓我們在以后的文章中慢慢道來吧。
消息分發小框架源碼:戳這里