事件回调注册
ghttp.Server
提供了事件回调注册功能,类似于其他框架的中间件
功能,相比较于中间件
,事件回调的特性更加简单。
...
ghttp.HOOK_AFTER_SERVE
在完成服务执行流程之后。
ghttp.HOOK_BEFORE_OUTPUT
向客户端输出返回内容之前。
ghttp.HOOK_AFTER_OUTPUT
向客户端输出返回内容之后。
ghttp.HOOK_BEFORE_CLOSE
(已废弃)在http请求关闭之前。
ghttp.HOOK_AFTER_CLOSE
(已废弃)在http请求关闭之后。
具体调用时机请参考图例所示。
事件优先级
由于事件的绑定也是使用的路由规则,因此它的优先级和【路由控制】章节介绍的优先级是一样的。
但是事件调用时和路由注册调用时的机制不一样,同一个路由规则下允许绑定多个事件回调方法,该路由下的事件调用会按照优先级进行调用
,假如优先级相等的路由规则,将会按照事件注册的顺序进行调用。
关于全局回调
我们往往使用绑定/*
这样的HOOK路由来实现全局的回调处理,这样是可以的。但是HOOK执行的优先是最低的,路由注册的越精确,优先级越高,越模糊的路由优先级越低,/*
就属于最模糊的路由。
为降低不同的模块耦合性,所有的路由往往不是在同一个地方进行注册。例如用户模块注册的HOOK(/user/*
),它将会被优先调用随后才可能是全局的HOOK;如果仅仅依靠注册顺序来控制优先级,在模块多路由多的时候优先级便很难管理。
关于业务函数调用顺序
建议 相同的业务(同一业务模块) 的多个处理函数(例如: A、B、C)放到同一个HOOK回调函数中进行处理,在注册的回调函数中自行管理业务处理函数的调用顺序(函数调用顺序: A-B-C)。
虽然同样的需求,注册多个相同HOOK的回调函数也可以实现,功能上不会有问题,但从设计的角度来讲,内聚性降低了,不便于业务函数管理。
Request.ExitHook
方法
当路由匹配到多个HOOK方法时,默认是按照路由匹配优先级顺序执行HOOK方法。当在HOOK方法中调用Request.ExitHook
方法后,后续的HOOK方法将不会被继续执行,作用类似HOOK方法覆盖。
接口鉴权控制
事件回调注册比较常见的应用是在对调用的接口进行鉴权控制/权限控制。该操作需要绑定HOOK_BEFORE_SERVE
事件,在该事件中会对所有匹配的接口请求(例如绑定/*
事件回调路由)服务执行前进行回调处理。当鉴权不通过时,需要调用r.ExitAll()
方法退出后续的服务执行(包括后续的事件回调执行)。
此外,在权限校验的事件回调函数中执行r.Redirect*
方法,又没有调用r.ExitAll()
方法退出业务执行,往往会产生http multiple response writeheader calls
错误提示。因为r.Redirect*
方法会往返回的header中写入Location
头;而随后的业务服务接口往往会往header写入Content-Type
/Content-Length
头,这两者有冲突造成的。
中间件与事件回调
中间件(Middleware
)与事件回调(HOOK
)是GF
框架的两大流程控制特性,两者都可用于控制请求流程,并且也都支持绑定特定的路由规则。但两者区别也是非常明显的。 1. 首先,中间件侧重于应用级的流程控制,而事件回调侧重于服务级流程控制;也就是说中间件的作用域仅限于应用,而事件回调的“权限”更强大,属于Server
级别,并可处理静态文件的请求回调。 1. 其次,中间件设计采用了“洋葱”设计模型;而事件回调采用的是特定事件的钩子触发设计。 1. 最后,中间件相对来说灵活性更高,也是比较推荐的流程控制方式;而事件回调比较简单,但灵活性较差。
Request.URL
与Request.Router
Request.Router
是匹配到的路由对象,包含路由注册信息,一般来说开发者不会用到。 Request.URL
是底层请求的URL对象(继承自标准库http.Request
),包含请求的URL地址信息,特别是Request.URL.Path
表示请求的URI地址。
因此,假如在服务回调函数中使用的话,Request.Router
是有值的,因为只有匹配到了路由才会调用服务回调方法。但是在事件回调函数中,该对象可能为nil
(表示没有匹配到服务回调函数路由)。特别是在使用事件回调对请求接口鉴权的时候,应当使用Request.URL
对象获取请求的URL信息,而不是Request.Router
。
静态文件事件
如果仅仅是提供API接口服务(包括前置静态文件服务代理如
nginx
),不涉及到静态文件服务,那么可以忽略该小节。
需要注意的是,事件回调同样能够匹配到符合路由规则的静态文件访问(静态文件特性在gf
框架中是默认开启的,我们可以使用WebServer相关配置来手动关闭,具体查看【Server配置管理】章节)。
例如,我们注册了一个/*
的全局匹配事件回调路由,那么/static/js/index.js
或者/upload/images/thumb.jpg
等等静态文件访问也会被匹配到,会进入到注册的事件回调函数中进行处理。
我们可以在事件回调函数中使用Request.IsFileRequest()
方法来判断该请求是否是静态文件请求,如果业务逻辑不需要静态文件的请求事件回调,那么在事件回调函数中直接忽略即可,以便进行选择性地处理。
示例1,基本使用
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/os/glog"
"github.com/gogf/gf/net/ghttp"
)
func main() {
// 基本事件回调使用
p := "/:name/info/{uid}"
s := g.Server()
s.BindHookHandlerByMap(p, map[string]ghttp.HandlerFunc{
ghttp.HOOK_BEFORE_SERVE : func(r *ghttp.Request){ glog.Println(ghttp.HOOK_BEFORE_SERVE) },
ghttp.HOOK_AFTER_SERVE : func(r *ghttp.Request){ glog.Println(ghttp.HOOK_AFTER_SERVE) },
ghttp.HOOK_BEFORE_OUTPUT : func(r *ghttp.Request){ glog.Println(ghttp.HOOK_BEFORE_OUTPUT) },
ghttp.HOOK_AFTER_OUTPUT : func(r *ghttp.Request){ glog.Println(ghttp.HOOK_AFTER_OUTPUT) },
})
s.BindHandler(p, func(r *ghttp.Request) {
r.Response.Write("用户:", r.Get("name"), ", uid:", r.Get("uid"))
})
s.SetPort(8199)
s.Run()
}
当访问 http://127.0.0.1:8199/john/info/10000 时,运行WebServer进程的终端将会按照事件的执行流程打印出对应的事件名称。
示例2,相同事件注册
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
// 优先调用的HOOK
func beforeServeHook1(r *ghttp.Request) {
r.SetParam("name", "GoFrame")
r.Response.Writeln("set name")
}
// 随后调用的HOOK
func beforeServeHook2(r *ghttp.Request) {
r.SetParam("site", "https://goframe.org")
r.Response.Writeln("set site")
}
// 允许对同一个路由同一个事件注册多个回调函数,按照注册顺序进行优先级调用。
// 为便于在路由表中对比查看优先级,这里讲HOOK回调函数单独定义为了两个函数。
func main() {
s := g.Server()
s.BindHandler("/", func(r *ghttp.Request) {
r.Response.Writeln(r.Get("name"))
r.Response.Writeln(r.Get("site"))
})
s.BindHookHandler("/", ghttp.HOOK_BEFORE_SERVE, beforeServeHook1)
s.BindHookHandler("/", ghttp.HOOK_BEFORE_SERVE, beforeServeHook2)
s.SetPort(8199)
s.Run()
}
...
set name
set site
GoFrame
https://goframe.org
示例3,改变业务逻辑
package main
import (
"fmt"
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
func main() {
s := g.Server()
// 多事件回调示例,事件1
pattern1 := "/:name/info"
s.BindHookHandlerByMap(pattern1, map[string]ghttp.HandlerFunc{
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
r.SetParam("uid", 1000)
},
})
s.BindHandler(pattern1, func(r *ghttp.Request) {
r.Response.Write("用户:", r.Get("name"), ", uid:", r.Get("uid"))
})
// 多事件回调示例,事件2
pattern2 := "/{object}/list/{page}.java"
s.BindHookHandlerByMap(pattern2, map[string]ghttp.HandlerFunc{
ghttp.HOOK_BEFORE_OUTPUT: func(r *ghttp.Request) {
r.Response.SetBuffer([]byte(
fmt.Sprintf("通过事件修改输出内容, object:%s, page:%s", r.Get("object"), r.GetRouterString("page"))),
)
},
})
s.BindHandler(pattern2, func(r *ghttp.Request) {
r.Response.Write(r.Router.Uri)
})
s.SetPort(8199)
s.Run()
}
通过事件1设置了访问/:name/info
路由规则时的GET参数;通过事件2,改变了当访问的路径匹配路由/{object}/list/{page}.java
时的输出结果。执行之后,访问以下URL查看效果: - http://127.0.0.1:8199/user/info - http://127.0.0.1:8199/user/list/1.java - http://127.0.0.1:8199/user/list/2.java
示例4,事件回调注册优先级
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
func main() {
s := g.Server()
s.BindHandler("/priority/show", func(r *ghttp.Request) {
r.Response.Writeln("priority service")
})
s.BindHookHandlerByMap("/priority/:name", map[string]ghttp.HandlerFunc{
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
r.Response.Writeln("/priority/:name")
},
})
s.BindHookHandlerByMap("/priority/*any", map[string]ghttp.HandlerFunc{
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
r.Response.Writeln("/priority/*any")
},
})
s.BindHookHandlerByMap("/priority/show", map[string]ghttp.HandlerFunc{
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
r.Response.Writeln("/priority/show")
},
})
s.SetPort(8199)
s.Run()
}
...
/priority/show
/priority/:name
/priority/*any
priority service
示例5,使用事件回调允许跨域请求
...