Day5:中间件
今天的任务是:
- 设计并实现 Web 框架的中间件(Middlewares)机制。
- 实现通用的
Logger
中间件,能够记录请求到响应所花费的时间,代码约 50 行。
中间件是什么
中间件简单来说就是非业务的技术类组件。Web 框架本身不可能理解所有的业务,因而不可能实现所有的功能。因此,框架需要一个接口,允许用户自定义功能,并嵌入到框架当中,仿佛这个功能就是框架原生支持的一样。因此,对中间件而言,需要考虑以下两个关键的点:
- 插入点在哪?使用框架的人不关心底层逻辑的具体实现,如果插入点太底层,中间件的逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 种调用相比没有太大的优势;
- 中间件的输入是什么?中间件的输入,决定了拓展能力。暴露的参数太少,用户发挥的空间有限。
Gee 参考 Gin,设计了中间件。
中间件设计
Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是 Context 对象。插入点是框架接收到请求初始化 Context 对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对 Context 进行二次加工。
另外通过调用(*Context).Next()
函数,中间件可等待用户自定义的 Handler 结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求处理的前后,做一些额外的操作。
例如,我们希望最终支持如下定义的中间件,c.Next()
表示等待执行其它的中间件或用户的 Handler:
func Logger() HandlerFunc {
return func(c *Context) {
// Start timer
t := time.Now()
// Process request
c.Next()
// Calculate resolution time
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
另外,支持设置多个中间件,依次进行调用。
在昨天的分组控制中提到,中间件是应用在 RouterGroup
上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。
按照之前我们设计 Gee 框架的逻辑,当接收到请求之后,首先进行路由匹配,该请求的所有信息都将会存储在 Context 当中。中间件也不例外,接收到请求后,应查找所有应作用于该路由的中间件,保存在 Context 中,依次进行调用。**保存在 Context 当中的原因是,**在设计中,中间件不仅作用在处理流程前,还有可能作用在处理流程后,即在用户定义的 Handler 处理完毕后,还可以执行剩下的操作。
为此,我们给 Context 添加两个参数,定义 Next 方法:
type Context struct {
// origin objects
// 封装了 http.ResponseWriter 和 *http.Request
Writer http.ResponseWriter
Req *http.Request
// request info
Path string
Method string
Params map[string]string
// response info
StatusCode int
// middleware
handlers []HandlerFunc
index int
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Writer: w,
Req: req,
Path: req.URL.Path,
Method: req.Method,
index: -1,
}
}
func (c *Context) Next() {
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
index 记录的是当前执行到了第几个中间件,当在中间件中调用 Next 方法,控制权就交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在 Next 方法之后定义的部分。
这部分内容直接参考了 Geektutu 大佬博客的原文:
最后一句话是重点,最终的调用顺序是part1 -> part3 -> Handler -> part4 -> part2
。原因是在执行 part1 之后调用了 Next,在 part3 之后也调用了 Next。
代码实现
定义 Use 函数,将中间件应用到某个 Group:
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var middlewares []HandlerFunc
for _, group := range engine.groups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...)
}
}
c := newContext(w, req)
c.handlers = middlewares
engine.router.handle(c)
}
ServeHTTP 函数也有一些变换,当我们接收到一个具体请求时,需要判断该请求适用于哪些中间件,在这里我们简单通过 URL 的前缀来判断。得到中间件列表后,赋值给 c.handlers
。
在 handle 函数中,将从路由匹配得到的 Handler 添加到 c.handlers
列表中,执行 c.Next()
。
func (r *router) handle(c *Context) {
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
key := c.Method + "-" + n.pattern
c.Params = params
c.handlers = append(c.handlers, r.handlers[key])
} else {
c.handlers = append(c.handlers, func(c *Context) {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
})
}
c.Next()
}
Demo
注意,首先我们应该在gee/context.go
中补充 *Context
对象的 Fail 方法:
func (c *Context) Fail(code int, err string) {
c.index = len(c.handlers)
c.JSON(code, H{"message": err})
}
之后更新 main 函数:
package main
import (
"gee/gee"
"log"
"net/http"
"time"
)
func onlyForV2() gee.HandlerFunc {
return func(c *gee.Context) {
// Start timer
t := time.Now()
// if a server error occurred
c.Fail(500, "Internal Server Error")
// Calculate resolution time
log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
func main() {
r := gee.New()
r.Use(gee.Logger()) // global middleware
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
v2 := r.Group("/v2")
v2.Use(onlyForV2()) // v2 group middleware
{
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
}
r.Run(":9999")
}
由于我们使用了 OnlyForV2
中间件,因此在访问/hello/:name
(:name
是可选的参数)时,浏览器显示的结果如下:
{"message":"Internal Server Error"}
同时,命令行将会记录日志信息:
2025/02/22 08:19:06 [200] /v2/hello/123 in 0s
将v2.Use(onlyForV2()) // v2 group middleware
注释掉即可正常显示,验证了中间件的有效性。