一般我们对前端传输的参数做校验时,可能都是以以下方式进行,如果再加上字段的长度、正则等校验的话就会显得代码很累赘。
// 新增/修改通用参数非空校验 if (StringUtil.isBlank(menu.getParentId())) { throw new LsException(ResultEnum.PARAM_MISSING_ERROR, "父级菜单ID不能为空!"); } if (StringUtil.isBlank(menu.getMenuName())) { throw new LsException(ResultEnum.PARAM_MISSING_ERROR, "菜单名称不能为空!"); } if (StringUtil.isBlank(menu.getMenuName())) { throw new LsException(ResultEnum.PARAM_MISSING_ERROR, "菜单类型不能为空!"); } /**>>>>>>>>>>>>>>>>>>>>>>>>枚举类字段校验开始<<<<<<<<<<<<<<<<<<<<<<<<<**/ // 校验菜单类型是否存在 if (Objects.nonNull(model.getMenuType())) { MenuEnum.MenuType menuType = MenuEnum.MenuType.getByType(model.getMenuType()); if (Objects.isNull(menuType)) { throw new LsException(ResultEnum.PARAM_CHECKED_ERROR, "此菜单类型不存在!"); } } // 校验打开方式是否存在 if (Objects.nonNull(model.getTarget())) { MenuEnum.Target target = MenuEnum.Target.getByTarget(model.getTarget()); if (Objects.isNull(target)) { throw new LsException(ResultEnum.PARAM_CHECKED_ERROR, "此打开方式不存在!"); } } // 校验菜单状态是否存在 if (Objects.nonNull(model.getVisible())) { MenuEnum.Visible visible = MenuEnum.Visible.getByVisible(model.getVisible()); if (Objects.isNull(visible)) { throw new LsException(ResultEnum.PARAM_CHECKED_ERROR, "此菜单状态不存在!"); } } /**>>>>>>>>>>>>>>>>>>>>>>>>枚举类字段校验结束<<<<<<<<<<<<<<<<<<<<<<<<<**/
改进方案,使用Hibernate Validator嵌入式注解处理器(概念可参考《深入理解Java虚拟机》——周志明第10.4节)进行表单验证:
1、表单验证工具类ModelValidator
/** * 软件版权:流沙~~ * 修改日期 修改人员 修改说明 * ========= =========== ===================== * 2019/9/30 liusha 新增 * ========= =========== ===================== */ package com.sand.common.util.validator; import com.sand.common.enums.CodeEnum; import com.sand.common.exception.BusinessException; import com.sand.common.util.lang3.StringUtil; import com.sand.common.util.spring.SpringUtil; import lombok.extern.slf4j.Slf4j; import javax.validation.ConstraintViolation; import javax.validation.Validator; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * 功能说明:表单验证器 * 开发人员:@author liusha * 开发日期:2019/9/30 13:57 * 功能描述:表单校验,验证非空、长度、正则等等 */ @Slf4j public class ModelValidator { /** * 校验失败条数key值 */ public static final String CHECKED_FAIL_NUM = "checkedFailNum"; /** * 校验失败原因key值 */ public static final String CHECKED_FAIL_MSG = "checkedFailMsg"; /** * 校验通过的数据列表key值 */ public static final String CHECKED_ENTITY_LIST = "checkedEntityList"; /** * 表单验证(对应实体类配置注解) * * @param entity 校验对象 * @param <T> */ public static <T extends Object> void checkModel(T entity) { checkModel(entity, null); } /** * 表单验证指定字段(对应实体类配置注解) * * @param entity 校验对象 * @param <T> */ public static <T extends Object> void checkModel(T entity, String fieldName) { try { Set<ConstraintViolation<T>> violationSet; if (StringUtil.isNotBlank(fieldName)) { violationSet = SpringUtil.getBean(Validator.class).validateProperty(entity, fieldName); } else { violationSet = SpringUtil.getBean(Validator.class).validate(entity); } if (violationSet.size() > 0) { String msg = violationSet.iterator().next().getMessage(); if (StringUtil.isBlank(msg)) { msg = "请求参数有误"; } throw new BusinessException(CodeEnum.PARAM_CHECKED_ERROR, msg); } } catch (Exception e) { log.error("表单验证出错,", e); String errorMsg = (e instanceof BusinessException) ? e.getMessage() : "表单验证出错"; throw new BusinessException(CodeEnum.PARAM_CHECKED_ERROR, errorMsg); } } /** * 批量表单验证(对应实体类配置注解) * * @param entityList 校验对象列表 * @param <T> * @return 校验结果 */ public static <T extends Object> Map<String, Object> checkModelList(List<T> entityList) { // 校验失败条数 int checkedFailNum = 0; // 校验失败原因 StringBuilder checkedFailMsg = new StringBuilder(); // 校验通过的数据列表 List<T> checkedEntityList = new ArrayList<>(); for (int i = 0; i < entityList.size(); i++) { try { checkModel(entityList.get(i)); } catch (Exception e) { checkedFailNum++; checkedFailMsg.append("第" + (i + 1) + "条数据:" + e.getMessage() + ";"); continue; } checkedEntityList.add(entityList.get(i)); } Map<String, Object> checkedMap = new HashMap<>(); checkedMap.put(CHECKED_FAIL_NUM, checkedFailNum); checkedMap.put(CHECKED_FAIL_MSG, checkedFailMsg); checkedMap.put(CHECKED_ENTITY_LIST, checkedEntityList); return checkedMap; } }
2、自定义枚举验证
1)、枚举注解@EnumValidAnnotation
/** * 软件版权:流沙~~ * 修改日期 修改人员 修改说明 * ========= =========== ===================== * 2019/9/26 liusha 新增 * ========= =========== ===================== */ package com.sand.base.annotation; import com.sand.base.util.lang3.StringUtil; import com.sand.base.util.validator.EnumValidator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 功能说明:验证枚举类 * 开发人员:@author liusha * 开发日期:2019/9/26 13:46 * 功能描述:对表单中存在枚举类型的字段进行校验 */ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = {EnumValidator.class}) @Documented public @interface EnumValidAnnotation { /** * 提示消息 * * @return */ String message() default StringUtil.EMPTY; /** * 对应的枚举类 * * @return */ Class<?>[] target() default {}; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
2)、枚举验证器EnumValidator
/** * 软件版权:流沙~~ * 修改日期 修改人员 修改说明 * ========= =========== ===================== * 2019/9/26 liusha 新增 * ========= =========== ===================== */ package com.sand.base.util.validator; import com.sand.base.annotation.EnumValidAnnotation; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.lang.reflect.Field; import java.util.Objects; /** * 功能说明:枚举验证器 * 开发人员:@author liusha * 开发日期:2019/9/26 13:51 * 功能描述:自定义枚举验证器 */ public class EnumValidator implements ConstraintValidator<EnumValidAnnotation, String> { /** * 枚举类 */ Class<?>[] clzs; @Override public void initialize(EnumValidAnnotation constraintAnnotation) { clzs = constraintAnnotation.target(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (clzs.length > 0) { try { for (Class<?> clz : clzs) { if (clz.isEnum()) { // 枚举类验证 Object[] objs = clz.getEnumConstants(); for (Object obj : objs) { Class<?> enumClz = obj.getClass(); Field[] fields = enumClz.getDeclaredFields(); for (Field field : fields) { // 访问私有成员属性开关 field.setAccessible(true); EnumValidAnnotation enumValidAnnotation = field.getAnnotation(EnumValidAnnotation.class); if (Objects.nonNull(enumValidAnnotation)) { // 获取成员属性的值 Object enumValue = field.get(obj); if (value.equals(enumValue.toString())) { return true; } } } } } } } catch (IllegalAccessException e) { e.printStackTrace(); } } else { return true; } return false; } }
3)、枚举类中要转换的成员属性也需要添加@EnumValidAnnotation(主要是为了通用)
/** * 软件版权:流沙~~ * 修改日期 修改人员 修改说明 * ========= =========== ===================== * 2019/8/28 liusha 新增 * ========= =========== ===================== */ package com.sand.sys.enums; import com.sand.base.annotation.EnumValidAnnotation; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Objects; /** * 功能说明:存放系统菜单枚举类 * 开发人员:@author liusha * 开发日期:2019/8/28 19:48 * 功能描述:使用@EnumValidAnnotation可用于表单校验,使用getBy*返回给客户端转译等 */ public final class MenuEnum { @Getter @AllArgsConstructor public enum MenuType { // 菜单类型 M("M", "目录"), C("C", "菜单"), F("F", "按钮"); @EnumValidAnnotation private final String type; private final String description; public static MenuType getByType(String type) { for (MenuType item : MenuType.values()) { if (Objects.equals(type, item.getType())) { return item; } } return null; } } @Getter @AllArgsConstructor public enum Target { // 打开方式 ITEM("_item", "页签中打开"), BLANK("_blank", "新窗口打开"), CURRENT("_current", "当前窗口打开"); @EnumValidAnnotation private final String target; private final String description; public static Target getByTarget(String target) { for (Target item : Target.values()) { if (Objects.equals(target, item.getTarget())) { return item; } } return null; } } @Getter @AllArgsConstructor public enum Visible { // 菜单状态 SHOW("0", "显示"), HIDE("1", "隐藏"); @EnumValidAnnotation private final String visible; private final String description; public static Visible getByVisible(String visible) { for (Visible item : Visible.values()) { if (Objects.equals(visible, item.getVisible())) { return item; } } return null; } } }
3、需要验证的实体类添加相应注解,@NotBlank、@Length以及自定义枚举注解@EnumValidAnnotation
/** * 软件版权:流沙~~ * 修改日期 修改人员 修改说明 * ========= =========== ===================== * 2019/8/26 liusha 新增 * ========= =========== ===================== */ package com.sand.sys.entity; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.sand.base.annotation.EnumValidAnnotation; import com.sand.base.constant.Constant; import com.sand.base.core.entity.BaseEntity; import com.sand.sys.enums.MenuEnum; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.experimental.Accessors; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotBlank; /** * 功能说明:系统菜单 * 开发人员:@author liusha * 开发日期:2019/8/26 13:38 * 功能描述:系统菜单 */ @Data @Accessors(chain = true) @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @TableName(Constant.TABLE_PREFIX_SYS + "menu") public class SysMenu extends BaseEntity { private static final long serialVersionUID = -2854114810573968874L; /** * 菜单ID */ @TableId private String menuId; /** * 父菜单ID */ @NotBlank(message = "[父级菜单ID]不能为空哟!") private String parentId; /** * 菜单名称 */ @NotBlank(message = "[菜单名称]不能为空哟!") @Length(max = 64, message = "[菜单名称]不能超过64个字符呢!") private String menuName; /** * 菜单类型(M目录 C菜单 F按钮) */ @NotBlank(message = "[菜单类型]不能为空哟!") @EnumValidAnnotation(message = "[菜单类型]不存在!", target = MenuEnum.MenuType.class) private String menuType; /** * 菜单URL */ @Length(max = 128, message = "[菜单URL]不能超过128个字符呢!") private String menuUrl; /** * 显示顺序 */ private int orderNum; /** * 打开方式(_item 页签中打开,_blank 新窗口打开,_current 当前窗口打开) */ @EnumValidAnnotation(message = "[打开方式]不存在!", target = MenuEnum.Target.class) private String target; /** * 菜单状态(0显示 1隐藏) */ @EnumValidAnnotation(message = "[菜单状态]不存在!", target = MenuEnum.Visible.class) private String visible; /** * 权限标识 */ @Length(max = 128, message = "[权限标识]不能超过128个字符呢!") private String purview; /** * 菜单图标 */ private String icon; }
4、如果需要使用表单验证,可以在对应的controller或者service上加上:
// 表单注解验证,非空,长度,正则等校验 ModelValidator.checkModel(model);
列举一些常用的Hibernate注解标签:
注解 |
适用的数据类型 |
说明 |
@AssertFalse |
Boolean, boolean |
验证注解的元素值是false |
@AssertTrue |
Boolean, boolean |
验证注解的元素值是true |
@DecimalMax(value=x) |
BigDecimal, BigInteger, String, byte,short, int, long and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type of Number andCharSequence. |
验证注解的元素值小于等于@ DecimalMax指定的value值 |
@DecimalMin(value=x) |
BigDecimal, BigInteger, String, byte,short, int, long and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type of Number andCharSequence. |
验证注解的元素值小于等于@ DecimalMin指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) |
BigDecimal, BigInteger, String, byte,short, int, long and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type of Number andCharSequence. |
验证注解的元素值的整数位数和小数位数上限 |
@Future |
java.util.Date, java.util.Calendar; Additionally supported by HV, if theJoda Time date/time API is on the class path: any implementations ofReadablePartial andReadableInstant. |
验证注解的元素值(日期类型)比当前时间晚 |
@Max(value=x) |
BigDecimal, BigInteger, byte, short,int, long and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type ofCharSequence (the numeric value represented by the character sequence is evaluated), any sub-type of Number. |
验证注解的元素值小于等于@Max指定的value值 |
@Min(value=x) |
BigDecimal, BigInteger, byte, short,int, long and the respective wrappers of the primitive types. Additionally supported by HV: any sub-type of CharSequence (the numeric value represented by the char sequence is evaluated), any sub-type of Number. |
验证注解的元素值大于等于@Min指定的value值 |
@NotNull |
Any type |
验证注解的元素值不是null |
@Null |
Any type |
验证注解的元素值是null |
@Past |
java.util.Date, java.util.Calendar; Additionally supported by HV, if theJoda Time date/time API is on the class path: any implementations ofReadablePartial andReadableInstant. |
验证注解的元素值(日期类型)比当前时间早 |
@Pattern(regex=正则表达式, flag=) |
String. Additionally supported by HV: any sub-type of CharSequence. |
验证注解的元素值与指定的正则表达式匹配 |
@Size(min=最小值, max=最大值) |
String, Collection, Map and arrays. Additionally supported by HV: any sub-type of CharSequence. |
验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Valid |
Any non-primitive type(引用类型) |
验证关联的对象,如账户对象里有一个订单对象,指定验证订单对象 |
@NotEmpty |
|
验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) |
|
验证注解的元素值在最小值和最大值之间 |
@NotBlank |
|
验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
@Length(min=下限, max=上限) |
|
验证注解的元素值长度在min和max区间内 |
|
|
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |