网站建设主流语言杭州seo关键词优化公司
文章目录
- 学习链接
- 基础使用
- 1.单个参数校验
- 全局异常处理器
- 2.实体类参数校验
- 3.嵌套校验
- 4.分组校验
- 5.自定义校验注解
- 自定义参数校验器
- springmvc参数校验使用方法
- 自定义错误消息模板
- 观察源码
- 自定义错误消息
- 创建ValidationMessages.properties
- 在Controller中使用
- 自定义参数校验器
- CheckEmailRegister
- CheckEmailRegisterValidator
- 在controller中使用
- 自定义枚举传参的校验器
- EmailCodeTypeEnum枚举类
- @Enums校验注解
- EnumsValidator
- 自定义枚举int值校验器
- EnumsType
- EnumsTypeValidator
- 使用示例
学习链接
spring boot mvc Validator 自定义校验(枚举值校验)
SpringBoot 参数校验的方法
Spring Validation最佳实践及其实现原理
注解系列——自定义注解验证器
基础使用
1.单个参数校验
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("/vadmin/cart")
@Validated // 开启校验(在类上面不能用@Valid,否则下面的校验注解无效)
public class CartController {@RequestMapping("deleteCart")@ResponseBody // 如果为null,这里将会抛出异常,可使用全局异常处理器捕获public Result deleteCart(@NotNull Long userId, @NotNull Long pid){ cartService.deleteCart(userId, pid);return Result.succ(null);}
}
// 普通参数校验(如上),在controller类上加@Validated注解才能生效(不要在类上使用@Valid,否则无效)。
// 如果将@Validated加在方法参数里面,普通参数校验将无效(不会做校验),
// 将@Validated加载controller类上并不会引起实体类的参数校验
// 即(实体类中有属性有校验注解,但是controller的方法参数里面没有写@Validated)
// 如果校验失败, 将会抛出ConstraintViolationException异常
全局异常处理器
@ControllerAdvice
public class MyExceptionHandler {@ExceptionHandler(MyException.class)@ResponseBodypublic Result myExceptionResult(MyException ex){return Result.fail(ex.getMessage());}@ExceptionHandler(Exception.class)@ResponseBodypublic Result exceptionResult(Exception ex){return Result.fail("内部错误: " + ex.getMessage());}}
2.实体类参数校验
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_cart")
public class Cart implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "tc_id", type = IdType.AUTO)private Long tcId;private Long cartId;private Long userId;@NotNull(message = "商品ID不能为空")private Long pid;@Min(value = 1,message = "至少添加一件商品")@NotNull(message = "必须添加商品数量信息")private Long pcount;}@RequestMapping("addToCart")
@ResponseBody // 启用校验 // 校验结果封装 // 后面可以接参数
public Result addToCart(@Validated Cart cart, BindingResult bindingResult,Man man) {if (bindingResult.hasErrors()) {StringBuilder sb = new StringBuilder();bindingResult.getAllErrors().forEach(error->{sb.append(error.getDefaultMessage() + " | ");});throw new MyException(sb.toString());// 处理校验结果,抛出异常,让全局异常处理器处理自定义异常}cartService.addCart(cart);return Result.succ(null);
}
// 实体类校验注意事项
// 1. 必须把@Validated写在controller方法里面,写在类上面无效;
// 2. 一个校验的实体类对应一个BindingResult,否则会抛出MethodArgumentNotValidException异常(全局异常处理器处理)
// 如:(@Validated Person person,BindingResult bindingResult1,
// @Validated Man man, BindingResult bindingResult) 是正确的
// 如:(@Validated Cart cart, BindingResult bindingResult,Man man) 是正确的
// 如:(@Validated Cart cart,Man man, BindingResult bindingResult) 是错误的
// 第2小点总结: 一个@Validated对应一个BindingResult,否则抛出异常
// 3. 如果需要把错误信息配置在配置文件中
// 在springboot的resources目录下新建【ValidationMessages.properties】
// 内容为:person.name.notnull=person.name用户名不能为空
// man.name.notnull=man.name用户名名不能为空
// 之后即可在实体类属性上引用: @NotNull(message = "{man.name.notnull}")
// 4. 如果前面有校验注解,但是后面如果不写BindingResult,controller就会抛出异常;
// 如果写了BindingResult那么就会进controller方法的逻辑
3.嵌套校验
@PostMapping("saveCareOrders") // 这里的@Validated触发对CareOrderFormDto类中的注解校验
public AjaxResult saveCareOrders(@Validated @RequestBody CareOrderFormDto careOrderFormDto) {}@Data
@AllArgsConstructor
@NoArgsConstructor
public class CareOrderFormDto implements Serializable {// 处方@Valid // 在这里触发对CareOrderDto类中的注解校验@NotNull // 如果这里不写@NotNull注解, 只写@Valid注解,那么如果前端如果根本就不传careOrderDto过来,// 那么就不会触发CareOrderDto类中的校验注解,传了的话,就做校验// 所以建议加上@NotNull注解,先触发@NotNull的非空校验,再通过@Valid触发CareOrderDto类中的校验注解private CareOrderDto careOrderDto;// 处方详情@Valid // 在这里触发对CareOrderItemDto类的注解校验@NotEmpty(message = "处方详情不能为空") // 将会触发集合不能为空的校验private List<CareOrderItemDto> careOrderItemDtoList;
}
4.分组校验
1.实体类
@Data
public class Person { // 读取resources目录下的ValidationMessages.properties文件 、归Agroup@NotNull(message = "{person.name.notnull}",groups = {Agroup.class})private String name;@NotNull(message = "{person.age.notnull}",groups = {Bgroup.class})private Integer age;// 不写分组,则默认属于Default.class组@NotNull(message = "address不能为空")private String address;
}
2.校验
@RequestMapping("test04") // 只会校验Agroup相关的
public String test04(@Validated({Agroup.class}) Person person,BindingResult result) {log.info("{},{}",person.getName(),person.getAge());return "ok";
}@RequestMapping("test05") // 只会校验Bgroup相关的
public String test05(@Validated({Bgroup.class}) Person person,BindingResult result) {log.info("{},{}",person.getName(),person.getAge());return "ok";
}@RequestMapping("test06") // 只会校验Agroup、Bgroup相关的
public String test06(@Validated({Agroup.class,Bgroup.class}) Person person,BindingResult result) {log.info("{},{}",person.getName(),person.getAge());return "ok";
}@RequestMapping("test07") // 校验Agroup、Bgroup再加上 没有分组的校验注解【加上默认的Default.class组的】
public String test07(@Validated({Agroup.class,Bgroup.class, Default.class}) Person person, BindingResult result) {log.info("{},{}",person.getName(),person.getAge());return "ok";
}@RequestMapping("test07") // 只会校验默认组的
public String test07(@Validated Person person, BindingResult result) {log.info("{},{}",person.getName(),person.getAge());return "ok";
}
5.自定义校验注解
1.自定义校验用法:
package com.zzhua.controller.validate;import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.TYPE,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckLenValidator.class)
public @interface CheckLen {String message() default "{字符串长度不符合要求}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};int len();
}public class CheckLenValidator implements ConstraintValidator<CheckLen, String> {private int len;public void initialize(CheckLen constraint) {this.len = constraint.len();}public boolean isValid(String obj, ConstraintValidatorContext context) {return obj != null && len == obj.length();}
}@RestController
@RequestMapping("test")
@Validated // 必须加这个并且加到这个位置(加到下面的test方法中无效, 因为i参数不是实体类; 实体类是加在下面方法中,才能生效// 并且这两种加的方法互不影响)
public class RoleController {@GetMapping("test")public AjaxResult test(@CheckLen(len=3) String i) {return AjaxResult.success("hello");}
}
2.内置的校验注解
// 查看 ConstraintHelper 这个类里面注册了很多 validator
// 这也是为什么内置校验注解没有校验器的原因 validatedBy={}
自定义参数校验器
springmvc参数校验使用方法
我们先总结一下在springMvc使用注解来实现参数校验的方法
-
在Controller接口上添加@Validated,可以触发在此controller类中,所有使用了校验注解的接口
如:@Validated // 添加此注解 @RestController public class LoginController {// 对单个参数的校验@GetMapping("login/captcha")public Result<Object> captcha(@NotNull(message = "type不能为空") Integer type) {return loginService.captcha(type, request,response);}// 对自定义类型参数校验//(注意这里只会校验registerDTO是否为null,而不会触发在EmailRegisterDTO 类上的校验注解校验)//(如果需要触发对EmailRegisterDTO 类上的校验注解校验,那么看下面的嵌套校验使用)@PostMapping("register/email") // 这里只是为了演示效果, 才把RequestBody的required属性置为false的。public Result<Boolean> registerEmail(@NotNull(message="参数不能为空") // 此处触发了对registerDTO实体类的校验@RequestBody(required = false) EmailRegisterDTO registerDTO) {return loginService.registerEmail(registerDTO);}}
-
在Controller类中的某个接口方法中在方法参数的前面加上@Valid或@Validate注解,即可触发该参数所对应的实体类A上的属性上所添加的校验注解。如果这个实体类A上的属性还是个实体类B(我们自己定义的类),那么可以继续在这个实体类A的这个属性(类型为实体类B)上添加@Valid注解,来触发对嵌套的实体类B上的加了校验注解的属性校验,即
嵌套校验
。
如:@Data public class EmailRegisterDTO {@Emailprivate String email;private String emailCode;private String password;private String checkPassword;@NotNull("customVo不能为空")@Valid // 1. 触发嵌套校验, 不使用此注解,将不会触发CustomVo上的类上使用的校验注解的校验// 2. 如果没有@NotNull注解校验,而只有@Valid注解,那就是如果前端传了customVo就校验,没传,那就不校验private CustomVo customVo;@NotEmpty("customVos不能为空")private List<CustomVo> customVos; }@Data public class CustomVo {@NotBlank("attr不能为空")private String attr; }// 需要在参数的前面加上@Valid注解,才能触发EmailRegisterDTO类上的校验注解的校验 @PostMapping("register/email") public Result<Boolean> registerEmail(@Valid @NotNull(message="参数不能为空") @RequestBody(required = false) EmailRegisterDTO registerDTO) {return Result.ok(true); }
自定义错误消息模板
观察源码
我们注意到源码里面,就比如@NotNull这个注解,里面的message使用了大括号,来引用消息内容。错误消息的内容可以到org/hibernate/validator/ValidationMessages_zh_CN.properties文件中找到。
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {String message() default "{javax.validation.constraints.NotNull.message}";Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };/*** Defines several {@link NotNull} annotations on the same element.** @see javax.validation.constraints.NotNull*/@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })@Retention(RUNTIME)@Documented@interface List {NotNull[] value();}
}
ValidationMessages_zh_CN.properties消息的内容如下,可以看到里面
- 可以使用
{注解的属性}
的方式引用注解里面的属性值, - 可以使用
${validatedValue}
的方式引用待校验的值 - 在
${..}
中可以使用表达式
javax.validation.constraints.AssertFalse.message = 只能为false
javax.validation.constraints.AssertTrue.message = 只能为true
javax.validation.constraints.DecimalMax.message = 必须小于或等于{value}
javax.validation.constraints.DecimalMin.message = 必须大于或等于{value}
javax.validation.constraints.Digits.message = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
javax.validation.constraints.Email.message = 不是一个合法的电子邮件地址
javax.validation.constraints.Future.message = 需要是一个将来的时间
javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
javax.validation.constraints.Max.message = 最大不能超过{value}
javax.validation.constraints.Min.message = 最小不能小于{value}
javax.validation.constraints.Negative.message = 必须是负数
javax.validation.constraints.NegativeOrZero.message = 必须是负数或零
javax.validation.constraints.NotBlank.message = 不能为空
javax.validation.constraints.NotEmpty.message = 不能为空
javax.validation.constraints.NotNull.message = 不能为null
javax.validation.constraints.Null.message = 必须为null
javax.validation.constraints.Past.message = 需要是一个过去的时间
javax.validation.constraints.PastOrPresent.message = 需要是一个过去或现在的时间
javax.validation.constraints.Pattern.message = 需要匹配正则表达式"{regexp}"
javax.validation.constraints.Positive.message = 必须是正数
javax.validation.constraints.PositiveOrZero.message = 必须是正数或零
javax.validation.constraints.Size.message = 个数必须在{min}和{max}之间org.hibernate.validator.constraints.CreditCardNumber.message = 不合法的信用卡号码
org.hibernate.validator.constraints.Currency.message = 不合法的货币 (必须是{value}其中之一)
org.hibernate.validator.constraints.EAN.message = 不合法的{type}条形码
org.hibernate.validator.constraints.Email.message = 不是一个合法的电子邮件地址
org.hibernate.validator.constraints.Length.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.CodePointLength.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.LuhnCheck.message = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配
org.hibernate.validator.constraints.Mod10Check.message = ${validatedValue}的校验码不合法, 模10校验和不匹配
org.hibernate.validator.constraints.Mod11Check.message = ${validatedValue}的校验码不合法, 模11校验和不匹配
org.hibernate.validator.constraints.ModCheck.message = ${validatedValue}的校验码不合法, ${modType}校验和不匹配
org.hibernate.validator.constraints.NotBlank.message = 不能为空
org.hibernate.validator.constraints.NotEmpty.message = 不能为空
org.hibernate.validator.constraints.ParametersScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.Range.message = 需要在{min}和{max}之间
org.hibernate.validator.constraints.SafeHtml.message = 可能有不安全的HTML内容
org.hibernate.validator.constraints.ScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.URL.message = 需要是一个合法的URLorg.hibernate.validator.constraints.time.DurationMax.message = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
org.hibernate.validator.constraints.time.DurationMin.message = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
自定义错误消息
创建ValidationMessages.properties
在resources目录下,创建ValidationMessages.properties文件,内容可以如下:
## ${validatedValue}去引用待校验的值,而{min}引用校验注解实例的属性值
a.b.c = ${validatedValue}-{min}这个不该为空的
在类上添加校验注解,并自定义消息模板内容
@Data
public class EmailRegisterDTO {@Length(message = "${validatedValue}不是一个合法的值",min = 3)@NotNull(message = "这个@length不校验null,还得我出马")private String email;// 使用{errorMsgkey} 大括号的形式去引用写在ValidationProperties的模板消息,也可以用来引用校验注解的属性值// ${validatedValue}去引用待校验的值@Length(message = "{a.b.c}-${validatedValue}-${validatedValue.length()}-{min}!!!",min = 3)private String emailCode;}
在Controller中使用
@Validated
@RestController
public class LoginController {@PostMapping("register/email")public Result<Boolean> registerEmail(@Valid @NotNull(message="参数不能为空") @RequestBody(required = false) EmailRegisterDTO registerDTO) {return Result.ok(true);}}
自定义参数校验器
上面这种校验,它只能对某个实体的的单个属性作校验,但是有的时候,一个实体类的2个属性是互相关联的(比如,注册的时候,需要用户输入两次密码,一个为密码,第二个为确认密码,前端做是必要的,但后台也应该要做),这时候,我们只能被迫在业务代码里写了。但是我们可以通过自定义参数校验器实现
CheckEmailRegister
@Target({ElementType.TYPE,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckEmailRegisterValidator.class)
public @interface CheckEmailRegister {// 可参考 ValidationMessages_zh_CN.properties 文件中的写法,这里可以用el表达式的语法去写逻辑String message() default "${validatedValue}中的${validatedValue.email}不合法,${validatedValue.email==null||validatedValue.email==''?'邮箱不能为空(⊙﹏⊙)':''}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};}
CheckEmailRegisterValidator
- 通过initialize方法,可以获取校验注解实例,可以方便的获取注解的属性,可以保存下来,以作后面校验使用
- isValid方法返回true或者false,表示是否校验通过,如果校验不通过,将会使用校验注解的message属性作为消息模板(功能同如上所述)
- 可以使用ConstraintValidatorContext提供的api,来传入消息模板,自定义消息错误提示
@Slf4j
public class CheckEmailRegisterValidator implements ConstraintValidator<CheckEmailRegister, EmailRegisterDTO> {@Overridepublic void initialize(CheckEmailRegister anno) {}@Overridepublic boolean isValid(EmailRegisterDTO registerDTO, ConstraintValidatorContext context) {if (registerDTO == null) {return false;}try {// 这里不要给消息模板, 测试一下自定义注解的默认的消息模板if (StringUtils.isEmpty(registerDTO.getEmail()) || registerDTO.getEmail().length() < 2) {return false;}if (StringUtils.isEmpty(registerDTO.getEmailCode())) {return newErrorMsg(context,"邮箱验证码不能为空");}if (StringUtils.isEmpty(registerDTO.getPassword())) {return newErrorMsg(context,"密码不能为空");}if (StringUtils.isEmpty(registerDTO.getCheckPassword())) {return newErrorMsg(context,"密码不能为空");}if (!Objects.equals(registerDTO.getPassword(), registerDTO.getCheckPassword())) {return newErrorMsg(context,"两次输入密码不一致," +"一个是:${validatedValue.password}," +"一个是:${validatedValue.checkPassword}");}} catch (RuntimeException ex) {log.error("校验发生异常: {}", ex.getMessage());return false;}return true;}private boolean newErrorMsg(ConstraintValidatorContext context, String errMsg) {context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate(errMsg).addConstraintViolation();return false;}}
在controller中使用
@Validated
@RestController
public class LoginController {@ApiOperation("邮箱注册账号")@PostMapping("web/register/email") // @Controller上加了@Validated注解,触发了@CheckEmailRegister 的校验,// 如果需要触发对registerDTO的类里面的属性上的校验注解的校验,这里需要加上@Validpublic Result<Boolean> registerEmail(@CheckEmailRegister @RequestBody EmailRegisterDTO registerDTO) {return loginService.registerEmail(registerDTO);}
}
自定义枚举传参的校验器
EmailCodeTypeEnum枚举类
@Getter
public enum EmailCodeTypeEnum {REGISTER(1, "registerTpl.ftl"),FIND_PWD(2, "findPwd.ftl"),;private Integer type;private String templateName;EmailCodeTypeEnum(int type, String templateName) {this.type = type;this.templateName = templateName;}public static EmailCodeTypeEnum type(Integer type) {for (EmailCodeTypeEnum emailCodeTypeEnum : EmailCodeTypeEnum.values()) {if (Objects.equals(emailCodeTypeEnum.type, type)) {return emailCodeTypeEnum;}}throw new RuntimeException();}
}
@Enums校验注解
@Target({ElementType.TYPE,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumsValidator.class)
public @interface Enums {String message() default "非法值:${validatedValue},请传入指定范围内的枚举值";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};// 校验的枚举类Class<?> enumClazz();// 校验的枚举类的属性String enumField();// null是否合法boolean nullValid() default false;}
EnumsValidator
public class EnumsValidator implements ConstraintValidator<Enums, Object> {// 校验的枚举类private Class<?> enumClazz;// 校验的枚举类的属性private Field enumField;private boolean nullValid;@Overridepublic void initialize(Enums enums) {this.nullValid = enums.nullValid();Class<?> enumsClass = enums.enumClazz();if (!enumsClass.isEnum()) {throw new RuntimeException(MessageFormat.format("%s不是枚举类", enums.enumClazz().getName()));}this.enumClazz = enumsClass;try {Field field = enumClazz.getDeclaredField(enums.enumField());field.setAccessible(true);this.enumField = field;} catch (NoSuchFieldException e) {throw new RuntimeException(MessageFormat.format("枚举类%s,无此%s属性", enums.enumClazz().getName(), enumField));}}@Overridepublic boolean isValid(Object value, ConstraintValidatorContext context) {if (value == null) {return nullValid; // null是否合法}for (Object enumConstant : this.enumClazz.getEnumConstants()) {try {if (Objects.equals(enumField.get(enumConstant), value)) {return true;}} catch (IllegalAccessException e) {return false;}}List<Object> enumList = Arrays.stream(this.enumClazz.getEnumConstants()).map(obj -> {try {return enumField.get(obj);} catch (IllegalAccessException e) {throw new RuntimeException("枚举校验发生错误");}}).collect(Collectors.toList());context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("允许的值为: "+ StringUtils.arrayToCommaDelimitedString(enumList.toArray())).addConstraintViolation();return false;}}
自定义枚举int值校验器
EnumsType
@Target({ElementType.TYPE,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumsTypeValidator.class)
public @interface EnumsType {String message() default "非法值:${validatedValue},请传入指定范围内的枚举值";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};int[] types();// null是否合法boolean nullValid() default false;}
EnumsTypeValidator
public class EnumsTypeValidator implements ConstraintValidator<EnumsType, Integer> {private List<Integer> typeList = new ArrayList<>();private boolean nullValid;@Overridepublic void initialize(EnumsType enums) {int[] types = enums.types();for (int type : types) {typeList.add(type);}Assert.notEmpty(this.typeList, "@EnumsType提供的枚举值不能为空数组");this.nullValid = enums.nullValid();}@Overridepublic boolean isValid(Integer value, ConstraintValidatorContext context) {if (value == null) {return nullValid;}for (Object type : this.typeList) {if (Objects.equals(type, value)) {return true;}}context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("非法值:${validatedValue},请传入" + Arrays.toString(this.typeList.toArray())).addConstraintViolation();return false;}
}
使用示例
@Validated
@RestController
public class LoginController {// 下面的type参数只能传入指定的1,2@GetMapping("login/captcha")public Result<Map<String,Object>> captcha(@EnumsType(types = {1,2}) Integer type) {return loginService.captcha(type, request,response);}
}