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.HandlerFunctype Frame struct { // 存储所有注册的过滤器 filters []Filter}// AddFilter 注册filterfunc (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治理框架