spring boot mvc Validator 自定义校验(枚举值校验)
SpringBoot 参数校验的方法
Spring Validation最佳实践及其实现原理
注解系列——自定义注解验证器
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)
@ResponseBody
public Result myExceptionResult(MyException ex){
return Result.fail(ex.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public Result exceptionResult(Exception ex){
return Result.fail("内部错误: " + ex.getMessage());
}
}
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方法的逻辑
@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;
}
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";
}
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使用注解来实现参数校验的方法
在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 {
@Email
private 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 = 需要是一个合法的URL
org.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 += '纳秒'}
在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;
}
@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个属性是互相关联的(比如,注册的时候,需要用户输入两次密码,一个为密码,第二个为确认密码,前端做是必要的,但后台也应该要做),这时候,我们只能被迫在业务代码里写了。但是我们可以通过自定义参数校验器实现
@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 {};
}
@Slf4j
public class CheckEmailRegisterValidator implements ConstraintValidator<CheckEmailRegister, EmailRegisterDTO> {
@Override
public void initialize(CheckEmailRegister anno) {
}
@Override
public 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;
}
}
@Validated
@RestController
public class LoginController {
@ApiOperation("邮箱注册账号")
@PostMapping("web/register/email") // @Controller上加了@Validated注解,触发了@CheckEmailRegister 的校验,
// 如果需要触发对registerDTO的类里面的属性上的校验注解的校验,这里需要加上@Valid
public Result<Boolean> registerEmail(@CheckEmailRegister @RequestBody EmailRegisterDTO registerDTO) {
return loginService.registerEmail(registerDTO);
}
}
@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();
}
}
@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;
}
public class EnumsValidator implements ConstraintValidator<Enums, Object> {
// 校验的枚举类
private Class<?> enumClazz;
// 校验的枚举类的属性
private Field enumField;
private boolean nullValid;
@Override
public 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));
}
}
@Override
public 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;
}
}
@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;
}
public class EnumsTypeValidator implements ConstraintValidator<EnumsType, Integer> {
private List<Integer> typeList = new ArrayList<>();
private boolean nullValid;
@Override
public void initialize(EnumsType enums) {
int[] types = enums.types();
for (int type : types) {
typeList.add(type);
}
Assert.notEmpty(this.typeList, "@EnumsType提供的枚举值不能为空数组");
this.nullValid = enums.nullValid();
}
@Override
public 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);
}
}