GVKun编程网logo

Go高效率开发Web参数校验的方式有哪些(go 参数校验)

9

在这篇文章中,我们将带领您了解Go高效率开发Web参数校验的方式有哪些的全貌,包括go参数校验的相关情况。同时,我们还将为您介绍有关golangvalidator参数校验的实现、javaweb中通过网

在这篇文章中,我们将带领您了解Go高效率开发Web参数校验的方式有哪些的全貌,包括go 参数校验的相关情况。同时,我们还将为您介绍有关golang validator参数校验的实现、java web中通过网页在线浏览各种文档的方式有哪些?、JAVA 参数校验的几种高级用法、JavaWeb中实现文件上传的方式有哪些?的知识,以帮助您更好地理解这个主题。

本文目录一览:

Go高效率开发Web参数校验的方式有哪些(go 参数校验)

Go高效率开发Web参数校验的方式有哪些(go 参数校验)

本篇内容介绍了“Go高效率开发Web参数校验的方式有哪些”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

web开发中,你肯定见到过各种各样的表单或接口数据校验:

  • 客户端参数校验:在数据提交到服务器之前,发生在浏览器端或者app应用端,相比服务器端校验,用户体验更好,能实时反馈用户的输入校验结果。

  • 服务器端参数校验:发生在客户端提交数据并被服务器端程序接收之后,通常服务器端校验都是发生在将数据写入数据库之前,如果数据没通过校验,则会直接从服务器端返回错误消息,并且告诉客户端发生错误的具体位置和原因,服务器端校验不像客户端校验那样有好的用户体验,因为它直到整个表单都提交后才能返回错误信息。但是服务器端校验是应用对抗错误,恶意数据的最后防线,在这之后,数据将被持久化至数据库。当今所有的服务端框架都提供了数据校验与过滤功能(让数据更安全)。

本文主要讨论服务器端参数校验

确保用户以正确格式输入数据,提交的数据能使后端应用程序正常工作,同时在一切用户的输入都是不可信的前提下(比如xss跨域脚本攻击,sql注入),参数验证是不可或缺的一环,也是很繁琐效率不高的一环,在对接表单提交或者api接口数据提交,程序里充斥着大量重复验证逻辑和if else语句,本文分析参数校验的三种方式,找出最优解,从而提高参数验证程序代码的开发效率。

需求场景:

常见的网站登陆场景

业务需求

接口一:
场景:输入手机号,获取短信验证码
校验需求:判断手机号非空,手机号格式是否正确
接口二:
场景:手机收到短信验证码,输入验证码,点击登陆
校验需求:1、判断手机号非空,手机号格式是否正确;2、验证码非空,验证码格式是否正确

技术选型:web框架gin

第一种实现方式:自定义实现校验逻辑

package main

func main() {
   engine := gin.New()

    engine := gin.New()

    ctrUser := controller.NewUser()
    engine.POST("/user/login", ctrUser.Login)

    ctrCaptcha := controller.NewCaptcha()
    engine.POST("/captcha/send", ctrCaptcha.Send)

    engine.Run()
}

--------------------------------------------------------------------------------
package controller

type Captcha struct {}

func (ctr *Captcha) Send(c *gin.Context) {
   mobile := c.PostForm("mobile")

   // 校验手机号逻辑
   if mobile == "" {
      c.JSON(http.StatusBadRequest, gin.H{"error": "手机号不能为空"})
      return
   }

   matched, _ := regexp.MatchString(`^(1[3-9][0-9]\d{8})$`, mobile)
   if !matched {
      c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
      return
   }

    c.JSON(http.StatusBadRequest, gin.H{"mobile": mobile})
}

type User struct {}

func (ctr *User) Login(c *gin.Context) {
   mobile := c.PostForm("mobile")
   code := c.PostForm("code")

   // 校验手机号逻辑
   if mobile == "" {
      c.JSON(http.StatusBadRequest, gin.H{"error": "手机号不能为空"})
      return
   }

   matched, _ := regexp.MatchString(`^(1[3-9][0-9]\d{8})$`, mobile)
   if !matched {
      c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
      return
   }

   // 校验手机号逻辑
   if code == "" {
      c.JSON(http.StatusBadRequest, gin.H{"error": "验证码不能为空"})
      return
   }

   if len(code) != 4 {
      c.JSON(http.StatusBadRequest, gin.H{"error": "验证码为4位"})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"mobile": mobile, "code": code})
}

源码链接

代码分析:
参数验证函数放在Controller层;
这是一种比较初级也是最朴素的实现方式,在现实代码review中经常遇到,这样实现会有什么问题?
1、手机号码验证逻辑重复;
2、违背了controller层的职责,controller层充斥着大量的验证函数(Controller层职责:从HTTP请求中获得信息,提取参数,并分发给不同的处理服务);

重复代码是软件质量下降的重大来源!!!

1、重复代码会造成维护成本的成倍增加;
2、需求的变动导致需要修改重复代码,如果遗漏某处重复的逻辑,就会产生bug(例如手机号码增加12开头的验证规则);
3、重复代码会导致项目代码体积变得臃肿;

聪明的开发者肯定第一时间想到一个解决办法:提取出验证逻辑,工具包util实现IsMobile函数

package util

func IsMobile(mobile string) bool {
   matched, _ := regexp.MatchString(`^(1[3-9][0-9]\d{8})$`, mobile)
   return matched
}

代码分析:
问题:代码会大量出现util.IsMobile、util.IsEmail等校验代码

思考:从面向对象的思想出发,IsMobile属于util的动作或行为吗?

第二种实现方式:模型绑定校验

技术选型:web框架gin自带的模型验证器中文提示不是很好用,这里使用govalidator 模型绑定校验是目前参数校验最主流的验证方式,每个编程语言的web框架基本都支持这种模式,模型绑定时将Http请求中的数据映射到模型对应的参数,参数可以是简单类型,如整形,字符串等,也可以是复杂类型,如Json,Json数组,对各种数据类型进行验证,然后抛出相应的错误信息。

源码链接

package request

func init() {
   validator.TagMap["IsMobile"] = func(value string) bool {
      return IsMobile(value)
   }
}

func IsMobile(value string) bool {
    matched, _ := regexp.MatchString(`^(1[1-9][0-9]\d{8})$`, value)
    return matched
}

type Captcha struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
}

type User struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
}
-------------------------------------------------------------------------------
package controller

type Captcha struct {}

func (ctr *Captcha) Send(c *gin.Context) {
   request := new(request.Captcha)
   if err := c.ShouldBind(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   if _, err := validator.ValidateStruct(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"data": request})
}

type User struct {}

func (ctr *User) Login(c *gin.Context) {
   request := new(request.User)
   if err := c.ShouldBind(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   if _, err := validator.ValidateStruct(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"data": request})
}

代码分析:
1、mobile校验逻辑同样重复(注释实现校验的逻辑重复,如错误提示"手机号不能为空"修改为"请填写手机号",需要修改两个地方)
2、validator.ValidateStruct函数会验证结构体所有属性

对于2问题不太好理解,举例解释
业务场景:用户注册功能,需要校验手机号、短信验证码、密码、昵称、生日
type User struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
   Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
   Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
   Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
}

代码分析:
登陆功能需要校验Mobile、Code属性;
注册功能需要校验Mobile、Code、Password、Nickname、Birthday属性;

如果代码校验共用User结构体,就产生了一个矛盾点,有两种方法可以解决这一问题:

  • 修改validator.ValidateStruct函数,增加校验白名单或黑名单,实现可以设置部分属性校验或者忽略校验部分属性;

// 只做Mobile、Code属性校验或者忽略Mobile、Code属性校验
validator.ValidateStruct(user, "Mobile", "Code") 

这种也是一种不错的解决方式,但是在项目实践中会遇到点小问题:
1、一个校验结构体有20个属性,只需要校验其中10个字段,不管用白名单还是黑名单都需要传10个字段;
2、手写字段名容易出错;
  • 新建不同的结构体,对应相应的接口绑定校验

type UserLogin struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
}

type UserRegister struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
   Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
   Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
   Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
}

代码解析:
用户登陆接口对应:UserLogin结构体
用户注册接口对应:UserRegister结构体

同样问题再次出现,Mobile、Code属性校验逻辑重复。

再介绍第三种参数校验方式之前,先审视一下刚才的一段代码:

if err := c.ShouldBind(&request); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  return
}

if _, err := validator.ValidateStruct(request); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  return
}

参数绑定校验的地方都需要出现这几行代码,我们可以修改gin源码,把govalidator库集成在gin中;
如何修改第三方库源代码参照项目 源码链接

在gin根目录增加context_validator.go文件,代码如下:
package gin

import (
   "github.com/asaskevich/govalidator"
)

type Validator interface {
   Validate() error
}

func (c *Context) ShouldB(data interface{}) error {
   if err := c.ShouldBind(data); err != nil {
      return err
   }

   if _, err := govalidator.ValidateStruct(data); err != nil {
      return err
   }

   var v Validator
   var ok bool
   if v, ok = data.(Validator); !ok {
      return nil
   }

   return v.Validate()
}

