到目前为止我们已经完成了 Web 框架的基础部分,使用 net/http 启动了一个 Web 服务,并且定义了自己的 Context,可以控制请求超时。
在前面的controller.go中有一个超时控制逻辑:
func FooControllerHandler(c *framework.Context) error {
...
// 在业务逻辑处理前,创建有定时器功能的 context
durationCtx, cancel := context.WithTimeout(c.BaseContext(), time.Duration(1*time.Second))
defer cancel()
go func() {
...
// 执行具体的业务逻辑
time.Sleep(10 * time.Second)
// ...
finish <- struct{}{}
}()
// 在业务逻辑处理后,操作输出逻辑...
select {
...
case <-finish:
fmt.Println("finish")
...
}
return nil
}
这部分由业务逻辑- time.Sleep 函数所代表的逻辑 和非业务逻辑- 比如创建 Context、通道等待 finish 信号等构成, 为了复用代码,我们将非业务逻辑抽象处理,封装好,对外提供使用,这就是中间件。
代码的组织顺序很清晰,先预处理请求,再处理业务逻辑,最后处理返回值,很符合设计模式中的装饰器模式。装饰器模式,顾名思义,就是在核心处理模块的外层增加一个又一个的装饰,类似洋葱。
现在,抽象出中间件的思路是不是就很清晰了,把核心业务逻辑先封装起来,然后一层一层添加装饰,最终让所有请求正序一层层通过装饰器,进入核心处理模块,再反序退出装饰器。原理就是这么简单,不难理解,我们接着看该如何实现。
使用函数嵌套方式实现中间件
装饰器模式是一层一层的,所以具体实现其实也不难想到,就是使用函数嵌套。
首先,我们封装核心的业务逻辑。就是说,这个中间件的输入是一个核心的业务逻辑 ControllerHandler,输出也应该是一个 ControllerHandler。所以对于一个超时控制器,我们可以定义一个中间件为 TimeoutHandler。
func TimeoutHandler(fun ControllerHandler, d time.Duration) ControllerHandler {
// 使用函数回调
return func(c *Context) error {
finish := make(chan struct{}, 1)
panicChan := make(chan interface{}, 1)
// 执行业务逻辑前预操作:初始化超时 context
durationCtx, cancel := context.WithTimeout(c.BaseContext(), d)
defer cancel()
c.request.WithContext(durationCtx)
go func() {
defer func() {
if p := recover(); p != nil {
panicChan <- p
}
}()
// 执行具体的业务逻辑
fun(c)
finish <- struct{}{}
}()
// 执行业务逻辑后操作
select {
case p := <-panicChan:
log.Println(p)
c.responseWriter.WriteHeader(500)
case <-finish:
fmt.Println("finish")
case <-durationCtx.Done():
c.SetHasTimeout()
c.responseWriter.Write([]byte("time out"))
}
return nil
}
}
中间件函数的返回值是一个匿名函数,这个匿名函数实现了 ControllerHandler 函数结构,参数为 Context,返回值为 error。
则注册路由就可以这样写了:
// 在核心业务逻辑 UserLoginController 之外,封装一层 TimeoutHandler
core.Get("/user/login", framework.TimeoutHandler(UserLoginController, time.Second))
这种函数嵌套方式,让下层中间件是上层中间件的参数,通过一层层嵌套实现了中间件的装饰器模式。
但是有两个问题:
- 中间件是循环嵌套的,当有多个中间件的时候,整个嵌套长度就会非常长,非常不优雅的
TimeoutHandler(LogHandler(recoveryHandler(UserLoginController)))
- 刚才的实现,只能为单个业务控制器设置中间件,不能批量设置
使用 pipeline 思想改造中间件
一层层嵌套不好用,如果我们将每个核心控制器所需要的中间件,使用一个数组链接(Chain)起来,形成一条流水线(Pipeline),就能完美解决这两个问题了。
这个 Pipeline 模型和前面的洋葱模型不一样的点在于,Middleware 不再以下一层的 ControllerHandler 为参数了,它只需要返回有自身中间件逻辑的 ControllerHandler。
// 超时控制器参数中ControllerHandler结构已经去掉
func Timeout(d time.Duration) framework.ControllerHandler {
// 使用函数回调
return func(c *framework.Context) error {
//...
}
}
我们可以将每个中间件构造出来的 ControllerHandler 和最终的业务逻辑的 ControllerHandler 结合在一起,成为一个 ControllerHandler 数组,也就是控制器链。在最终执行业务代码的时候,能一个个调用控制器链路上的控制器。
这个想法其实是非常自然的,因为中间件中创造出来的 ControllerHandler 匿名函数,和最终的控制器业务逻辑 ControllerHandler,都是同样的结构,所以我们可以选用 Controllerhander 的数组,来表示某个路由的业务逻辑。
第一步,我们需要修改路由节点 node。
在 node 节点中将原先的 Handler,替换为控制器链路 Handlers。
第二步,我们修改 Context 结构。
在中间件注册的回调函数中,只有 framework.Context 这个数据结构作为参数,所以在 Context 中也需要保存这个控制器链路 (handlers),并且要记录下当前执行到了哪个控制器(index)。
第三步,来实现链条调用方式。
为了控制实现链条的逐步调用,我们为 Context 实现一个 Next 方法。这个 Next 方法每调用一次,就将这个控制器链路的调用控制器,往后移动一步。继续在框架文件夹中的 context.go 文件里写:
// 核心函数,调用context的下一个函数
func (ctx *Context) Next() error {
ctx.index++
if ctx.index < len(ctx.handlers) {
if err := ctx.handlers[ctx.index](ctx); err != nil {
return err
}
}
return nil
}
Next() 函数会在框架的两个地方被调用:
- 第一个是在此次请求处理的入口处,即 Core 的 ServeHttp;
- 第二个是在每个中间件的逻辑代码中,用于调用下个中间件。
如何注册控制器链路
- Core 和 Group 单独设计一个 Use 函数,为其数据结构负责的路由批量设置中间件
- 为 Core 和 Group 注册单个路由的 Get / Post / Put / Delete 函数,设置中间件
// core中使用use注册中间件
core.Use(
middleware.Test1(),
middleware.Test2())
// group中使用use注册中间件
subjectApi := core.Group("/subject")
subjectApi.Use(middleware.Test3())
// 注册路由规则
func registerRouter(core *framework.Core) {
// 在core中使用middleware.Test3() 为单个路由增加中间件
core.Get("/user/login", middleware.Test3(), UserLoginController)
// 批量通用前缀
subjectApi := core.Group("/subject")
{
...
// 在group中使用middleware.Test3() 为单个路由增加中间件
subjectApi.Get("/:id", middleware.Test3(), SubjectGetController)
}
}
基本的中间件: Recovery
// recovery机制,将协程中的函数异常进行捕获
func Recovery() framework.ControllerHandler {
// 使用函数回调
return func(c *framework.Context) error {
// 核心在增加这个recover机制,捕获c.Next()出现的panic
defer func() {
if err := recover(); err != nil {
c.Json(500, err)
}
}()
// 使用next执行具体的业务逻辑
c.Next()
return nil
}
}
【小结】
- 中间件函数返回的也是ControlHandler,因此将中间件和路由处理函数作成一个链表,一起挂在node节点上
- 查找到路由函数后,之前是一个函数直接调用即可,现在是一个链表,要按顺序逐个调用,因为函数都是接收ctx参数,返回err,因此要将此链表也存放到ctx中,并且再存一个idx字段表示执行到哪一步骤。具体的执行需要定义一个ctx.Next()方法,每个中间件内部要调用此方法,才能实现链式调用
- 注册路由中间件时,有批量Use方式和单个Get、Post、Put、Delete方式,最终的目的都是将所有中间件和处理函数作为链表挂到node节点上,以待后代找到链表并逐个调用。