对生成器模式不太了解的读者建议先翻看一下生成器模式
PART1. 使用生成器模式创建Middleware
middlewares/accessLog/middlewareBuilder.go
:
package accessLog
import "web"
// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct{}
// Build 本方法用于构建一个日志中间件
func (b *AccessMiddlewareBuilder) Build() web.Middleware {
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
next(ctx)
// 在这里记录日志 例如:命中的路由/HTTP动词/请求参数等
}
}
}
Tips:相比于示例,这里只是没有定义IBuilder
接口,本质上还是为了调用不同的Build()
函数能够得到不同"特征"(或者也可以说是不同功能)的中间件.我个人觉得这里定义接口,意义不大.原因:这个场景下没有示例中的Director
类.换言之,这个场景下没有哪个类负责"编排中间件的构建过程",因为每个中间件在实例化时所需的字段或函数是不同的,没有办法让某个类去针对所有的中间件统一完成这个过程
PART2. 定义中间件结构
记录如下内容:
middlewares/accessLog/accessLog.go
:
package accessLog
// accessLog 本结构体用于定义日志内容
type accessLog struct {
Host string `json:"host,omitempty"` // Host 请求的主机地址
Route string `json:"route,omitempty"` // Route 命中的路由
HTTPMethod string `json:"http_method,omitempty"` // HTTPMethod 请求的HTTP动词
Path string `json:"path,omitempty"` // Path 请求的路径 即:uri
}
PART3. 获取要记录的字段值
3.1 基本实现
middlewares/accessLog/middlewareBuilder.go
:
package accessLog
import "web"
// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct{}
// Build 本方法用于构建一个日志中间件
func (b *AccessMiddlewareBuilder) Build() web.Middleware {
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
// 构建日志内容
defer func() {
log := accessLog{
Host: ctx.Req.Host,
Route: "",
HTTPMethod: ctx.Req.Method,
Path: ctx.Req.URL.Path,
}
// TODO: 写入日志
}()
next(ctx)
}
}
}
这里把记录的日志放在defer中的原因:
直接写在next(ctx)
之后不合适,因为next(ctx)
意味着执行责任链上的后续中间件.在后续中间件的执行过程中有可能出现panic,那样的话整个进程就结束了,因此写在next(ctx)
之后的代码也就无法被执行
写在next(ctx)
之前,也不太合适.2个原因:
如果请求没有命中任何路由,有可能就不需要记录日志了,写在next(ctx)
之前就意味着无论是否命中路由都记录日志
如果在记录日志的过程中出现panic,就会因为这些非关键流程影响到关键流程
因此写在defer中最合适
3.2 记录命中的路由
这里的问题在于,不能直接从命中的节点上取路由,因为命中节点上的路由只是全路由中的最后一段,而非全路由.因此这里修改的思路是:
新增路由时,在对应的节点(即HandleFunc所在的节点)上记录全路由
匹配路由时,查找到对应节点后,将节点的全路由记录到Context上
step1. Context结构体增加用于记录命中的路由的字段
context.go
:
// Context HandleFunc的上下文
type Context struct {
Req *http.Request // Req 请求
Resp http.ResponseWriter // Resp 响应
PathParams map[string]string // PathParams 路径参数名值对
queryValue url.Values // queryValue 查询参数名值对
MatchRoute string // MatchRoute 请求命中的路由
}
step2. Node结构体中增加用于记录命中该节点时的全路由的字段
node.go
:
package web
import (
"fmt"
"strings"
)
// node 路由树的节点
type node struct {
path string // path 当前节点的路径
children map[string]*node // children 子路由路径到子节点的映射
wildcardChild *node // wildcardChild 通配符子节点
paramChild *node // paramChild 参数子节点
HandleFunc // HandleFunc 路由对应的业务逻辑
fullRoute string // fullRoute 命中该节点时的完整路由
}
step3. 添加节点时记录全路由
在原先基础上,创建节点并设置HandleFunc后,设置全路由即可
router.go
:
// addRoute 注册路由到路由森林中的路由树上
func (r *router) addRoute(method string, path string, handleFunc HandleFunc) {
if path == "" {
panic("web: 路由不能为空字符串")
}
if path[0] != '/' {
panic("web: 路由必须以 '/' 开头")
}
if path != "/" && path[len(path)-1] == '/' {
panic("web: 路由不能以 '/' 结尾")
}
root, ok := r.trees[method]
if !ok {
root = &node{
path: "/",
}
r.trees[method] = root
}
if path == "/" {
if root.HandleFunc != nil {
panic("web: 路由冲突,重复注册路由 [/] ")
}
root.HandleFunc = handleFunc
// 记录节点的全路由
root.fullRoute = path
return
}
path = strings.TrimLeft(path, "/")
segments := strings.Split(path, "/")
target := root
for _, segment := range segments {
if segment == "" {
panic("web: 路由中不得包含连续的'/'")
}
child := target.childOrCreate(segment)
target = child
}
if target.HandleFunc != nil {
panic(fmt.Sprintf("web: 路由冲突,重复注册路由 [%s] ", path))
}
target.HandleFunc = handleFunc
// 记录节点的全路由
target.fullRoute = path
}
step4. 查找到节点后将节点的全路由赋值给Context
httpServer.go
:
// serve 查找路由树并执行命中的业务逻辑
func (s *HTTPServer) serve(ctx *Context) {
method := ctx.Req.Method
path := ctx.Req.URL.Path
targetNode, ok := s.findRoute(method, path)
if !ok || targetNode.node.HandleFunc == nil {
ctx.Resp.WriteHeader(http.StatusNotFound)
_, _ = ctx.Resp.Write([]byte("Not Found"))
return
}
ctx.PathParams = targetNode.pathParams
// 命中节点则将节点的全路由设置到上下文中
ctx.MatchRoute = targetNode.node.fullRoute
targetNode.node.HandleFunc(ctx)
}
step5. 中间件中从Context中取值即可
middlewares/accessLog/middlewareBuilder.go
:
package accessLog
import "web"
// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct{}
// Build 本方法用于构建一个日志中间件
func (b *AccessMiddlewareBuilder) Build() web.Middleware {
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
// 构建日志内容
defer func() {
log := accessLog{
Host: ctx.Req.Host,
Route: ctx.MatchRoute,
HTTPMethod: ctx.Req.Method,
Path: ctx.Req.URL.Path,
}
// TODO: 写入日志
}()
next(ctx)
}
}
}
PART4. 写日志操作
4.1 定义记录日志的函数
这里不要直接写死(写成log.Print()
之类的),因为框架的使用者不一定想要以这种方式记录日志.这里的关键点在于:把定义记录日志过程的能力,交给框架的使用者
middlewares/accessLog/middlewareBuilder.go
:
package accessLog
import (
"encoding/json"
"web"
)
// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct {
logFunc func(content string) // logFunc 用于记录日志的函数
}
// SetLogFunc 本方法用于设置记录日志的函数
func (b *AccessMiddlewareBuilder) SetLogFunc(logFunc func(string)) {
b.logFunc = logFunc
}
4.2 调用记录日志的函数
middlewares/accessLog/middlewareBuilder.go
:
package accessLog
import (
"encoding/json"
"web"
)
// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct {
logFunc func(content string) // logFunc 用于记录日志的函数
}
// SetLogFunc 本方法用于设置记录日志的函数
func (b *AccessMiddlewareBuilder) SetLogFunc(logFunc func(string)) {
b.logFunc = logFunc
}
// Build 本方法用于构建一个日志中间件
func (b *AccessMiddlewareBuilder) Build() web.Middleware {
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
// 构建日志内容
defer func() {
log := accessLog{
Host: ctx.Req.Host,
Route: ctx.MatchRoute,
HTTPMethod: ctx.Req.Method,
Path: ctx.Req.URL.Path,
}
// 记录日志
logJsonBytes, _ := json.Marshal(log)
b.logFunc(string(logJsonBytes))
}()
next(ctx)
}
}
}
PART5. 传递中间件
对函数选项模式不太了解的读者建议先翻看一下函数选项模式
5.1 定义选项函数类型
option.go
:
package web
// Option 本类型为 HttpServer 的选项函数
// 本类型的每个不同实例均用于修改 HttpServer 的不同字段值
type Option func(server *HTTPServer)
5.2 定义With()函数
httpServer.go
:
// ServerWithMiddlewares 本函数用于根据给定的 Middleware 列表,创建
// 修改 HttpServer 实例的 middlewares 字段值的选项函数
func ServerWithMiddlewares(middlewares ...Middleware) Option {
return func(server *HTTPServer) {
server.middlewares = middlewares
}
}
5.3 实例化HttpServer时根据选项函数修改成员属性的值
httpServer.go
:
// NewHTTPServer 根据给定的 Option 列表(每个 Option 均表示要修改一个 HttpServer 的成员属性),创建HTTP服务器
func NewHTTPServer(options ...Option) *HTTPServer {
httpServer := &HTTPServer{
router: newRouter(),
}
for _, option := range options {
option(httpServer)
}
return httpServer
}
Tips:相比于示例,这里也没有使用IOption
接口,因为意义也不大.面向接口编程本质上是为了面向客户端代码时隐藏具体实现,这里我认为如果定义了IOption
接口,事情反而更加复杂了,因为你还要定义这个接口的各种不同实现,例如MiddlewareOption
,PortOption
(这里我们假定HTTPSserver
还有一个名为port
的字段)等
PART6. 测试
middleware_test.go
:
package accessLog
import (
"fmt"
"testing"
"web"
)
// Test_Middleware 本函数用于测试 accessLog 是否工作正常
func Test_Middleware(t *testing.T) {
// step1. 创建中间件
logFunc := func(content string) {
fmt.Printf("%#v\n", content)
}
middlewareBuilder := AccessMiddlewareBuilder{
logFunc: logFunc,
}
accessLogMiddleware := middlewareBuilder.Build()
// step2. 创建HTTPServer
middlewareOption := web.ServerWithMiddlewares(accessLogMiddleware)
httpServer := web.NewHTTPServer(middlewareOption)
// step3. 启动HTTPServer
handleFunc := func(ctx *web.Context) {
ctx.Resp.Write([]byte("hello"))
}
httpServer.GET("/user/show", handleFunc)
httpServer.Start(":8081")
}
Last updated