controller层的参数绑定校验代码如下:

type User struct {}

func (ctr *User) Register(c *gin.Context) {
   request := new(request.UserRegister)
   if err := c.ShouldB(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"data": request})
}

代码分析:
增加了Validator接口,校验模型实现Validator接口,可以完成更为复杂的多参数联合校验检查逻辑,如检查密码和重复密码是否相等

type UserRegister struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
   Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
   RePassword string `form:"rePassword" valid:"required~重复密码不能为空,stringlength(6|18)~重复密码6-18个字符"`
   Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
   Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
}

func (req *UserRegister) Validate() error {
   if req.Password != req.RePassword {
      return errors.New("两次密码不一致")
   }

   return nil
}

模型校验是通过反射机制来实现,众所周知反射的效率都不高,现在gin框架集成govalidator,gin原有的校验功能就显得多余,小伙伴们可以从ShouldBind函数从下追,把自带的校验功能屏蔽,提高框架效率。

第三种实现方式:拆解模型字段,组合结构体

解决字段校验逻辑重复的最终方法就是拆解字段为独立结构体,通过多个字段结构体的不同组合为所需的校验结构体,代码如下:
源码链接

package captcha

type CodeS struct {
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
}


package user

type PasswordS struct {
   Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
}

type RePasswordS struct {
   RePassword string `form:"rePassword" valid:"required~重复密码不能为空,stringlength(6|18)~重复密码6-18个字符"`
}

type NicknameS struct {
   Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
}

type BirthdayS struct {
   Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
}

type UserLogin struct {
   MobileS
   captcha.CodeS
}

type UserRegister struct {
   MobileS
   captcha.CodeS
   user.PasswordS
   user.RePasswordS
   user.NicknameS
   user.BirthdayS
}

func (req *UserRegister) Validate() error {
   if req.Password() != req.RePassword() {
      return errors.New("两次密码不一致")
   }

   return nil
}

代码解析:
为什么字段结构体都加了S?
1、结构体包含匿名结构体不能调用匿名结构体同名属性,匿名结构体加S标识为结构体

示例代码不能很好的展示项目结构,可以查看源代码

代码分析:

  • 独立的字段结构体通常以表名为包名定义范围,比如商品名称和分类名称字段名都为Name,但是所需定义的校验逻辑(字符长度等)很有可能不同;

  • 每一个接口建立对应的验证结构体:

接口user/login:    对应请求结构体UserLogin
接口user/register: 对应请求结构体UserRegister
接口captcha/send:  对应请求结构体CaptchaSend
  • 公用的字段结构体例如ID、Mobile建立单独的文件;

总结:
一、验证逻辑封装在各自的实体中,由request层实体负责验证逻辑,验证逻辑不会散落在项目代码的各个地方,当验证逻辑改变时,找到对应的实体修改就可以了,这就是代码的高内聚;

二、通过不同实体的嵌套组合就可以实现多样的验证需求,使得代码的可重用性大大增强,这就是代码的低耦合

独立字段结构体组合成不同的校验结构体,这种方式在实际项目开发中有很大的灵活性,可以满足参数校验比较多变复杂的需求场景,小伙伴可以在项目开发中慢慢体会。

参数绑定校验在项目中遇到的几个问题

源码链接1、需要提交参数为json或json数组如何校验绑定?

type ColumnCreateArticle struct {
   IDS
   article.TitleS
}

type ColumnCreate struct {
   column.TitleS
   Article *ColumnCreateArticle `form:"article"`
   Articles []ColumnCreateArticle `form:"articles"`
}

2、严格遵循一个接口对应一个校验结构体

func (ctr *Column) Detail(c *gin.Context) {
   request := new(request.IDS)
   if err := c.ShouldB(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"data": request})
}

示例代码获取文章专栏详情的接口,参数为专栏id,因为只有一个id参数,如果刚开始图省事,没有建立对应独立的ColumnDetail校验结构体,后期接口增加参数(例如来源等),还是要改动这一块代码,增加代码的不确定性

3、布尔参数的三种状态

type ColumnDetail struct {
   IDS
   // 为真显示重点文章,为否显示非重点文章,为nil都显示
   ArticleIsImportant *bool `form:"articleIsImportant"`
}

column?id=1&articleIsImportant=true    ArticleIsImportant为true
column?id=1&articleIsImportant=false   ArticleIsImportant为false
column?id=1                            ArticleIsIm

“Go高效率开发Web参数校验的方式有哪些”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注小编网站,小编将为大家输出更多高质量的实用文章!

golang validator参数校验的实现

golang validator参数校验的实现

今天在改后台页面,参数校验错误时输出全是英文,使用着很难看懂到底时什么错了
故而决定去做i18n前端国际化. 改的时候踩了很多坑,故而记录一下,顺便记录以下查问题的方式。

效果

从原来的Title is required变为标题为必填字段

完成后的代码:

这里主要定义了初始化了一个中文的trans和Validate的变量,并对其做初始化
初始化主要做了以下事情:

注册了TagName函数
// RegisterTagNameFunc registers a function to get alternate names for StructFields.
这个方法主要就是提供一个tag的解析器,返回一个Field替代的字符串
我自己是定义了一个label的tag用于替换

注册了validate的翻译函数
直接使用了原来提供的中文转换,对required等标签做对应的国际化

package service

import (
  zhongwen "github.com/go-playground/locales/zh"
  ut "github.com/go-playground/universal-translator"
  "github.com/go-playground/validator/v10"
  zh_translations "github.com/go-playground/validator/v10/translations/zh"
  "reflect"
  "strings"
)

var Validate *validator.Validate
var trans ut.Translator

func init() {
  zh := zhongwen.New()
  uni := ut.New(zh, zh)
  trans, _ = uni.GetTranslator("zh")

  Validate = validator.New()
  Validate.RegisterTagNameFunc(func(field reflect.StructField) string {
    label := field.Tag.Get("label")
    if label == "" {
      return field.Name
    }
    return label
  })
  zh_translations.RegisterDefaultTranslations(Validate, trans)
}
func Translate(errs validator.ValidationErrors) string {
  var errList []string
  for _, e := range errs {
    // can translate each error one at a time.
    errList = append(errList,e.Translate(trans))
  }
  return strings.Join(errList,"|")
}

调用方式

type ArticlesPost struct {
  Title      string `json:"title" validate:"required,max=32,min=4" label:"标题"`
}
var ap ArticlePost
err = service.Validate.Struct(ap)
if err!=nil{
 errStr =Translate(errs)
 fmt.Sprintln(errStr)
}

思路

  • 最刚开始去百度查了,无果
  • 查了iris的文档,也无果
  • 去看了validate的文档,找到了universal-translator 这个包,可以初步将is required等样式改为必填字段
  • 还是没法将字段名映射成中文,google搜索到了How can I translate fieldName? #364这个issue,评论里给出了en.Add("MyField", "Field", false)的方式添加字段的映射,最后在alidate.RegisterTranslation注册required的时候,通过T方法转换成对应的中文fld, _ := ut.T(fe.Field()),考虑到要每次都注册Struct的字段,而且全局的同一个key肯定没法定义不同的值,弃用
  • 第一次想着是不是校验本身已经提供了对应的位置,看了interface,有些英文半知半解,没找到结果,放弃
  • 继续,想到是不是可以自定义tag,然后重写type TranslationFunc func(ut ut.Translator, fe FieldError) string 函数,想在这个翻译阶段,去动态过去struct中那个tag的值,这样就不会重复了.
  • 研究了这个函数的传参,FieldError中已经只剩下字段对应的数据了,无法获取到tag信息,差点已经想放弃了
  • 再次研究validator关于tag的函数

第一个是设置一个新的tag来替换validate,另一个的说明是注册一个方法来为结构体字段获取替换的名字

仔细看看说明,果然就是这个,在看看TagNameFunc的签名,参数是reflect.StructField,能够拿到tag等一系列信息

// TagNameFunc allows for adding of a custom tag name parser
type TagNameFunc func(field reflect.StructField) string
// SetTagName allows for changing of the default tag name of ''validate''
func (v *Validate) SetTagName(name string) {
  v.tagName = name
}

// RegisterTagNameFunc registers a function to get alternate names for StructFields.
//
// eg. to use the names which have been specified for JSON representations of structs, rather than normal Go field names:
//
//  validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
//    name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
//    if name == "-" {
//      return ""
//    }
//    return name
//  })
func (v *Validate) RegisterTagNameFunc(fn TagNameFunc) {
  v.tagNameFunc = fn
  v.hasTagNameFunc = true
}

至此,终于找到了正确的解决方案

总结

在这里发现为了解决这个问题走了很多弯路,查了一大堆资料才发现甚至原来就有提供该功能。

发现自己的几个问题:

  • 英文不是很好,偶尔有些单词不认识,阻止了进一步发现问题,这里也突然想到,英语好一些确实可以在学编程这个路子上受益匪浅
  • 看文档不是很仔细,鄙人觉得大部分的编程问题都不是很高深,能读得懂错误是什么意思,然后去查查文档或者搜索引擎就能解决,另一个是大部分的编程文档还是英文好一些,细节性的东西在翻译的时候可能会被略掉。

