当前位置: 首页 > 工具软件 > Echo Go > 使用案例 >

golang web网络框架echo中间件执行顺序问题分析

梁福
2023-12-01
  • 学习echo源码
    • 学习echo.go(servehttp+start+add)
    • 学习router.go(insert+find)
    • 学习group.go(struct+group+add)
    • 学习middleware(middleware chain and custom middleware)

概述

echo框架最巧妙的地方在于echo内部结构体的设计,核心的地方在于路由是如何通过lcp(最长前缀匹配)插入到字典树中,又是如何通过lcp找到对应的路由并且通过链式结构保重中间件按照顺序运行的设计思路,比如:

  • group的处理方式,group的结构体中保存有echo实例,但是echo结构体中不含有group,聪明的同学从这里就可以猜出来,这种结构体的设计说明他的所有路由都是插入到echo实例中的,和group没有任何关系,group唯一做的事情就是提供了组级的中间件和prefix,而且从对于echo来说最多只有1级goup子组,在创建2级子组时他会把上一级的group前缀和中间件都取出来,在重新重goup.echo.group中创建一个1级子组
  • 他的echo结构体中用router保存该实例下的所有路由,但是router结构体中有保存有这个echo的实例,形成了一个死循环可以一直套娃套下去(这种无限套娃的设计看起来似乎不是很优雅,但是好像也没有更好的办法了)

中间件

echo的中间件逻辑很有趣,他采用了一种链式的结构来保证各种级别的中间件(根组级,组级,路由级)的执行顺序,下面我将以group组下的路由注册为例,来解释中间件的如何保证顺序的,示例代码如下

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	//声明echo实例
	e := echo.New()

	//注册根组中间件
	e.Use(middleware.Recover())

	//创建一个子组/admin
	g := e.Group("/admin")

	//在使用子组中间件之前注册一个路由
	g.GET("/", hello)

	//组级中间件
	g.Use(middleware.Logger())

	//在使用子组中间件之后注册一个路由
	g.GET("/log", log)

	//启动echo
	e.Logger.Fatal(e.Start(":1323"))
}

func hello(c echo.Context) error {
	return c.String(http.StatusOK, "hello world!")
}
func log(c echo.Context) error {
	return c.String(http.StatusOK, "logger active")
}

1. 注册子组
可以看到注册子组的时候,并没有在echo中加入子组,子组也没有继承根组的所有中间件,仅仅是声明了group结构体

// Group creates a new router group with prefix and optional group-level middleware.
func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
	g = &Group{prefix: prefix, echo: e}
	g.Use(m...)
	return
}

2.注册子组级的路由
所有的GET,POST,PATCH,PULL等方法都会调用add来加入路由中,这里的h就是我们的逻辑处理函数hello

// GET implements `Echo#GET()` for sub-routes within the Group.
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
	return g.Add(http.MethodGet, path, h, m...)
}

3.添加路由
可以看到在添加路由的时候,他把当前路由特有的中间件,而在这个子组级别声明的所有中间件都频道了一起,并且通过前缀prefix的拼接拼出来了全路径,然后调用了echo.add加入到echo实例中的路由,而不是加入到group级别的路由

// Add implements `Echo#Add()` for sub-routes within the Group.
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
	// Combine into a new slice to avoid accidentally passing the same slice for
	// multiple routes, which would lead to later add() calls overwriting the
	// middleware from earlier calls.
	m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware))
	m = append(m, g.middleware...)
	m = append(m, middleware...)
	return g.echo.add(g.host, method, g.prefix+path, handler, m...)
}

4.加入字典树
middware:这里的中间件有2个来源
1种是来自于直接调用e.add(),此时middleware是表示来自于路由级别的专属中间件,当这个路由被匹配到的时候触发
2中是来自于group.echo.add(),此时的middleware是来自路由中间件+组中间件 按先后顺序拼起来的,当这个路由被撇配到的时候触发
也就是说不论是嵌套了多少层的group他最后的路由都是加在根组echo中,并且会把每一层的·组中间件·+ 最后的·路由中间件· 全都拼起来
这样如果组嵌套的很深,组中间件很多,路由很多的时候,就会有大量的重复中间件保存在hanlder中
这里就能看出来他把组中间件也作为handler一起加入了路由的字典树中,并且已经拍好了顺序,可以判断这个逻辑就是导致为什么echo,use没有顺序,而group,use有顺序

//host:""
//path:"/admin"
//handler: 处理函数
func (e *Echo) add(host, method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
	//从reflect中获取函数名,使用runtime运行时指针指向的函数获取函数名
	name := handlerName(handler)
	//检查当前的host有没有对应的router,新建host的时候会创建和这个hostname对应的router,但是新建的host路由只能在group层
	router := e.findRouter(host)
	//加上这句可以保证echo.use和注册路由的先后顺序
	//middleware = append(middleware, e.middleware...)

	//加入字典树
	router.Add(method, path, func(c Context) error {
		h := applyMiddleware(handler, middleware...)
		return h(c)
	})

	//创建新的路由结构体
	r := &Route{
		Method: method,
		Path:   path,
		Name:   name,
	}

	//map结构,方法和路径 用于唯一确定一个route,
	//方法+路由 -> 处理的函数名
	e.router.routes[method+path] = r
	return r
}

5.创建中间件链结构
把中间件和处理函数组成chain
其中h作为最后一个执行的处理函数,同时h本身也可以作为一个链式结构的hanlder,只要形如:
func(c context) error{
h := applyMiddleware(hanlder,middleware)
return h©
}
middleware则按照声明顺序执行,通过倒序的遍历把hanlder函数传入middleware可以让每一个传入的hanlder作为next参数传入middlerware内的hanlder函数并形成链式结构
返回值作为第一个需要执行的hanlder其中的next函数和next函数的next函数…都已经赋值完成,通过h© 即可触发中间件的链式执行


func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {
	for i := len(middleware) - 1; i >= 0; i-- {
		h = middleware[i](h)
	}
	return h
}

6.路由匹配

  • 可以看到这里把echo中注册的所有中间件都加进来了,说明对一个路由来说,不论是在e.use前还是e.use后,只要是加入的中间件都会执行不论先后
  • 根本原因是 没有把echo根组下的中间件像那些·组中间件·和·路由中间件·在注册的时候加入进handler,而是在请求来的时候match的时候加入,而这时候所有的use早就执行完了,所以没法区分出先后顺序了
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Acquire context
	c := e.pool.Get().(*context)
	c.Reset(r, w)
	h := NotFoundHandler

	if e.premiddleware == nil {
		e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
		//这里的h就是注册路由的时候 存在 methodhandler映射表中的处理函数 是集成了 处理函数(可选)+路由中间件(可选)+组中间件(可选)
		h = c.Handler()
		
		h = applyMiddleware(h, e.middleware...)
	} else {
		h = func(c Context) error {
			e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
			h := c.Handler()
			h = applyMiddleware(h, e.middleware...)
			return h(c)
		}
		h = applyMiddleware(h, e.premiddleware...)
	}

	// Execute chain
	if err := h(c); err != nil {
		e.HTTPErrorHandler(err, c)
	}

	// Release context
	e.pool.Put(c)
}
 类似资料: