filter(也稱middleware)是我們平時業務中用的非常廣泛的框架元件,很多web框架、微服務框架都有整合。通常在一些請求的前後,我們會把比較通用的邏輯都會放到filter元件來實現。如打請求日誌、耗時、許可權、介面限流等通用邏輯。那麼接下來我會和你一起實現一個filter元件,同時讓你瞭解到,它是如何從0到1搭建起來的,具體在演進過程中遇到了哪些問題,是如何解決的。
從一個簡單的server說起
我們看這樣一段程式碼。首先我們在服務端開啟了一個http server,配置了/這個路由,hello函式處理這個路由的請求,並往body中寫入hello字串響應給客戶端。我們通過訪問127.0.0.1:8080就可以看到響應結果。具體的實現如下:
// 模擬業務程式碼
func hello(wr http.ResponseWriter, r *http.Request) {
wr.Write([]byte("hello"))
}
func main() {
http.HandleFunc("/", hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
列印請求耗時v1.0
接下來有一個需求,需要列印這個請求執行的時間,這個也是我們業務中比較常見的場景。我們可能會這樣實現,在hello這個handler方法中加入時間計算邏輯,主函式不變:
// 模擬業務程式碼
func hello(wr http.ResponseWriter, r *http.Request) {
// 增加計算執行時間邏輯
start := time.Now()
wr.Write([]byte("hello"))
timeElapsed := time.Since(start)
// 列印請求耗時
fmt.Println(timeElapsed)
}
func main() {
http.HandleFunc("/", hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
但是這樣實現仍然有一定問題。假設我們有一萬個請求路徑定義、所以有一萬個handler和它對應,我們在這一萬個handler中,如果都要加上請求執行時間的計算,那必然代價是相當大的。
為了提升程式碼複用率,我們使用filter元件來解決此類問題。大多數web框架或微服務框架都提供了這個元件,在有些框架中也叫做middleware。
filter登場
filter的基本思路,是把功能性(業務程式碼)與非功能性(非業務程式碼)分離,保證對業務程式碼無侵入,同時提高程式碼複用性。在講解2.0的需求實現之前,我們先回顧一下1.0中比較重要的函式呼叫http.HandleFunc("/", hello)
這個函式會接收一個路由規則pattern,以及這個路由對應的處理函式handler。我們一般的業務邏輯都會寫在handler裡面,在這裡就是hello函式。我們接下來看一下http.HandleFunc()函式的詳細定義:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
這裡要注意一下,標準庫中又把func(ResponseWriter, *Request)這個func重新定義成一個型別別名HandlerFunc:
type HandlerFunc func(ResponseWriter, *Request)
所以我們一開始用的http.HandleFunc()函式定義,可以直接簡化成這樣:
func HandleFunc(pattern string, handler HandlerFunc)
我們只要把「HandlerFunc型別」與「HandleFunc函式」區分開就可以一目瞭然了。因為hello這個使用者函式也符合HandlerFunc這個型別的定義,所以自然可以直接傳給http.HandlerFunc函式。而HandlerFunc型別其實是Handler介面的一個實現,Handler介面的實現如下,它只有ServeHTTP這一個方法:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
HandlerFunc就是標準庫中提供的預設的Handler介面實現,所以它要實現ServeHTTP方法。它在ServeHTTP中只做了一件事,那就是呼叫使用者傳入的handler,執行具體的業務邏輯,在我們這裡就是執行hello(),列印字串,整個請求響應流程結束
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
列印請求耗時v2.0
所以我們能想到的比較容易的辦法,就是把傳入的使用者業務函式hello在外面包一層,而非在hello裡面去加列印時間的程式碼。我們可以單獨定義一個timeFilter函式,他接收一個引數f,也是http.HandlerFunc型別,然後在我們傳入的f前後加上的time.Now、time.Since程式碼。
這裡注意,timeFilter最終返回值也是一個http.HandlerFunc函式型別,因為畢竟最終還是要傳給http.HandleFunc函式的,所以filter必須也要返回這個型別,這樣就可以實現最終業務程式碼與非業務程式碼分離的同時,實現列印請求時間。詳細實現如下:
// 列印請求時間filter,和具體的業務邏輯hello解耦
func timeFilter(f http.HandlerFunc) http.HandlerFunc {
return func(wr http.ResponseWriter, r *http.Request) {
start := time.Now()
// 這裡就是上面我們看過HandlerFun型別中ServeHTTP的預設實現,會直接呼叫f()執行業務邏輯,這裡就是我們的hello,最終會列印出字串
f.ServeHTTP(wr, r)
timeElapsed := time.Since(start)
// 列印請求耗時
fmt.Println(timeElapsed)
}
}
func hello(wr http.ResponseWriter, r *http.Request) {
wr.Write([]byte("hello\n"))
}
func main() {
// 在hello的外面包上一層timeFilter
http.HandleFunc("/", timeFilter(hello))
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
然而這樣還是有兩個問題:
- 如果有十萬個路由,那我要在這十萬個路由上,每個都去加上相同的包裹程式碼嗎?
- 如果有十萬個filter,那我們要包裹十萬層嗎,程式碼可讀性會非常差
目前的實現很可能造成以下後果:
http.HandleFunc("/123", filter3(filter2(filter1(hello))))
http.HandleFunc("/456", filter3(filter2(filter1(hello))))
http.HandleFunc("/789", filter3(filter2(filter1(hello))))
http.HandleFunc("/135", filter3(filter2(filter1(hello))))
...
那麼如何更優雅的去管理filter與路由之間的關係,能夠讓filter3(filter2(filter1(hello)))只寫一次就能作用到所有路由上呢?
列印請求耗時v3.0
我們可以想到,我們先把filter的定義抽出來單獨定義為Filter型別,然後可以定義一個結構體Frame,裡面的filters欄位用來專門管理所有的filter。這裡可以從main函式看起。我們新增了timeFilter、路由、最終開啟服務,大體上和1.0版本的流程是一樣的:
// Filter型別定義
type Filter func(f http.HandlerFunc) http.HandlerFunc
type Frame struct {
// 儲存所有註冊的過濾器
filters []Filter
}
// AddFilter 註冊filter
func (r *Frame) AddFilter(filter Filter) {
r.filters = append(r.filters, filter)
}
// AddRoute 註冊路由,並把handler按filter新增順序包起來。這裡採用了遞迴實現比較好理解,後面會講迭代實現
func (r *Frame) AddRoute(pattern string, f http.HandlerFunc) {
r.process(pattern, f, len(r.filters) - 1)
}
func (r *Frame) process(pattern string, f http.HandlerFunc, index int) {
if index == -1 {
http.HandleFunc(pattern, f)
return
}
fWrap := r.filters[index](f)
index--
r.process(pattern, fWrap, index)
}
// Start 框架啟動
func (r *Frame) Start() {
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
func main() {
r := &Frame{}
r.AddFilter(timeFilter)
r.AddFilter(logFilter)
r.AddRoute("/", hello)
r.Start()
}
r.AddRoute之前都很好理解,初始化主結構,並把我們定義好的filter放到主結構中的切片統一管理。接下來AddRoute這裡是核心邏輯,接下來我們詳細講解一下
AddRoute
r.AddRoute("/", hello) 其實和v1.0裡的 http.HandleFunc("/", hello) 其實一摸一樣,只不過內部增加了filter的邏輯。在r.AddRoute內部會呼叫process函式,我將引數全部替換成具體的值:
r.process("/", hello, 1)
那麼在process內部,首先index不等於-1,往下執行到
fWrap := r.filters[index](f)
他的含義就是,取出第index個filter,當前是r.filters[1],r.filters[1]就是我們的logFilter,logFilter接收一個f(這裡就是hello),logFilter裡的f.ServerHTTP可以直接看成執行f(),即hello,相當於直接用hello裡的邏輯替換掉了logFilter裡的f.ServerHTTP這一行,在下圖裡用箭頭表示。最後將logFilter的返回值賦值給fWrap,將包裹後的fWrap繼續往下遞迴,index--:
同理,接下來的遞迴引數為:
r.process("/", hello, 0)
這裡就輪到r.filters[0]了,即timeFilter,過程同上:
最後一輪遞迴,index = -1,即所有filter都處理完了,我們就可以最終和v1.0一樣,呼叫http.HandleFunc(pattern, f)將最終我們層層包裹後的f,最終註冊上去,整個流程結束:
AddRoute的遞迴版本相對容易理解,我也同樣用迭代實現了一個版本。每次迴圈會在本層filter將f包裹後重新賦值給f,這樣就可以將之前包裹後的f沿用到下一輪迭代,基於上一輪的f繼續包裹剩餘的filter。在gin框架中就用了迭代這種方式來實現:
// AddRouteIter AddRoute的迭代實現
func (r *Frame) AddRouteIter(pattern string, f http.HandlerFunc) {
filtersLen := len(r.filters)
for i := filtersLen; i >= 0; i-- {
f = r.filters[i](f)
}
http.HandleFunc(pattern, f)
}
這種filter的實現也叫做洋蔥模式,最裡層是我們的業務邏輯helllo,然後外面是logFilter、在外面是timeFilter,很像這個洋蔥,相信到這裡你已經可以體會到了:
小結
我們從最開始1.0版本業務邏輯和非業務邏輯耦合嚴重,到2.0版本引入filter但實現仍不優雅,到3.0版本解決2.0版本的遺留問題,最終實現了一個簡易的filter管理框架
關注我們
歡迎對本系列文章感興趣的讀者訂閱我們的公眾號,關注博主下次不迷路~