GoFrame提供了优雅的中间件请求控制方式,该方式也是主流的WebServer提供的请求流程控制方式,基于中间件设计可以为WebServer提供更灵活强大的插件机制。经典的中间件洋葱模型:

中间件定义

中间件的定义和普通HTTP执行方法HandlerFunc一样,但是可以在Request参数中使用Middleware属性对象来控制请求流程。

我们拿一个跨域请求的中间件定义来示例说明一下:

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

可以看到在该中间件中执行完成跨域请求处理的逻辑后,使用r.Middleware.Next()方法进一步执行下一个流程;如果这个时候直接退出不调用r.Middleware.Next()方法的话,将会退出后续的执行流程(例如可以用于请求的鉴权处理)。

中间件类型

中间件的类型分为两种:前置中间件和后置中间件。前置即在路由服务函数调用之前调用,后置即在其后调用。

前置中间件

其定义类似于:

func Middleware(r *ghttp.Request) {
	// 中间件处理逻辑
	r.Middleware.Next()
}

后置中间件

其定义类似于:

func Middleware(r *ghttp.Request) {
	r.Middleware.Next()
	// 中间件处理逻辑
}

中间件注册

中间件的注册有多种方式,参考接口文档: https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp

全局中间件

func (s *Server) Use(handlers ...HandlerFunc)

全局中间件是可以独立使用的请求拦截方法,通过路由规则的方式进行注册,绑定到Server上,由于中间件需要执行请求拦截操作,因此往往是使用"模糊匹配"或者"命名匹配"规则。 

全局中间件仅对动态请求拦截有效,无法拦截静态文件请求。

分组路由中间件

func (g *RouterGroup) Middleware(handlers ...HandlerFunc) *RouterGroup

分组路由中注册的中间件绑定到当前分组路由中的所有的服务请求上,当服务请求被执行前会调用到其绑定的中间件方法。 分组路由仅有一个Middleware的中间件注册方法。分组路由中间件与全局中间件不同之处在于,分组路由中间件无法独立使用,必须在分组路由注册中使用,并且绑定到当前分组路由中所有的路由上作为路由方法的一部分。

执行优先级

全局中间件

由于全局中间件也是通过路由规则执行,那么也会存在执行优先级:

  1. 首先,由于全局中间件是基于模糊路由匹配,因此当同一个路由匹配到多个中间件时,会按照路由的深度优先规则执行,具体请查看路由章节;
  2. 其次,同一个路由规则下,会按照中间件的注册先后顺序执行,中间件的注册方法也支持同时按照先后顺序注册多个中间件;
  3. 最后,为避免优先级混淆和后续管理,建议将所有中间件放到同一个地方进行先后顺序注册来控制执行优先级;

这里的建议来参考于gRPC的拦截器设计,没有过多的路由控制,仅在一个地方同一个方法统一注册。往往越简单,越容易理解,也便于长期维护。

分组路由中间件

分组路由中间件是绑定到分组路由上的服务方法,不存在路由规则匹配,因此只会按照注册的先后顺序执行。参考后续示例或如下代码的执行结果。

package main

import (
	"context"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/gogf/gf/v2/os/glog"
)

type HelloReq struct {
	g.Meta `path:"/hello" method:"get"`
}
type HelloRes struct {
}

type Hello struct{}

func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) {
	glog.Debug(ctx, "中")
	return
}

func RequestHandle1(r *ghttp.Request) {
	glog.Debug(r.GetCtx(), "前1")
	r.Middleware.Next()
}

func RequestHandle2(r *ghttp.Request) {
	glog.Debug(r.GetCtx(), "前2")
	r.Middleware.Next()
}

func RequestHandle3(r *ghttp.Request) {
	glog.Debug(r.GetCtx(), "前3")
	r.Middleware.Next()
}

func RequestHandle4(r *ghttp.Request) {
	glog.Debug(r.GetCtx(), "前4")
	r.Middleware.Next()
}

func ResponseHandle1(r *ghttp.Request) {
	r.Middleware.Next()
	glog.Debug(r.GetCtx(), "后1")
}

func ResponseHandle2(r *ghttp.Request) {
	r.Middleware.Next()
	glog.Debug(r.GetCtx(), "后2")
}

func ResponseHandle3(r *ghttp.Request) {
	r.Middleware.Next()
	glog.Debug(r.GetCtx(), "后3")
}

func ResponseHandle4(r *ghttp.Request) {
	r.Middleware.Next()
	glog.Debug(r.GetCtx(), "后4")
}

func main() {
	s := g.Server()
	s.Use(ghttp.MiddlewareHandlerResponse)
	s.Group("/", func(group *ghttp.RouterGroup) {
		// 前置中间件
		group.Middleware(RequestHandle1)
		group.Middleware(RequestHandle2)

		// 后置中间件
		group.Middleware(ResponseHandle1)
		group.Middleware(ResponseHandle2)

		group.Group("/sub", func(group *ghttp.RouterGroup) {
			// 前置中间件
			group.Middleware(RequestHandle3)
			group.Middleware(RequestHandle4)

			// 后置中间件
			group.Middleware(ResponseHandle3)
			group.Middleware(ResponseHandle4)

			group.Bind(new(Hello))
		})
	})
	s.Run()
}

执行结果如下:

执行中断

分组路由中间件中,我们可以在前置中间件Next()调用之前通过return对本次请求进行中断,在打断后,后续所有前置中间件同级及子级后置中间件请求处理方法将不会被执行。

如上面用来展示分组路由中间件优先级的代码中:

  • RequestHandle1Next()调用前进行中断,则只会执行RequestHandle1
  • RequestHandle2Next()调用前进行中断,则只会执行RequestHandle1RequestHandle2
  • RequestHandle3Next()调用前进行中断,则会执行RequestHandle1RequestHandle2RequestHandle3以及ResponseHandle2ResponseHandle1
  • RequestHandle4Next()调用前进行中断,则会执行RequestHandle1RequestHandle2RequestHandle3RequestHandle4以及ResponseHandle2ResponseHandle1

上述的中断实例都不会执行请求处理方法

除了常见的return中间件中终端后续处理流程大方式外,框架还提供了Exit相关的方法来在代码执行处强制中断执行流程,具体可参考章节:数据返回-Exit控制