不说废话开头先给出maven依赖,写教程不贴出依赖赶紧都是耍流氓。
<!-- 属性效验-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
用场景说话
场景一:前端传过来的字段如何在后台做效验,最老的方法就是if else显得不是很灵活。如果前端传来100个字段就得写许多多余的代码。
第一个场景就是在后台创建的实体和前端传来的字段做对应映射,加上JSR303注解来做灵活的效验
1:给Bean实体添加校验注解:javax.validation.constraints(大部分注解都在这个包下),并定义自己的message提示如下:
@NotBlank(message = "品牌名不能为空") private String name; @NotNull(message = "显示状态不能为空") private Integer showStatus;
注意:@NotBlank这个注解效验非空是只能效验字符串而@NotNull可以效验所有。所以我这里单独特意拿这两个做演示
2:开启校验功能@Validated很多人问为什么不用@Valid其实这个也可以但是不利于后面扩展功能。@Validated是在@Valid上又封装了一层,
比如加上了分组功能这个后面说 (效果:校验错误以后会有默认的响应;)在给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
示列Controller:
/** * 保存 */ @RequestMapping("/save") //@RequiresPermissions("product:brand:save") public R save(@Validated @RequestBody BrandEntity brand, BindingResult result) { /** * result.hasErrors()如果前端传来的值和在实体类标注的注解不对应就返回false。 * 比如在实体在name字段标注了@NotBlank(message = "品牌名不能为空")而前端没传name就返回了false */ if (result.hasErrors()) { Map<String, String> map = new HashMap<>(); //1、result.getFieldErrors()获取校验的错误结果对象然后遍历 for (FieldError fieldError : result.getFieldErrors()) { //FieldError 获取到错误提示 String message = fieldError.getDefaultMessage(); //获取错误的属性的名字 String field = fieldError.getField(); map.put(field, message); } return R.error(400, "提交的数据不合法").put("data", map); } else { brandService.save(brand); } return R.ok(); }
示列实体
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; public class Test { /** * 品牌名 */ @NotBlank(message = "品牌名不能为空") private String name; /** * 显示状态[0-不显示;1-显示] */ @NotNull(message = "显示状态不能为空") private Integer showStatus; }
效果演示1:什么参数都不传的
效果演示:只传一个
效果演示:全部参数都传
场景二:综合以上场景我们知道了如何效验和捕获错误信息然后返回给前端,但接下来就遇到了一个问题。
如果按照这样的写法就会在每个方法都写判断和捕获消息遍历的代码。母庸质疑这是愚蠢人的做法,接下来这个场景就解决这种愚蠢人的做法
可能很多小伙伴已经想到了全局异常捕获,没错就是全局异常捕获。首先我们得知道前端传来的值和我们效验的规则不一样然后抛出的是什么异常。
MethodArgumentNotValidException没错就是这样异常你们可以自己去试我就不再演示了,具体开接下来完整代码列子
import com.weikang.common.exception.BizCodeEnume; import com.weikang.common.utils.R; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.ModelAndView; import java.util.HashMap; import java.util.Map; /** * 集中处理所有异常 */ @Slf4j //@ResponseBody //@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller") //RestControllerAdvice这个注解的意思是ControllerAdvice增加实现了 // ResponseBody返回全 // 异常处理 //全局数据绑定 //全局数据预处理 //basePackages = "com.weikang.gulimall.product.controller"扫描需要捕获的控制层 @RestControllerAdvice(basePackages = "com.weikang.gulimall.product.controller") public class GulimallExceptionControllerAdvice { /** * 写应该针对某些异常的方法来捕获MethodArgumentNotValidException * 这个就是数据效验抛出的异常我们来进行捕获 * @ExceptionHandler用这个注解把需要捕获的异常类给定义上 */ @ExceptionHandler(value= MethodArgumentNotValidException.class) public R handleVaildException(MethodArgumentNotValidException e){ log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass()); //通过异常类的这个方法getBindingResult()可以得到BindingResult得到了这个就还是和之前在控制层的操作一样了 BindingResult bindingResult = e.getBindingResult(); //遍历循环我这是流遍历大家都懂的 Map<String,String> errorMap = new HashMap<>(); bindingResult.getFieldErrors().forEach((fieldError)->{ errorMap.put(fieldError.getField(),fieldError.getDefaultMessage()); }); //统一返回我这用到了枚举哈哈哈大家可以根据自己方便来我是为了规范 return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap); } }
场景三:以上两个场景我们解决了统一处理前端传来的字段效验和统一处理了返回效验的错误消息提示返回。接下来一个场景就是这样的。
比如我们添加一个账户有name,age,sex这些字段必填然后在实体类加上了效验注解都现在了这些字段必填。接下来我们又有一个
修改接口。根据id修改name。前端只传了id和name这样的话我们在实体标注了很多效验注解这样就已经冲突了。接下来我们
就用分组效验来解决这种场景。意思是在添加的时候我们使用添加分组的效验注解,
在修改的时候使用修改效验的注解这样就不会冲突(分组校验(多场景的复杂校验)
1:做分组效验第一步就是@NotBlank(message = "品牌名必须提交",groups = {AddGroup.class})加上groups = {AddGroup.class}
这个属性AddGroup.class这个类是什么呢,其实这个是我们自己定义的一个接口来标注这个属于什么分组
这样既可
2:第二步我们就在控制层方法上来定义使用这个分组
注意:在方法上标记了这个分组后,就同样只会效验在实体类标记了这个分组的字段会对应上,如果实体类没标记这个分组就不会效验。
同理如果修改也想效验这个字段就在定义一个修改分组
最后效果就是这样了在实体类标注了添加分组和修改分组这样的话无论是添加的时候还是修改的时候都会效验这个字段了,如果只加上添加分组不添加修改分组的话这个就只会在添加上起作用
场景四:针对以上方案我们基本满足了大部分场景接下来有一个场景就是showStatus字段状态判段1代表显示0代表不显示接下来我们用什么注解呢。大家都想得到的是用以下注解
@Pattern(regexp="^[a-zAz]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
这个注解可以在里面写正则表达式,但是当我们遇到以下比较复杂的逻辑判断的时候这个注解也会满足不了我们需求,
所以接下来我们来实现一个自定义注解来处理我们业务需求
自定义校验步骤:
1:编写一个自定义的校验注解,怎样编写呢嗯来看看jsr303官方写的我们就模仿他们,我们就以@NotNull来模仿看以下参数图
通过以上官方JSR303规范我们可以模仿写应该我们自己的效验器注解
2:在我们实体类上标注上我们自定义的这个注解
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class},message = "显示状态只能为0或者1") private Integer showStatus;
我们自定义注解上是没有定义注解值的vals={0,1}所以我们在去写一个来做效验
import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.NotNull; import java.lang.annotation.Documented; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {ListValueConstraintValidator.class }) //自己编写的注解效验器 public @interface ListValue { String message() default "{com.weikang.common.valid.ListValue.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; int[] vals() default { }; }
看上面代码我写了ListValueConstraintValidator.class自定义效验器所以我们去定义一个来做效验让自定义注解和自定义效验器关联上
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.Set; /** * implements ConstraintValidator<ListValue,Integer>实现这个接口,但是这个接口有两个泛型 * 一个是我们自定义的注解另一个是在自定义注解里面定义的效验值的类型 */ public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> { Set<Integer> val=new HashSet<Integer>(); @Override public void initialize(ListValue constraintAnnotation) { int[] vals = constraintAnnotation.vals(); //这个我们可以取到所有在注解上填写的条件然后我们循环放进set for (int i : vals) { val.add(i); } } @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { return val.contains(value); //包含返回true不包含返回false } }
结束end