Github地址

如标题所描述的,Droplet 是一个 轻量中间层框架,何为中间层呢?

通常来说,我们的程序(注意这里我们仅仅讨论程序的范围,而非作为一个系统,因此这里不设计如 LB、Gateway、Mesh等内容,因为它们都处于程序以外)按不同的职责可以分为不同的层次,而按照不同的设计风格,常见的如下:

  • 三层架构:UIL(UserInterfaceLayer), BLL(BusinessLogicLayer), DAL(DataAccessLayer)
  • DDD分层架构(参考ddd-oriented-microservice):ApplicationLayer,DomainLayer,InfrastructureLayer
  • 洋葱架构(参考Onion Architecture ):Application, Infrastructure, ApplicationService, DomainService, DomainModel。

Tips

洋葱架构其实也是基于DDD的,它是DDD分层架构的升级版本。

但是今天我想用于解释中间层的架构并非以上的任何一种,它也源自于DDD的分层架构,不过我配合了六边形架构来说明它,分层图如下:

在六边形架构中有个规则:依赖只能是由外部指向内部。

因此从外层到最内层分别是:

分层 职责
Access 程序的接入层(在六边形架构中这被称为输入适配器),通常位于整个请求 or 任务的起点,它可能是某种Web框架,也可能是一些队列的消费框架等。
Application 程序应用层,包含了一些非业务的逻辑,如:业务逻辑的编排、参数绑定、校验、请求日志、链路上报、状态读取等等
Domain & Utils 在最中心的地方我放入了两个层次描述:Domain 与 Utils,这两个分层都应该是位于依赖的最底层,意味着他们不应该引用本项目的其他层次。Domain层主要包含核心的业务逻辑,而Utils则是一些程序任何地方都可能会引用的代码段,比如常量定义、数据结构和语法糖等等
Infrastructure 基础设施层(在六边形架构中这被称为输出适配器),程序所有需要对外进行信息交换 or 功能依赖时都会放置在这一层实现,通常来说这些功能都是被依赖的那部分,因此我们如果要满足依赖约束的话,这里必须要引入 DIP(Dependency inversion principle),即在Application、Domain中定义依赖,而 Infrastructure 来实现它们,这样保证了它们是可被替换的

六边形架构优点在于解耦程序中业务无关的部分,以保证它们都是可被替换与扩展的

而 Droplet 就工作在 Application 层,它的核心能力只有一个:提供基于pipeline的请求/响应处理能力

可能有人会疑问,几乎每个框架都会实现类似的能力,为什么我们需要 Droplet 呢?

别急,我们来看看这些框架自带的 pipeline/middleware 存在什么弊端。

根据上面的架构图我们可以知道诸如 gin、go-restful、fasthttp 之类的http框架都是工作在 Access 层,因此框架自带的 pipeline/middleware 存在以下两个弊端:

  1. 框架绑定: 这个很容易理解,这些机制只能工作于特定的框架下,如果切换框架则需要需要调整代码,除了中间件的代码外,我时常也会见到程序在 API Handler 中耦合了大量框架相关的代码,比如:读取参数(header, query, body等)、根据业务结果回写响应等,这些代码渗透到了业务程序中(有时它们甚至会比业务代码占用了更多的行数),这加大了业务开发同学的维护成本,同时也降低了程序的可扩展性。

一些相关的BadCase

想象一下:

  • 你一直在使用 gin,但是有一天运营拿着数据找到你,说机器占用的成本太高了,而你发现只要切换到 fasthttp 就能为你带来更高的性能,但是从 gin -> fasthttp 你需要调整大量的 API handler 代码,这可太让人头疼了。
  • API handler中充斥了诸如 param, ok := req.Query("param") / param, ok := req.Header.Get("param") / err := xxx.Bind(req, &param) 之类的代码,这和业务毫无关系
  1. 没有请求/响应的结构化实体: 如果有开发过这些框架中间件的同学一定知道,大部分框架中间件的协议定义都是以 http.Request/httpResponse 为主体的,这意味着如果不做任何前置处理,你只能通过字节数组来感知 请求与响应 这在部分场景都不太方便,比如:根据请求、响应的结构体是否具备某些特征(比如接口)来执行某些特定的业务通用逻辑;又或者想在中间件中融入一些自动化的参数校验逻辑,因为你没有一个具体的结构化对象;再或者你不想要在每一个 API handler 中去设置一个响应的 Wrapper(通常它类似于 {code: 0, msg: "", data:{}}),想要在中间件去自动包装上它,也很难执行;最后就是——如果只依靠 http.Request/httpResponse,你也难在中间件感知到其他参与者的处理状态,。

