• 从零实现Web框架Geo教程-中间件-05



    本教程参考:七天用Go从零实现Web框架Gee教程


    中间件是什么

    中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。因此,对中间件而言,需要考虑2个比较关键的点:

    • 插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。
    • 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。

    那对于一个 Web 框架而言,中间件应该设计成什么样呢?接下来的实现,基本参考了 Gin 框架。


    中间件设计

    Geo 的中间件的定义与路由映射的 Handler 一致,处理的输入是Context对象。插入点是框架接收到请求初始化Context对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context进行二次加工。另外通过调用(*Context).Next()函数,中间件可等待用户自己定义的 Handler处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Geo 的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,c.Next()表示等待执行其他的中间件或用户的Handler:

    • logger.go
    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))
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    另外,支持设置多个中间件,依次进行调用。

    我们上一篇文章分组控制 Group Control中讲到,中间件是应用在RouterGroup上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。那为什么不作用在每一条路由规则上呢?作用在某条路由规则,那还不如用户直接在 Handler 中调用直观。只作用在某条路由规则的功能通用性太差,不适合定义为中间件。

    我们之前的框架设计是这样的,当接收到请求后,匹配路由,该请求的所有信息都保存在Context中。中间件也不例外,接收到请求后,应查找所有应作用于该路由的中间件,保存在Context中,依次进行调用。为什么依次调用后,还需要在Context中保存呢?因为在设计中,中间件不仅作用在处理流程前,也可以作用在处理流程后,即在用户定义的 Handler 处理完毕后,还可以执行剩下的操作。

    为此,我们给Context添加了2个参数,定义了Next方法:

    • context.go
    //Context 内部维护当前请求的一系列信息
    type Context struct {
    	//req和res
    	Writer http.ResponseWriter
    	Req    *http.Request
    	//关于请求的相关信息
    	Path   string
    	Method string
    	Params map[string]string
    	//关于响应相关信息
    	StatusCode int
    	//中间件
    	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)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    index是记录当前执行到第几个中间件,当在中间件中调用Next方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在Next方法之后定义的部分。如果我们将用户在映射路由时定义的Handler添加到c.handlers列表中,结果会怎么样呢?想必你已经猜到了。

    func A(c *Context) {
        part1
        c.Next()
        part2
    }
    func B(c *Context) {
        part3
        c.Next()
        part4
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    假设我们应用了中间件 A 和 B,和路由映射的 Handler。c.handlers是这样的[A, B, Handler],c.index初始化为-1。调用c.Next(),接下来的流程是这样的:

    • c.index++,c.index 变为 0
    • 0 < 3,调用 c.handlers[0],即 A
    • 执行 part1,调用 c.Next()
    • c.index++,c.index 变为 1
    • 1 < 3,调用 c.handlers[1],即 B
    • 执行 part3,调用 c.Next()
    • c.index++,c.index 变为 2
    • 2 < 3,调用 c.handlers[2],即Handler
    • Handler 调用完毕,返回到 B 中的 part4,执行 part4
    • part4 执行完毕,返回到 A 中的 part2,执行 part2
    • part2 执行完毕,结束。

    一句话说清楚重点,最终的顺序是part1 -> part3 -> Handler -> part 4 -> part2。恰恰满足了我们对中间件的要求,接下来看调用部分的代码,就能全部串起来了。


    代码实现

    定义Use函数,将中间件应用到某个 Group 。

    • geo.go
    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ServeHTTP 函数也有变化,当我们接收到一个具体请求时,要判断该请求适用于哪些中间件,在这里我们简单通过 URL 的前缀来判断。得到中间件列表后,赋值给 c.handlers

    handle 函数中,将从路由匹配得到的 Handler 添加到 c.handlers列表中,执行c.Next()

    • router.go
    func (r *router) handle(c *Context) {
    	//先通过当前请求方法,和真实请求路径
    	//从前缀树中获取到对应的node节点和动态参数
    	n, params := r.getRoute(c.Method, c.Path)
    	if n != nil {
    		//将当前请求对应的动态参数绑定到context上
    		c.Params = params
    		key := c.Method + "-" + n.pattern
    
    		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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    使用 Demo

    • Context内部提供的Fail和Abort方法
    func (c *Context) Fail(code int, err string) {
        //停止调用后续中间件
    	c.index = len(c.handlers)
    	c.JSON(code, H{"message": err})
    }
    
    func (c *Context) Abort() {
        //停止调用后续中间件
    	c.index = len(c.handlers)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • main.go
    package main
    
    import (
    	"geo"
    	"log"
    	"net/http"
    	"time"
    )
    
    func onlyForV2() geo.HandlerFunc {
    	return func(c *geo.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 := geo.New()
    	//全局中间件
    	r.Use(geo.Logger())
    	r.GET("/", func(c *geo.Context) {
    		c.HTML(http.StatusOK, "

    Hello geo

    "
    ) }) v2 := r.Group("/v2") //针对v2 group的中间件 v2.Use(onlyForV2()) { v2.GET("/hello/:name", func(c *geo.Context) { c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) }) } r.Run(":9999") }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
  • 相关阅读:
    Oracle数据库迁移- UID不一致导致的种种问题
    2022/09/04 day01:Linux背景
    总结 - 组件通用封装思路(组件封装)
    ssm毕设项目学生兼职信息管理系统66n7e(java+VUE+Mybatis+Maven+Mysql+sprnig)
    工程数学笔记 | 傅里叶级数/变换的本质理解
    internship:利用EasyPoi 读取word文档生成JavaBean
    【Axios学习 二】一文通透Axios跨域和封装
    Java中文与Base64互转(解决中文乱码的问题)
    JavaScript如何查找和访问HTML页面中的HTML元素
    基于istio实现单集群地域故障转移
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/126566683