Hibernate Validator 校验框架的使用经验

前言

其实我是一个工作不久的新人,虽然经常写着大家常说的增删改查功能,但还是快乐充实的不行~

说到业务开发就离不开入参校验(或者以前说的表单验证),看着同事一遍遍写啊写的 if else 我就感觉这不是一个很好的编程实践,虽然看着逻辑很直观简单,但是重复的代码也太多了吧···

在寻找解决方案的过程中了解到两个框架,Hibernate Validator 和 OVal 。我先学会了使用Hibernate Validator ,后来在没仔细看OVal的情况下感觉差不多(不好意思,有空再去细细看看它的文档),因为我们的项目中已经有Hibernate Validator的依赖,所以就继续使用这个了(好吧,其实是因为我没仔细看OVal的文档)。

在使用过程中,我这个菜鸟自然是碰到了不少坑,不过还好一一解决了,没有影响按时发版上线,那么就趁记忆还新鲜的时候纪录一下。

使用注解校验

能用的注解很多其他博客也列出来了,我这里简单的引用一下其他文章的内容,并加上一些最新可用的注解吧。(貌似是网上最全的,我参考官方文档补充的)

注解可以使用的类型作用
Bean Validation API 中列出的******************************
@AssertFalseBoolean, boolean验证注解的元素值是false
@AssertTrueBoolean, boolean验证注解的元素值是true
@DecimalMax(value=值)BigDecimal,BigInteger, byte, short, int, long,等任何Number或CharSequence(存储的是数字)或 javax.money.MonetaryAmount 子类型验证注解的元素值小于等于@ DecimalMax指定的value值
@DecimalMin(value=值)和@DecimalMax要求一样验证注解的元素值大于等于@ DecimalMin指定的value值
@Digits(integer=整数位数, fraction=小数位数)和@DecimalMax要求一样验证注解的元素值的整数位数和小数位数上限
@Email(regexp=正则表达式, flag=标志的模式)CharSequence子类型(如String)验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Futurejava.util.Date, java.util.Calendar; Joda Time类库的日期类型; Java8 增加的java.time中的类,等验证注解的元素值(日期类型)比当前时间晚
@FutureOrPresent与@Future要求一样验证注解的元素值(日期类型)等于当前时间或比当前时间晚
@Max(value=值)和@DecimalMax要求一样验证注解的元素值小于等于@Max指定的value值
@Min(value=值)和@DecimalMax要求一样验证注解的元素值大于等于@Min指定的value值
@NotBlankCharSequence子类型验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@NotEmptyCharSequence子类型、Collection、Map、数组验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotNull任意类型验证注解的元素值不是null
@Negative和@DecimalMax要求一样验证注解的元素是不是负数,0不能通过验证
@NegativeOrZero和@DecimalMax要求一样验证注解的元素是不是非负数
@Null任意类型验证注解的元素值是null
@Past与@Future要求一样验证注解的元素值(日期类型)比当前时间早
@PastOrPresent与@Future要求一样验证注解的元素值(日期类型)比当前时间早或等于当前时间
@Pattern(regexp=正则表达式,flag=标志的模式)String,任何CharSequence的子类型验证注解的元素值与指定的正则表达式匹配
@Positive和@DecimalMax要求一样验证注解的元素是不是正数,0不能通过验证
@PositiveOrZero和@DecimalMax要求一样验证注解的元素是不是非正数
@Size(min=下限, max=上限)字符串、Collection、Map、数组等验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
Hibernate Validator 额外提供的,很多类型我也没用过,具体如何设置里面的参数还需要学习,或者可以参考源码******************************
@CreditCardNumber(ignoreNonDigitCharacters=是否忽略其中的非数字字符,默认false)CharSequence校验字符是否通过 Luhn 算法,这个校验的目的是校验用户错误的,不能真的检测信用卡号的有效性
@Currency(value=)任何 javax.money.MonetaryAmount 的子类校验货币的单位是否在 javax.money.MonetaryAmount 中
@DurationMax(days=天数, hours=小时数, minutes=分钟数, seconds=秒数, millis=毫秒数, nanos=纳秒数, inclusive=是否允许相等)java.time.Duration判断被注解的元素是否不大于注解的时间长度
@DurationMin(days=天数, hours=小时数, minutes=分钟数, seconds=秒数, millis=毫秒数, nanos=纳秒数, inclusive=是否允许相等)java.time.Duration判断被注解的元素是否不小于注解的时间长度
@EANCharSequence校验字符序列是否是有效的国际商品编号
@ISBNCharSequence校验字符序列是否是有效的国际标准书号
@Length(min=下限, max=上限)CharSequence子类型验证注解的元素值长度在min和max区间内
@CodePointLength(min=最小值, max=最大值, normalizationStrategy=如果设置就校验归一化值)CharSequence校验Unicode 中的 code point 的长度是否在注解的最大最小值之间
@LuhnCheck(startIndex=被注解序列子数字开始位置 , endIndex=被注解序列子数字结束位置, checkDigitIndex=检查点位置, ignoreNonDigitCharacters=是否忽略非数字字符)CharSequence校验数字是否满足 Luhn 校验算法
@Mod10Check(multiplier=对于奇数的乘数(默认3), weight=对于偶数的权重(默认1), startIndex=被注解序列子数字开始位置, endIndex=被注解序列子数字结束位置, checkDigitIndex=检查点位置, ignoreNonDigitCharacters=是否忽略非数字字符)CharSequence校验数字是否能通过 mod 10 算法
@Mod11Check(threshold=mod11乘数增长的阈值, startIndex=被注解序列子数字开始位置, endIndex=被注解序列子数字结束位置, checkDigitIndex=检查点位置, ignoreNonDigitCharacters=是否忽略非数字字符, treatCheck10As=指定当mod 11校验和等于10时要使用的校验位, treatCheck11As=指定当mod 11校验和等于11时要使用的校验位)CharSequence校验数字能否通过 mod 11 算法
@Range(min=最小值, max=最大值)BigDecimal, BigInteger, CharSequence, byte, short, int, long等原子类型和包装类型验证注解的元素值在最小值和最大值之间
@SafeHtml(whitelistType=可预先定义白名单的类型 , additionalTags=允许添加的无属性标签, additionalTagsWithAttributes=允许添加特定的带属性标签, baseURI=允许指定特定URI来解析)CharSequence校验字段是否含有潜在的威胁片段
@ScriptAssert(lang=任何能在 JSR223 中找到的脚本语言或者表达式, script=, alias=, reportOn=如果使用在类级别上,可以指定用在特定的属性上)任何类型校验给定的脚本能否成功根据注解的属性执行
@UniqueElements集合Collection校验集合含有独一无二的元素,是否相等通过 equals() 方法判断
@URL(protocol=协议, host=主机名, port=端口, regexp=正则表达式, flags=)CharSequence根据RFC2396来校验是否是一个有效的 URL,regexp和flags允许我们指定一个特殊的正则表达式
@Valid任何非原子类型指定递归验证关联的对象;如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证

