参数校验Jakarta Bean Validation学习

不断学习更新中…
同教程代码

  • 码云

1.背景

我们在平时的学习与工作中,都需要对参数进行校验,比如在注册时,用户名密码不能为空,用户名长度必须小于10等等。虽然有些校验在前端页面会进行验证,但是后端为了增加健壮性也需要对这些参数进行判断(比如绕过前端页面而直接调用了接口,参数的合法性未知),可能就会在controller或者service中就会有如下代码的出现

package com.beemo.validation.controller;

import com.beemo.validation.demo1.entity.Student;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.Objects;

@RestController
public class DemoController {

    @RequestMapping("/demo")
    public String saveDemo(@RequestBody Student student) {
        if (StringUtils.isEmpty(student.getName())) {
            return "学生名称不能为空";
        }
        if (student.getName().length() > 10) {
            return "学生名称长度不能超过10位";
        }
        if (Objects.isNull(student.getAge())) {
            return "学生年龄不能为空";
        }
        if (student.getAge() <= 0) {
            return "学生年龄不能为负数";
        }
        if (Objects.isNull(student.getNumber())) {
            return "学号不能为空";
        }
        if (student.getNumber().length() != 10) {
            return "学号长度必须为10";
        }
        // 其他判断
        // 调用service的方法等
        return "ok";
    }

    @Data
    class Student {
		/**
	     * 姓名
	     */
	    private String name;
	
	    /**
	     * 年龄
	     */
	    private Integer age;
	
	    /**
	     * 学号
	     */
	    private String number;
    }
}

从例子中可以看到,这仅仅是一个实体类3个字段的简单验证,就已经占据了很多的篇幅,也需要我们进行手动编写这种判断代码,比较费时,代码读起来也没什么营养,大部分都是在判断合法性,等我们真正读到想要的业务逻辑代码可能需要往下翻好久,那么有没有办法能够让我们更简洁更优雅的去验证这些参数呢

2. Jakarta Bean Validation

2.1 Jakarta Bean Validation简介

首先要知道Jakarta就是Java更名之后的名称,Jakarta Bean Validation也就是Java Bean Validation,是一套Java的规范,它可以

  • 通过使用注解的方式在对象模型上表达约束
  • 以扩展的方式编写自定义约束
  • 提供了用于验证对象和对象图的API
  • 提供了用于验证方法和构造方法的参数和返回值的API
  • 报告违反约定的集合
  • 运行在Java SE,并且集成在Jakarta EE8中
    例如:
 public class User {

    private String email;

    @NotNull @Email
    public String getEmail() {
      return email;
    }

    public void setEmail(String email) {
      this.email = email;
    }
}

public class UserService {
	public void createUser(@Email String email, @NotNull String name) {
	    ...
    }
}

虽然可以手动运行校验,但更加自然的做法是让其他规则和框架在适时对数据进行校验(用户在表示框架中进行输入,业务服务通过CDI执行,实体通过JPA插入或者更新)
换句话说,即运行一次,到处约束

2.2 相关网址

  • 首页
  • 2.0首页
  • 2.0官方规范学习文档

在2020年2月份已经发布了3.0.0-M1版本
其中Jakarta Bean Validation只是一套标准,我们需要使用其他组织机构提供的实现来进行验证,官方支持的为Hibernate Validator

3.动手实践

3.1 所需环境

这里JDK使用了JDK1.8,使用maven进行所需jar文件依赖,使用springboot搭建框架脚手架,使用lombok简化代码
如果用的不是这几个可以适当修改,大同小异,而且springboot以及或其他依赖的版本每天都在变化,各个版本之间难免有或多或少的差别,可能细节处与本文章有所不同,需要大家知晓,并且根据自己的版本进行调整(比如spring-boot-starter-parent版本2.2.7与2.3.0在验证异常时返回json格式与内容就有很大不同)

3.2 搭建空框架

  • 使用spring initializr创建springboot项目,依次选择添加webvalidation以及lombok模块,生成的pom.xml依赖如下。我这里spring-boot-starter-parent的版本为2.3.0,再添加其他所需的pom依赖
...

<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.0.RELEASEversion>
...

<dependency>
 	<groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-validationartifactId>
dependency>
<!- lombok -->
<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
    <version>1.18.12version>
dependency>

