「造個輪子」——cicada 源碼分析

「造個輪子」——cicada 源碼分析

來自專欄 crossoverJie的知乎專欄

前言

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

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

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

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

技術選型

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

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

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

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

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

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

路由規則

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

查看源碼其實很容易看出具體的步驟,注釋也很明顯。

這裡只分析重點功能。

先來考慮下需求。

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

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

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

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

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

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

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

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

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

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

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

路由策略

其中核心的源碼在 routeAction 方法中。

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

全局掃描代碼:

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

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

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

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

傳參方式

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

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

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

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

  1. public interface Param extends Map<String, Object> {
  2. /**
  3. * get String
  4. * @param param
  5. * @return
  6. */
  7. String getString(String param);
  8. /**
  9. * get Integer
  10. * @param param
  11. * @return
  12. */
  13. Integer getInteger(String param);
  14. /**
  15. * get Long
  16. * @param param
  17. * @return
  18. */
  19. Long getLong(String param);
  20. /**
  21. * get Double
  22. * @param param
  23. * @return
  24. */
  25. Double getDouble(String param);
  26. /**
  27. * get Float
  28. * @param param
  29. * @return
  30. */
  31. Float getFloat(String param);
  32. /**
  33. * get Boolean
  34. * @param param
  35. * @return
  36. */
  37. Boolean getBoolean(String param) ;
  38. }

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

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

  1. Param paramMap = buildParamMap(queryStringDecoder);

業務執行

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

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

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

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

  1. WorkAction action = (WorkAction) actionClazz.newInstance();
  2. WorkRes execute = action.execute(paramMap);

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

響應返回

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

所有的響應數據都需要封裝到這個對象中。

這個沒啥好說的,都是一些基本數據。

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

攔截器設計

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

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

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

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

同時在這兩個方法中執行具體的調用。

這裡重點要看看 interceptorBefore 方法。

其中也是加入了一個緩存,盡量的減少反射操作。

適配器

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

因此也提供了一個適配器 AbstractCicadaInterceptorAdapter

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

類似於這樣:

總結

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

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

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

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

最後貼下項目地址:

github.com/TogetherOS/c

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

weixin.qq.com/r/eTtlff3 (二維碼自動識別)

推薦閱讀:

Iperf 源代碼分析(一)
來聊聊源碼學習

TAG:源代碼 | Netty | HTTP |