javax.validation和hibernate-validator参数校验

@Validated和@Valid的区别

在Controller中校验方法参数时,使用@Valid和@Validated并无特殊差异(若不需要分组校验的话)
@Valid:标准JSR-303规范的标记型注解,用来标记验证属性和方法返回值,进行级联和递归校验
@Validated:Spring的注解,是标准JSR-303的一个变种(补充),提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制

方法级别:
@Validated注解可以用于类级别,用于支持Spring进行方法级别的参数校验。@Valid可以用在属性级别约束,用来表示级联校验。
@Validated只能用在类、方法和参数上,而@Valid可用于方法、字段、构造器和参数上

如何使用

这两个包要同时导入!

   <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>2.0.1.Final</version>
      </dependency>
      <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.0.7.Final</version>
      </dependency>
    @PostMapping("/tabletSign/pushInfo/patient")
    public AjaxResult pushInfoPatient(@Valid @RequestBody BizPatient bizPatient) {
    }
public class BizPatient {
    private static final long serialVersionUID = 1L;
    @NotNull(message = "id不能为空")
    private Long patientId;
}

如何分组校验?

有时候我们需要在不同的Controller中校验不同的字段

Controller

    @PostMapping("/tabletSign/pushInfo/patient")
    public AjaxResult pushInfoPatient(
        @Validated(BizPatient.SaveGroup.class) @RequestBody BizPatient bizPatient) {
    }

    @PostMapping("/tabletSign/patient/signerInfo")
    public AjaxResult getSignerInfo(
        @Validated(BizPatient.SelectGroup.class) @RequestBody BizPatient bizPatient) {
    }

javaBean

public class BizPatient {
    private static final long serialVersionUID = 1L;
    /**
     * $column.columnComment
     */
    //非空判断
    @NotNull(groups = {SaveGroup.class, SelectGroup.class}, message = "patientId 不能为空")
    private Long patientId
}

如何校验关联对象?

    @PostMapping(value = "/saveOrUpdate")
    public GbmResult saveOrUpdate(@RequestBody @Validated GdVo gdVo) {
    }


@Data
public class GdVo {
    @Valid
    private GdAfterSalesDto gdAfterSalesDto;
    @Valid
    private List<GdProcessRecordDto> gdProcessRecordDto;
}

手动校验工具类

  • 有时候注解不生效,我们可以手动校验
  • 或者一个接口同时做add和update。如addAndUpdate接口。这个时候我们不好使用分组校验。只能手动校验
import org.springframework.validation.BindingResult;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
import java.util.stream.Collectors;
public class ValidParameterUtils {
    private static Validator validator;

    static {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    public static void validParameter(BindingResult validResult){
        if (validResult.hasErrors()){
            throw new GBMException(validResult.getFieldError().getDefaultMessage(),GbmResultCode.PARAMETER_EXCEPTION.code());
        }
    }
    public static void validateEntity(Object object, Class<?>... groups)
            throws GBMException {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
        if (!constraintViolations.isEmpty()) {
            StringBuilder msg = new StringBuilder();
            for(ConstraintViolation<Object> constraint:  constraintViolations){
                msg.append(constraint.getMessage()).append("  ");
            }
            throw new GBMException(msg.toString(),GbmResultCode.FAIL.code());
        }
    }



    /**
     * @Des 返回错误信息
     * @Author yinkai
     * @Date 2022/2/28 9:24
     */
    public static String validateEntityRString(Object object, Class<?>... groups) {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
        return constraintViolations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining("  "));
    }
}

使用

public class ZskQuestionsAndAnswersVo {

    @NotNull(groups = {AddGroup.class}, message = "knowledgeId为空")
    private Long knowledgeId;

