在上一篇文章中,咱們已經能夠實現一個性能較高,且支持RESTful風格的路由了。可是,在Web應用的開發中,咱們還須要一些能夠被擴展的功能。git
所以,在設計框架的過程當中,應該留出能夠擴展的空間,好比:日誌記錄、故障恢復等功能,若是咱們把這些業務邏輯全都塞進Controller
/Handler
中,會顯得代碼特別的冗餘,雜亂。github
因此在這篇文章中,咱們來探究如何更優雅的設計這些中間件。數組
好比咱們要實現一個日誌記錄的功能,咱們能夠用這種簡單粗暴的方式:bash
package main
import (
"fmt"
"net/http"
"time"
)
func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
record(r.URL.Path)
fmt.Fprintf(w, "Hello World !")
}
func main() {
http.HandleFunc("/hello", helloWorldHandler)
http.ListenAndServe(":8000", nil)
}
func record(path string) {
fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + path)
}
複製代碼
若是這樣作的話,確實是實現了咱們的目標,記錄了訪問的日誌。app
可是,這樣一點都不優雅。框架
每個Handler
內部都須要調用record函數
,而後再把須要記錄的path
做爲參數傳進record函數
中。函數
若是這樣作,無論咱們須要添加什麼樣的額外功能,都必須得把這個額外的功能和咱們的業務邏輯緊緊地綁定到一塊兒,不能實現擴展功能與業務邏輯間的解耦。post
既然在上面的實現中,記錄日誌和業務實現徹底的耦合在了一塊兒,那麼咱們能不能把他們的業務實現解耦開來呢?性能
來看這段代碼:ui
func record(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
method := r.Method
fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + method + " " + path)
}
func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
record(w ,r)
fmt.Fprintf(w, "Hello World !")
}
複製代碼
在這裏,咱們已經把業務實現和日誌記錄的耦合給解開了一部分。
咱們只須要在業務代碼中,調用record(w,r)
函數,把請求的內容做爲參數傳進record函數
中,而後在record
這個方法內記錄日誌。這個時候,咱們能夠在方法內部任意的處理請求,保存如請求路徑、請求方法等數據。而這個過程,對業務實現是透明的。
這樣作的話,咱們只須要在處理業務邏輯的Handler
中調用函數,而後把參數傳進去。而這個函數的具體實現,則是與業務邏輯無關的。
那麼,有沒有辦法能夠把業務邏輯和擴展功能徹底分開,讓業務代碼裏只有業務代碼,使代碼變得更加整潔呢?咱們接着往下看。
咱們在上一篇文章裏面,分析了httprouter這個包的實現。因此咱們直接對他動手,修改他的代碼,增長一個AddBeforeHandle
方法,使得這個路由具備擴展性。
注意,這裏的AddBeforeHandle方法是做者本身編寫的,具體的實現過程能夠看後文。
在此以前,咱們來看看效果:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
)
func Hello(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Hello World!\n")
}
func record(w http.ResponseWriter, r *http.Request){
path := r.URL.Path
method := r.Method
fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + method + " " + path)
}
func main() {
router := httprouter.New()
router.AddBeforeHandle(record)
router.GET("/hello", Hello)
log.Fatal(http.ListenAndServe(":8080", router))
}
複製代碼
這部分的代碼和上一篇的幾乎徹底同樣。也是建立一個路由,將/hello
這個路徑和Hello
這個處理器綁定在GET
的這顆前綴樹中,而後開始監聽8080端口。
這裏比較重要的是main方法
裏面的第二行:
router.AddBeforeHandle(record)
複製代碼
從方法名能夠看出,這個方法是在Handle以前增長了一個處理過程。
再看看參數,就是咱們上面提到的記錄訪問日誌的方法,這個方法記錄了請求的URL
,請求的方法,以及時間。
而在咱們的Hello(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
函數中,已經不包含任何其餘的業務邏輯了。
此時,這個Handler
只專一於處理業務邏輯,至於別的,交給別的函數去實現。這樣,就實現了徹底的解耦。
下面咱們來看看具體的實現過程:
先來看看AddBeforeHandle
這個方法:
func (r *Router) AddBeforeHandle(fn func(w http.ResponseWriter, req *http.Request)) {
r.beforeHandler = fn
}
複製代碼
這個方法很簡單,也就是接收一個處理器類型的參數,而後賦值給Router
中的字段beforeHandler
。
這個名爲beforeHandler
字段也是咱們新增在Router
中的,相信你也能看得出來了,所謂的AddBeforeHandle
方法,就是把咱們傳進去的處理函數,保存在Router
中,在須要的時候調用他。
那麼咱們來看看,何時會調用這個方法。下面列出的這個方法,在上一篇文章有提到,是關於httprouter
是如何處理路由的:
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
if root := r.trees[req.Method]; root != nil {
if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
if r.beforeHandler != nil{
r.beforeHandler(w, req)
}
if ps != nil {
handle(w, req, *ps)
r.putParams(ps)
} else {
handle(w, req, nil)
}
return
}
}
...
}
複製代碼
注意看,router在找到了Handler,準備執行以前,咱們添加了這麼幾行:
if r.beforeHandler != nil{
r.beforeHandler(w, req)
}
複製代碼
也就是說,若是咱們以前調用了AddBeforeHandle
方法,給beforeHandler
這個字段賦了值,那麼他就不會爲nil
,而後調用這個函數。這也就實現了咱們的目的,在處理請求以前,先執行咱們設置的函數。
如今咱們已經實現了一個徹底解耦的中間件。而且,這個中間件是能夠任意配置的。你能夠拿來作日誌記錄,也能夠作權限校驗等等,並且這些功能還不會對Handler中的業務邏輯形成影響。
若是你是個Java開發者,你可能會以爲這個很像Filter
,或者是AOP
。
可是,和過濾器不一樣的是,咱們不只能夠在請求到來以前處理,也能夠在請求完成以後處理。好比這個請求發生了一些panic
,你能夠在最後處理它,或者你能夠記錄這個請求的時間等等,你要作的,只是在Handle
方法以後,調用你所註冊的方法。
好比:
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
if root := r.trees[req.Method]; root != nil {
if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
if r.beforeHandler != nil{
r.beforeHandler(w, req)
}
if ps != nil {
handle(w, req, *ps)
r.putParams(ps)
} else {
handle(w, req, nil)
}
if r.afterHandler != nil {
r.afterHandler(w, req)
}
return
}
}
...
}
複製代碼
咱們只是添加了一個afterHandler
方法,就是這麼的簡單。
那麼問題來了:如今這樣的處理操做,咱們僅僅只能在請求前和請求後各自添加一箇中間件。若是咱們想要添加任意多箇中間件,該怎麼作呢?
能夠先本身思考一下,而後咱們來看看在gin
中,是怎麼實現的。
衆所周知,在閱讀源碼以前,必定要先看看他是怎麼用的:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func Hello(ctx *gin.Context) {
fmt.Fprint(ctx.Writer, "Hello World!\n")
}
func main() {
router := gin.New()
router.Use(gin.Logger(), gin.Recovery())
router.GET("/hello", Hello)
router.Run(":8080")
}
複製代碼
能夠看到,在gin
中,使用中間件的方法和上文中咱們所設計的是差很少的。都是業務和中間件徹底解耦,而且在註冊路由的時候,添加進去。
可是咱們注意到,在gin
中是不分Handle
以前仍是Handle
以後的。那麼他是如何作到的呢,咱們來看看源碼。
先從Use方法看起:
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
複製代碼
在這裏咱們先無論group
這個東西,他是路由分組,和咱們這篇文章沒有關係,咱們先無論他。咱們只須要看到append
方法。Use方法就是把參數裏面的函數,所有增長到group.Handlers
中。這裏的group.Handlers
,是一個Handler
類型的數組。
因此,在gin中,每個中間件,也是Handler
類型的。
在上一節咱們留了一個問題,要怎麼實現多箇中間件。答案就在這裏了,用數組保存。
那麼問題又來了:怎麼保證調用的順序呢?
咱們繼續往下看看路由的註冊:
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
複製代碼
這裏是否是也有點熟悉呢?和上一篇文章提到的httprouter
很類似,咱們直接看group.handle
:
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
複製代碼
在這段代碼中,第一行關於path
的咱們先無論,這個也是和路由分組有關的,簡單來講就是拼接出完整的請求path
。
先看看第二行,方法名是combineHandlers
,咱們能夠猜想一下這個方法的做用,把各個Handler結合起來。看看詳細的代碼:
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
複製代碼
先解釋一下,這裏返回的HandlersChain
類型,是Handler
的數組。
也就是說,在這個方法裏面,把以前放入group中的中間件,和當前路由的Handler,組合成一個新的數組。
而且,中間件在前面,路由Handler在後面。注意,這個順序很重要。
而後咱們繼續往下,執行完這個方法以後執行的就是addRoute
方法了。在這裏不展開講。因此最重要的是,這裏把中間件和Handler全都組合在了一塊兒,綁定到了這個前綴樹上。
到了這裏註冊方面的內容已經結束了,咱們來看看他是怎麼處理各個中間件的調用順序。
由於咱們的目的是看路由是怎麼處理請求的,因此咱們直接看gin
的ServeHTTP
方法:
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
複製代碼
這裏要注意的是*Context
,他是對請求的封裝,包含了有responseWriter
,*http.Request
等。
咱們繼續往下看看handleHTTPRequest(c)
這個方法:
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
...
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
...
}
...
}
複製代碼
在這個方法中,其實和以前咱們研究的httprouter
是很類似的。也是先根據請求方法
找到相對應的前綴樹,而後獲取相對應的Handler
,並把獲取到的handler
數組保存在Context
中。
這裏咱們注意看c.Next()方法,他是gin中關於中間件的調用最精妙的部分。咱們來看看:
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
複製代碼
咱們能夠看到,當調用這個Next()
方法的時候,會增長保存在Context
中的下標,而後根據這個下標的順序執行handler
。
而在前面咱們有提到,咱們把中間件排在了這個handler
數組的前面,先執行中間件,而後最後纔是執行用戶自定義的handler
。
咱們再來看看日誌記錄這個中間件:
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
...
return func(c *Context) {
//開始計時
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
c.Next()
...
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
...
}
}
複製代碼
能夠看到,先開始計時,而後調用了c.Next()
這個方法,而後才結束計時。
那麼咱們能夠由此推斷,c.Next()
後面的代碼,是執行完用戶自定義的Handler
才執行的。
也就是說,其實中間件的業務邏輯是這樣的:
func Middleware(c *gin.Context){
//請求前執行
c.Next()
//請求後執行
}
複製代碼
首先,謝謝你能看到這裏。
簡單的來說,咱們應該考慮解耦合,使得業務代碼能夠專一於業務,中間件專一於實現功能。爲了實現這點,咱們能夠修改路由的實現邏輯,在執行Handler
的先後加入中間件的調用。
在本文中,可能會有不少的疏漏。若是在閱讀的過程當中,有哪些解釋不到位,或者做者的理解出現了一些差錯,也請你留言指正。
再次感謝~
PS:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~