文章目录

  • 一、关于JSR
  • 1.1什么是JSR?
  • 1.2什么是JSR-303?
  • 1.3JSR-303的作用?
  • 二、从if判断到jsr303
  • 2.1提供一个场景
  • 2.1首先使用if判断
  • 2.3jsr303使用
  • 三、jsr303相关校验注解导图展示
  • 四、@Valid和@Validated的区别
  • 五、@Valid和@Validated高级使用
  • 5.1Valid级联校验
  • 5.2@Validated分组校验
  • 5.2.1什么是分组校验
  • 5.2.2.引出jsr303校验注解的groups方法
  • 5.2.3.@Validated注解value方法开启指定分组校验
  • 5.2.4.检验结果
  • 5.2.5.结论
  • 5.3@Validated分组校验顺序
  • 5.3.1为什么需要?
  • 5.3.2如何实现?
  • 5.4@Validated非实体类校验
  • 5.4.1.使用@Validated注解
  • 5.4.2.GlobalExceptionHandler类添加异常处理方法
  • 5.4.3测试结果
  • 六、自定义注解校验
  • 6.1自定义注解@ListValue
  • 6.2使用自定义注解
  • 6.3请求方法
  • 6.4测试结果
  • 七、补充@PathVariable注解校验
  • 7.1@PathVariable作用
  • 7.2使用正则表达式校验路径变量
  • 7.3自定义类实现ErrorController接口
  • 7.4测试结果
  • 八、导图总结

一、关于JSR

1.1什么是JSR?

JSR是Java Specification Requests的缩写,意思是Java规范提案,是指向JCP(Java Community Rrocess)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。

比如:

Web应用技术

  • Java Servlet 3.0 (JSR 315)
  • JavaServer Faces 2.0 (JSR 314)
  • JavaServer Pages 2.2/Expression Language 2.2 (JSR 245)
  • Standard Tag Library for JavaServer Pages (JSTL) 1.2 (JSR 52)
  • Debugging Support for Other Languages 1.0 (JSR 45)

1.2什么是JSR-303?

JSR-303是javaee 6中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation的参考实现. Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint,目前Springboot内置了Hibernate Validator。

1.3JSR-303的作用?

用于对Java Bean的字段值进行校验,确保输入进来的数据在语义上是正确的,使验证逻辑从业务代码中脱离出来。

JSR303是运行时数据验证框架,验证之后验证的错误信息会马上返回。

二、从if判断到jsr303

2.1提供一个场景

提供一个实体类,该实体类含有一个属性

@Data
public class ExceptionQuery {
    private String userName;
}

要求用户提供的姓名不能为空,下面开始校验该属性

2.1首先使用if判断

@PostMapping("/query")
public Result  getResult(@RequestBody ExceptionQuery query){
    //校验参数
    if (StringUtils.isEmpty(query.getUserName())){
        //抛出异常
    }
    return Result.buildSuccess(query);
}

这个方法本省是没有问题的,但是如果入参有很多属性需要校验,那么程序中就会存在很多if判断,这样显的代码很冗余。有没有方法替代if判断呢?当然是有的,使用jsr303可以解决该问题

2.3jsr303使用

1.为属性加@NotBlank属性

@Data
public class ExceptionQuery {

    @NotBlank(message = "用户姓名必须提交")
    private String userName;

}

注:像@NotBlank一类的校验注解都可以重写message属性

public @interface NotBlank {
    
    //default表明可以重写message属性
    String message() default "{javax.validation.constraints.NotBlank.message}";
    //省略...
}

2.在方法入参参数上加@Valid注解

@PostMapping("/query")
public Result  getResult(@Valid @RequestBody ExceptionQuery query){
    return Result.buildSuccess(query);
}

3.postman测试结果

实体java 实体303_spring boot


从输出结果上可以看到重写后的message属性,而对于错误的输出结果Spring也提供了对应类存储错误信息,这个类就是BindingResult,将上面的方法加上BindingResult参数,如下代码

@PostMapping("/query")
public Result  getResult(@Valid @RequestBody ExceptionQuery query, BindingResult result){
    if(result.hasErrors()){
        Map<String,String> map = new HashMap<>();
        result.getFieldErrors().forEach(item->{
            //获取发生错误时的message
            String message = item.getDefaultMessage();
            //获取发生错误的字段
            String field = item.getField();
            map.put(field,message);
            System.out.println(field+":"+message);
        });
        return Result.buildFailure(400,"参数错误",map);
    }
    return Result.buildSuccess(query);
}

输出结果

实体java 实体303_spring_02

然而,咱们不能对每个方法都传入一个BindingResult参数,可以使用统一异常处理输出错误信息

4.配合异常处理

首先说下为什么能通过异常处理输出错误信息?

我们定位到一个类MethodArgumentNotValidException,该类也是Spring提供的,包含BindingResult引用,因此该类可以拿到输出的错误信息

实体java 实体303_spring_03

异常处理代码

@Slf4j
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler {
    
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidationException(MethodArgumentNotValidException e) {
        log.error(ErrorStatus.ILLEGAL_DATA.getMessage() + ":" + e.getMessage());
        Map<String,Object> map=new HashMap<>();
        
        //拿到异常输出信息
        BindingResult bindingResult = e.getBindingResult();
        bindingResult.getFieldErrors().forEach(fieldError -> {
            String message = fieldError.getDefaultMessage();
            String field = fieldError.getField();
            map.put(field,message);
        });
        return Result.buildFailure(ErrorStatus.ILLEGAL_DATA.getCode(),String.valueOf(getFirstOrNull(map)),map);
    }

    //拿到map第一个值
    private static Object getFirstOrNull(Map<String, Object> map) {
        Object obj = null;
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            obj = entry.getValue();
            if (obj != null) {
                break;
            }
        }
        return  obj;
    }
}

加上异常处理后的方法代码如下所示,关于@ControllerAdvice全局异常处理请看这一篇博客

@PostMapping("/query")
public Result  getResult(@Valid @RequestBody ExceptionQuery query){
    return Result.buildSuccess(query);
}

是不是很简洁,下面看下输出结果

实体java 实体303_spring_04

三、jsr303相关校验注解导图展示

实体java 实体303_java_05


注意:@Validated和@Valid注解都是开启注解校验功能的注解

下面举个例子说明一下

阐述基本用法可能不是那么适合,到实际应用改下就行

实体java 实体303_实体java_06


输出结果

实体java 实体303_实体java_07

四、@Valid和@Validated的区别

上述例子咱们用的是@Valid进行校验的,用@Validated注解也可以,那么他两的区别是什么呢?

所属不同:
该注解所属包为:javax.validation.Valid,而 @Validated是Spring基于@Valid进一步封装,并提供了一些高级功能,如分组,分组顺序等

使用位置不同:
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性上
@Valid:可以用在方法、构造函数、方法参数和成员属性上

正是由于@Valid能用于成员属性(字段)上,因此@Valid可以提供级联校验,关于级联校验下面就要阐述

五、@Valid和@Validated高级使用

5.1Valid级联校验

级联校验也叫嵌套检测,嵌套即一个实体类包含另一个实体类

举个例子

下面有个People实体类

实体java 实体303_spring boot_08


该类还有两个属性,一个是名字,一个头发,因为一个人有很多头发(暂不考虑光头哈哈),Hair实体类如下

实体java 实体303_java_09


下面写一个方法检测下用户数据

在方法参数上用@Validated或者@Valid都可以

@PostMapping("/add")
public Result getPeople(@Validated @RequestBody People people){
    return Result.buildSuccess(people);
}

测试结果如下

实体java 实体303_实体java_10


暴露一个问题

没有输出Hair实体类对数据的校验结果,可以证明@Validated和@Valid加在方法参数前,都不会对嵌套的实体类Hair进行检测

实现检测

为了能够完成对嵌套实体类的检测,我们需要在属性上使用@Vaild注解

实体java 实体303_spring_11

看下此时的输出结果

实体java 实体303_spring boot_12

5.2@Validated分组校验

5.2.1什么是分组校验

分组校验是由@Validated的value方法提供的,用于开启指定的组校验,分别作用不同的业务场景中,下面举个例子说明下

5.2.2.引出jsr303校验注解的groups方法

假如使用Mybatis做持久化框架(其他也可以),我们知道当向数据库插入一条数据时,这条数据id是自动生成,不用用户传入的,而当我们修改一条数据时,id需要用户传入,因此在修改操作时需要对id进行校验。像以上修改和插入对id不同的操作,这个时候就要使用groups方法对id进行分组,如下图所示

实体java 实体303_jsr303校验_13


为了涵盖多种情况,加入了两个属性,personName属性在AddGroup.class,UpdateGroup.class时都会校验,personAge属性在不指定分组时校验

5.2.3.@Validated注解value方法开启指定分组校验

实体java 实体303_jsr303校验_14

5.2.4.检验结果

下面使用一样的数据请求三个方法,得到的结果如下

模拟数据,该数据都不符合要求
{
    "personId":"",
    "personName":"",
    "personAge":0
}

getPerson方法不指定分组
校验了@Range(min=1,max=400,message="年龄提交有误"),该注解不含groups方法
{
    "success": false,
    "code": 10003,
    "msg": "年龄提交有误",
    "data": {
        "personAge": "年龄提交有误"
    }
}


addPerson方法指定AddGroup分组
校验了@NotBlank(message = "名字不能为空",groups = {AddGroup.class,UpdateGroup.class})
{
    "success": false,
    "code": 10003,
    "msg": "名字不能为空",
    "data": {
        "personName": "名字不能为空"
    }
}

updatePerson方法指定UpdateGroup分组
校验了@NotBlank(message = "id不能为空",groups = UpdateGroup.class)和
@NotBlank(message = "名字不能为空",groups = {AddGroup.class,UpdateGroup.class})
{
    "success": false,
    "code": 10003,
    "msg": "名字不能为空",
    "data": {
        "personName": "名字不能为空",
        "personId": "id不能为空"
    }
}

5.2.5.结论

如果校验注解添加上groups方法并指定分组,只有@Validated注解value方法指定该分组,才会开启校验注解的校验数据功能

同样的如果校验注解没有groups指定分组,则@Validated注解value方法为默认分组时才会开启

5.3@Validated分组校验顺序

5.3.1为什么需要?

默认情况下,分组间的约束验证是无序的,然而某些情况下分组间的校验顺序却很重要,比如第二组约束验证依赖于第一组稳定状态来进行,这个时候需要分组按照顺序校验

5.3.2如何实现?

分组校验顺序由@GroupSequence注解实现

举个例子

1.列出分组顺序

public interface First {}

public interface Second {}

//此时first、second顺序校验
@GroupSequence({First.class,Second.class})
public interface Group {}

2.实体类及请求方法

@Data
public class UserGroupSequence {

    @NotEmpty(message = "id不能为空",groups = First.class)
    private String userId;

    @NotEmpty(message = "姓名不能空",groups = First.class)
    @Size(min = 3,max = 8,message = "姓名长度在3到8之间",groups = Second.class)
    private String userName;
}


@RestController
@RequestMapping("/group")
public class UserGroupSequenceController {

    //此时用的是value方法指定的是Group接口
    @PostMapping("/add")
    public Result addGroup(@Validated(value = Group.class) @RequestBody UserGroupSequence sequence){
        return Result.buildSuccess(sequence);
    }
}

3.测试结果

模拟数据
{
    "userId":"",
    "userName":""
}

结果
{
    "success": false,
    "code": 10003,
    "msg": "姓名不能空",
    "data": {
        "userName": "姓名不能空",
        "userId": "id不能为空"
    }
}

4.结论

@GroupSequence注解指定分组顺序时,如果First标识的校验注解没有通过,则Second标识的注解不会生效

读者可以测试下,如果把userName上两个校验注解groups去掉,输出结果是"姓名不能空"及"姓名长度在3到8之间"交替出现

5.4@Validated非实体类校验

上面校验都是对实体类的校验,下面来介绍下对非实体类的校验

5.4.1.使用@Validated注解

这里特别要注意一点就是@Validated注解对于非实体类的校验,在类上注解才会起效果

@Validated
public class AnnotationController {
    
    @GetMapping("/getage")
    public Result getAge(@Range(min = 3,max = 8,message = "年龄在3-8岁") @RequestParam String age){
        return Result.buildSuccess(age);
    }
}

5.4.2.GlobalExceptionHandler类添加异常处理方法

@ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public Result resolveConstraintViolationException(ConstraintViolationException ex){

        Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
        
        //对异常信息进行处理
        if(!CollectionUtils.isEmpty(constraintViolations)){
            StringBuilder msgBuilder = new StringBuilder();
            for(ConstraintViolation constraintViolation :constraintViolations){
                msgBuilder.append(constraintViolation.getMessage()).append(",");
            }
            String errorMessage = msgBuilder.toString();
            if(errorMessage.length()>1){
                errorMessage = errorMessage.substring(0,errorMessage.length()-1);
            }
            return Result.buildFailure(ErrorStatus.ILLEGAL_DATA.getCode(),errorMessage);
        }
        return Result.buildFailure(ErrorStatus.ILLEGAL_DATA.getCode(),ex.getMessage());
    }

5.4.3测试结果

http://localhost:8082/annotation/getage?age=1

{
    "success": false,
    "code": 10003,
    "msg": "年龄在3-8岁",
    "data": null
}

六、自定义注解校验

提供一个场景,假如一个字段只能让用户传入特定的值,比如判断是否显示的属性isShow,只能取0和1,下面我们实现这一个功能

6.1自定义注解@ListValue

@Documented
//该注解由哪个类校验
@Constraint(validatedBy = {ListValue.ListValueConstraintValidator.class})
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ListValue {

    String message() default "{com.thinkcoder.annotation.ListValue.message}";

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

    Class<? extends Payload>[] payload() default { };

    int[] value();

    class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
        private Set<Integer> set=new HashSet<>();

        //获取value属性指定的数字,存入set集合
        @Override
        public void initialize(ListValue constraintAnnotation) {
            int[] value = constraintAnnotation.value();
            for (int i : value) {
                set.add(i);
            }
        }

        //校验用户输入数据是否在set集合中
        //isValid第一个参数传入要校验属性的类型
        @Override
        public boolean isValid(Integer value, ConstraintValidatorContext context) {
            return  set.contains(value);
        }
    }
}

上面的代码需要说明以下几点

  • 校验类要实现ConstraintValidator<ListValue,Integer>接口;第一个泛型参数是自定义注解,第二个泛型参数是要校验的属性类型
  • initalize方法:获取到用户使用自定义注解中的数据
  • isValid方法:实现校验逻辑,结果是返回boolean类型
  • @Constraint注解:将自定义注解和校验类联系起来

6.2使用自定义注解

@Data
public class AnnotationQuery {

    @ListValue(value = {0,1},message = "数值只能是0或者1")
    private Integer isShow;
}

6.3请求方法

@PostMapping("/add")
public Result addAnnotation(@Validated @RequestBody AnnotationQuery query){
    return Result.buildSuccess(query);
}

6.4测试结果

模拟数据
{
    "isShow":-1
}

测试结果
{
    "success": false,
    "code": 10003,
    "msg": "数值只能是0或者1",
    "data": {
        "isShow": "数值只能是0或者1"
    }
}

七、补充@PathVariable注解校验

7.1@PathVariable作用

用来对指定请求的URL路径里面的变量,比如@GetMapping("/get/{id}"),其中{id}就是这个请求的变量,可以通过@PathVariable来获取。

和@RequestParam的区别是,@RequestParam用来获得静态的URL请求入参

7.2使用正则表达式校验路径变量

下面正则表达式表示id值只能是数字,如果不是数字报出404路径找不到的异常

@GetMapping("/get/{id:\\d+}")
public Result getId(@PathVariable(name="id") String userId){
    return Result.buildSuccess(userId);
}

7.3自定义类实现ErrorController接口

说下为什么要这么做?

@ControllerAdive注解只能处理进入控制器方法抛出的异常,ErrorController接口可以处理全局异常,而404路径找不到异常不是控制器方法抛出的,此时还没有进入控制器方法。ErrorController处理404异常时会跳转到/error路径,此时会返回错误的html页。为了让返回结果统一,重写下ErrorController接口的getErrorPath方法

@RestController
public class MyErrorController extends BasicErrorController {

    @Autowired
    public MyErrorController(ErrorAttributes errorAttributes,
                             ServerProperties serverProperties,
                             List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, serverProperties.getError(), errorViewResolvers);
    }

    //处理html请求
    @Override
    public ModelAndView errorHtml(HttpServletRequest request,
                                  HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML));
        ModelAndView modelAndView = new ModelAndView("myErrorPage", model, status);
        return modelAndView;
    }

    //处理json请求
    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,
                isIncludeStackTrace(request, MediaType.ALL));

        Map<String,Object> resultBody=new HashMap<>(16);
        resultBody.put("success",false);
        resultBody.put("code",body.get("status"));
        resultBody.put("msg",body.get("error"));

        return new ResponseEntity<>(resultBody, HttpStatus.OK);
    }
}

7.4测试结果

实例:
http://localhost:8082/get/aa

结果:
{
    "success": false,
    "code": 404,
    "msg": "请求接口不存在请检查路径",
}

八、导图总结

通过以上叙述基本包含了常用开发用到的校验注解,下面用导图总结下

实体java 实体303_jsr303校验_15