使用方法

Bean上注解的添加

在我们写业务代码时,主要就是用来校验前端传来的参数,一般我会根据前端需要回传的参数建立特定的DTO,然后在各个变量(属性)上添加它们所需的校验注解,一个例子如下(字段设置不一定合理,主要为了多展示几种情况):

public class User {
	/**
	 * 暂存校验组
	 */
	public interface Save {}
	/**
	 * 提交校验组
	 */
	public interface Submit {}
    /**
	 * 用户id
	 */
    @NotBlank(message = "id不能为空", groups = {Submit.class})
    @Length(max = 32, message = "id长度最大为32", group = {Save.class, Submit.class})
    private Integer id;
	/**
	 * 用户名
	 */
    @NotBlank(message = "用户名不能为空", groups = {Submit.class})
    @Length(max = 50, message = "用户名长度最大为50", group = {Save.class, Submit.class})
    private String username;
    /**
	 * 年龄
	 */
	@NotNull(message = "年龄不能为空", groups = {Submit.class})
    @Min(value = 18, message = "未成年不能注册", group = {Save.class, Submit.class})
    private Integer age;
    /**
	 * 手机号
	 */
	@NotBlank(message = "手机号不能为空", groups = {Submit.class})
    @Pattern(regexp = "1[0-9]{10}", message = "手机号格式错误", group = {Save.class, Submit.class})
    private String phone;
	/**
	 * 出生日期
	 */
	@DateTimeFormat(pattern = "yyyy-MM-dd")
	@NotNull(message = "出身日期不能为空", groups = {Submit.class})
	@Past(message = "出生日期不能后于当前日期"), group = {Save.class, Submit.class}
	@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+08")
    private Date birthday;
	/**
	 * 注册费
	 */
	@NotNull(message = "注册费不能为空不能为空", groups = {Submit.class})
  	@Digits(integer = 10, fraction = 2, message = "注册费金额格式有误", group = {Save.class, Submit.class})
    private BigDecimal money;

    // getters and setters ...

提一下上面的几个注意点:

  1. 我首先定义了两个接口,用于区分两种校验组。在暂存校验组中,不会校验必填字段,只关注填了内容的字段内容是否合理,如果字段为null是能通过格式校验的,这个可以看看这些注解的javadoc就知道了;
  2. 在提交校验组中,就需要校验必填项是否都填了,同时也要校验格式;
  3. 对于时间类的字段,需要加@DateTimeFormat用来反序列化时告诉fastjson(我们用了阿里巴巴这个工具)如何转换日期时间格式;
  4. 如果返回参数也用这个类进行序列化的话,需要在时间类上加注解@JsonFormat告诉工具如何序列化时间的格式;

在controller入参中实现校验

在现在前后端分离的架构中,一般前端以json格式传入的参数,会通过spring自动绑定到controller中的入参,那么我的目的就是在入参绑定时去校验参数,从而将这一部分的校验逻辑剥离出业务逻辑。

这里以上面的bean为例,模拟一个注册接口,虽然注册一般不会有暂存的情况,但是为了演示校验组的用法,这里还是写了【暂存】和【提交】两种状态。

@RestController
@RequestMapping(value = "/user")
public class UserController {
	
	@Autowired
	private UserRepository userRepository;

	@PostMapping("/save")
	public User saveUser(@Validated({User.Save.class}) User user) {
		return userRepository.save(user);
	}

	@PostMapping("/submit")
	public User submitUser(@Validated({User.Submit.class}) User user) {
		return userRepository.save(user);
	}
	
}

我们在入参前面加上注解 @Validated 并跟上校验组,表示这个入参需要在绑定参数时校验,并使用指定校验组校验。这里我没有像有些文章介绍那样,在被校验的入参后面再增加一个 BindingResult 用于接收校验错误,这样的话,处理校验错误的逻辑还是要放在我们的方法中,下一节将介绍如何处理。

当然这种校验方法不仅局限于controller,也能用在其他方法上,不过处理抛出的校验错误就不能借鉴下一节的方法了,可以自己写一个注解,定义切面用AOP来处理。

通过AOP统一处理controller入参校验错误

以下是直接使用spring提供的注解来处理controller抛出的异常,其实也相当于使用了AOP。

另外,关于 @ExceptionHandler 注解中的两个异常:第一个异常 BindException 是在controller中对入参使用 @Validated(由spring提供) 注解产生的异常;第二个异常 MethodArgumentNotValidException 是在controller中对入参使用 @Valid(由javax提供)注解产生的异常,这里推荐使用前一个注解 @Validated ,因为这样可以使用校验组的写法,而第二个注解无法填写更多参数。

@ControllerAdvice
public class ExceptionHandlerAdvice {

	private static final Logger logger = LoggerFactory.getLogger(ExceptionHandlerAdvice.class);

	@ExceptionHandler(value = {BindException.class, MethodArgumentNotValidException.class})
	public void handleValidException(HttpServletResponse response, Exception e) {
		// ····自己想做的一些处理

		// 处理校验错误
		BindingResult bindingResult;
		String resultMsg;
		if (e instanceof BindException) {
			bindingResult = ((BindException) e).getBindingResult();
		} else {
			bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
		}
		if (bindingResult.hasFieldErrors()) {
			// 如果是类型错误的话,我们没法在bean中设置错误消息,所以单独处理
			if ("typeMismatch".equals(bindingResult.getFieldError().getcode())) {
				resultMsg = bindingResult.getFieldError().getField() + "类型错误"} else {
				esultMsg = bindingResult.getFieldError().getDefaultMessage();
			}
		}

		// ····拿到错误信息,就可以自己想做的其他处理,如输出报错内容,记录日志等
	}

}

其他问题

json序列化的一些注意事项

  1. 前面提到过的@JsonFormat,可以用来对付时间参数的序列化,当数据库中存的时间不含时区时,直接取出放到 Date 类中并序列化,会造成时区的时间差,这时候在这个注解中添加如下属性就可以解决问题 timezone = "GMT+08"
  2. 如果POJO中的时间想设置为Java8中提供的 LocalDateTime等类,低版本的 fastjson 无法支持指定格式传入,只能以 yyyy-MM-ddThh-mm-ss 的格式传入;如果你给这个属性添加注解 @DateTimeFormat(pattern = "yyyy-MM-dd") 企图将这样形式的时间转换成LocalDate,需要测试一下,并看看你的 fastjson 支不支持。

最后

其实我还有一些校验的需求没有得到满足(比如前一个属性选“是”就校验后面几个,“否”就校验另外几个),当然自己也没有把这个校验框架的功能全部发挥出来,为了更好的了解这个框架,目前正在翻译它的最新文档,欢迎大家捧场,最新版在GitHub中HibernateValidator6.0.13_zh,本博客中也会跟着更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值