<dependency>
    <groupId>com.google.guavagroupId>
    <artifactId>guavaartifactId>
    <version>29.0-jreversion>
dependency>

3.3 编写代码

编写背景:模拟英雄联盟游戏的技能与英雄的保存

这里的命名遵循外服名称而不是国服直译,例如英雄为champion而不是hero,技能为ability而不是skill

3.3.1 实体类

  • 英雄
package com.beemo.validation.demo2.entity;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * 英雄entity
 */
@Data
public class Champion {

    /**
     * 英雄名称
     */
    @NotBlank(message = "英雄名称不能为空")
    private String name;

    /**
     * 英雄头衔
     */
    @NotBlank(message = "英雄头衔不能为空")
    private String title;

    /**
     * 英雄描述
     */
    @NotBlank(message = "英雄描述不能为空")
    private String description;

    /**
     * 英雄类型
     * 坦克、刺客、射手、法师、辅助以及战士
     */
    @NotNull(message = "英雄类型不能为空")
    private Byte type;
}
  • 技能entity
package com.beemo.validation.demo2.entity;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * 技能
 */
@Data
public class Ability {

    /**
     * 技能名称
     */
    @NotBlank(message = "技能名称不能为空")
    private String name;

    /**
     * 技能描述
     */
    @NotBlank(message = "技能描述不能为空")
    private String description;

    /**
     * 技能类型
     * 例如魔法值、怒气、能量等
     */
    @NotNull(message = "技能类型不能为空")
    private Byte type;
}

3.3.2 控制层

  • 英雄controller
package com.beemo.validation.demo2.controller;

import com.beemo.validation.demo2.entity.Champion;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("/demo2/champion")
@Validated
public class ChampionController {

    /**
     * 保存
     * @param entity 要保存的英雄实体
     * @return 保存结果
     */
    @PostMapping("save")
    public String save(@Valid @RequestBody Champion entity) {
        // 调用service等
        return "ok";
    }
}
  • 技能controller
package com.beemo.validation.demo2.controller;

import com.beemo.validation.demo2.entity.Ability;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;


@RestController
@RequestMapping("/demo2/ability")
@Validated
public class AbilityController {

    /**
     * 保存
     * @param entity 要保存的技能实体
     * @return 保存结果
     */
    @PostMapping("save")
    public String save(@Valid @RequestBody Ability entity) {
        // 调用service等
        return "ok";
    }
}

3.3.3 测试

使用postman或其他工具发送POST请求,进行验证,我们直接输入我们参数直接传一个内容为空的json,查看结果
参数校验Jakarta Bean Validation学习_第1张图片
可以看到,这里返回了400异常,意为参数错误
我们再把所有参数补全,再试一下
参数校验Jakarta Bean Validation学习_第2张图片
可以看到,如果我们把参数补全之后,返回的是“ok”,即进入controller执行该方法。
那么,例子中添加的几个注解都是什么意思,有什么作用,而且注解中写的message信息在验证后并没有输出,那么我们怎么样输出这些message呢

4. 注解含义

4.1 开启验证

首先我们看controller类最上方,我们标注了@Validataed,该注解的含义是:这个类要启用参数校验。在save方法的参数中标注了@Valid,含义为我们要对紧跟的实体进行校验,而具体校验的内容,为实体类中的我们的定义的约束
Ability类举例,在name字段上方标记了@NotBlank,意为定义了该字段不允许为空的约束,如果name为空,校验就不通过,就会返回我们之前碰到的400异常。而type字段也标注了@NotNull,也定义了该字段不允许为空的约束,具体的区别以及其他内置的约束如3.5所示

4.2 内置约束

内置约束位于javax.validation.constraints保内,列表如下

4.2.1 @Null

  • 被标注元素必须为null
  • 接收任意类型

比如在创建一个英雄时,ID需要由数据库自增生成,而不是我们自定义,那么该我们在接收前台传递的json时就必须为空

4.2.2 @NotNull

  • 被标注元素必须不为null
  • 接收任意类型

定义一个字段不能为空,例如技能类型或者英雄名称

4.2.3 @AssertTrue

  • 被标注元素必须true
  • 支持的类型为boolean以及Boolean
  • null被认为是有效的

要么为null,否则必须为true

