「造個輪子」——cicada 原始碼分析

crossoverJie發表於2019-02-28

前言

兩天前寫了文章《「造個輪子」——cicada(輕量級 WEB 框架)》 向大家介紹了 cicada 之後收到很多反饋,也有許多不錯的建議。

同時在 GitHub 也收穫了 80 幾顆 小♥♥(絕對不是刷的。。)

「造個輪子」——cicada 原始碼分析

也有朋友希望能出一個原始碼介紹,本文就目前的 v1.0.1 版本來一起分析分析。

沒有看錯,剛釋出就修復了一個 bug,想要試用的請升級到 1.0.1 吧。

技術選型

一般在做一個新玩意之前都會有技術選型的過程,但這點在做 cicada 的時候卻異常簡單。

因為我的需求是想提供一個高效能的 HTTP 服務,縱觀整個開源界其實選擇不多。

加上最近我在做 Netty 相關的開發,所以自然而然就選擇了它。

同時 Netty 自帶了對 HTTP 協議的編解碼器,可以非常簡單快速的開發一個 HTTP 伺服器。我只需要把精力放在引數處理、路由等業務處理上即可。

同時 Netty 也是基於 NIO 實現,效能上也有保證。關於 Netty 相關內容可以參考這裡

下面來重點分析其中的各個過程。

路由規則

最核心的自然就是 HTTP 的處理 handle,對應的就是 HttpHandle 類。

「造個輪子」——cicada 原始碼分析

檢視原始碼其實很容易看出具體的步驟,註釋也很明顯。

這裡只分析重點功能。

先來考慮下需求。

首先作為一個 HTTP 框架,自然是得讓使用者能有地方來實現業務程式碼;就像我們們現在使用 SpringMVC 時寫的 controller 一樣。

其實當時考慮過三種方案:

  • 像 SpringMVC 一樣定義註解,只要宣告瞭對應註解我就認為這是一個業務類。
  • 用過 Struts2 的同學應該有印象,它的業務類 Action 都是配置到一個 XML 中;在裡面配置介面對應的業務處理類。
  • 同樣的思路,只是把 XML 檔案換成 properties 配置檔案,在裡面編寫 JSON 格式的對應關係。

這時就得分析各個方案的優缺點了。

方案二和三其實就是 XML 和 json 的對比了;XML 會讓維護者感到結構清晰,同時便於維護和新增。

JSON 就不太方便處理了,並且在這樣的場景並不用於傳輸自然也發揮不出優勢。

最後考慮到現在流行的 SpringBoot 都在去 XML,要是再搞一個依賴於 XML 的東西也跟不上大家的使用習慣。

於是就採用類似於 SpringMVC 這樣的註解形式。

既然採用了註解,那框架怎麼知道使用者訪問某個介面時能對應到業務類呢?

所以首先第一步自然是需要將加有註解的類全部掃描一遍,放到一個本地快取中。

這樣才能方便後續的路由定位。

路由策略

其中核心的原始碼在 routeAction 方法中。

「造個輪子」——cicada 原始碼分析

首先會全域性掃描使用了 @CicadaAction 的註解,然後再根據請求地址找到對應的業務類。

全域性掃描程式碼:

「造個輪子」——cicada 原始碼分析

首先是獲取到專案中自定義的所有類,然後判斷是否加有 @CicadaAction 註解。

是目標類則把他快取到一個本地 Map 中,方便下次訪問時可以不再掃描直接從快取中獲取即可(反射很耗效能)。

執行完 routeAction 後會獲得真正的業務類型別。

Class<?> actionClazz = routeAction(queryStringDecoder, appConfig);

傳參方式

拿到業務類的類型別之後就成功一大半了,只需要反射生成它的物件然後執行方法即可。

在執行方法之前又要涉及到一個問題,引數我該怎麼傳遞呢?

考慮到靈活性我採用了最簡答 Map 方式。

因此定義了一個通用的 Param 介面並繼承了 Map 介面。

public interface Param extends Map<String, Object> {

    /**
     * get String
     * @param param
     * @return
     */
    String getString(String param);

    /**
     * get Integer
     * @param param
     * @return
     */
    Integer getInteger(String param);

    /**
     * get Long
     * @param param
     * @return
     */
    Long getLong(String param);

    /**
     * get Double
     * @param param
     * @return
     */
    Double getDouble(String param);

    /**
     * get Float
     * @param param
     * @return
     */
    Float getFloat(String param);

    /**
     * get Boolean
     * @param param
     * @return
     */
    Boolean getBoolean(String param) ;
}
複製程式碼

其中封裝了幾種基本型別的獲取方式。

同時在 buildParamMap() 方法中,將介面中的引數封裝到這個 Map 中。

Param paramMap = buildParamMap(queryStringDecoder);
複製程式碼

「造個輪子」——cicada 原始碼分析

業務執行

最後只需要執行業務即可;由於在上文已經獲取到業務類的類型別,所以這裡通過反射即可呼叫。

同時也定義了一個業務類需要實現的一個通用介面 WorkAction,想要實現具體業務只要實現它就行。

「造個輪子」——cicada 原始碼分析

而這裡的方法引數自然就是剛才定義的引數介面 Param

由於所有的業務類都是實現了 WorkAction,所以在反射時都可以定義為 WorkAction 物件。

WorkAction action = (WorkAction) actionClazz.newInstance();
WorkRes execute = action.execute(paramMap);
複製程式碼

最後將構建好的引數 map 傳入即可。

響應返回

有了請求那自然也得有響應,觀察剛才定義的 WorkAction 介面可以發現其實定義了一個 WorkRes 響應類。

所有的響應資料都需要封裝到這個物件中。

「造個輪子」——cicada 原始碼分析

這個沒啥好說的,都是一些基本資料。

最後在 responseMsg() 方法中將響應資料編碼為 JSON 輸出即可。

「造個輪子」——cicada 原始碼分析

攔截器設計

攔截器也是一個框架基本的功能,用處非常多。

cicada 的實現原理非常簡單,就是在 WorkAction 介面執行業務邏輯之前呼叫一個方法、執行完畢之後呼叫另一個方法。

也是同樣的思路需要定義一個介面 CicadaInterceptor,其中有兩個方法。

「造個輪子」——cicada 原始碼分析

看方法名字自然也能看出具體作用。

「造個輪子」——cicada 原始碼分析

同時在這兩個方法中執行具體的呼叫。

這裡重點要看看 interceptorBefore 方法。

「造個輪子」——cicada 原始碼分析

其中也是加入了一個快取,儘量的減少反射操作。

介面卡

就這樣的攔截器介面是夠用了,但並不是所有的業務都需要實現兩個介面。

因此也提供了一個介面卡 AbstractCicadaInterceptorAdapter

「造個輪子」——cicada 原始碼分析

它作為一個抽象類實現了 CicadaInterceptor 介面,這樣後續的攔截業務也可繼承該介面選擇性的實現方法即可。

類似於這樣:

「造個輪子」——cicada 原始碼分析

總結

v1.0.1 版本的 cicada 就介紹完畢了,其中的原理和原始碼都比較簡單。

大量使用了反射和一些設計模式、多型等應用,這方面經驗較少的朋友可以參考下。

同時也有很多不足;比如傳參後續會考慮更加優雅的方式、攔截器目前寫的比較死,後續會利用動態代理實現自定義攔截。

其實 cicada 只是利用週末兩天時間做的,bug 肯定少不了;也歡迎大家在 GitHub 上提 issue 參與。

最後貼下專案地址:

github.com/TogetherOS/…

你的點贊與轉發是最大的支援。

「造個輪子」——cicada 原始碼分析

相關文章