    @Length(max = 100, min = 1, message = "问题必须在1-100字符之间")
    @NotBlank(groups = {AddGroup.class}, message = "problemContent为空")
    private String problemContent;
    @Length(max = 500, min = 1, message = "回答必须在1-500字符之间")
    @NotBlank(groups = {AddGroup.class}, message = "answer为空")
    private String answer;

}

    @PostMapping(value = "/addQuestionsAndAnswers")
    public GbmResult addQuestionsAndAnswers(@RequestParam("img") MultipartFile[] img,
                                            @RequestParam("vedio") MultipartFile[] vedio,
                                            @Valid ZskQuestionsAndAnswersVo zskQuestionsAndAnswersVo) {
        ValidParameterUtils.validateEntity(zskQuestionsAndAnswersVo,AddGroup.class);
    }

还需定义全局异常处理器

@RestControllerAdvice
@Order(100)
public class GBMExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());


    //处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
    @ExceptionHandler(org.springframework.validation.BindException.class)
    @ResponseBody
    public GbmResult BindExceptionHandler(BindingResult e) {
        String message = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return GbmResult.error(GbmResultCode.PARAMETER_EXCEPTION.getCode(),message);
    }

校验List

Controller类上加@Validated

@Validated
public class ZskKnowledgeController {

handel方法上加

@PostMapping(value = "/saveOrUpdateZskAccessories")
    public GbmResult saveOrUpdateZskAccessories(@RequestBody @Valid List<ZskAccessoriesListType> zskKnowledgeVoList) {

注解含义

@Pattern(regexp = "1[3|4|5|7|8][0-9]\d{8}",message = "手机号码格式不正确")
@NotEmpty(message ="returnAndExchangeInformation 不能为空")
@NotNull(message ="knowledgeId 不能为空")
@Digits(integer = 10, fraction = 2, message = "补发运费格式错误")
@Length(max = 50, min = 1, message = "配件名称必须在1-50字符之间")

Constraint 详细信息
@AssertFalse 该值必须为False
@AssertTrue 该值必须为True
@DecimalMax(value,inclusive) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 ,inclusive表示是否包含该值
@DecimalMin(value,inclusive) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 ,inclusive表示是否包含该值
@Digits 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Email 该值必须为邮箱格式
@Future 被注释的元素必须是一个将来的日期
@FutureOrPresent 被注释的元素必须是一个现在或将来的日期
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Negative 该值必须小于0
@NegativeOrZero 该值必须小于等于0
@NotBlank 该值不为空字符串,例如“ ”
@NotEmpty 该值不为空字符串
@NotNull 该值不为Null
@Null 该值必须为Null
@Past 被注释的元素必须是一个过去的日期
@PastOrPresent 被注释的元素必须是一个过去或现在的日期
@Pattern(regexp) 匹配正则
@Positive 该值必须大于0
@PositiveOrZero 该值必须大于等于0
@Size(min,max) 数组大小必须在[min,max]这个区间

自定义注解

手动实现一个自定义注解,做到灵活指定字符串字段只包含数字、字母、特殊符号、中文的校验


import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
        validatedBy = {ContainCharValidator.class}
)
public @interface ContainChar {
    String message() default "";

    Class<?>[] groups() default {};

    //必须包含这个,否则报错
    //javax.validation.ConstraintDefinitionException: HV000074: com.gbm.cloud.treasure.entity.zsk.ContainChar contains Constraint annotation, but does not contain a payload parameter.
    Class<? extends Payload>[] payload() default {};

    ContainCharEnum[] value() default {ContainCharEnum.CHINESE, ContainCharEnum.NUMBER, ContainCharEnum.LETTER, ContainCharEnum.SYMBOL};

}

/**
 * @Des
 * @Author yinkai
 * @Date 2022/3/1 14:38
 */
public class ContainCharValidator implements ConstraintValidator<ContainChar, String> {
    private String message;
    private ContainCharEnum[] values;
    private Class<?>[] groups;

    @Override
    public void initialize(ContainChar constraintAnnotation) {
        this.message = constraintAnnotation.message();
        this.values = constraintAnnotation.value();
        this.groups = constraintAnnotation.groups();
    }

    /**
     * @Des 遍历,全都不包含才返回false
     * @Author yinkai
     * @Date 2022/3/1 13:49
     */
    public boolean isValid2(String value, ConstraintValidatorContext context) {
        for (ContainCharEnum containCharEnum : values) {
            switch (containCharEnum) {
                case CHINESE:
                    if (!CHINESE.getPattern().matcher(value).find()) {
                        //禁止默认消息返回
                        context.disableDefaultConstraintViolation();
                        //自定义返回消息
                        context.buildConstraintViolationWithTemplate(message+"不包含"+containCharEnum).addConstraintViolation();
                        return false;
                    }
                    break;
                case NUMBER:
                    if (!NUMBER.getPattern().matcher(value).find()) {
                        //禁止默认消息返回
                        context.disableDefaultConstraintViolation();
                        //自定义返回消息
                        context.buildConstraintViolationWithTemplate(message+"不包含"+containCharEnum).addConstraintViolation();
                        return false;
                    }
                    break;
                case SYMBOL:
                    if (!SYMBOL.getPattern().matcher(value).find()) {
                        //禁止默认消息返回
                        context.disableDefaultConstraintViolation();
                        //自定义返回消息
                        context.buildConstraintViolationWithTemplate(message+"不包含"+containCharEnum).addConstraintViolation();
                        return false;
                    }
                    break;
                case LETTER:
                    if (!LETTER.getPattern().matcher(value).find()) {
                        //禁止默认消息返回
                        context.disableDefaultConstraintViolation();
                        //自定义返回消息
                        context.buildConstraintViolationWithTemplate(message+"不包含"+containCharEnum).addConstraintViolation();
                        return false;
                    }
                    break;
                default:
                    break;
            }
        }
        return true;
    }


    //遍历,全都不包含才返回false || 包含之外的就返回false
    // !(包含一个 && 只包含内部)
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        HashSet<Boolean> booleans = new HashSet<>(2);
        StringBuilder stringBuilder = new StringBuilder();
        for (ContainCharEnum containCharEnum : values) {
            booleans.add(containCharEnum.getPattern().matcher(value).find());
            stringBuilder.append(containCharEnum);
        }
        //不包含true-->全都是false-->全都不包含
        if (!booleans.contains(Boolean.TRUE)) {
            //禁止默认消息返回
            context.disableDefaultConstraintViolation();
            //自定义返回消息
            context.buildConstraintViolationWithTemplate(message + value + "不包含 " + stringBuilder).addConstraintViolation();
            return false;
        }
        Set<ContainCharEnum> noFindSet = Arrays.stream(values()).filter(m -> !ArrayUtil.contains(values, m)).collect(Collectors.toSet());
        for (ContainCharEnum containCharEnum : noFindSet) {
            if (containCharEnum.getPattern().matcher(value).find()) {
                //禁止默认消息返回
                context.disableDefaultConstraintViolation();
                //自定义返回消息
                context.buildConstraintViolationWithTemplate(message + value + "不能包含 " + containCharEnum).addConstraintViolation();
                return false;
            }
        }
        return true;
    }
}

public enum ContainCharEnum {
    CHINESE(0, "中文",Pattern.compile("[\u4E00-\u9FA5|\\!|\\,|\\。|\\(|\\)|\\《|\\》|\\“|\\”|\\?|\\:|\\;|\\【|\\】]")),
    NUMBER(1, "数字", Pattern.compile("[0-9]")),
    LETTER(2, "字母",Pattern.compile(".*[a-zA-Z]+.*")),
    SYMBOL(3, "特殊符号",Pattern.compile(".*[`~!@#$%^&*()+=|{}':;',\\[\\]·.<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?\\\\]+.*"));

    @EnumValue//标记数据库存的值是code
    private Integer code;
    @JsonValue
    private String desc;
    private Pattern pattern;


    ContainCharEnum(Integer code, String desc,Pattern pattern) {
        this.code = code;
        this.desc = desc;
        this.pattern = pattern;
    }

    @Override
    public String toString() {
        return desc;
    }

    public int getValue() {
        return code;
    }

    public Pattern getPattern() {
        return pattern;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355