32-validator参数校验

官方文档:https://pkg.go.dev/github.com/go-playground/validator#pkg-overview

做业务开发的三板斧:

  1. 参数处理
    1. 获取参数
      1. query参数 ?name=张三
      2. form表单
      3. json
      4. path 路径参数
    2. 参数校验 --> validator
  2. 业务逻辑
  3. 返回响应

在web开发中一个不可避免的环节就是对请求参数进行校验,通常我们会在代码中定义与请求参数相对应的模型(结构体),借助模型绑定快捷的解析请求中的参数,例如gin框架中的BindShouldBind系列方法。本文就以gin框架请求参数校验为例,介绍一些valiator库的实用技巧。

gin框架使用github.com/go-playground/validator进行参数校验,我们需要在定义结构体时使用bindingtag标识相关校验规则,可以查看https://pkg.go.dev/github.com/go-playground/validator?utm_source=godoc#hdr-Baked_In_Validators_and_Tags查看所有支持的tag。

原理

就是利用反射去结构体字段找规则,对每个字段的值按规则进行校验。

你写一个包,让别人用。你在文档里告诉别人结构体字段里要用aping 作为tag.

type data struct {
  ID int `aping:"id"`
}
基本示例

首先来看gin框架内置使用validator做参数校验的基本示例。

package main

import (
	"fmt"

	"github.com/gin-gonic/gin"
)

// 定义一个与前端传入参数的结构体
type SignUpParam struct {
	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
	Name       string `json:"name" binding:"required"`
	Email      string `json:"email" binding:"required,email"`
	Password   string `json:"password" binding:"required"`
	RePassword string `json:"repassword" binding:"required,eqfield=Password"`
}

func main() {
	r := gin.Default()

	r.POST("/sign", func(c *gin.Context) {
		var u SignUpParam
		if err := c.ShouldBind(&u); err != nil {
			c.JSON(200, gin.H{
				"msg": err.Error(),
			})
			return
		}
		// 将信息保存到数据库...

		c.JSON(200, gin.H{
			"msg":  "success",
			"data": fmt.Sprintf("%#v\n", u),
		})
	})

	if err := r.Run(":1234"); err != nil {
		panic(err)
	}
}

用postman发送一个带有json格式的请求:

curl -H "Content-type: application/json" -X POST -d '{"name":"爱写代码的小男孩","age":28,"email":"123@qq.com"}' http://127.0.0.1:1234/sign

输出结果:

"Key: 'SignUpParam.Password' Error:Field validation for 'Password' failed on the 'required' tag\nKey: 'SignUpParam.RePassword' Error:Field validation for 'RePassword' failed on the 'required' tag"

从最终的输出结果可以看到 validator 的检验生效了,但是错误提示的字段不是特别友好,我们可能需要将它翻译成中文。

翻译校验错误提示信息

validator库本身是支持国际化的,借助相应的语言包可以实现校验错误提示信息的自动翻译。下面的示例代码演示了如何将错误信息翻译成中文,翻译成其他语言的方法类似。

package main

import (
	"fmt"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/locales/en"
	"github.com/go-playground/locales/zh"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	enTranslations "github.com/go-playground/validator/v10/translations/en"
	zhTranslations "github.com/go-playground/validator/v10/translations/zh"
)

// 定义一个与前端传入参数的结构体
type SignUpParam struct {
	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
	Name       string `json:"name" binding:"required"`
	Email      string `json:"email" binding:"required,email"`
	Password   string `json:"password" binding:"required"`
	RePassword string `json:"repassword" binding:"required,eqfield=Password"`
}

// 定义一个全局翻译器T
var trans ut.Translator

// 初始化翻译器
func InitTrans(locale string) (err error) {
	// 修改gin框架中的valiadtor属性,实现自定制
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {

		zhT := zh.New() // 中文翻译器
		enT := en.New() // 英文翻译器

		// 第一个参数是备用的语言环境
		// 后面的参数是应该支持的语言环境(支持多个)
		// uni := ut.New(zhT,zhT) 也是可以的
		uni := ut.New(enT, zhT, enT)

		// locale 通常取决于http请求头的 'Accept-Language'
		var ok bool
		// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
		trans, ok = uni.GetTranslator(locale)
		if !ok {
			return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
		}

		// 注册翻译器
		switch locale {
		case "en":
			err = enTranslations.RegisterDefaultTranslations(v, trans)
		case "zh":
			err = zhTranslations.RegisterDefaultTranslations(v, trans)
		default:
			err = enTranslations.RegisterDefaultTranslations(v, trans)
		}
		return
	}
	return
}

func main() {

	// 初始化翻译器
	if err := InitTrans("zh"); err != nil {
		fmt.Printf("init trans failed, err:%v\n", err)
		return
	}

	r := gin.Default()

	r.POST("/sign", func(c *gin.Context) {
		var u SignUpParam
		if err := c.ShouldBind(&u); err != nil {
			// 获取valadtor.valiadtionErrors类型的errors
			errs, ok := err.(validator.ValidationErrors)
			if !ok {
				// 非valiadtor.ValidationErrors类型错误直接返回
				c.JSON(200, gin.H{
					"msg": err.Error(),
				})
				return
			}
			// validator.ValidationErrors类型错误进行翻译
			c.JSON(200, gin.H{
				"msg": errs.Translate(trans),
			})
			return
		}
		// 将信息保存到数据库...

		c.JSON(200, gin.H{
			"msg":  "success",
			"data": fmt.Sprintf("%#v\n", u),
		})
	})

	if err := r.Run(":1234"); err != nil {
		panic(err)
	}
}

用postman发送一个带有json格式的请求:

curl -H "Content-type: application/json" -X POST -d '{"name":"爱写代码的小男孩","age":28,"email":"123@qq.com"}' http://127.0.0.1:1234/sign

输出结果:

"msg": {
        "SignUpParam.Password": "Password为必填字段",
        "SignUpParam.RePassword": "RePassword为必填字段"
    }
自定义错误提示信息的字段名

上面的错误提示看起来是可以了,但是还是差点意思,首先是错误提示中的字段并不是请求中使用的字段,例如:RePassword是我们后端定义的结构体字段名,而请求中使用的是re_password字段。如何是错误提示中的字段使用自定义的名称。

只需要在初始化翻译器的时候像下面一样添加一个获取jsontag的自定义方法即可。

func InitTrans(locale string) (err error) {
	// 修改gin框架中的valiadtor属性,实现自定制
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {

		// 注册一个获取json tag的自定义方法
		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
			if name == "-" {
				return ""
			}
			return name
		})

		zhT := zh.New() // 中文翻译器
		enT := en.New() // 英文翻译器
    ...

再尝试发请求,看一下效果:

{
    "msg": {
        "SignUpParam.email": "email必须是一个有效的邮箱",
        "SignUpParam.password": "password为必填字段",
        "SignUpParam.repassword": "repassword为必填字段"
    }
}

可以看到现在的错误信息使用的就是我们结构体中jsontag设置的名称了。

但还是有点瑕疵,那就是最终的错误提示信息中心还有我们后端定义的结构体名称—SignUpParam,这个名称其实是不需要随错误返回给前端的,前端并不需要这个值,我们想办法去掉。

这里参考https://github.com/go-playground/validator/issues/633#issuecomment-654382345提供的方法,定义一个去掉结构体名称前缀的自定义方法:

func removeTopStruct(fields map[string]string) map[string]string {
	res := map[string]string{}
	for field, err := range fields {
		res[field[strings.Index(field, ".")+1:]] = err
	}
	return res
}

我们在代码中使用上述函数将翻译后的errors做一下处理即可:

if err := c.ShouldBind(&u); err != nil {
	// 获取validator.ValidationErrors类型的errors
	errs, ok := err.(validator.ValidationErrors)
	if !ok {
		// 非validator.ValidationErrors类型错误直接返回
		c.JSON(http.StatusOK, gin.H{
			"msg": err.Error(),
		})
		return
	}
	// validator.ValidationErrors类型错误则进行翻译
	// 并使用removeTopStruct函数去除字段名中的结构体名称标识
	c.JSON(http.StatusOK, gin.H{
		"msg": removeTopStruct(errs.Translate(trans)),
	})
	return
}

看一下最终的效果:

{
    "msg": {
        "email": "email必须是一个有效的邮箱",
        "password": "password为必填字段",
        "repassword": "repassword为必填字段"
    }
}

这一次看起来比较符合我们预期的标准了。

自定义结构体校验方法

上面的校验还是有点小问题,就是当涉及到一些复杂的校验规则,比如re_password字段需要和password字段的值相等这样的校验规则,我们的自定义错误提示字段名称方法就不能很好解决错误提示信息中的其他字段名称了。

curl -H "Content-type: application/json" -X POST -d '{"name":"爱写代码的小男孩","age":28,"email":"123.com","password":"root123","re_password":"123root"}
' http://127.0.0.1:1234/sign

{
    "msg": {
        "email": "email必须是一个有效的邮箱",
        "password": "password为必填字段",
        "repassword": "repassword必须等于Password"
    }
}

可以看到re_password字段的提示信息中还是出现了Password这个结构体字段的名称。

如果想将上面的Passsword字段也改为和jsontag一致的名称,就需要我们自定义结构体校验的方法。

例如:

// SignUpParamStructLevelValidation 自定义SignUpParam结构体校验函数
func SignUpParamStructLevelValidation(sl validator.StructLevel) {
	su := sl.Current().Interface().(SignUpParam)

	if su.Password != su.RePassword {
		// 输出错误提示信息,最后一个参数就是传递的param
		sl.ReportError(su.RePassword, "re_password", "RePassword", "eqfield", "password")
	}
}

然后在初始化校验器的函数中注册该自定义校验方法即可:

func InitTrans(locale string) (err error) {
	// 修改gin框架中的Validator引擎属性,实现自定制
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {

		// ... liwenzhou.com ...
    
		// 为SignUpParam注册自定义校验方法
		v.RegisterStructValidation(SignUpParamStructLevelValidation, SignUpParam{})

		zhT := zh.New() // 中文翻译器
		enT := en.New() // 英文翻译器

		// ... liwenzhou.com ...
}

最终再请求一次,看一下效果:

{
    "msg": {
        "email": "email必须是一个有效的邮箱",
        "re_password": "re_password必须等于password"
    }
}

这一次re_password字段的错误提示信息就符合预期了。

自定义字段校验方法

除了上面介绍到的自定义结构体校验方法,validator还支持为某个字段自定义校验方法,并使用RegisterValidation()注册到校验器实例中。

接下来我们来为SignUpParam添加一个需要使用自定义校验方法checkDate做参数校验的字段Date

type SignUpParam struct {
	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
	Name       string `json:"name" binding:"required"`
	Email      string `json:"email" binding:"required,email"`
	Password   string `json:"password" binding:"required"`
	RePassword string `json:"re_password" binding:"required,eqfield=Password"`
	// 需要使用自定义校验方法checkDate做参数校验的字段Date
	Date       string `json:"date" binding:"required,datetime=2006-01-02,checkDate"`
}

其中datetime=2006-01-02是内置的用于校验日期类参数是否满足指定格式要求的tag。 如果传入的date参数不满足2006-01-02这种格式就会提示如下错误:

{"msg":{"date":"date的格式必须是2006-01-02"}}

针对date字段除了内置的datetime=2006-01-02提供的格式要求外,假设我们还要求该字段的时间必须是一个未来的时间(晚于当前时间),像这样针对某个字段的特殊校验需求就需要我们使用自定义字段校验方法了。

首先我们要在需要执行自定义校验的字段后面添加自定义tag,这里使用的是checkDate,注意使用英文分号分隔开。

// customFunc 自定义字段级别校验方法
func customFunc(fl validator.FieldLevel) bool {
	date, err := time.Parse("2006-01-02", fl.Field().String())
	if err != nil {
		return false
	}
	if date.Before(time.Now()) {
		return false
	}
	return true
}

定义好了字段及其自定义校验方法后,就需要将它们联系起来并注册到我们的校验器实例中。

// 在校验器注册自定义的校验方法
if err := v.RegisterValidation("checkDate", customFunc); err != nil {
	return err
}

这样,我们就可以对请求参数中date字段执行自定义的checkDate进行校验了。 我们发送如下请求测试一下:

curl -H "Content-type: application/json" -X POST -d '{"name":"爱写代码小男孩","age":28,"email":"123@qq.com","password":"123", "re_password": "123", "date":"2020-01-02"}' http://127.0.0.1:1234/sign

此时得到的响应结果是:

{"msg":{"date":"Key: 'SignUpParam.date' Error:Field validation for 'date' failed on the 'checkDate' tag"}}

这…自定义字段级别的校验方法的错误提示信息很“简单粗暴”,和我们上面的中文提示风格有出入,必须想办法搞定它呀!

自定义翻译方法

我们现在需要为自定义字段校验方法提供一个自定义的翻译方法,从而实现该字段错误提示信息的自定义显示。

// registerTranslator 为自定义字段添加翻译功能
func registerTranslator(tag string, msg string) validator.RegisterTranslationsFunc {
	return func(trans ut.Translator) error {
		if err := trans.Add(tag, msg, false); err != nil {
			return err
		}
		return nil
	}
}

// translate 自定义字段的翻译方法
func translate(trans ut.Translator, fe validator.FieldError) string {
	msg, err := trans.T(fe.Tag(), fe.Field())
	if err != nil {
		panic(fe.(error).Error())
	}
	return msg
}

定义好了相关翻译方法之后,我们在InitTrans函数中通过调用RegisterTranslation()方法来注册我们自定义的翻译方法。

// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
	// ...liwenzhou.com...
	
		// 注册翻译器
		switch locale {
		case "en":
			err = enTranslations.RegisterDefaultTranslations(v, trans)
		case "zh":
			err = zhTranslations.RegisterDefaultTranslations(v, trans)
		default:
			err = enTranslations.RegisterDefaultTranslations(v, trans)
		}
		if err != nil {
			return err
		}
		// 注意!因为这里会使用到trans实例
		// 所以这一步注册要放到trans初始化的后面
		if err := v.RegisterTranslation(
			"checkDate",
			trans,
			registerTranslator("checkDate", "{0}必须要晚于当前日期"),
			translate,
		); err != nil {
			return err
		}
		return
	}
	return
}