到此这篇关于golang validator参数校验的实现的文章就介绍到这了,更多相关golang validator参数校验内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

您可能感兴趣的文章:
  • golang常用库之字段参数验证库-validator使用详解
  • golang validator库参数校验实用技巧干货
  • golan参数校验Validator
  • Go高效率开发Web参数校验三种方式实例

java web中通过网页在线浏览各种文档的方式有哪些?

java web中通过网页在线浏览各种文档的方式有哪些?

比如:1.jcom    2.jacob 3.JODConverter

它们各有各的缺点,究竟哪种方式更稳定一些?

它们都是将文档转换为swf然后加载在网页中,但现在html5的出现将可能会代替swf的使用,如果浏览器不支持flash插件,那怎么办?

JAVA 参数校验的几种高级用法

JAVA 参数校验的几种高级用法

在 Java 开发过程中,参数校验是一个非常关键的部分。简单的校验规则,如通过 @NotNull@NotBlank@Size 等注解能够很方便地实现常见的校验逻辑,但对于稍微复杂一些的场景,比如多个参数之间的逻辑关系依赖,或者业务上下文相关的校验,注解往往无法胜任。为了能够优雅地处理这些复杂的校验场景,我们需要采用更加灵活且扩展性强的方案。

背景问题解读

让我们具体看一下你提到的场景。假设有一个接口,包含以下两个参数:

private boolean switch;
private String str;

根据业务逻辑,当 switchtrue 时,str 必须不为 null,否则接口请求应被视为无效。这样的逻辑在注解中难以直接实现,因为 @NotNull 只适用于单一字段,而无法根据其他字段的值来决定当前字段是否应该校验为非空。这就是为什么我们需要探索更加灵活的校验策略。

解决方案探讨

为了优雅地解决类似的参数校验问题,我们可以采用以下几种策略:

1. 手动编写逻辑校验

最直接的方法是在业务逻辑层中手动编写校验规则。通过简单的 if 语句,可以实现对参数之间逻辑关系的验证。这种方式是最常见、最基础的实现方式。代码如下:

public void validateInput(boolean switch, String str) {
    if (switch && str == null) {
        throw new IllegalArgumentException("When switch is true, str must not be null.");
    }
}

优点在于它非常直观,逻辑清晰,适合一些简单的场景。但缺点同样显而易见,手动校验的方式分散在代码中,容易造成重复校验逻辑,并且随着业务复杂度的增加,代码的可读性和可维护性都会下降。

2. 定制校验注解

为了更好地分离校验逻辑和业务逻辑,Java 提供了自定义注解的功能,结合 javax.validation 中的 Constraint 注解,能够实现基于注解的复杂校验。

步骤如下:

  1. 定义自定义注解:
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { SwitchStrValidator.class })
public @interface ValidSwitchStr {
    String message() default "Invalid parameters";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

这里定义了一个新的注解 @ValidSwitchStr,它将作用于类级别(ElementType.TYPE),并且通过 Constraint 指定了一个验证器 SwitchStrValidator

  1. 编写验证器:
public class SwitchStrValidator implements ConstraintValidator<ValidSwitchStr, MyRequest> {

    @Override
    public boolean isValid(MyRequest request, ConstraintValidatorContext context) {
        if (request.isSwitch() && request.getStr() == null) {
            return false;
        }
        return true;
    }
}

这里的 SwitchStrValidator 实现了 ConstraintValidator 接口,并将 isValid 方法的逻辑定义为:当 switchtrue 时,str 必须不为 null

  1. 在类上使用自定义注解:
@ValidSwitchStr
public class MyRequest {
    private boolean switch;
    private String str;

    // getters and setters
}

通过这样的方式,我们可以将校验逻辑封装到注解和验证器中,业务代码中只需要声明 @ValidSwitchStr,校验逻辑会自动生效。这种方式的好处在于分离了业务逻辑和校验逻辑,使得代码更加模块化,校验规则清晰明确。

3. 使用 Spring Validator 机制

在 Spring 框架中,Validator 是一个常见的校验机制。相比于简单的手动校验逻辑,Validator 提供了更好的可扩展性和复用性。我们可以通过实现 org.springframework.validation.Validator 接口,编写复杂的校验逻辑。

  1. 编写自定义 Validator:
@Component
public class MyRequestValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return MyRequest.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        MyRequest request = (MyRequest) target;

        if (request.isSwitch() && request.getStr() == null) {
            errors.rejectValue("str", "str.null", "When switch is true, str must not be null.");
        }
    }
}
  1. 在 Controller 中使用 Validator:
@RestController
public class MyController {

    @Autowired
    private MyRequestValidator myRequestValidator;

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(myRequestValidator);
    }

    @PostMapping("/submit")
    public ResponseEntity<String> submit(@Valid @RequestBody MyRequest request, BindingResult result) {
        if (result.hasErrors()) {
            return ResponseEntity.badRequest().body(result.getAllErrors().toString());
        }

        // 处理业务逻辑
        return ResponseEntity.ok("Success");
    }
}

通过 @InitBinder 方法将 Validator 绑定到 WebDataBinder 上,我们可以在请求处理之前进行自定义校验。这样做不仅能够实现灵活的校验逻辑,还可以将错误信息集中处理。

4. 使用 Bean Validation API 进行高级校验

如果项目使用的是 Java EE 或者 Spring 框架,Bean Validation(即 JSR 303/JSR 380)提供了强大的校验支持。通过结合 @Valid 和自定义约束注解,能够解决复杂的参数校验问题。这里我们以组合式校验为例,演示如何使用 Bean Validation 进行高级参数校验。

  1. 定义类级别的校验:

    假设有一个接口,包含多个相关参数,而这些参数之间的约束逻辑相对复杂,如下所示:

    public class AdvancedRequest {
    
        @NotNull
        private Boolean switch;
    
        private String str;
    
        // getters and setters
    }

    switch 决定了 str 的合法性。为了处理这种场景,可以使用类级别的校验注解:

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = AdvancedRequestValidator.class)
    public @interface ValidAdvancedRequest {
        String message() default "Invalid request parameters";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    }
  2. 实现验证逻辑:

    编写对应的验证器,实现 isValid 方法,来完成实际的校验逻辑:

    public class AdvancedRequestValidator implements ConstraintValidator<ValidAdvancedRequest, AdvancedRequest> {
    
        @Override
        public boolean isValid(AdvancedRequest request, ConstraintValidatorContext context) {
            if (request.getSwitch() != null && request.getSwitch() && request.getStr() == null) {
                return false;
            }
            return true;
        }
    }
  3. 使用注解:

    最后,将这个校验注解应用到 AdvancedRequest 类上:

    @ValidAdvancedRequest
    public class AdvancedRequest {
        // Fields, getters, and setters
    }

5. 使用第三方库:如 Apache Commons 或 Google Guava

除了手动编写逻辑或使用框架自带的工具,还可以引入一些第三方库来简化参数校验。比如 Apache Commons 提供了 Validate 类,可以帮助我们快速进行常见的校验。

示例代码如下:

public void validateInput(boolean switch, String str) {
    if (switch) {
        Validate.notNull(str, "When switch is true, str must not be null");
    }
}

这种方式虽然简单,但仍然只是手动校验的一个变种,没有解决复用和可维护性的问题。它适合那些项目较为简单的场景,如果项目规模较大,建议还是采用更正式的校验机制。

6. 在大型项目中的应用案例

在一个真实的企业级项目中,假设你正在开发一个电商平台的订单服务。订单创建时,用户需要提供支付方式和支付凭证。但有些支付方式(如信用卡支付)要求提供完整的支付凭证,而其他支付方式(如货到付款)则不需要提供这些信息。这就类似于上述的 switchstr 的问题。

在这种情况下,如果我们用手动校验或者简单的注解来处理,代码会变得非常繁琐。因此,采用定制化的注解和验证器机制是更好的选择。在订单创建的请求对象上,我们

可以创建一个类似 @ValidPayment 的注解,用于根据支付方式动态验证支付凭证是否为必填字段。

这不仅让代码更加清晰,也让团队能够快速扩展和维护相关的校验逻辑。

结语

通过以上几种方法,复杂的参数校验问题可以在 Java 中得到优雅的解决。无论是手动校验、自定义注解、Spring Validator 机制,还是使用 Bean Validation API,每种方案都有其独特的优势和适用场景。在实际项目中,选择适合的方案需要根据具体的业务需求和项目规模来决定。

无论采用哪种方式,都要注意保持校验逻辑的简洁、模块化和可维护性,使代码在应对复杂的业务变化时能够轻松应对。

JavaWeb中实现文件上传的方式有哪些?

JavaWeb中实现文件上传的方式有哪些?

问题:JavaWeb中实现文件上传的方式有哪些?

文件上传的方式

  • Servlet2.5 方式
  • Servlet3.0 方式
  • SpringMVC 方式

案例实操

Servlet2.5 方式

文件上传涉及到前台页面的编写和后台服务器端代码的编写,前台发送文件,后台接收并保存文件,这才是一个完整的文件上传。

