前言:
最近单位在新开发一个系统,我主要负责订单模块的开发,这个模块的数据结构嵌套层数比较多,如果自己写一个校验方法,校验比较麻烦。所以,我选择了Validation校验框架。技术是一把双刃剑,Validation对非空,数据大小,长度这样简单的校验一个注解就可以搞定。但是对一些复杂的校验,明显感觉不是特别友好。Validation也对复杂的校验提供了自定义注解的解决方案,但是,可能还有一些复杂的校验Validation不能实现,这些就需要换另一些方式去实现。
正文:
一、Validation中的注解:
1.Bean Validation 的注解:
注解 | 作用 |
---|---|
@Valid | 被注释的元素是一个对象,需要检查此对象的所有字段值 |
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
2.Hibernate Validator的注解:
注解 | 作用 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length(min=, max=) | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range(min=, max=) | 被注释的元素必须在合适的范围内 |
@NotBlank | 被注释的字符串的必须非空 |
@URL(protocol=, host=, port=, regexp=, flags=) | 被注释的字符串必须是一个有效的url |
@CreditCardNumber | 被注释的字符串必须通过Luhn校验算法, 银行卡,信用卡等号码一般都用Luhn 计算合法性 |
3. @Validated与@Valid的区别:
规范:@Validated(Spring's JSR-303 规范,是标准 JSR-303 的一个变种),javax提供了@Valid(标准JSR-303规范);
支持分组:@Validated支持分组,而@Valid不支持;
注解使用场景:@Validated 可以用在类型、方法和方法参数上。但不能用在成员属性上 ; @Valid 可以用在方法、构造函数、方法参数和成员属性上。
此外:@Valid支持嵌套校验
4. @NotNull、@NotEmpty、@NotBlank的区别:
@NotNull:任何对象的value不能为null
@NotEmpty:集合对象的元素不为0,即集合不为空,也可以用于字符串不为null
@NotBlank:只能用于字符串不为null,并且字符串trim()以后length要大于0
二、自定义注解的使用:
Validation框架的自定义注解实现必要容易,主要是分两步:第一步,自定义一个注解类,第二步,重写校验器。下面,我用一个支持校验枚举类范围的自定义注解来具体来说一下:
0.自定义一个枚举类(订单类型枚举类):
package com.hanxiaozhang.order.constant;
/**
* 〈一句话功能简述〉<br>
* 〈订单类型枚举类〉
*
* @author hanxinghua
* @create 2020/7/22
* @since 1.0.0
*/
public enum OrderTypeEnum {
//订单类型:0:日用品、1:服装、9:其他
DAILY_NEED(0, "日用品"),
CLOTHING(1, "服装"),
OTHER(9, "其他");
private Integer code;
private String name;
OrderTypeEnum(Integer code, String name) {
this.code = code;
this.name = name;
}
public Integer getCode() {
return code;
}
public String getName() {
return name;
}
/**
* 校验Code范围
*
* @param code
* @return
*/
public static boolean isValidCode(Integer code) {
if (code == null) {
return false;
}
for (OrderTypeEnum status : OrderTypeEnum.values()) {
if (status.getCode().equals(code)) {
return true;
}
}
return false;
}
}
1.自定义枚举范围校验的注解:
package com.hanxiaozhang.validation.annotation;
import com.hanxiaozhang.validation.validator.EnumRangeValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 功能描述: <br>
* 〈枚举类范围校验器注解〉
*
* @Author:hanxinghua
* @Date: 2020/7/20
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = EnumRangeValidator.class) // 校验器
public @interface EnumRange {
/**
* 默认提示消息
*
* @return
*/
String message() default "类型错误!";
/**
* 分组
*
* @return
*/
Class<?>[] groups() default {};
/**
* 负载
*
* @return
*/
Class<? extends Payload>[] payload() default {};
/**
* 枚举类的Class
*
* @return
*/
Class<? extends Enum<?>> enumClass();
/**
* 校验方法
*
* @return
*/
String enumValidMethod();
}
2.自定义枚举范围校验的校验器:
package com.hanxiaozhang.validation.validator;
import com.hanxiaozhang.validation.annotation.EnumRange;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
/**
* 〈一句话功能简述〉<br>
* 〈枚举类范围校验器〉
*
* @author hanxinghua
* @create 2020/7/20
* @since 1.0.0
*/
public class EnumRangeValidator implements ConstraintValidator<EnumRange, Object> {
private Class<? extends Enum<?>> enumClass;
private String enumValidMethod;
@Override
public void initialize(EnumRange enumRange) {
enumClass = enumRange.enumClass();
enumValidMethod = enumRange.enumValidMethod();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) {
return true;
}
if (enumClass == null || enumValidMethod == null) {
return true;
}
Class<?> valueClass = value.getClass();
try {
// 反射获取方法
Method method = enumClass.getMethod(enumValidMethod, valueClass);
// 校验 enumValidMethod 是否为 boolean类型方法
if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) {
throw new RuntimeException(enumValidMethod+" method return is not boolean type in the "+enumClass+" class");
}
// 校验 enumValidMethod 是否为 静态类型方法
if(!Modifier.isStatic(method.getModifiers())) {
throw new RuntimeException(enumValidMethod+ " method is not static method in the "+enumClass+" class");
}
// 反射调用该方法
Boolean result = (Boolean)method.invoke(null, value);
return result == null ? false : result;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException | SecurityException e) {
throw new RuntimeException("This "+enumValidMethod+"( "+valueClass+" ) method does not exist in the "+enumClass, e);
}
}
}
3.使用:
package com.hanxiaozhang.order.entity;
import com.hanxiaozhang.order.constant.OrderTypeEnum;
import com.hanxiaozhang.validation.annotation.EnumRange;
import com.hanxiaozhang.validation.annotation.PriceNotEmpty;
import com.hanxiaozhang.validation.group.CreateGroupValidation;
import com.hanxiaozhang.validation.group.UpdateGroupValidation;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.NotBlank;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 〈一句话功能简述〉<br>
* 〈OrderDTO〉
*
* @author hanxinghua
* @create 2020/7/22
* @since 1.0.0
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Order {
private Long id;
@Valid
@NotBlank(message = "订单名称不能为空!",groups = {CreateGroupValidation.class, UpdateGroupValidation.class})
private String orderName;
@Valid
@NotBlank(message = "订单编号不能为空!",groups = {UpdateGroupValidation.class})
private String orderNo;
@EnumRange(message = "订单类型错误!",enumClass = OrderTypeEnum.class, enumValidMethod ="isValidCode",groups = {CreateGroupValidation.class, UpdateGroupValidation.class} )
@NotNull(message = "订单类型为空!",groups = {CreateGroupValidation.class, UpdateGroupValidation.class})
private Integer type;
private Integer number;
@PriceNotEmpty(groups = {CreateGroupValidation.class, UpdateGroupValidation.class})
private BigDecimal price;
}
三、特别说明:
1.校验器isValid()方法ConstraintValidatorContext的使用:
ConstraintValidatorContext在这个环境上下文中,我们可以获取当前校验对象属性的父节点,即当前对象的所有信息。但是有些情况获取不到,获取当前对象所有信息的方法如下:
ConstraintValidatorContextImpl constraintValidatorContext = (ConstraintValidatorContextImpl)context;
List<ConstraintViolationCreationContext> constraintViolationCreationContexts = constraintValidatorContext.getConstraintViolationCreationContexts();
ConstraintViolationCreationContext constraintViolationCreationContext = constraintViolationCreationContexts.get(0);
Object object = constraintViolationCreationContext.getPath().getLeafNode().getParent().getValue();
Order order = (Order) object;