这样再次尝试发送请求,就能得到想要的错误提示信息了。

{"msg":{"date":"date必须要晚于当前日期"}}
定制化操作

翻译

自定义结构体级别的校验方法

自定义字段级别的校验方法

单独使用 validator 做结构体数据校验
type Data struct {
	Email string `validate:"required,email"`
	Phone string `validate:"required"`
}

func f1() {
	// 声明一个校验器对象
	v := validator.New()
	su := Data{
		Email: "xxx",
	}
	// 针对v做定制化的操作,比如翻译、自定义校验规则...
	// 对结构体的字段进行校验
	err := v.Struct(&su)
	fmt.Println(err)
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Hibernate Validator是一个用于Java Bean验证的框架,它提供了一组注解和API,用于验证Java Bean的属性值是否符合指定的规则和约束。它可以用于验证表单数据、REST API请求、数据库实体等各种场景下的数据。Hibernate Validator支持多种验证规则,包括基本数据类型、字符串、日期、集合、数组等。它还支持自定义验证规则和错误消息。使用Hibernate Validator可以有效地减少代码量和提高开发效率。 ### 回答2: Hibernate Validator是一个流行的Java开源校验框架,它是基于JSR 303规范(Bean验证)的实现。它提供了一组注释和API,用于对JavaBean进行验证和校验。 Hibernate Validator提供了很多内置的校验注释,比如@NotNull、@NotEmpty、@Min、@Max等。这些注释可以直接应用在JavaBean的属性上,通过注释指定的校验规则来验证属性的值是否合法。同时,Hibernate Validator还支持自定义校验注释,可以根据业务需求来定义新的注释,并实现相应的校验逻辑。 Hibernate Validator不仅可以对单个属性进行校验,还支持对整个JavaBean进行校验。例如,可以在JavaBean的类级别上添加@Valid注释,来验证整个对象的合法性。此外,还可以通过分组校验来实现在不同场景下不同的校验规则。 除了注释方式外,Hibernate Validator还提供了一套强大的API,通过编程方式来进行校验和验证。通过ValidatorFactory和Validator两个主要的接口,可以创建Validator对象并执行校验操作。可以验证单个属性的值,也可以验证整个JavaBean对象。 Hibernate Validator还提供了国际化的支持,可以根据不同的区域设置显示不同的错误信息。同时,还能够将校验错误信息与具体的校验注释进行关联,方便开发者快速定位问题。 总结来说,Hibernate Validator提供了一种有效、灵活和方便的方式来对JavaBean进行校验。它的注释和API丰富多样,并且支持自定义校验规则,同时还提供了国际化和错误信息关联等特性,使得校验过程更加强大和可控。它在Java开发中的应用越来越广泛,为开发者提供了一种便捷的校验解决方案。 ### 回答3: Hibernate Validator是一个基于JSR 380规范的校验框架,它可以轻松地对Java对象进行校验。通过使用Hibernate Validator开发人员可以在应用程序中方便地添加校验规则,并且可以验证这些规则是否被满足。 Hibernate Validator提供了一组注解,这些注解可以附加在JavaBean的属性上,以标识需要进行校验的规则。例如,@NotNull注解用于确保属性的值不为空,@Size注解用于确保字符串类型的属性的长度在指定范围内,等等。除了注解外,Hibernate Validator还提供了一些内置的校验器,用于验证各种数据类型的属性,例如字符串、数字、日期等。 使用Hibernate Validator进行校验非常简单。只需要在需要校验的JavaBean上添加注解,并在需要校验的时候调用校验方法即可。校验方法会返回一个包含校验结果的对象,开发人员可以根据需要进行处理。校验方法还可以接受一个可选的校验分组参数,用于校验不同场景下的不同规则。 Hibernate Validator还提供了一些扩展功能,用于自定义校验规则。开发人员可以创建自定义的校验注解,并编写相应的校验器来实现特定的校验逻辑。这使得Hibernate Validator非常灵活,可以满足各种不同的校验需求。 总结来说,Hibernate Validator是一个强大而灵活的校验框架,能够方便地对Java对象进行校验。使用Hibernate Validator可以增加应用程序的稳定性和可靠性,减少错误和异常的发生。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值