04 中间件-提高框架的可拓展性

到目前为止我们已经完成了 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))

这种函数嵌套方式,让下层中间件是上层中间件的参数,通过一层层嵌套实现了中间件的装饰器模式。
但是有两个问题:

  1. 中间件是循环嵌套的,当有多个中间件的时候,整个嵌套长度就会非常长,非常不优雅的
TimeoutHandler(LogHandler(recoveryHandler(UserLoginController)))
  1. 刚才的实现,只能为单个业务控制器设置中间件,不能批量设置

使用 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
  }
}

【小结】

  1. 中间件函数返回的也是ControlHandler,因此将中间件和路由处理函数作成一个链表,一起挂在node节点上
  2. 查找到路由函数后,之前是一个函数直接调用即可,现在是一个链表,要按顺序逐个调用,因为函数都是接收ctx参数,返回err,因此要将此链表也存放到ctx中,并且再存一个idx字段表示执行到哪一步骤。具体的执行需要定义一个ctx.Next()方法,每个中间件内部要调用此方法,才能实现链式调用
  3. 注册路由中间件时,有批量Use方式和单个Get、Post、Put、Delete方式,最终的目的都是将所有中间件和处理函数作为链表挂到node节点上,以待后代找到链表并逐个调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值