202 Star 1.9K Fork 613

张奇峰 / GinSkeleton

 / 详情

http请求的参数校验和业务逻辑应该解耦

待办的
创建于  
2020-11-25 15:38

目前项目里面参数校验和业务逻辑是耦合在一起的,感觉不是很清晰,看名称是参数校验,其实内部包含业务逻辑,例如下面的代码

vApi.GET("news", validatorFactory.Create(consts.ValidatorPrefix+"HomeNews"))

事实上validatorFactory.Create返回的是个参数校验器,但是目前参数校验器内部又会去调用真正的业务逻辑。
我觉得可以参考spring mvc的做法,参数校验也设计成一个middleware,利用gin依赖的validator工具来处理参数校验,当校验通过后,通过context.Next()调用真正的业务逻辑。

评论 (7)

zhoudaxiang 创建了任务
zhoudaxiang 关联仓库设置为张奇峰/GoSkeleton
展开全部操作日志

表单参数验证器和控制器的调用的确是存在一点代码耦合,这是个已知问题。

gin的路由参数支持63个回调函数,
如果按照以下规则设置:
第一个逻辑 表单参数验证器代码对参数进行校验,通过以后,进行下一个函数回调。
第二个逻辑 调用控制器相关代码.
代码如下:
users.GET(
"index",
validatorFactory.Create(consts.ValidatorPrefix+"UsersShow"),
(&web.Users{}).Show
)
这样同样也可以降低耦合度. 不知道是否符合你的预期?

只不过这样的方式如果实际业务当中,控制器存在比较多的Aop(业务切面),的话,就会导致路由的import快速增多。

2020.11.27 更新增加了主线解耦文档,
你可以参考一下,自己再实际使用时,可以自行进行解耦:
https://gitee.com/daitougege/GinSkeleton/blob/master/docs/low_coupling.md

看了解耦的代码,我觉得基本是认同的。我今天也在思考解耦的问题,我这边主要是参考了spring mvc中controller的思路,controller函数通过注解后,函数的签名已经完全是和业务相关的,可以不包含http的相关信息,例如怎么序列化body,怎么验证body等,我觉得这种方式可以让开发者专注,于业务开发,想想我们现在代码,往往都依赖gin context去获取各种参数,cxt.BindJSON(...)cxt.Query(...)这种代码几乎每个handler里面都要写,你这边的设计,我觉得没有根本改变这种模式,只是减少了代码量。我自己写了个demo,也是通过反射和约定,做了一个简化的代理,让我们能比较灵活的,更加专注于业务的方式定义handler,你这边有时间的话,也可以参考一下。只是一个想法,也许方向就是错误的,如果你有意见和建议,欢迎指正。

package main

import (
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"reflect"
	"time"
)

func main() {
	route := gin.Default()
	route.POST("user", ginHandlerWrapper(userHandler, 4))
	route.Run(":8080")
}

//-------------------------------------
// 业务逻辑包装函数
// f是一个函数,是真正的处理业务逻辑的handler
// mode代表入参的信息,我们约定入参的第一个参数是*gin.Context,后面最多有三个参数,按照约定的顺序分别是body,query和header
// mode最后三位如果是1就表示有一个对应入参,否则没有,例如mode=0x101,则表示handler函数是f(*gin.Context, *bodyStruct{}, *headerStruct{})
// 返回值我们约定是(data interface{}, err error)
// 这样处理的好处是:
// 1. 我们可以统一的处理参数校验,返回的数据封装,返回的错误处理
// 2. 一定程度的自由定义handler函数,handler函数的入参都是真正要处理的业务参数,e.g. body,使其进一步和gin解耦,不依赖gin的context;这一点我是参考spring mvc中controller的设计
func ginHandlerWrapper(f interface{}, mode uint8) (hf gin.HandlerFunc) {
	ft := reflect.TypeOf(f)
	if ft.Kind() != reflect.Func {
		panic("not func")
	}
	in := make([]*inParamsInfo, 0)
	var j uint8 = 4
	index := 1 // 第一个参数默认是context
	for i := 0; i < 3; i++ {
		if mode&j == j {
			pt := ft.In(index)
			pt = pt.Elem()
			param := reflect.New(pt).Interface()
			in = append(in, &inParamsInfo{
				typ:   info[i],
				value: param,
			})
			index++
		}
		j = j >> 1
	}

	fv := reflect.ValueOf(f)
	hf = func(cxt *gin.Context) {
		inParams := make([]reflect.Value, 0)
		inParams = append(inParams, reflect.ValueOf(cxt))
		var e error
		for _, pi := range in {
			switch pi.typ {
			case body: // body
				e = cxt.BindJSON(pi.value)
			case query: // query
				e = cxt.BindQuery(pi.value)
			case header: // header
				e = cxt.BindHeader(pi.value)
			}
			if e != nil {
				cxt.JSON(400, "invalid params")
				return
			}
			inParams = append(inParams, reflect.ValueOf(pi.value))
		}
		results := fv.Call(inParams)
		if results[1].Interface() != nil {
			cxt.JSON(500, "internal error")
			return
		}
		cxt.JSON(200, &CommonResponse{
			Code:      0,
			Info:      "",
			Data:      results[0].Interface(),
			Timestamp: time.Now().Unix(),
		})
	}
	return
}

const (
	body uint8 = iota
	query
	header
)

// 入参顺序
var info = []uint8{body, query, header}

type inParamsInfo struct {
	typ   uint8 // 入参类型
	value interface{}
}
// 统一返回
type CommonResponse struct {
	Code      int
	Info      string
	Data      interface{}
	Timestamp int64
}

//-------------------------------------------------
// 处理业务逻辑的handler
type user struct {
	Name string `json:"name" binding:"required,min=6,max=20"`
	Age  int    `json:"age" binding:"required"`
}

func userHandler(cxt *gin.Context, u *user) (data interface{}, err error) {
	bs, _ := json.Marshal(u)
	fmt.Printf("user: %s\n", bs)
	data = &user{
		Name: u.Name + "!!!",
		Age:  u.Age + 1,
	}
	return
}

多谢,周末仔细看一下你的思路...

@zhoudaxiang
我周末运行了一下你这段代码,先说目前我个人关注的几个小的问题,也不一定都是真正的问题,仅供大家探讨一下:
1.ginHandlerWrapper 该函数的代码段应该放置在 hf = func(cxt *gin.Context) { ... } 之间,否则函数被注册的时候就直接执行了,该函数应该是请求到达时才执行。
2.业务处理函数,如果除了 context 参数之外,约定3个参数,就缺少了 form-data 、 x-www-form-urlencoded 类型的参数,盎然这个问题可以通过扩展参数个数补充进去。
3. 你说的"于业务开发,想想我们现在代码,往往都依赖gin context去获取各种参数,cxt.BindJSON(...),cxt.Query(...)这种代码几乎每个handler里面都要写"。 按照goskeleton目前的设计,第一层逻辑表单参数验证之后,参数就自动绑定了上下文 *gin.context 上面,后续的任何地方都直接按照json标签对应的键直接获取,没有你说的这个情况。
4.这种模式虽然对代码进行了解耦,但是每个回调函数都需要反射一次,相比 上下文 *gin.context 性能肯定低不少,gin的 context 就是一个 0x开头的10位指针,由该指针贯穿整个request到response的生命周期,所有主线数据都绑定在 context 上,这样做在性能和易于理解上都占有优势。

其他方面:
1.你这个模式除了你说的2点优势之外,gin-swagger包 https://github.com/swaggo/gin-swagger 也用到了包裹模式,肯定是具备某些优势的。
2.这个思想先保留着,看看在实应用中如何优雅地贯穿进去。

@zhoudaxiang
我周末运行了一下你这段代码,先说目前我个人关注的几个小的问题,也不一定都是真正的问题,仅供大家探讨一下:
1.ginHandlerWrapper 该函数的代码段应该放置在 hf = func(cxt *gin.Context) { ... } 之间,否则函数被注册的时候就直接执行了,该函数应该是请求到达时才执行。
2.业务处理函数,如果除了 context 参数之外,约定3个参数,就缺少了 form-data 、 x-www-form-urlencoded 类型的参数,盎然这个问题可以通过扩展参数个数补充进去。
3. 你说的"于业务开发,想想我们现在代码,往往都依赖gin context去获取各种参数,cxt.BindJSON(...),cxt.Query(...)这种代码几乎每个handler里面都要写"。 按照goskeleton目前的设计,第一层逻辑表单参数验证之后,参数就自动绑定了上下文 *gin.context 上面,后续的任何地方都直接按照json标签对应的键直接获取,没有你说的这个情况。
4.这种模式虽然对代码进行了解耦,但是每个回调函数都需要反射一次,相比 上下文 *gin.context 性能肯定低不少,gin的 context 就是一个 0x开头的10位指针,由该指针贯穿整个request到response的生命周期,所有主线数据都绑定在 context 上,这样做在性能和易于理解上都占有优势。
其他方面:
1.你这个模式除了你说的2点优势之外,gin-swagger包 https://github.com/swaggo/gin-swagger 也用到了包裹模式,肯定是具备某些优势的。
2.这个思想先保留着,看看在实应用中如何优雅地贯穿进去。

@张奇峰 非常感谢你的回复,回头我再仔细看看你的建议。目前针对你说的4点,我简单回复下:

  1. 其实没太看懂,ginHandlerWrapper返回了gin.HandlerginHandlerWrapper也只会在注册时候执行一次;
  2. 参数问题,目前感觉确实没有好的解决方式,如果go2支持参数tag,才能真的优雅的解决这个问题;
  3. 我同意你的说法
  4. 所有数据都在context上面,我觉得这种方式增加了数据篡改的风险,特别是在context中set一些特殊的数据,性能肯定好,不过可读性不高

Faygo

关于校验器解耦,可以参考下faygo框架的思路:https://gitee.com/henrylee/faygo

主要用到了反射,性能可能会有影响,但可读性特别强。

登录 后才可以发表评论

状态
负责人
里程碑
Pull Requests
关联的 Pull Requests 被合并后可能会关闭此 issue
分支
开始日期   -   截止日期
-
置顶选项
优先级
参与者(3)
1630834 daitougege 1578956384 8274512 zhouyinan alulu 1616219442
Go
1
https://gitee.com/daitougege/GinSkeleton.git
git@gitee.com:daitougege/GinSkeleton.git
daitougege
GinSkeleton
GinSkeleton

搜索帮助