1) 前台页面

在做文件上传的时候,会有一个上传文件的界面,首先我们需要一个表单,并且表单的请求方式为 POST;其次我们的 form 表单的 enctype 必须设为”multipart/form-data”即 enctype=“multipart/form-data” 意思是设置表单的 MIME 编码。默认情况下这个编码格式是 ”application/x-www-form-urlencoded”,不能用于文件上传;只有使用了 multipart/form-data 才能完整地传递文件数据。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>上传文件</title>
</head>
<body>
    <form action="uploadServlet" method="post" enctype="multipart/form-data">
        文件:<input type="file" name="myfile"/>
        <input type="submit" value="上传" />
    </form>
</body>
</html> 

2) 后台 commons-fileupload 的使用

首先需要导入第三方jar包,http://commons.apache.org/ 下载 commons-io 和 commons-fileupload 两个jar的资源。解压并导入到项目中。commons-fileupload.jar 是文件上传的核心包 commons-io.jar 是 fileupload 的依赖包,同时又是一个工具包。

介绍一下使用到的几个核心类

  DiskFileItemFactory – 设置磁盘空间,保存临时文件。只是一个工具类

  ServletFileUpload – 文件上传的核心类,此类接收 request,并解析

  ServletFileUpload.parseRequest(request); – List 解析 request

  1、创建一个 DiskFileItemFactory 工厂类,并制定临时文件和大小

  2、创建 ServletFileUpload 核心类,接收临时文件,做请求的转换

  3、通过 ServletFileUpload 类转换原始请求,得到 FileItem 集合

  4、遍历集合中的各个元素并处理

  5、判断每个元素是否是普通表单项,如果是则按照普通表单项处理

  6、如果不是普通表单项,则是文件,通过处理的方式进行处理(上传)

public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 设定编码,可以获取中文文件名
        request.setCharacterEncoding("UTF-8");
        // 获取tomcat下的upload目录的路径
        String path = getServletContext().getRealPath("/upload");
        // 临时文件目录
        String tempPath = getServletContext().getRealPath("/temp");
        // 检查我们是否有文件上传请求
        // boolean isMultipart = ServletFileUpload.isMultipartContent(req);
        // 1、声明DiskFileItemFactory工厂类,用于在指定磁盘上设置一个临时目录
        DiskFileItemFactory disk = new DiskFileItemFactory(1024 * 10, new File(tempPath));
        // 2、声明ServletFileUpload,接收上面的临时文件。也可以默认值
        ServletFileUpload up = new ServletFileUpload(disk);
        // 3、解析request
        try {
            List<FileItem> list = up.parseRequest(request);
            if (list.size() > 0) {
                for (FileItem file : list) {
                    // 判断是否是普通的表单项
                    if (file.isFormField()) {
                        String fieldName = file.getFieldName();
                        // 中文乱码,此时还需要指定获取数据的编码方式
                        // String value = file.getString();
                        String value = file.getString("UTF-8");
                        System.out.println(fieldName + "=" + value);
                    } else { // 说明是一个文件
                        // 获取文件本身的名称
                        String fileName = file.getName();
                        System.out.println(file.getFieldName());
                        // 处理文件名称
                        fileName = fileName.substring(fileName.lastIndexOf("") + 1);
                        System.out.println("old Name : " + fileName);
                        // 修改名称
                        String extName = fileName.substring(fileName.lastIndexOf("."));
                        String newName = UUID.randomUUID().toString().replace("-", "") + extName;
                        // 保存新的名称,并写出到新文件中
                        file.write(new File(path + "/" + newName));
                        System.out.println("文件名是:" + fileName);
                        System.out.println("文件大小是:" + file.getSize());
                        file.delete();
                    }
                }
            }
        } catch (FileUploadException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

} 

Servlet3.0 方式

使用注解 @MultipartConfig 将一个 Servlet 标识为支持文件上传。Servlet3.0 将 multipart/form-data 的 POST 请求封装成 Part,通过 Part 对上传的文件进行操作。

前台

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>上传文件</title>
</head>
<body>
    <form action="upload" method="post" enctype="multipart/form-data">
        姓名:<input type="text" name="uname"/>
        文件:<input type="file" name="myfile"/>
        <input type="submit" value="上传" />
    </form>
</body>
</html> 

后台

