今兒個是冬至,所謂“冬大過年”,公司也應景五點鐘就放大伙兒回家吃餃子喝羊肉湯了,而我本著極高的職業素養依然堅持留在公司(實則因為沒餃子吃沒羊肉湯喝,只能呆公司吃食堂……)。趁著這一個多小時的時間,想跟大家介紹下前段時間整的一個基於netty http協議棧的輕量級流程控制組件 nettice(點此查看代碼),目前已經實現了一些功能並將持續完善,希望能為大伙兒切實解決一點開發問題(或者至少提供一些思路)。
服務的流程,簡單來說就是在一次交互過程中,對 client 端而言,是從請求的組裝、發送,再到響應的接收、解析和業務處理的一個順序流;對 server 端而言,是從請求的接收、解析和業務處理,再到響應的組裝、發送的一個順序流。而本文所說的流程控制組件,指的是在使用 netty http 協議棧開發 http server 的過程中,保證流程按照該順序流執行,同時抽象出通用的非業務邏輯並對上層透明,使開發人員只需關注業務邏輯的底層實現。
一個 http server 往往需要處理多種業務邏輯,每一個業務邏輯都對應著一個請求消息和一個響應消息,服務端需要把這些不同的消息自動分發到對應的業務邏輯中處理。
然而使用 netty http 協議棧開發過 http server 的童鞋都應該有所了解,netty 並沒有提供消息分發組件。
這種情況下只能通過請求消息中的某個特殊標識(如某個字段值)來區分業務,使用 switch case 來處理。但這種方式下,隨著業務邏輯的增多,switch case 代碼塊將越來越長,大大影響代碼可讀性;並且每次新增、刪除業務邏輯時,都需要修改這段邏輯代碼,後期維護也越來越麻煩。
此外,使用 netty http 協議棧時,並沒有提供客戶端 parameter 到服務端業務 method 入參的直接解析和映射。
這句話是什麼意思呢?舉個栗子,你在客戶端使用 httpclient 給 netty http 服務端發送了一個消息,傳遞參數為“project=nettice&author=cyfonly”,而服務端有個業務方法 public void bizHandle(String project, String author),那麼在調用 bizHandle 這個方法前,你肯定得先手動寫代碼解析客戶端的請求參數解析出 project 和 author 兩個 key 對應的 value。
那麼問題來了,當業務邏輯越來越多,針對每個業務邏輯的請求,你都不得不單獨寫一段參數解析的代碼。這是多麼X疼的一件事情啊,而且後面還有一大堆業務邏輯代碼要寫呢!
有沒有辦法可以避免通過寫 switch case 代碼段來分發請求,並且使用統一方法來解析所有的請求參數呢?
當然有,nettice 就是為解決這個而誕生的啦~~
nettice 到底能做些什麼呢?
消息分發的整體設計如下(一圖勝千言):
如何使用 nettice?
nettice 作為一個組件使用起來時很簡單,此處使用具體的栗子來說明(demo代碼請點此查看)。
首先是引入 nettice-core.jar,或者直接使用 nettice-core 源碼作為 maven 項目的 module(目前沒有上傳到 maven 倉庫,暫時沒法通過 pom 依賴來引入)。然後定義 nettice 組件的必要配置 nettice.xml:
<?xml version="1.0" encoding="UTF-8"?> <router> <action-package> <package>com.server.action</package> </action-package> <namespaces> <!--按包分配命名空間,多個匹配項時,采用目錄級別最多的--> <namespace name="/nettp/" packages="com.server.action.*"></namespace> <namespace name="/nettp/sub/" packages="com.server.action.sub"></namespace> </namespaces> </router>
最後在服務端中添加消息分發handler:
.addLast("dispatcher",new ActionDispatcher())
好了,現在就可以使用 nettice 的功能啦!
特別注意,業務處理類需繼承 BaseAction 才能被 nettice 組件識別!
使用方法名作為 URI 映射關鍵字,如果項目中存在同樣名字的方法會產生沖突,開發者可以使用 @Namespaces 注解或者在 nettice.xml 配置中添加 namespaces 來修改 URI 映射,以規避此問題。
例如 com.server.action.DemoAction 提供了 returnTextUseNamespace() 方法,com.server.action.sub.SubDemoAction 也提供了 returnTextUseNamespace() 方法,但兩個方法實現不同功能。nettice 組件默認使用方法名進行 URI 映射,那麼上述兩個 returnTextUseNamespace() 方法會產生沖突,開發者可以使用 @Namespace 注解修改 URI 映射:
package com.server.action; public class DemoAction extends BaseAction{ @Namespace("/nettp/demo/") public Render returnTextUseNamespace(@Read(key="id") Integer id, @Read(key="project") String project){ //do something return new Render(RenderType.TEXT, "returnTextUseNamespace in [DemoAction]"); } }
package com.server.action.sub; public class SubDemoAction extends BaseAction{ @Namespace("/nettp/subdemo/") public Render returnTextUseNamespace(@Read(key="ids") Integer[] ids, @Read(key="names") List<String> names){ //do something return new Render(RenderType.TEXT, "returnTextUseNamespace in [SubDemoAction]"); } }
也可以在 nettice.xml 中設置:
<namespaces> <namespace name="/nettp/demo/" packages="com.server.action.*"></namespace> <namespace name="/nettp/subdemo/" packages="com.server.action.sub"></namespace> </namespaces>
使用 @Read 注解可以自動裝配請求數組,支持不同的類型(基本類型、List、Array 和 Map),可以設置默認值(目前僅支持基本類型設置 defaultValue)。
這個例子演示了從 HttpRequest 中獲取基本類型的方法,如果沒有值會自動設置默認值。
客戶端請求:
private static void sendGetPriType() throws Exception{ String path = "http://127.0.0.1:8080/nettp/primTypeTest.action?"; String getUrl = path + "id=10001&project=nettice&author=cyfonly"; java.net.URL url = new java.net.URL(getUrl); java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setDoOutput(true); conn.connect(); if(conn.getResponseCode() == 200){ BufferedReader in = new BufferedReader(new InputStreamReader((InputStream) conn.getInputStream(), "UTF-8")); String msg = in.readLine(); System.out.println("msg: " + msg); in.close(); } conn.disconnect(); }
服務端 method:
public Render primTypeTest(@Read(key="id", defaultValue="1" ) Integer id, @Read(key="project") String project, @Read(key="author") String author){ System.out.println("Receive parameters: id=" + id + ",project=" + project + ",author=" + author); return new Render(RenderType.TEXT, "Received your primTypeTest request.[from primTypeTest]"); }
輸出結果:
Receive parameters: id=10001,project=nettice,author=cyfonly
這個例子演示了從 HttpRequest 中獲取 List/Array 類型的方法。
客戶端請求:
private static void sendPostJsonArrayAndList() throws Exception{ String path = "http://127.0.0.1:8080/nettp/sub/arrayListTypeTest.action"; JSONObject obj = new JSONObject(); int[] ids = {1,2,3}; List<String> names = new ArrayList<String>(); names.add("aaaa"); names.add("bbbb"); obj.put("ids", ids); obj.put("names", names); String jsonStr = obj.toJSONString(); byte[] data = jsonStr.getBytes(); java.net.URL url = new java.net.URL(path); java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); conn.setRequestProperty("Content-Length", String.valueOf(data.length)); OutputStream outStream = conn.getOutputStream(); outStream.write(data); outStream.flush(); outStream.close(); if(conn.getResponseCode() == 200){ BufferedReader in = new BufferedReader(new InputStreamReader((InputStream) conn.getInputStream(), "UTF-8")); String msg = in.readLine(); System.out.println("msg: " + msg); in.close(); } conn.disconnect(); }
服務端 method:
public Render arrayListTypeTest(@Read(key="ids") Integer[] ids, @Read(key="names") List<String> names){ System.out.println("server output ids:"); for(int i=0; i<ids.length; i++){ System.out.println(ids[i]); } System.out.println("server output names:"); for(String item : names){ System.out.println(item); } JSONObject obj = new JSONObject(); obj.put("code", 0); obj.put("msg", "Received your Array/List request.[from arrayListTypeTest()]");
return new Render(RenderType.JSON, obj.toJSONString()); }
輸出結果:
server output ids: 1 2 3 server output names: aaaa bbbb
Map 類型解析
這個例子演示了從 HttpRequest 中獲取 Map 類型的方法。
客戶端代碼:
private static void sendPostJsonMap() throws Exception{ String path = "http://127.0.0.1:8080/nettp/sub/mapTypeTest.action"; JSONObject obj = new JSONObject(); Map<String, String> srcmap = new HashMap<String, String>(); srcmap.put("project", "nettice"); srcmap.put("author", "cyfonly"); obj.put("srcmap", srcmap); String jsonStr = obj.toJSONString(); byte[] data = jsonStr.getBytes(); java.net.URL url = new java.net.URL(path); java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); conn.setRequestProperty("Content-Length", String.valueOf(data.length)); OutputStream outStream = conn.getOutputStream(); outStream.write(data); outStream.flush(); outStream.close(); if(conn.getResponseCode() == 200){ BufferedReader in = new BufferedReader(new InputStreamReader((InputStream) conn.getInputStream(), "UTF-8")); String msg = in.readLine(); System.out.println("msg: " + msg); in.close(); } conn.disconnect(); }
服務端 method:
public Render mapTypeTest(@Read(key="srcmap") Map<String,String> srcmap){ System.out.println("server output srcmap:"); for(String key : srcmap.keySet()){ System.out.println(key + "=" + srcmap.get(key)); } JSONObject obj = new JSONObject(); obj.put("code", 0); obj.put("msg", "Received your Map request.[from mapTypeTest]"); return new Render(RenderType.JSON, obj.toJSONString()); }
輸出結果:
server output srcmap: author=cyfonly project=nettice
注意,使用 Map 時限定了只能存在一個 Map 參數。
處理方法可以通過返回 Render 對象向客戶端返回特定格式的數據,一個 Render 對象由枚舉類型 RenderType 和 data 兩部分組成。nettice 組件會通過 RenderType 來為 Response 設置合適的 Content-Type,開發者也可以擴展 Render 以及相關類來實現更多的類型支持。
例如這是一個返回 JSON 對象的例子,客戶端將收到一個 Json 對象:
public Render postPriMap(){ JSONObject obj = new JSONObject(); obj.put("code", 0); obj.put("msg", "had received your request."); return new Render(RenderType.JSON, obj.toJSONString()); }
正如開頭說的那樣,目前 nettice 實現了部分功能,在性能上也暫時沒有太多的時間做優化,所以後續肯定會繼續完善。目前有計劃做的事情如下:
但就目前而言,nettice 確實解決了使用 netty http 協議棧開發 http server 的一些痛點。
好了,晚餐時間到,暫時先介紹這麼多。如有未介紹到或者介紹不夠詳細的,將會完善本文,請持續關注~~
希望有興趣的童鞋可以仔細研讀代碼,若有更好的想法歡迎通過評論或者加本人QQ(869827095)私下交流,或者和本人一起編碼實現,都是非常歡迎的。