4.2.4 @AssertFalse

  • 被标注元素必须false
  • 支持的类型为boolean以及Boolean
  • null被认为是有效的

要么为null,否则必须为false

4.2.5 @Min

  • 被标注元素必须为是一个数字,其值必须大于等于指定的最小值
  • 支持的类型为BigDecimalBigIntegerbyteshortintlong 以及各自的包装类
  • 注意double以及float由于舍入错误而不被支持
  • null被认为是有效的

4.2.6 @Max

  • 被标注元素必须为是一个数字,其值必须小于等于指定的最大值
  • 支持的类型为BigDecimalBigIntegerbyteshortintlong 以及各自的包装类
  • 注意double以及float由于舍入错误而不被支持
  • null被认为是有效的

4.2.7 @DecimalMin

  • 被标注元素必须为是一个数字,其值必须大于等于指定的最小值
  • 支持的类型为BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的包装类
  • 注意double以及float由于舍入错误而不被支持
  • null被认为是有效的

4.2.8 @DecimalMax

  • 被标注元素必须为是一个数字,其值必须小于等于指定的最大值
  • 支持的类型为BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的包装类
  • 注意double以及float由于舍入错误而不被支持
  • null被认为是有效的

4.2.9 @Negative

  • 被标注元素必须为是一个严格意义上的负数(即0被认为是无效的)
  • 支持的类型为BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包装类
  • null被认为是有效的

4.2.10 @NegativeOrZero

  • 被标注元素必须为是负数或者0
  • 支持的类型为BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包装类
  • null被认为是有效的

4.2.11 @Positive

  • 被标注元素必须为是一个严格意义上的正数(即0被认为是无效的)
  • 支持的类型为BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包装类
  • null被认为是有效的

4.2.12 @Positive OrZero

  • 被标注元素必须为是正数或者0
  • 支持的类型为BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包装类
  • null被认为是有效的

4.2.13 @Size

  • 被标注元素的大小必须在指定的边界区间
  • 支持的类型为CharSequence(计算字符序列的长度) 、Collection(计算集合的大小)、Map(计算map的大小) 、Array(计算数组的长度)
  • null被认为是有效的

4.2.14 @Digits

  • 被标注元素必须是在可接受范围内的数字
  • 支持的类型为BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的包装类
  • null被认为是有效的

4.2.15 @Past

  • 被标注元素必须是过去的某个时刻、日期或者时间
  • “现在”的概念是附加在Validator或者ValidatorFactory中的ClockProvider定义的,默认的ClockProvider根据虚拟机定义了当前时间,如果需要的话,会应用当前默认时区
  • 支持的类型为java.util.Datejava.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTime}java.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate 以及各自的包装类
  • null被认为是有效的

4.2.16 @PastOrPresent

  • 被标注元素必须是过去或现在的某个时刻、日期或者时间
  • “现在”的概念是附加在Validator或者ValidatorFactory中的ClockProvider定义的,默认的ClockProvider根据虚拟机定义了当前时间,如果需要的话,会应用当前默认时区
  • “现在”的概念相对的定义在使用的约束上,例如,如果约束在Year上,那么现在表示当前年份
  • 支持的类型为java.util.Datejava.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTime}java.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate 以及各自的包装类
  • null被认为是有效的

4.2.17 @Future

  • 被标注元素必须是未来的某个时刻、日期或者时间
  • “现在”的概念是附加在Validator或者ValidatorFactory中的ClockProvider定义的,默认的ClockProvider根据虚拟机定义了当前时间,如果需要的话,会应用当前默认时区
  • 支持的类型为java.util.Datejava.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTime}java.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate 以及各自的包装类
  • null被认为是有效的

4.2.18 @FutureOrPresent

  • 被标注元素必须是未来或现在的某个时刻、日期或者时间
  • “现在”的概念是附加在Validator或者ValidatorFactory中的ClockProvider定义的,默认的ClockProvider根据虚拟机定义了当前时间,如果需要的话,会应用当前默认时区
  • “现在”的概念相对的定义在使用的约束上,例如,如果约束在Year上,那么现在表示当前年份
  • 支持的类型为java.util.Datejava.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTime}java.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate 以及各自的包装类
  • null被认为是有效的

4.2.19 @Pattern

  • 被标注的CharSequence必须匹配指定的正则表达式,该正则表达式遵循Java的正则表达式规定
  • 支持的类型为CharSequence
  • null被认为是有效的

