内容简介:在上一篇文章中,我们已经可以实现一个性能较高,且支持RESTful风格的路由了。但是,在Web应用的开发中,我们还需要一些可以被扩展的功能。因此,在设计框架的过程中,应该留出可以扩展的空间,比如:日志记录、故障恢复等功能,如果我们把这些业务逻辑全都塞进所以在这篇文章中,我们来探究如何更优雅的设计这些中间件。
摘要
在上一篇文章中,我们已经可以实现一个性能较高,且支持RESTful风格的路由了。但是,在Web应用的开发中,我们还需要一些可以被扩展的功能。
因此,在设计框架的过程中,应该留出可以扩展的空间,比如:日志记录、故障恢复等功能,如果我们把这些业务逻辑全都塞进 Controller
/ Handler
中,会显得代码特别的冗余,杂乱。
所以在这篇文章中,我们来探究如何更优雅的设计这些中间件。
1 耦合的实现方式
比如我们要实现一个日志记录的功能,我们可以用这种 简单粗暴 的方式:
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) }
如果这样做的话,确实是实现了我们的目标,记录了访问的日志。
但是,这样一点都不优雅。
每一个 Handler
内部都需要调用 record函数
,然后再把需要记录的 path
作为参数传进 record函数
中。
如果这样做,不管我们需要添加什么样的额外功能,都必须得把这个额外的功能和我们的业务逻辑牢牢地绑定到一起,不能实现扩展功能与业务逻辑间的解耦。
2 将记录与实现解耦
既然在上面的实现中,记录日志和业务实现完全的耦合在了一起,那么我们能不能把他们的业务实现解耦开来呢?
来看这段代码:
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
中调用函数,然后把参数传进去。而这个函数的具体实现,则是 与业务逻辑无关 的。
那么,有没有办法可以把业务逻辑和扩展功能 完全分开 ,让业务代码里只有业务代码,使代码变得更加整洁呢?我们接着往下看。
3 设计中间件
我们在上一篇文章里面,分析了 httprouter 这个包的实现。所以我们直接对他动手,修改他的代码,使得这个路由具有扩展性。
3.1 效果
在此之前,我们来看看效果:
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
只 专注于 处理业务逻辑,至于别的, 交给别的函数去实现 。这样,就实现了完全的 解耦 。
下面我们来看看具体的实现过程:
3.2 具体实现
先来看看 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
,然后调用这个函数。这也就实现了我们的目的,在处理请求之前,先执行我们设置的函数。
3.3 思考
现在我们已经实现了一个 完全解耦 的中间件。并且,这个中间件是可以任意配置的。你可以拿来做日志记录,也可以做权限校验等等,而且这些功能还不会对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
中,是怎么实现的。
4 Gin的中间件
4.1 使用
总所周知,在阅读源码之前,一定要先看看他是怎么用的:
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
之后的。那么他是如何做到的呢,我们来看看源码。
4.2 源码解释
先从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() //请求后执行 }
5 写在最后
首先,谢谢你能看到这里。
简单的来讲,我们应该考虑 解耦合 ,使得业务代码可以专注于业务,中间件专注于实现功能。为了实现这点,我们可以修改路由的实现逻辑,在执行 Handler
的前后加入中间件的调用。
在本文中,可能会有很多的疏漏。如果在阅读的过程中,有哪些解释不到位,或者作者的理解出现了一些差错,也请你留言指正。
再次感谢~
PS:如果有其他的问题,也可以在公众号找到作者。并且,所有文章第一时间会在公众号更新,欢迎来找作者玩~
以上所述就是小编给大家介绍的《Golang Web入门(3):如何优雅的设计中间件》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。