我们在项目开发中,经常会对一些参数进行校验,比如非空校验、长度校验,以及定制的业务校验规则等,如果使用if/else语句来对请求的每一个参数一一校验,就会出现大量与业务逻辑无关的代码,繁重不堪且繁琐的校验,会大大降低我们的工作效率,而且准确性也无法保证。为保证数据的正确性、完整性,前后端都需要进行数据检验。本文对开源 boot-admin 项目的后端校验实践进行总结,以飨码友。
boot-admin 是一款采用前后端分离模式、基于 SpringCloud 微服务架构的SaaS后台管理框架。系统内置基础管理、权限管理、运行管理、定义管理、代码生成器和办公管理6个功能模块,集成分布式事务 Seata、工作流引擎 Flowable、业务规则引擎 Drools、后台作业调度框架 Quartz 等,技术栈包括 Mybatis-plus、Redis、Nacos、Seata、Flowable、Drools、Quartz、SpringCloud、Springboot Admin Gateway、Liquibase、jwt、Openfeign、I18n等。
项目源码仓库github
项目源码仓库gitee
引入Maven依赖
javax.validation validation-api 2.0.1.Final
参数校验实践定义校验对象
@Data/** 组合校验注解(方式1) **/@OverallValid(value = "check1" ,message="女士不得小于16岁。")@OverallValid(value = "check2" ,message="男士不得小于18岁。")public class User { //字符个数检测(内置注解) @Size(min = 1,max = 10,message = "姓名长度必须为1到10") //占用空间长度检测(自定义注解) @StringLength(min = 1,max = 12,message = "姓名的保存长度不允许超过12个字节。") private String name; //利用枚举类检测(自定义注解) @EnumValid(target = SexEnum.class, message = "性别的取值范围是【1】和【2】") private String sex; //注意 @NotNull @NotEmpty @NotBlank 的区别 @NotBlank(message = "姓氏是必填项。") private String firstName; @Min(value = 10,message = "年龄最小为10") @Max(value = 100,message = "年龄最大为100") private Integer age; @Past(message = "出生时间必须为过去时间") private Date birth; @NotEmpty(message = "兴趣不能为空") private List interest; //嵌套检测 @Valid private List children; @Valid private User father; @Valid private User mother; /** 组合校验(方式2) **/ @BooleanValid(message = "男性年龄需在60岁以下") public boolean getValid1(){ if(sex.equalsIgnoreCase("1") && age >= 60 ){ return false; } return true; } /** 组合校验(方式2) **/ @BooleanValid(message = "女性年龄需在55岁以下") public boolean getValid2(){ if(sex.equalsIgnoreCase("2") && age >= 55 ){ return false; } return true; } /** 组合校验(方式1)方法 **/ public boolean check1(){ if(sex.equalsIgnoreCase("2") && age < 16 ){ return false; } return true; } /** 组合校验(方式1)方法 **/ public boolean check2(){ if(sex.equalsIgnoreCase("1") && age < 18 ){ return false; } return true; }}
相关枚举类:
public enum SexEnum { 男("1"),女("2"); private final String value; SexEnum(String value) { this.value = value; } public String getValue() { return value; }}
参数校验(在 Controller 中使用)
@RestController@RequestMapping("/api/system")@Slf4jpublic class DemoController { //注入校验信息采集器 @Resource private FormValidator formValidator; @PostMapping("/free/user/check") public ResultDTO check(@Valid @RequestBody User user, BindingResult bindingResult, HttpServletRequest request) throws Exception{ /** 参数校验 **/ if (bindingResult.hasErrors()) { return formValidator.generateMessage(bindingResult); } /** 继续执行业务逻辑 **/ return ResultDTO.success(); }}
在Controller中使用的校验结果信息采集器实现
接口定义:
public interface FormValidator { ResultDTO generateMessage(BindingResult bindingResult) throws Exception;}
类实现:
@Service@Slf4jpublic class FormValidatorImpl implements FormValidator { @Override public ResultDTO generateMessage(BindingResult bindingResult) throws Exception { String msg = this.getMessage(bindingResult); return ResultDTO.failureCustom(msg); } /** * 生成校验结果 * @param bindingResult * @return */ private String getMessage(BindingResult bindingResult){ log.info(bindingResult.toString()); List objectErrorList=bindingResult.getAllErrors(); String msg= this.getFormValidErrsMsgNoBr(objectErrorList); log.info(msg); return msg; } private String getFormValidErrsMsgNoBr(List objectErrorList) { if (objectErrorList==null) { return ""; } StringBuffer csv = new StringBuffer(); csv.append("数据验证未通过:["); for (int i = 0; i 0){ csv.append("],["); } csv.append(objectErrorList.get(i).getDefaultMessage()); } csv.append("]"); return csv.toString(); }}
相关注解介绍JSR-303 规范常用注解
以下列举常用内置注解,可直接使用。
注解 | 描述 |
---|---|
@Valid | 对po实体尽心校验 |
@AssertFalse | 所注解的元素必须是Boolean类型,且值为false |
@AssertTrue | 所注解的元素必须是Boolean类型,且值为true |
@DecimalMax | 所注解的元素必须是数字,且值小于等于给定的值 |
@DecimalMin | 所注解的元素必须是数字,且值大于等于给定的值 |
@Digits | 所注解的元素必须是数字,且值必须是指定的位数 |
@Future | 所注解的元素必须是将来某个日期 |
@Max | 所注解的元素必须是数字,且值小于等于给定的值 |
@Min | 所注解的元素必须是数字,且值大于等于给定的值 |
@Range | 所注解的元素需在指定范围区间内 |
@NotNull | 所注解的元素值不能为null |
@NotBlank | 所注解的元素值有内容 |
@Null | 所注解的元素值为null |
@Past | 所注解的元素必须是某个过去的日期 |
@PastOrPresent | 所注解的元素必须是过去某个或现在日期 |
@Pattern | 所注解的元素必须满足给定的正则表达式 |
@Size | 所注解的元素必须是String、集合或数组,且长度大小需保证在给定范围之内 |
所注解的元素需满足Email格式 |
自定义注解
仅仅使用内置的注解,无法满足复杂的业务需求,故扩展下面几个自定义注解。
UTF-8 字符串长度校验
对字符串长度的校验目的,一般是用于保证数据表字段可以容纳,当字符串内容是中文时,内置的 @Size 是不适用的,此时就需要自行扩展 UTF-8 字符串长度校验。
注解类:
@Target( { METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})@Retention(RUNTIME)@Documented@Constraint(validatedBy = {StringLengthValidator.class})public @interface StringLength { int max() default 4000; int min() default 0; String message() default "字符串长度不符合要求。"; Class[] groups() default {}; Class[] payload() default {};}
注解类实现:
@Slf4jpublic class StringLengthValidator implements ConstraintValidator { private int max; private int min; @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { try { if(StringUtils.isBlank(value)){ if(min > 0){ return false; }else { return true; } } byte[] tmpbyte = value.getBytes("UTF-8"); int length = tmpbyte.length; if(length max){ return false; } return true; }catch (Exception ex){ log.error("注解校验StringLength发生异常。"); log.error(ex.getMessage(),ex); return false; } } @Override public void initialize(StringLength constraintAnnotation) { max = constraintAnnotation.max(); min = constraintAnnotation.min(); }}
手机号码校验
注解类:
@Target( { METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})@Retention(RUNTIME)@Documented@Constraint(validatedBy = {MobileValidator.class})public @interface Mobile { String regexp() default ""; String message() default "手机号码格式不正确"; Class[] groups() default {}; Class[] payload() default {};}
注解类实现:
public class MobileValidator implements ConstraintValidator { /** * 手机号的正则表达式. */ private static Pattern pattern = Pattern.compile( "^0?(13[0-9]|14[0-9]|15[0-9]|16[0-9]|17[0-9]|18[0-9]|19[0-9])[0-9]{8}$"); @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { Matcher m = pattern.matcher(value); return m.matches(); } @Override public void initialize(Mobile constraintAnnotation) {}}
这里对手机号码的校验使用了正则表达式,也可以直接使用内置注解 @Pattern 定义校验规则。
枚举类整数值校验
有时需要校验参数值必须是系统定义的枚举值(整数值),此时需要扩展以下注解。
注解类:
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Constraint(validatedBy = {EnumIntegerValidator.class})public @interface EnumIntegerValid { String message() default ""; Class[] groups() default {}; Class[] payload() default {}; /** * 目标枚举类 */ Class target() default Class.class; /** * 是否忽略空值 */ boolean ignoreEmpty() default true;}
注解类实现:
@Slf4jpublic class EnumIntegerValidator implements ConstraintValidator { /** 枚举校验注解 */ private EnumIntegerValid annotation; @Override public void initialize(EnumIntegerValid constraintAnnotation) { annotation = constraintAnnotation; } @Override public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) { boolean result = false; Class cls = annotation.target(); boolean ignoreEmpty = annotation.ignoreEmpty(); // target为枚举,并且value有值,或者不忽视空值,才进行校验 if (cls.isEnum() && value != null) { Object[] objects = cls.getEnumConstants(); try { Method method = cls.getMethod("getValue"); for (Object obj : objects) { Object code = method.invoke(obj); if (value.compareTo((Integer) code) == 0) { result = true; break; } } } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { log.warn("EnumValidator call isValid() method exception."); result = false; } } else { result = true; } return result; }}
枚举类字符串校验
有时需要校验参数值必须是系统定义的枚举值(字符串),此时需要扩展以下注解。
注解类:
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Constraint(validatedBy = {EnumValidator.class})public @interface EnumValid { String message() default ""; Class[] groups() default {}; Class[] payload() default {}; /** * 目标枚举类 */ Class target() default Class.class; /** * 是否忽略空值 */ boolean ignoreEmpty() default true;}
注解类实现:
@Slf4jpublic class EnumValidator implements ConstraintValidator { /** 枚举校验注解 */ private EnumValid annotation; @Override public void initialize(EnumValid constraintAnnotation) { annotation = constraintAnnotation; } @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { boolean result = false; Class cls = annotation.target(); boolean ignoreEmpty = annotation.ignoreEmpty(); // target为枚举,并且value有值,或者不忽视空值,才进行校验 boolean fitCheck = cls.isEnum() && (isNotEmpty(value) || !ignoreEmpty); if (fitCheck) { Object[] objects = cls.getEnumConstants(); try { Method method = cls.getMethod("getValue"); for (Object obj : objects) { Object code = method.invoke(obj); if (value.equals(code.toString())) { result = true; break; } } } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { log.warn("EnumValidator call isValid() method exception."); result = false; } } else { result = true; } return result; }}
Bean 内多属性组合校验(组合校验)
此类校验一般属于业务逻辑校验,常常要求多个属性符合一定的逻辑设定。此时需要在Bean中编写校验方法,并在类定义前面添加自定义注解 @OverallValid 或者在方法前面加上自定义注解 @BooleanValid
方式1:
注解在类定义前面,类方法要求:
- 方法的可访问属性:public
- 方法的返回类型: boolean
@OverallValid注解类:
@Target({METHOD, FIELD,TYPE})@Retention(RUNTIME)@Repeatable(OverallValids.class)@Documented@Constraint(validatedBy = {OverallValidImpl.class})public @interface OverallValid { String value() default "overallValid"; String message() default "组合校验未通过。"; Class[] groups() default {}; Class[] payload() default {};}
上面注解要求可重复使用,使用了 @Repeatable(OverallValids.class),OverallValids 代码如下:
@Target({METHOD, FIELD,TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface OverallValids { OverallValid[] value();}
使用注入的方法名,通过反射执行该方法,得到校验结果。注解实现如下:
@Slf4jpublic class OverallValidImpl implements ConstraintValidator { private String functionName; @Override public void initialize(OverallValid overallValid) { functionName = overallValid.value(); } @Override public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) { try { //得到方法对象 Method checkMethod = o.getClass().getMethod(functionName); //调用方法,得到返回值 Object checkRet = checkMethod.invoke(o); return Boolean.valueOf(checkRet.toString()); }catch (Exception ex){ log.error("综合校验异常。"); log.error(ex.getMessage(),ex); } return false; }}
方式2:
注解在方法前面,类方法要求:
- 方法的可访问属性:public
- 方法的返回类型: boolean
- 方法名称格式:get+首字母大写驼峰,如 getValid1
@BooleanValid注解类:
@Target( { METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})@Retention(RUNTIME)@Documented@Constraint(validatedBy = {BooleanValidImpl.class})public @interface BooleanValid { boolean value() default true; String message() default "综合校验未通过。"; Class[] groups() default {}; Class[] payload() default {};}
类实现:
@Slf4jpublic class BooleanValidImpl implements ConstraintValidator { @Override public boolean isValid(Boolean value, ConstraintValidatorContext constraintValidatorContext) { return value; } @Override public void initialize(BooleanValid constraintAnnotation) { }}
嵌套校验
在成员属性上加注解 @Valid ,意味着对该成员属性进行嵌套校验,校验规则按该成员的内部校验注解执行。
本文来自博客园,作者:超然楼,转载请注明原文链接:https://www.cnblogs.com/soft1314/p/17380059.html