4.2.20 @NotEmpty

  • 被标注元素必须不为null或者空(以字符串举例,不为null并且不为“”)
  • 支持的类型为CharSequence(计算字符序列的长度) 、Collection(计算集合的大小)、Map(计算map的大小) 、Array(计算数组的长度)

4.2.21 @NotBlank

  • 被标注元素必须不为null,并且必须包含至少一个非空格的字符
  • 支持的类型为CharSequence

4.2.22 @Email

  • 字符串必须是格式良好的电子邮件地址
  • 支持的类型为CharSequence

5. 异常模块

还有一个问题,就是我们定义的message没有生效,比如“技能名称不能为空”,并没有出现在返回结果中,取而代之的是400异常,那么怎样才能返回我们想要的message呢
首先我们在controller当中定一个一个方法,用@ExceptionHandler注解标注一下,用来获取controller抛出的异常,然后我们跟踪一下断点,看一下到底是什么异常

package com.beemo.validation.demo2.controller;

import com.beemo.validation.demo2.entity.Ability;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.ConstraintViolationException;
import javax.validation.Valid;


@RestController
@RequestMapping("/demo2/ability")
@Validated
public class AbilityController {

    /**
     * 保存
     * @param entity 要保存的技能实体
     * @return 保存结果
     */
    @PostMapping("save")
    public String save(@Valid @RequestBody Ability entity) {
        // 调用service等
        return "ok";
    }

    @ExceptionHandler
    public void handleException(Exception e) {
        e.printStackTrace();
    }
}

参数校验Jakarta Bean Validation学习_第3张图片抛出的是org.springframework.web.bind.MethodArgumentNotValidException
在看一下DEBUG窗口中的每个参数,发现bindingResult->errors->field和defaultMessage,一个违反约束的字段名称,另一个是违我们自定义的message
参数校验Jakarta Bean Validation学习_第4张图片
此时我们就可以进行处理,返回我们想要的结果,而不是抛出400

5.1 优化返回值

在实际开发中,一般不会返回一个“ok”或者“success”这种字符串,通常情况下会返回一个json字符串,其中包含

  • 一个表示结果的状态值,例如HTML状态码或自定义状态值
  • 一个返回消息,解释该状态值或结果
  • 承载数据
package com.beemo.demo2.common;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Map;

@AllArgsConstructor
@NoArgsConstructor
public class R {

    private int code;
    private String msg;
    private Object data;

    public static R success() {
        return success(null);
    }

    public static R success(Object data) {
        return new R(1, "操作成功", data);
    }

    public static R violateConstraint(List<Map<String, String>> violation) {
        return new R(2, "参数校验未通过", violation);
    }
}

修改controller

package com.beemo.demo2.controller;

import com.beemo.demo2.common.R;
import com.beemo.demo2.entity.Ability;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;


@RestController
@RequestMapping("/demo2/ability")
@Validated
public class AbilityController {

    /**
     * 保存
     * @param entity 要保存的技能实体
     * @return 保存结果
     */
    @PostMapping("save")
    public R save(@Valid @RequestBody Ability entity) {
        // 调用service等
        return R.success();
    }
}

将异常处理方法提出,标注@ControllerAdvice注解,使得每个controller的异常都可以用该方法处理,并修改返回值,并且如果是单独提出来一个模块,需要在启引用该模块的启动类上加扫描

package com.beemo.common.config;

import com.beemo.common.common.R;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;
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 javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.stream.Collectors;

@ControllerAdvice
@ResponseBody
public class MyExceptionHandler {

    @ExceptionHandler
    public R handleException(MethodArgumentNotValidException e) {
        List<String> violations = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).
                collect(Collectors.toList());
        return R.violateConstraint(violations);
    }
}


package com.beemo.demo2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.beemo.*")
public class Demo2Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo2Application.class, args);
    }

}

然后我们再测试一下
参数校验Jakarta Bean Validation学习_第5张图片
发现得到的结果再也不是400异常,而是我们指定的message集合了

6. 验证非前台传递的参数

除了在controller验证前台传递的参数之外,有时我们还需要验证诸如自己new的对象,或者从其他方法查询出来的对象,这时候我们可能需要把这些操作放在service层或其他层