相信我说的这些问题,使用过的同学应该都有所感触,而这些问题并非难以解决,它们中的大部分基本都是可以通过自行建立一套约定来得以缓解(比如将这些信息都通过 context 去获取),而 Droplet 也是诞生于我在过往团队中去克服这些问题的实践之中,是一个相对可靠的实现。

工作原理

带着上面提到的这些问题,我们来看看 Droplet 的工作原理是怎样的,如下图所示:

如我所说的那样,Droplet 的核心在于 提供基于pipeline的请求/响应处理能力,因此我们可以看见这个图中涉及的所有模块都是基于 pipleline,可以说 Droplet 的所有能力都是由其扩展而来。

这里我们先介绍下图中出现的几个中间件(Middleware,这是组成 pipepine 关键元素):

  1. HttpInfoInjector: 注入 http 相关的一些信息,如 requestid, http.Request 等
  2. RespReshape: 根据 handler 的响应结果来进行一些调整,包括:发生错误时设置上默认的错误码、错误信息;如果缺少响应 wrapper 时包装上配置好的 wrapper
  3. HttpInput: 如果你设置了 API 的输入参数类型,那么该中间件会自动根据 Content-Typestruct tag 来读取对应的参数值,同时自动使用 validator 来检测参数错误
  4. TrafficLog: 如名字所示,如果你配置了响应的 logger,那么该中间件会执行日志记录。请注意该中间件工作在其他默认中间件的后面、你的handler之前,因此它统计的耗时是你业务函数的真正耗时,而不包含其他中间件的耗时时间,你可以考虑通过 网关Mesh 来记录完整的接口耗时。

Tips

  • Middleware 处理请求和响应顺序是相反的——即第一个处理请求的中间件它会是最后一个处理响应的。
  • 框架工作在应用层的优势有两点:
    • 与接入层框架解耦,保证绝项目代码可平滑 扩展/切换 其他接入层框架
    • 能够获取到结构化的接口 输入参数输出参数 你可以对其进行更具精细的切面操作

GetStart

这里以 Gin 为例,其他框架类似。

首先获取对应 wrapper 的 submodule:

go get github.com/shiningrush/droplet/wrapper/gin

// if you want to ensure the droplet is latest, you can get droplet
go get github.com/shiningrush/droplet

然后程序代码如下:


package main import (
"reflect" "github.com/gin-gonic/gin"
"github.com/shiningrush/droplet/core"
"github.com/shiningrush/droplet/wrapper"
ginwrap "github.com/shiningrush/droplet/wrapper/gin"
) func main() {
r := gin.Default() // 使用 wrapper 包装原始的 API
r.POST("/json_input/:id", ginwrap.Wraps(JsonInputDo, wrapper.InputType(reflect.TypeOf(&JsonInput{}))))
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
} type JsonInput struct {
// 从 path 读取, 并且为必须参数
ID string `auto_read:"id,path" json:"id" validate:"required"`
// 从 header 读取, 并且为必须参数
User string `auto_read:"user,header" json:"user" validate:"required"`
// 从 json unmarshal 后的ips字段读取
IPs []string `json:"ips"`
// 从 json unmarshal 后的 count 字段读取
Count int `json:"count"`
// 读取原始的 http body,接收参数类型必须为 []byte or io.ReadCloser
Body []byte `auto_read:"@body"`
} func JsonInputDo(ctx core.Context) (interface{}, error) {
input := ctx.Input().(*JsonInput) return input, nil
}

参数绑定

Usage 一节中所展示的,我们可以通过 wrapper.InputType 选项来告诉 Droplet 是否期望自动化进行参数绑定,如果某些场景下你不需要从 Body 进行自动的参数绑定了,可以通过显式的选项来禁止它,如:

r.POST("/json_input/:id", ginwrap.Wraps(JsonInputDo, wrapper.InputType(reflect.TypeOf(&JsonInput{}), wrapper.DisableUnmarshalBody())))

参数绑定的Tag格式如下:

auto_read: ({key},{source}) or @body

其中值如下:

  • key: 用于到各个来源中匹配对应值
  • source: 可选值有 query, header, path, body(缺省默认)
  • @body: 特殊的取值,意味着获取原生的body作为字段值,此时你的字段类型应该为 []byte or io.ReadCloser

同时 Droplet 会自动使用 validator 对入参进行校验,因此你可以使用其 tag 来辅助验证参数合法性。

响应整形

通常来说API都会在响应的最外层进行一层包装,比如 Droplet 自带的 wrapper 如下所示:

{
"code": 0, // API 错误码
"message": "", // API 消息
"data": {}, // 响应数据
"request_id": "" // 请求ID
}

当然你可以完全去掉这个默认 Wrapper 或者 使用满足你们团队规范的 Wrapper(需要实现 data.HttpResponse 接口) 来替换它:

type NativeJsonResp struct {
data interface{}
} func (n *NativeJsonResp) Set(code int, msg string, data interface{}) {
n.data = data
} func (n *NativeJsonResp) SetReqID(reqId string) {
} func (n *NativeJsonResp) MarshalJSON() ([]byte, error) {
return json.Marshal(n.data)
} func main() {
...
droplet.Option.ResponseNewFunc = func() data.HttpResponse {
return &NativeJsonResp{}
}
...
}

对于另外一些并不需要 Wrapper 或者 你想要自行控制返回的内容时可以在 Handler 中使用一些实现了特定接口的返回值,如下所示:

func GetLoginQRCode(ctx droplet.Context) (interface{}, error) {
type makeQRCodeResp struct {
SceneID string `json:"scene_id"`
State int `json:"state"`
Url string `json:"url"`
} var resp makeQRCodeResp
if err := goreq.Get(UrlMakeQRCode, goreq.SetHeader(fakeClientHeader()), goreq.JsonResp(&resp)).Do(); err != nil {
return nil, fmt.Errorf("get qrcode failed: %w", err)
} return &data.RawResponse{
StatusCode: http.StatusOK,
Body: []byte(fmt.Sprintf(QRCodeBase, resp.SceneID, resp.SceneID, resp.Url)),
}, nil
}

类似的还有 data.FileResponsedata.SpecCodeResponse,根据其名字你可以在需要的场景选择它们。

同时在整形过程中,为了业务研发不再需要关心错误处理,Droplet 会自动将 err != nil 的响应转化到 codemessage 字段上。

如下图所示:

func ErrorAPI(ctx droplet.Context) (interface{}, error) {
return nil, errors.New("failed")
}

那么你将得到如下的响应:

{
"code": 10000,
"message": "failed"
}

当然,你可以使用 data.BaseError 来指定你想返回的错误码:

func ErrorAPI(ctx droplet.Context) (interface{}, error) {
return nil, data.BaseError{Code: 100, Message: "custom message"}
}

Tips

  • 这些特定的响应其背后都是实现了某一类接口,如果有需要你也完全可以自行实现。

流量记录

Droplet 自带了记录 API 出参与入参的能力,但是默认所有记录信息都会被抛弃,如果想要启用它,你需要实现 Droplet 的全局 Logger,如下所示:

import (
"github.com/shiningrush/droplet/log"
) func main() {
...
// CustomLogger 需要实现 log.Interface
log.DefLogger = &CustomLogger{} // droplet 默认只会记录 Path,Method,耗时等信息,如果你需要打印 API 的输入与输出,可以在全局选项中开启(在Wraps函数中也可指定)
droplet.Option.TrafficLogOpt = &middleware.TrafficLogOpt{
LogReq: true,
LogResp: true,
}
...
}

自定义中间件

实现一个自定义中间件很简单,你只需要实现与 Hanler 类似的接口即可,下图是一个简单的中间件,它会用于检测输入参数是否需要 Quota 并执行相关逻辑:

type DemoMiddleware struct {
// 继承基本的middleware,里面有用于实现处理链路的公共逻辑
middleware.BaseMiddleware
} func (mw *HttpInputMiddleware) Handle(ctx core.Context) error {
if ck, ok := ctx.Input().(QuotaChecker); !ok {
if err := ck.IsQuotaEnough(); err != nil {
return err
}
} // 调用下一个中间件,有需要的话你也可以在响应返回后执行部分逻辑
return mw.Handle(ctx)
} func main() {
// 如果你需要所有API都添加该中间件,可以在全局选项中将你的中间件编排
droplet.Option.Orchestrator = func(mws []core.Middleware) []core.Middleware {
return append(mws, &DemoMiddleware{})
} ...
// 在单个API上启用
r.POST("/json_input/:id", ginwrap.Wraps(APIHandler,
wrapper.Orchestrator(func(mws []core.Middleware) []core.Middleware {
return append(mws, &DemoMiddleware{})
})))
...
}

Tips

Q: 为什么使用 Orchestrator 这样的形式来配置中间件,而非通过 Priorty 之类的权重来实现中间件的编排,这样在未来可以做到通过配置文件来调整中间件

A: 主要出于几个考虑

  1. 考虑现代微服务的架构下,多数业务无关的通用能力都会下沉到网关以及Mesh,因此一个服务的切面不会太多,在通过这样的方式来配置,成本是可以接受的。
  2. 通过 Orchestrator 方式,用户还可以任意操作已添加的中间件,比如移除一些不必要的中间件,这是权重的方式无法做到的。
  3. 当然如果以后有需要,现在的设计并不妨碍我们支持基于权重的方式

小结

正如文中所说,Droplet 的核心目标是 提供位于应用层的、pipeline 形式的请求处理能力,并以此为基础提供了一些开箱即用的中间件。

它对项目带来的收益总结为几点:

  • 提供了框架无关的请求处理能力,这使得我们的服务更具韧性
  • 在应用层我们可以接触到 已序列化后的接口输入 以及 尚未序列化的接口输出 ,这使得我们在离业务更近的地方进行切面操作,进而将更多的通用代码沉淀到切面而降低业务代码的复杂度,更聚焦业务逻辑。

希望 Droplet 能对你有所帮助与启发。

Droplet——一款轻量的Golang应用层框架的更多相关文章

  1. 推荐一款轻量小众却高效免费开源windows热键脚本语言Autohotkey

    写在前面的话 Autohotkey是一款轻量小众但高效免费开源的windows热键脚本语言,游戏操纵.鼠标操作.键盘快捷重定义,快捷短语等等,只有你想不到,没有它做不到,神器中的神器呀,相见恨晚. 安 ...

  2. 五款轻量型bug管理工具横向测评

    五款轻量型bug管理工具横向测评 最近正在使用的本地bug管理软件又出问题了,已经记不清这是第几次了,每次出现问题都要耗费大量的时间精力去网上寻找解决方案,劳心劳力.为了避免再次出现这样的情况,我决定 ...

  3. Snack3 3.2 发布,轻量的Json+Jsonpath框架

    Snack3 是一个轻量的 JSON + Jsonpath 框架. 借鉴了 Javascript 所有变量由 var 申明,及 Xml dom 一切都是 Node 的设计.其下一切数据都以ONode表 ...

  4. Wenaox 一款轻量性能好的微信小程序状态管理库

    感慨一下!!! 从开始开发 wenaox 从开始到现在,,时不时更新一下,改一改 bug,却发现已经快 1 年了 orz 虽然很少人用 hhh,但偶尔也会有人提一些问题,我就知道还有人用的~ 感兴趣的 ...

  5. Dzz任务板初版完成笔记-仿trello私有部署的一款轻量团队任务协作工具。

    刚完成了第一个版本Dzz任务板的设计开发,记录下设计思路和完成情况. Dzz任务板是DzzOffice中的团队协作套件中的一款应用,它需要安装在DzzOffice中使用. 主界面中需要能够快速简单的创 ...

  6. 2016年31款轻量高效的开源JavaScript插件和库

    目前有很多网站设计师和开发者喜欢使用由JavaScript开发的插件和库,但同时面临一个苦恼的问题:它们中的大多数实在是太累赘而且常常降低网站的性能.其实,其中也有不少轻量级的插件和库,它们不仅轻巧有 ...

  7. 2016年31款轻量高效的开源 JavaScript 插件和库

    目前有很多网站设计师和开发者喜欢使用由JavaScript开发的插件和库,但同时面临一个苦恼的问题:它们中的大多数实在是太累赘而且常常降低网站的性能.其实,其中也有不少轻量级的插件和库,它们不仅轻巧有 ...

  8. 2016 年 31 款轻量高效的开源 JavaScript 插件和库

    目前有很多网站设计师和开发者喜欢使用由JavaScript开发的插件和库,但同时面临一个苦恼的问题:它们中的大多数实在是太累赘而且常常降低网站的性能.其实,其中也有不少轻量级的插件和库,它们不仅轻巧有 ...

  9. 关于对MyBatis.net框架的学习笔记( MyBatis.net是一款灵活性极大,sql由开发者自行在xml中编写, 轻量的ORM映射框架). 同时避免了sql硬编码到代码中不易维护的问题...

    对于为什么要用ORM,为什么又要选择MyBatis.net,这个问题希望读者自行查找资料.这里直接贴出相关的调试笔记. 步骤1)下载与引用. http://code.google.com/p/myba ...

