一. AOP是什么?
AOP (Aspect Oriented Programming),面向切面编程。核心在于将横向关注点从业务中剥离出来。
横向关注点:就是那些跟业务没啥关系,但是每个业务又必须要处理的。常见的有几类:
- 可观测性:logging、metric 和 tracing
- 安全相关:登录、鉴权与权限控制
- 错误处理:例如错误页面支持
- 可用性保证:熔断限流和降级等
基本上 Web 框架都会设计自己的 AOP 方案。
二. go 主流 web 框架的 AOP 方案设计
2.1 Beego 设计
Beego 早期的时候设计完善的回调机制,大体上有三类:
Middleware:它的缺陷是基本脱离了 Beego 的控制。通常来说 Beego 的 HTTPServer 本身是一个 Handler,用户自己开发一个 http.Handler,然后将两者结合在一起。因此用户的 http.Handler 就没法子利用 Beego 内部的数据了。Filter:Beego 允许用户注册不同时机运行的 Filter。这些Filter 都是单向的,而不是环绕式的。FilterChain:FilterChain 可以看做是可以利用 Beego 内部数据的 Middleware。它将 Filter 组织成链,每一个 Filter 都要考虑调用下一个 Filter,否则就会中断执行。但同时,这些 Filter 也失去了指定时机运行的能力。
AOP 这种东西是最为简单最为方便扩展的。所以作为一个中间件设计者,要考虑并不是说提供越多实现越好,而是只提供绝大多数用户会用到,而且用起来没什么区别的那种实现。
2.2 Gin 设计
Gin 同样提供了 HandlerFunc ,用于来被用户注册于不同时机运行的方法,HandlerChain 可以看做是将被注册的 n 个 HandlerFunc 组织成链
通过处理 http 请求时,不断的通过链式调用下一个 注册的 handlerFunc ,与 beego 不同的是,调用 next 是由 context 来完成的。
metric 实现:github.com/penglongli/…
tracing 实现:github.com/opentracing…
2.3 Echo 设计
Echo 和 Beego 的 FilterChain 基本一样,依赖于 MiddlewareFunc 返回的 HandlerFunc 主动调用 next。
2.4 Iris 设计
Iris、Echo 和 Beego 的设计没什么本质区别,无非是名字不同,以及构造方式上的差异。
三. AOP 方案核心
3.1 Middleware
这里也叫 Middleware ,它的定义也是接收 一个 HandleFunc 作为输入,返回一个 HandleFunc。
Middleware 的提供者要决定何时调用 next 。它既是一个责任链模式,也是一个洋葱模式,后面可以看到在中间件设计里会大量使用这个模式。
// middleware.go
type Middleware func(next HandleFunc) HandleFunc
// server.go
func (s *HTTPServer) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
ctx := &Context{
Request: req,
Response: writer,
}
// 最后一个应该是 HTTPServer 执行路由匹配,执行用户代码
root := s.serve
// 从后往前组装
for i := len(s.mdls) - 1; i >= 0; i-- {
root = s.mdls[i](root)
}
root(ctx)
}
3.2 调用 next
这里是构造 handlerChain 的过程, 构造完成后,正常执行下来,最终会到用户业务逻辑里面
某个 HandleFunc 没有调用 next,那么正常执行,最终会到用户业务逻辑里面执行流程就被打断了
四. 可观测性 Middleware
4.1 AccessLog
在日常工作中,用户可能希望能够记录所有进来的请求,以支持 DEBUG,也就是所谓的 AccessLog。Beego 里面支持了 AccessLog,Iris 也支持了AccessLog。这里提供一个简单的 AccessLog Middleware。
func NewBuilder() *MiddlewareBuilder {
return &MiddlewareBuilder{
logFunc: func(accessLog string) {
log.Println(accessLog)
},
}
}
func (b *MiddlewareBuilder) LogFunc(logFunc func(accessLog string)) *MiddlewareBuilder {
b.logFunc = logFunc
return b
}
type MiddlewareBuilder struct {
logFunc func(accessLog string)
}
type accessLog struct {
Host string
Route string
HTTPMethod string `json:"http_method"`
Path string
}
func (b *MiddlewareBuilder) Build() web.Middleware {
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
defer func() {
l := accessLog{
Host: ctx.Request.Host,
Path: ctx.Request.URL.Path,
HTTPMethod: ctx.Request.Method,
Route: ctx.MatchedRoute,
}
val, _ := json.Marshal(l)
b.logFunc(string(val))
}()
next(ctx)
}
}
}
在 defer 里面才最终输出日志,因为:
- 确保即便 next 里面发生了 panic,也能将请求记录下来
- 获得 MatchedRoute:它只有在执行了 next 之后才能获得,因为依赖于最终的路由树匹配(HTTPServer.serve),将匹配结果缓存到了MatchedRoute
AccessLog 潜在问题
- 太死板了,只记录寥寥几个字段,如果用户要记录更多的数据怎么办?
- 固定采用了 json 作为序列化的方式,要是用户想要用别的序列化方式,例如 protobuf,怎么办?
- 让用户自己写一个 AccessLog!默认提供的实现是给大多数的普通用户使用,也相当于一个例子,有需要的用户可以参考这个实现写自己的实现。
4.2 Tracing 链路追踪
Tracing:踪迹,它记录从收到请求到返回响应的整个过程。在分布式环境下,一般代表着请求从 Web 收到,沿着后续微服务链条传递,得到响应再返回到前端的过程。
需要注意的是,trace 不仅仅包含服务调用信息,实际上整条链路上的信息都可能包含在内,包括:
- RPC 调用
- HTTP 调用
- 数据库查询
- 丢消息
- 业务步
这些步骤,也可以进一步被细分,分成更加细的 span。
Tracing相关的几个概念:
tracer:表示用来记录 trace(踪迹)的实例, 一般来说,tracer 会有一个对应的接口。span:代表 trace 中的一段。因此 trace 本身 也可以看做是一个 span。span 本身是一个层 级概念,因此有父子关系。一个 trace 的 span 可以看做是多叉树。
这里有两个空隙,如果一个空隙很长的话,那么 说明打点不够详细。
用什么 tracing 工具 ?
目前在业界里面, tracing 工具还处于百花齐放阶段, 有很多开源实现。例如SkyWalking、Zipkin、Jeager 等。 那么该怎么支持这些开源框架?
- 为每个 tracing 框架写一个 Middleware
- 定义一个统一的 API,允许用户注入自己的 tracing 实现
4.2.1 自定义 API
一般来说,如果一个中间件设计者想要摆脱对任何第三方的依赖,都会定义自己的 API,常见有:
- 定义 Log API
- 定义 Config API
- 定义 Tracing API
- 定义 Metrics API
这种缺点:
- 过度设计:有些时候这些 API 只有一个默认实现, 而且也没有人愿意提供扩展实现
- API 设计得并不咋样
如果自己设计不好 API,就别用这种设计方
4.2.1 用 OpenTelemetry API
采用 OpenTracing API 作为抽象层。 那么所有支持 OpenTracing API 的框架,用户都可 以使用。
4.2.1.1 OpenTelemetry 简介
OpenTelemetry 是 OpenTracing 和 OpenCensus 合并而来。
- OpenTelemetry 同时支持了 logging、tracing 和 metrics
- OpenTelemetry 提供了各种语言的 SDK
- OpenTelemetry 适配了各种开源框架,如 Zipkin、Jeager、Prometheus, 是新时代的可观测性平台
4.2.1.2 OpenTelemetry GO SDK 入门
func TestOpenTelemetry(t *testing.T) {
ctx := context.Background()
tracer := otel.GetTracerProvider().Tracer(
"gitee.com/geektime-geekbang/geektime-go/extra/opentelemetry")
// 如果 ctx 已经和一个 span 绑定了,那么新的 span 就是老的 span 的儿子
// ctx, span := tracer.Start(ctx, "opentelemetry-demo", trace.WithAttributes())
ctx, span := tracer.Start(ctx, "opentelemetry-demo",
trace.WithAttributes(attribute.String("version", "1")))
defer span.End()
// 重置名字
span.SetName("otel-demo")
span.SetAttributes(attribute.Int("status", 200))
span.AddEvent("马老师,发生什么事了")
}
TracerProvider:用于构造 tracer 实例。Provider在平时开发中也很常见,有时候可以看作是轻量级的工厂模式。tracer:追踪者,也就是用于构造 trace 的东西- 构造 tracer 需要一个 instrumentationName, 一般来说就是你构造 tracer 的地方的包名(其 实主要保证唯一就好了)
span:调用 tracer 上的 Start 方法。如果传入的 context 里面已经有一个 span 了,那么新创建的 span 就是老的 span 的儿子。span 要记住调用 End。
4.2.1.3 OpenTelemetry 与 Zipkin 和 Jeager 的结合
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/exporters/zipkin"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
"log"
"os"
)
const (
JeagerURL = "http://127.0.0.1:16686/api/traces"
ZipkinURL = "http://127.0.0.1:19411/api/v2/spans"
)
func initJeager() {
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(JeagerURL)))
if err != nil {
panic(err)
}
tp := sdktrace.NewTracerProvider(
// Always be sure to batch in production.
sdktrace.WithBatcher(exp),
// Record information about this application in a Resource.
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("opentelemetry-demo"),
attribute.String("environment", "dev"),
attribute.Int64("ID", 1),
)),
)
// 核心就在于构造出一个 TracerProvider,并且调用 otel.SetTracerProvider。
otel.SetTracerProvider(tp)
}
func initZipkin() {
exporter, err := zipkin.New(
ZipkinURL,
zipkin.WithLogger(log.New(os.Stderr, "opentelemetry-demo", log.Ldate|log.Ltime|log.Llongfile)),
)
if err != nil {
panic(err)
}
batcher := sdktrace.NewBatchSpanProcessor(exporter)
tp := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(batcher),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("opentelemetry-demo"),
)),
)
otel.SetTracerProvider(tp)
}
核心就在于构造出一个 TracerProvider,并且调用 otel.SetTracerProvide
4.2.1.4 OpenTelemetry Middleware
(1) Builder 模式
package opentelemetry
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
web "web/v7"
)
const defaultInstrumentationName = "go/web/middle/opentelemetry"
type MiddlewareBuilder struct {
Tracer trace.Tracer
}
func NewBuilder() *MiddlewareBuilder {
return &MiddlewareBuilder{}
}
func (b *MiddlewareBuilder) Build() web.Middleware {
if b.Tracer == nil {
b.Tracer = otel.GetTracerProvider().Tracer(defaultInstrumentationName)
}
initZipkin()
return func(next web.HandleFunc) web.HandleFunc {
}
可以考虑允许用户指定 tracer。不过这个设计的意义不是特别大,大多数用户都不会设置。
(2) 关联上游
func (b *MiddlewareBuilder) Build() web.Middleware {
if b.Tracer == nil {
b.Tracer = otel.GetTracerProvider().Tracer(defaultInstrumentationName)
}
initZipkin()
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
// 为了和上游链路连在一起,也就是发起 HTTP 请求的客户端 (关联上下游)
reqCtx := ctx.Request.Context()
reqCtx = otel.GetTextMapPropagator().Extract(reqCtx,
propagation.HeaderCarrier(ctx.Request.Header),
)
// 设置各种值
reqCtx, span := b.Tracer.Start(reqCtx, "unknown", trace.WithAttributes())
span.SetAttributes(attribute.String("http.method", ctx.Request.Method))
span.SetAttributes(attribute.String("peer.hostname", ctx.Request.Host))
span.SetAttributes(attribute.String("http.url", ctx.Request.URL.String()))
span.SetAttributes(attribute.String("http.scheme", ctx.Request.URL.Scheme))
span.SetAttributes(attribute.String("span.kind", "server"))
span.SetAttributes(attribute.String("component", "web"))
span.SetAttributes(attribute.String("peer.address", ctx.Request.RemoteAddr))
span.SetAttributes(attribute.String("http.proto", ctx.Request.Proto))
}
}
}
- 首先就是为了和上游链路连在一起,也就是发起 HTTP 请求的客户端,将
ctx.Req.Context传入Extract方法,再传入Start方法获得一个新的span,其实就是上游 span 的儿子 - 其次是用
SetAttributes设置各种值
(3) 响应状态
func (b *MiddlewareBuilder) Build() web.Middleware {
if b.Tracer == nil {
b.Tracer = otel.GetTracerProvider().Tracer(defaultInstrumentationName)
}
initZipkin()
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
// 为了和上游链路连在一起,也就是发起 HTTP 请求的客户端 (关联上下游)
reqCtx := ctx.Request.Context()
reqCtx = otel.GetTextMapPropagator().Extract(reqCtx,
propagation.HeaderCarrier(ctx.Request.Header),
)
// 设置各种值
reqCtx, span := b.Tracer.Start(reqCtx, "unknown", trace.WithAttributes())
span.SetAttributes(attribute.String("http.method", ctx.Request.Method))
span.SetAttributes(attribute.String("peer.hostname", ctx.Request.Host))
span.SetAttributes(attribute.String("http.url", ctx.Request.URL.String()))
span.SetAttributes(attribute.String("http.scheme", ctx.Request.URL.Scheme))
span.SetAttributes(attribute.String("span.kind", "server"))
span.SetAttributes(attribute.String("component", "web"))
span.SetAttributes(attribute.String("peer.address", ctx.Request.RemoteAddr))
span.SetAttributes(attribute.String("http.proto", ctx.Request.Proto))
// span.End 执行之后,就意味着 span 本身已经确定无疑了,将不能再变化了
defer span.End()
// 将 带有 链路追踪信息的 reqCtx 设置回request
ctx.Request = ctx.Request.WithContext(reqCtx)
next(ctx)
// 使用命中的路由来作为 span 的名字
if ctx.MatchedRoute != "" {
span.SetName(ctx.MatchedRoute)
}
}
}
}
注意这里尝试记录命中的路由 MatchedRoute,而不是整个路径。这是因为 URL 可能非常复杂。 另外还有一个问题:能不能记录状态码?答案是可以的,这就是获取运行结果的 AOP 方案
4.3 获取运行结果
// context.go
type Context struct {
Req *http.Request
// Response 原生的 ResponseWriter。当你直接使用 Response 的时候,
// 那么相当于你绕开了 RespStatusCode 和 RespData。
// 响应数据直接被发送到前端,其它中间件将无法修改响应
// 其实我们也可以考虑将这个做成私有的
Resp http.ResponseWriter
// 缓存的响应部分
// 这部分数据会在最后刷新
RespStatusCode int
RespData []byte
}
func (ctx *Context) RespJSONOK(val any) error {
return ctx.RespJSON(http.StatusOK, val)
}
func (ctx *Context) RespJSON(code int, val any) error {
bs, err := json.Marshal(val)
if err != nil {
return err
}
ctx.Response.WriteHeader(code)
_, err = ctx.Response.Write(bs)
return err
}
// server.go
func (s *HTTPServer) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
ctx := &Context{
Request: req,
Response: writer,
tplEngine: s.TplEngine,
}
// 最后一个应该是 HTTPServer 执行路由匹配,执行用户代码
root := s.serve
// 从后往前组装
for i := len(s.mdls) - 1; i >= 0; i-- {
root = s.mdls[i](root)
}
// 第一个应该是回写响应的
// 因为它在调用next之后才回写响应,
// 所以实际上 Response 是最后一个步骤
var m Middleware = func(next HandleFunc) HandleFunc {
return func(ctx *Context) {
next(ctx)
s.flashResp(ctx)
}
}
root = m(root)
root(ctx)
// s.serve(ctx)
}
func (s *HTTPServer) flashResp(ctx *Context) {
if ctx.RespStatusCode > 0 {
ctx.Response.WriteHeader(ctx.RespStatusCode)
}
_, err := ctx.Response.Write(ctx.RespData)
if err != nil {
log.Fatalln("回写响应失败", err)
}
}
// \middleware\opentelemetry\tracing.go
func (b *MiddlewareBuilder) Build() web.Middleware {
if b.Tracer == nil {
b.Tracer = otel.GetTracerProvider().Tracer(defaultInstrumentationName)
}
initZipkin()
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
// 为了和上游链路连在一起,也就是发起 HTTP 请求的客户端 (关联上下游)
reqCtx := ctx.Request.Context()
reqCtx = otel.GetTextMapPropagator().Extract(reqCtx,
propagation.HeaderCarrier(ctx.Request.Header),
)
// 设置各种值
reqCtx, span := b.Tracer.Start(reqCtx, "unknown", trace.WithAttributes())
span.SetAttributes(attribute.String("http.method", ctx.Request.Method))
span.SetAttributes(attribute.String("peer.hostname", ctx.Request.Host))
span.SetAttributes(attribute.String("http.url", ctx.Request.URL.String()))
span.SetAttributes(attribute.String("http.scheme", ctx.Request.URL.Scheme))
span.SetAttributes(attribute.String("span.kind", "server"))
span.SetAttributes(attribute.String("component", "web"))
span.SetAttributes(attribute.String("peer.address", ctx.Request.RemoteAddr))
span.SetAttributes(attribute.String("http.proto", ctx.Request.Proto))
// span.End 执行之后,就意味着 span 本身已经确定无疑了,将不能再变化了
defer span.End()
// 将 带有 链路追踪信息的 reqCtx 设置回request
ctx.Request = ctx.Request.WithContext(reqCtx)
next(ctx)
// 使用命中的路由来作为 span 的名字
if ctx.MatchedRoute != "" {
span.SetName(ctx.MatchedRoute)
}
// 怎么拿到响应的状态呢?比如说用户有没有返回错误,响应码是多少,怎么办?
span.SetAttributes(attribute.Int("http.status", ctx.RespStatusCode))
}
}
}
(1) HTTPServer serve 和 flashResp
// context.go
func (ctx *Context) RespJSON(code int, val any) error {
bs, err := json.Marshal(val)
if err != nil {
return err
}
//ctx.Response.WriteHeader(code)
//_, err = ctx.Response.Write(bs)
ctx.RespStatusCode = code
ctx.RespData = bs
return err
}
// server.go
func (s *HTTPServer) flashResp(ctx *Context) {
if ctx.RespStatusCode > 0 {
ctx.Response.WriteHeader(ctx.RespStatusCode)
}
_, err := ctx.Response.Write(ctx.RespData)
if err != nil {
log.Fatalln("回写响应失败", err)
}
}
(2) HTTPServer serve、flashResp 和其它 Middleware
(3) OpenTelemetry Middleware 运行结果
package opentelemetry
import (
"go.opentelemetry.io/otel"
"testing"
"time"
web "web/v7"
)
func TestMiddlewareBuilder_Build(t *testing.T) {
tracer := otel.GetTracerProvider().Tracer("")
testinitZipkin(t)
s := web.NewHTTPServer()
s.Get("/", func(ctx *web.Context) {
ctx.Response.Write([]byte("hello, world"))
})
s.Get("/order", func(ctx *web.Context) {
c, span := tracer.Start(ctx.Request.Context(), "first_layer")
defer span.End()
c, second := tracer.Start(c, "second_layer")
time.Sleep(time.Second)
c, third1 := tracer.Start(c, "third_layer_1")
time.Sleep(100 * time.Millisecond)
third1.End()
c, third2 := tracer.Start(c, "third_layer_1")
time.Sleep(300 * time.Millisecond)
third2.End()
second.End()
ctx.RespStatusCode = 200
ctx.RespData = []byte("hello, world")
})
s.Get("/user", func(ctx *web.Context) {
c, span := tracer.Start(ctx.Request.Context(), "first_layer")
defer span.End()
c, second := tracer.Start(c, "second_layer")
time.Sleep(time.Second)
c, third1 := tracer.Start(c, "third_layer_1")
time.Sleep(100 * time.Millisecond)
third1.End()
c, third2 := tracer.Start(c, "third_layer_2")
time.Sleep(300 * time.Millisecond)
third2.End()
second.End()
ctx.RespStatusCode = 200
ctx.RespData = []byte("hello, world")
})
s.Use((&MiddlewareBuilder{Tracer: tracer}).Build())
s.Start(":8081")
}
(4) OpenTelemetry Middleware 以 Zipkin 为例
4.4 Metrics
4.4.1 Prometheus
(1) Prometheus Metrics 类型:
Counter:计数器,统计次数,比如说某件事发生了多少次Gauge:度量,它可以增加也可以减少,比如说当 前正在处理的请求数Histogram:柱状图,对观察对象进行采样,然后 分到一个个桶里面Summary:采样点按照百分位进行统计,比如 99 线、999 线
Prometheus 一般是和 Grafana 结合使用,Grafana 作为数据展示平台。
(2) Prometheus 基本用法
- Namespace 和 Subsystem 根据公司规范来设定。
- 记得调用 MustRegister 把观察者注册进去。
(3) Prometheus Verctor 用法
- 创建一个 Vector (向量),设置 ConstLabels 和 Labels
- 使用 WithLabelValues 来获得具体的收集器
(4) Prometheus Middleware
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"strconv"
"time"
web "web/v7"
)
func (m *MiddlewareBuilder) Build() web.Middleware {
// 创建一个 Vector (向量)其实就是观察者,设置 ConstLabels 和 Labels
summaryVec := prometheus.NewSummaryVec(prometheus.SummaryOpts{
Name: m.Name,
Subsystem: m.Subsystem,
ConstLabels: m.ConstLabels,
Help: m.Help,
}, []string{"pattern", "method", "status"})
// 记得调用 MustRegister 把观 察者注册进去。
prometheus.MustRegister(summaryVec)
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
startTime := time.Now()
next(ctx)
endTime := time.Now()
go report(endTime.Sub(startTime), ctx, summaryVec)
}
}
}
func report(dur time.Duration, ctx *web.Context, vec prometheus.ObserverVec) {
status := ctx.RespStatusCode
route := "unknown"
if ctx.MatchedRoute != "" {
route = ctx.MatchedRoute
}
// 将响应时间记录成毫秒数
ms := dur / time.Millisecond
// 使用 WithLabelValues 来获得具体的收集器
vec.WithLabelValues(
route, ctx.Request.Method, strconv.Itoa(status)).Observe(float64(ms))
}
type MiddlewareBuilder struct {
Name string
Subsystem string
ConstLabels map[string]string
Help string
}
这里将将响应时间记录成毫秒数
4.5 错误处理
func (s *HTTPServer) serve(ctx *Context) {
mi, ok := s.findRoute(ctx.Request.Method, ctx.Request.URL.Path)
if !ok || mi.n.handler == nil {
ctx.Response.WriteHeader(404)
ctx.Response.Write([]byte("Not Found"))
return
}
ctx.PathParams = mi.pathParams
// 命中的路由需要缓存起来
ctx.MatchedRoute = mi.n.route
mi.n.handler(ctx)
}
通常有一个需求:如果一个响应返回了 404, 那么应该重定向到一个默认页面,比如说重定位到 首页。 具体该怎么处理呢?
这里有一个很棘手的点:不是所有的 404 都是要重定向的。比如说你是异步加载数据的 RESTful 请求,在打开页面之后异步加载用户详情,即便 404 了也不应该重定向。
现在假设允许用户注册不同的状态码的回调,例如说 404 或 500 之类的,步骤如下:
定义 middleware
package errhdl
import (
web "web/v7"
)
func (m *MiddlewareBuilder) Build() web.Middleware {
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
next(ctx)
resp, ok := m.resp[ctx.RespStatusCode]
if ok {
ctx.RespData = resp
}
}
}
}
// RegisterError 将注册一个错误码,并且返回特定的错误数据
// 这个错误数据可以是一个字符串,也可以是一个页面
func (m *MiddlewareBuilder) RegisterError(code int, resp []byte) *MiddlewareBuilder {
m.resp[code] = resp
return m
}
func NewBuilder() *MiddlewareBuilder {
return &MiddlewareBuilder{
// 这里可以非常大方,因为在预计中用户会关心的错误码不可能超过 64
resp: make(map[int][]byte, 64),
}
}
type MiddlewareBuilder struct {
resp map[int][]byte
redirect map[int]string
}
准备 404 页面模板,并注册错误处理中间件
package errhdl
import (
"bytes"
"html/template"
"testing"
web "web/v7"
)
func TestNewMiddlewareBuilder(t *testing.T) {
s := web.NewHTTPServer()
s.Get("/user", func(ctx *web.Context) {
ctx.RespData = []byte("hello, world")
})
page := `
<html>
<h1>404 NOT FOUND 我的自定义错误页面</h1>
</html>
`
tpl, err := template.New("404").Parse(page)
if err != nil {
t.Fatal(err)
}
buffer := &bytes.Buffer{}
err = tpl.Execute(buffer, nil)
if err != nil {
t.Fatal(err)
}
s.Use(NewBuilder().
RegisterError(404, buffer.Bytes()).Build())
s.Start(":8081")
}
4.6 从 panic 中恢复
同样使用 Build 模式构造 middleware
package recovery
import (
"log"
"net/http"
web "web/v7"
)
func (m *MiddlewareBuilder) Build() web.Middleware {
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
defer func() {
if err := recover(); err != nil {
ctx.RespStatusCode = m.StatusCode
ctx.RespData = []byte(m.ErrMsg)
// 万一 LogFunc 也panic,那我们也无能为力了
m.LogFunc(ctx)
}
}()
// 这里就是before route, before execute
next(ctx)
// 这里就是after route, after execute
}
}
}
func NewBuilder() *MiddlewareBuilder {
return &MiddlewareBuilder{
StatusCode: http.StatusInternalServerError,
ErrMsg: "服务器未知错误,请联系管理员!",
LogFunc: func(ctx *web.Context) {
log.Println(string(ctx.RespData))
},
}
}
type MiddlewareBuilder struct {
StatusCode int
ErrMsg string
LogFunc func(ctx *web.Context)
}
使用时提供 Use 方法对目标 middlware 注册
package recovery
import (
"log"
"testing"
web "web/v7"
)
func TestMiddlewareBuilder_Build(t *testing.T) {
s := web.NewHTTPServer()
s.Get("/user", func(ctx *web.Context) {
ctx.RespData = []byte("hello, world")
})
s.Get("/panic", func(ctx *web.Context) {
panic("闲着没事 panic")
})
s.Use((&MiddlewareBuilder{
StatusCode: 500,
ErrMsg: "请求 Panic 了",
LogFunc: func(ctx *web.Context) {
log.Println(ctx.Request.URL.Path)
},
}).Build())
s.Start(":8081")
}
4.7 思考
4.7.1 要不要考虑时机问题?
例如:要不要设计类似 Beego 那种复杂的在不同阶段运行的 Filter?
这个问题和之前讨论的其它问题都不太一样,是因为用户完全没办法自己支持,只能依赖于框架支持(也就是必须 侵入式地修改框架)。
理论上来说是需要考虑的,但可以推迟到用户真正需要的时候再来评估。 因为大多数场景都是不需要考虑的,已有的设计完全能够满足。
4.7.2 Middleware 要不要考虑顺序问题
理论上来说,每一个 Middleware 都应该不依赖于其它的 Middleware。
但这只是一个美好的愿景,比如现在已经实现的几个 Middleware 里面,Panic 很显然应该在最外层,也就是紧接着 flashResp 那里,错误处理应该在可观测性之后。 又比如,从业务上来看,鉴权应该在很靠前的位置,限流可以在鉴权前面,也可以在鉴权后面,取决于业务……
4.7.3 Middleware 要不要考虑分路由问题
// UseV1 会执行路由匹配,只有匹配上了的 mdls 才会生效
// 这个只需要稍微改造一下路由树就可以实现
func (s *HTTPServer) UseV1(method string, path string, mdls ...Middleware) {
s.addRoute(method, path, nil, mdls...)
}
前面所有的 Middleware 都是对所有请求生效的。 但忽略了一个很常见的场景:
用户希望区分不同的路由,进行不同的处理。例如:公开页面,用户不需要登录,但是有一些页面,用户就需要登录。
这里可以直接将 UseV1 的功能合并到各个路由注册的功能中, 但这里必须允许 handleFunc 的传参为 nil
// server.go
func (s *HTTPServer) Delete(path string, handleFunc HandleFunc, mdls ...Middleware) {
s.addRoute(http.MethodDelete, path, handleFunc, mdls...)
}
func (s *HTTPServer) Post(path string, handleFunc HandleFunc, mdls ...Middleware) {
s.addRoute(http.MethodPost, path, handleFunc, mdls...)
}
func (s *HTTPServer) Get(path string, handleFunc HandleFunc, mdls ...Middleware) {
s.addRoute(http.MethodGet, path, handleFunc, mdls...)
}
// router.go
func (r *router) findMdls(root *node, segs []string) []Middleware {
mdls := make([]Middleware, 0, 10)
// 用来存放节点的队列
if len(root.mdls) > 0 {
mdls = append(mdls, root.mdls...)
}
queue := []*node{root}
for _, seg := range segs {
queueLen := len(queue)
for i := 0; i < queueLen; i++ {
cur := queue[0]
curChilds, curMdls := cur.childMldsOf(seg)
queue = append(queue, curChilds...)
mdls = append(mdls, curMdls...)
queue = queue[1:len(queue)]
}
}
return mdls
}
func (r *router) findAndLoadMdls(root *node) {
// 用来存放节点的队列
queue := []*node{root}
for len(queue) > 0 {
queueLen := len(queue)
for i := 0; i < queueLen; i++ {
cur := queue[0]
if cur.route != "" {
segs := strings.Split(strings.Trim(cur.route, "/"), "/")
cur.matchMdls = r.findMdls(root, segs)
} else {
cur.matchMdls = cur.mdls
}
curChilds := cur.onlyChildNodesOf(cur.path)
queue = append(queue, curChilds...)
queue = queue[1:len(queue)]
}
}
}
func (n *node) onlyChildNodesOf(path string) []*node {
res := make([]*node, 0, 10)
if n.children != nil && len(n.children) > 0 {
if n.path == path {
for _, staticNode := range n.children {
res = append(res, staticNode)
}
} else {
staticNode, ok := n.children[path]
if ok {
res = append(res, staticNode)
}
}
}
if n.regChild != nil {
res = append(res, n.regChild)
}
if n.paramChild != nil {
res = append(res, n.paramChild)
}
if n.starChild != nil {
res = append(res, n.starChild)
}
return res
}
func (n *node) childMldsOf(path string) ([]*node, []Middleware) {
res := make([]*node, 0, 10)
mdls := make([]Middleware, 0, 10)
if n.children != nil && len(n.children) > 0 {
staticNode, ok := n.children[path]
if ok {
mdls = append(mdls, staticNode.mdls...)
res = append(res, staticNode)
}
}
if n.regChild != nil {
mdls = append(mdls, n.regChild.mdls...)
res = append(res, n.regChild)
}
if n.paramChild != nil {
mdls = append(mdls, n.paramChild.mdls...)
res = append(res, n.paramChild)
}
if n.starChild != nil {
mdls = append(mdls, n.starChild.mdls...)
res = append(res, n.starChild)
}
return res, mdls
}
// server.go
func (s *HTTPServer) Start(addr string) error {
linstener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
for _, root := range s.trees {
s.findAndLoadMdls(root)
}
println("成功监听地址", addr)
return http.Serve(linstener, s)
// return http.ListenAndServe(addr, s)
}
五. AOP 总结
- 什么是 AOP ? AOP 就是面向切面编程,用于解决横向关注点问题,如可观测性问题、安全问题等。
- 什么是洋葱模式?形如洋葱,拥有一个核心,这个核心一般就是业务逻辑。而后在这个核心外面层层包裹,每一层就是一个 Middleware。一般用洋葱模式来无侵入式地增强核心功能,或者解决 AOP 问题。
- 什么是责任链模式?不同的 Handler 组成一条链,链条上的每一环都有自己功能。一方面可以用责任链模式将复杂逻辑分成链条上的不同步骤,另外一方面也可以灵活地在链条上添加新的 Handelr。
- 怎么实现?最简单的方案就函数式方案,还有一种是集中调度的模式。
- 什么是可观测性?也就是 logging、metrics 和 tracing
- 常用的可观测性的框架有哪些?你举例自己公司用的,开源的 OpenTelemetry、SkyWalking、 Prometheus
- 怎么集成可观测性框架?一般都是利用 Middleware 机制,不仅仅是 Web 框架,几乎所有的框架都有 类似 Middleware 的机制
- Prometheus 的 Histogram 和 Summary 分别是什么?直接解释 Histogram 和 Summary 的概念
- 全链路追踪(tracing)的几个概念?解释一下 tracer、tracing 和 span 的概念
- tracing 是怎么构建的?核心在于解释清楚 tracing 进程内和跨进程的运作
- HTTP 应该观测一些什么数据?也就是我们 OpenTelemetry 和 Promtheus 两个 Middleware 里面写的 那些指标
- 什么是 99 线、999线?就是响应的比例,99% 的响应、99.9 % 的响应的响应时间在多少以