6.1 调用非本类的校验方法

例如我们自己new了一个对象,然后调用其他类的一个验证方法
建立一个service接口以及一个实现类
我们在实现类上,模拟controller校验,加上@Validated以及@Valid注解

package com.beemo.demo3.service;

import com.beemo.demo3.entity.Ability;

/**
 * 技能service接口
 */
public interface IAbilityService {

    /**
     * 保存
     * @param ability
     */
    void saveOne(Ability ability);
}

package com.beemo.demo3.service.impl;

import com.beemo.demo3.entity.Ability;
import com.beemo.demo3.service.IAbilityService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;

/**
 * 技能service实现类
 */
@Validated
@Service
public class AbilityServiceImpl implements IAbilityService {

    @Override
    public void saveOne(@Valid @NotNull Ability ability) {
        System.out.println("通过校验");
        // 进行保存操作等...
    }

然后在controller中调用该方法

package com.beemo.demo3.controller;

import com.beemo.demo3.entity.Ability;
import com.beemo.demo3.service.IAbilityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/demo3/ability")
@Validated
public class AbilityController {

    @Autowired
    private IAbilityService abilityService;

    /**
     * 保存
     * @return 保存结果
     */
    @PostMapping("save")
    public String save() {
        // new
        Ability ability = new Ability();
        abilityService.saveOne(ability);
        return "ok";
    }
}

我们进行测试发现,并没有我们符合想象的返回R,相反在后台控制台报了一个异常

javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method AbilityServiceImpl#saveOne(Ability) redefines the configuration of IAbilityService#saveOne(Ability).
	at org.hibernate.validator.internal.metadata.aggregated.rule.OverridingMethodMustNotAlterParameterConstraints.apply(OverridingMethodMustNotAlterParameterConstraints.java:24) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.assertCorrectnessOfConfiguration(ExecutableMetaData.java:462) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:380) ~[
	......

一个重写的方法禁止重新定义参数的约束配置,但是方法AbilityServiceImpl#saveOne(Ability) 重新定义了 IAbilityService#saveOne(Ability)的配置

翻译过来就是
如果你的接口没有定义约束,那么你的实现类就不能够定义该约束

按照异常信息,我们试着将验证放在接口中在尝试一下

package com.beemo.demo3.service;

import com.beemo.demo3.entity.Ability;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

@Validated
/**
 * 技能service接口
 */
public interface IAbilityService {

    /**
     * 保存
     * @param ability
     */
    void saveOne(@Valid @NotNull Ability ability);
}

测试之后发现返回结果为500异常,这次控制器打印异常信息明显跟上次不一样,貌似确实是通过校验了,只不过抛出的异常不一样

javax.validation.ConstraintViolationException: saveOne.ability: 不能为null
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	......

我们发现在service层中如果违法约束抛出的异常为ConstraintViolationException,而并非在controller中的MethodArgumentNotValidException
我们再次改进异常处理方法,然后跟踪一下异常的信息
参数校验Jakarta Bean Validation学习_第6张图片
根据调试的信息,我们就可以处理我们的返回值了

    @ExceptionHandler
    public R handleException2(ConstraintViolationException e) {
        List<String> violations = e.getConstraintViolations().stream()
        		.map(ConstraintViolation::getMessageTemplate).collect(Collectors.toList());
        return R.violateConstraint(violations);
    }

再测试一下
参数校验Jakarta Bean Validation学习_第7张图片
测试成功

6.2 调用本类的校验方法

场景:我们需要从EXCEL中读取数据,然后保存数据库中,需要判断每一条记录,如果正确就进行保存,如果失败则打印日志,接口和实现类如下

package com.beemo.demo3.service;

import com.beemo.demo3.entity.Ability;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

@Validated
/**
 * 技能service接口
 */
public interface IAbilityService {

    /**
     * 保存
     * @param ability
     */
    void saveOne(@Valid @NotNull Ability ability);

    /**
     * 批量保存EXCEL中的数据
     */
    void saveOnesFromExcel();
}
package com.beemo.demo3.service.impl;

import com.beemo.demo3.entity.Ability;
import com.beemo.demo3.service.IAbilityService;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.validation.ConstraintViolationException;
import java.util.List;

/**
 * 技能service实现类
 */
@Service
@Slf4j
public class AbilityServiceImpl implements IAbilityService {

    @Override
    public void saveOne(Ability ability) {
        System.out.println("通过校验");
        // 进行保存操作等...
    }

    /**
     * 批量保存EXCEL中的数据
     */
    @Override
    public void saveOnesFromExcel() {
        List<Ability> data = readFromExcel();
        for (int i = 0, size = data.size(); i < size; i ++) {
            try {
                saveOne(data.get(i));
                System.out.println("第" + i + "条记录保存成功");
            } catch (ConstraintViolationException e) {
                log.error("第" + i + "条记录违法约束:" + e.getMessage());
            } catch (Exception e) {
                log.error("第" + i + "条记录保存失败");
            }
        }
    }

    /**
     * 从EXCEL中读取
     * @return
     */
    private List<Ability> readFromExcel() {
        return Lists.newArrayList(new Ability(null, null, (byte)1),
                new Ability(null, "测试描述", null),
                new Ability("测试名称", null, null),
                new Ability("约德尔诱捕器", "布置一个陷阱,陷阱可以束缚敌方英雄2秒并将目标暴露在己方视野内3秒。", (byte)1));
    }
}

我们模拟了一个从EXCEL中读取list的方法,然后调用了save方法,该方法有参数验证,我们来进行测试
参数校验Jakarta Bean Validation学习_第8张图片
控制台打印成功,证明我们的约束并没有成功,但是我们的写法看似没问题
其实这个原因就是因为第一个方法saveFromExcel并没有标注验证,不论该方法怎么调用本类的验证方法都不会生效,此问题原因同@Transactional以及@Aysnc标注的方法,其本质原因是因为代理的问题,这里不做过多探讨,解决该问题的方法有三种

  1. (不推荐)将验证方法移到其他类中 。这种方法奏效,但是无缘无故需要多建立一个service,有时候可能就是一个空方法,只不过参数有验证,其他不知道的小伙伴看到可能会比较懵
  2. 注入ApplicationContext获取bean
    @Autowired
    private ApplicationContext applicationContext;
	
	 /**
     * 批量保存EXCEL中的数据
     */
    @Override
    public void saveOnesFromExcel() {
        List<Ability> data = readFromExcel();
        for (int i = 0, size = data.size(); i < size; i ++) {
            try {
                applicationContext.getBean(IAbilityService.class).saveOne(data.get(i));
                System.out.println("第" + i + "条记录保存成功");
            } catch (ConstraintViolationException e) {
                log.error("第" + i + "条记录违法约束:" + e.getMessage());
            } catch (Exception e) {
                log.error("第" + i + "条记录保存失败");
            }
        }
    }

在这里插入图片描述
3. 通过注入自己来获取当前类的实例,再调用该实例的方法。需要加@Lazy注解防止自我注入时spring抛出org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.env.Environment' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}异常

@Autowired
@Lazy
private IAbilityService abilityService;
/**
  * 批量保存EXCEL中的数据
  */
@Override
public void saveOnesFromExcel() {
    List<Ability> data = readFromExcel();
    for (int i = 0, size = data.size(); i < size; i ++) {
        try {
            abilityService.saveOne(data.get(i));
            System.out.println("第" + i + "条记录保存成功");
        } catch (ConstraintViolationException e) {
            log.error("第" + i + "条记录违法约束:" + e.getMessage());
        } catch (Exception e) {
            log.error("第" + i + "条记录保存失败");
        }
    }
}

6.3 关于@Validated的位置

我们已经清楚,约束配置的注解,例如@Valid@NotNull等,需要在接口上进行配置,那么@Validated需要标注在哪里呢,答案是接口和实现类都可以,但是标注位置不同,也有一些区别

  1. 标注在接口:意为实现类都回开启验证
  2. 标注在实现类:意为标注该注解的实现类才会开启验证,如果有一个实现类未标注@Validated,那么即使接口有约束配置,也不会在该实现类上进行校验

6.4 关于实现类需要不需要标注约束配置

个人感觉有优点优缺点
优点:一般看代码的时候,都不会看接口,而是直接看实现类。如果标注在实现类上,可以更直观的看到该方法的约束配置
缺点:必须与接口完全对应,如果接口修改约束配置,那么实现类必须相应的进行修改,否则会抛出异常

你可能感兴趣的:(java)