随机推荐

  1. LuoguP1020 导弹拦截 (LIS)

    最长不降和单升 #include <iostream> #include <cstdio> #include <cstring> #include <algo ...

  2. 听,引擎的声音「GitHub 热点速览 v.22.33」

    这期的热点速览异常 Cool,因为有呜呜声内燃机引擎加成的 engine-simengine-sim 坐镇,听到如此曼妙的引擎声,相比你的人生也在高速上升吧.还有,自己搭建个服务就能在本地用上 AI ...

  3. java学习第五天异常机制.day14

    异常处理机制 确保程序的正常执行.这种机制称为异常处理机制 异常对象 常用方法 方法介绍 toString 返回异常类型和异常信息 getMessage 返回异常信息 printStackTrace ...

  4. Codeforces 1715E - Long Way Home

    又是废掉的一个div2啊 第一次在学校熬夜打cf,开心还看到了自己最喜欢的斜率优化ohhh 链接 :E - Long Way Home 看到那个平方就可以靠感觉认为是斜率优化了.... 感觉似不似有点 ...

  5. C#基础_理解类

    构造函数主要是用来创建对象时为对象赋初值来初始化对象.总与new运算符一起使用在创建对象的语句中 .A a=new A(); 构造函数具有和类一样的名称:但它是一个函数具有函数的所有特性,同一个类里面 ...

  6. Spring的俩大核心概念:IOC、AOP

    1.Spring 有两个核心部分: IOC 和 Aop (1)IOC:控制反转,把创建对象过程交给 Spring 进行管理   (2)Aop:面向切面,不修改源代码进行功能增强 2.Spring 特点 ...

  7. C++ 性能小测 1 二维数组的遍历效率

    C++ 性能小测 1 二维数组的遍历效率 遍历二维数组时,常规思路是使用一个嵌套循环.一方面,由于 CPU 使用了分支预测技术,因此通常将循环次数最多循环的放在最内层.另一方面,由于二维数组是按行存储 ...

  8. Java方法总结

    什么是方法 何谓方法 就是一个方法只完成一个功能,这样利于后期的扩展 例子: public static void main(String[] args) {    System.out.printl ...

  9. for--else大坑问题

    这是一次偶然发现的问题,做一下记录 a = [{'w0', 'b0', 'v0'}, {'w1', 'b1', 'v1'}, {'w2', 'b2', 'v2'}] for i in a: for j ...

  10. ubuntu安装BricsCAD

    目录 目录 下载软件包 安装deb软件包 sudo dpkg -i *.deb 将Crack/bricsys.lic中hostid= <YOURHOSTID>替代本机MAC地址(中间不要冒 ...