@WebServlet("/upload")
@MultipartConfig
public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("上传文件...");
        // 设置编码
        request.setCharacterEncoding("UTF-8");
        // 获取普通表单项参数
        String uname = request.getParameter("uname");
        System.out.println(uname);
        // 上传文件
        // 得到part对象 request.getpart(name):name代表的是表单中file元素的name属性值
        Part part = request.getPart("myfile");
        // 得到文件存放的路径
        String path = request.getServletContext().getRealPath("/");
        // 得到文件名
        String fileName = part.getSubmittedFileName();
        // 上传
        part.write(path + fileName);
    }

} 

SpringMVC 方式

Pom 文件修改 添加 commons-fileupload 依赖

<dependency> 

    <groupId>commons-fileupload</groupId> 

    <artifactId>commons-fileupload</artifactId> 

    <version>1.3.2</version> 

</dependency> 

servlet-context.xml

<bean id="multipartResolver">

    <property name="maxUploadSize"> 

    <value>104857600</value> 

    </property> 

    <property name="maxInMemorySize"> 

    <value>4096</value> 

    </property> 

</bean> 

FileController

import java.io.File; 

import java.io.IOException; 

import javax.servlet.http.HttpServletRequest; 

import org.springframework.stereotype.Controller; 

import org.springframework.web.bind.annotation.RequestMapping; 

import org.springframework.web.multipart.MultipartFile; 

import org.springframework.web.multipart.MultipartHttpServletRequest; 

import org.springframework.web.servlet.ModelAndView; 

@Controller 

public class FileController { 

    @RequestMapping("/uploadFile") 

    public ModelAndView uploadFile(HttpServletRequest request){ 

        ModelAndView mv=new ModelAndView(); 

        mv.setViewName("result"); 

        MultipartHttpServletRequest mr=(MultipartHttpServletRequest) request; 

        MultipartFile multipartFile= mr.getFile("file"); 

        String path=request.getSession().getServletContext().getRealPath("upload"); 

        System.out.println(path); 

        if(null!=multipartFile&&!multipartFile.isEmpty()){ 

            String fileName=multipartFile.getOriginalFilename(); 

        try { 

            multipartFile.transferTo(new File(path,fileName)); 

            mv.addObject("msg", "文件上传成功!"); 

        } catch (Exception e) { 

            mv.addObject("msg", "上传失败!"); 

            e.printStackTrace(); 

        } 

        } 

        return mv; 

    } 

} 

前台表单

<form action="uploadFile" method="post" enctype="multipart/form-data"> 

    <input type="file" name="file"/> 

    <button type="submit"> 提交</button> 

</form> 

扩展~MIME

MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。

它是一个互联网标准,扩展了电子邮件标准,使其能够支持:

非ASCII字符文本;非文本格式附件(二进制、声音、图像等);由多部分(multiple parts)组成的消息体;包含非ASCII字符的头信息(Header information)。

这个标准被定义在RFC 2045、RFC 2046、RFC 2047、RFC 2048、RFC 2049等RFC中。 MIME改善了由RFC 822转变而来的RFC 2822,这些旧标准规定电子邮件标准并不允许在邮件消息中使用7位ASCII字符集以外的字符。正因如此,一些非英语字符消息和二进制文件,图像,声音等非文字消息原本都不能在电子邮件中传输(MIME可以)。MIME规定了用于表示各种各样的数据类型的符号化方法。 此外,在万维网中使用的HTTP协议中也使用了MIME的框架,标准被扩展为互联网媒体类型。

查看不同文件对应的 MIME 类型,推荐大家一种方式,以 Tomcat为例,它下面的 web.xml 文件可以查看所有的MIME类型,通过 Ctrl + F 搜索快速找到你想知道的文件对应的 MIME 类型。

。 MIME改善了由RFC 822转变而来的RFC 2822,这些旧标准规定电子邮件标准并不允许在邮件消息中使用7位ASCII字符集以外的字符。正因如此,一些非英语字符消息和二进制文件,图像,声音等非文字消息原本都不能在电子邮件中传输(MIME可以)。MIME规定了用于表示各种各样的数据类型的符号化方法。 此外,在万维网中使用的HTTP协议中也使用了MIME的框架,标准被扩展为互联网媒体类型。

查看不同文件对应的 MIME 类型,推荐大家一种方式,以 Tomcat为例,它下面的 web.xml 文件可以查看所有的MIME类型,通过 Ctrl + F 搜索快速找到你想知道的文件对应的 MIME 类型。

我们今天的关于Go高效率开发Web参数校验的方式有哪些go 参数校验的分享就到这里,谢谢您的阅读,如果想了解更多关于golang validator参数校验的实现、java web中通过网页在线浏览各种文档的方式有哪些?、JAVA 参数校验的几种高级用法、JavaWeb中实现文件上传的方式有哪些?的相关信息,可以在本站进行搜索。

本文标签: