目录
2.2.3 Hibernate Validator 扩展注解
1. 校验需求
对前端传来入参做数据合法性检查。
2. 校验方法
2.1 拼接式校验
2.1.1 定义
在业务成对request中的参数一个个做非空判断。
2.1.2 举例
Stu.java
@Data
@EqualsAndHashCode(callSuper = false)
public class Stu implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id",type = IdType.AUTO)
private int id;
private String name;
private int age;
private String gender;
private String address;
private Date birth;
}
StuServiceImpl.java
@Service
public class StuServiceImpl extends ServiceImpl<StuMapper, Stu> implements StuServiceI {
@Override
public Result add(Stu stu) {
if(stu.getName()==null){
return Result.fail("学生名不能为空");
}
if(stu.getAddress()==null){
return Result.fail("学生地址不能为空");
}
this.save(stu);
return Result.success();
}
}
2.1.3 缺点
如果实体类字段很多,不易维护,给业务层增加负担。
2.2 声明式校验 JSR303
2.2.1 定义
声明式校验,也称JSR303校验。
JRS303是Java为Bean数据合法性校验提供的标准框架,JSR303通过在Bean属性上标注类似于@NotNull、@NotBlank实现数据合法性校验。
2.2.2 相关注解
注解 | 功能 |
---|---|
@Null | 验证对象是否为 null |
@NotNull | 验证对象是否不为 null |
@AssertTrue | 验证 Boolean 对象是否为 true |
@AssertTrue | 验证 Boolean 对象是否为 false |
@Max(value) | 验证 Number 和 String 对象是否小于等于指定值 |
@Min(value) | 验证 Number 和 String 对象是否大于等于指定值 |
@DecimalMax(value) | 验证注解的元素值小于等于 @DecimalMax 指定的 value 值 |
@DecimalMin(value) | 验证注解的元素值大于等于 @DecimalMin 指定的 value 值 |
@Digits(integer,fraction) | 验证字符串是否符合指定格式的数字,integer 指定整数精度,fraction 指定小数精度 |
@Size(min,max) | 验证对象长度是否在给定的范围内 |
@Past | 验证 Date 和 Calendar 对象是否在当前时间之前 |
@Future | 验证 Date 和 Calendar 对象是否在当前时间之后 |
@Pattern | 验证 String 对象是否符合正则表达式的规则 |
@NotBlank | 检查字符串是不是 Null,被 Trim 的长度是否大于0,只对字符串,且会去掉前后空格 |
@URL | 验证是否是合法的 url |
@CreditCardNumber | 验证是否是合法的信用卡号 |
2.2.3 Hibernate Validator 扩展注解
注解 | 功能 |
---|---|
验证是否是合法的邮箱 | |
@Length(min,max) | 验证字符串的长度必须在指定范围内 |
@NotEmpty | 检查元素是否为 Null 或 Empty |
@Range(min,max,message) | 验证属性值必须在合适的范围内 |
2.2.4 基本使用
2.2.4.1 pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
注意:validation 启动依赖里 包含jakarta:el:3.0.4.jar 和hibernate-validator:6.1.7.Final.jar
2.2.4.2 实体类Stu.java
@Data
@EqualsAndHashCode(callSuper = false)
public class Stu implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id",type = IdType.AUTO)
private int id;
@NotBlank(message = "姓名不能为空")
@Length(max = 20, message = "用户名称的长度不能超过20")
private String name;
private int age;
private String gender;
@NotBlank(message = "性别不能为空")
private String address;
private Date birth;
}
2.2.4.3 控制层StuController.java
使用@Valid注解。
如果想进行分组校验可使用@Validated注解,这里不做演示。
@RestController
@RequestMapping("/stu")
public class StuController {
@Resource
private StuServiceI stuService;
@PostMapping
public Result add(@Valid @RequestBody Stu stu){
stuService.save(stu);
return Result.success("新增成功");
}
}
2.2.4.4 全局异常处理ExceptionCatch.java
数据合法校验的异常是MethodArgumentNotValidException,多个数据合法校验异常,只提示用户第一个。
@RestControllerAdvice
public class ExceptionCatch {
private static ImmutableMap.Builder<Class<? extends Exception>,ExceptionResult> builder = ImmutableMap.builder();
/**
* 自定义异常处理
* @param e
* @return
*/
@ExceptionHandler(CustomException.class)
public ResponseResult handlerRuntimeException(CustomException e){
ExceptionResult result = e.getExceptionResult();
return ResponseResult.fail(result.retCode(),result.retMsg());
}
@ExceptionHandler(value= MethodArgumentNotValidException.class)
public ResponseResult validateException(MethodArgumentNotValidException e){
BindingResult bindingResult=e.getBindingResult();
Map<String,Object> map=new HashMap<>();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
FieldError fieldError = fieldErrors.get(0);
map.put("field",fieldError.getField());
map.put("msg",fieldError.getDefaultMessage());
return ResponseResult.fail("数据校验异常",map);
}
/**
* 其他异常处理
* 除运行时异常以外的异常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public ResponseResult handlerException(Exception e){
ExceptionResult result = builder.build().get(e.getClass());
if(ObjectUtil.isNotNull(result)){
return ResponseResult.fail(result.retCode(),result.retMsg());
}
return ResponseResult.fail(e.getMessage());
}
}
2.2.4.5 测试
2.2.5 分组校验(多场景的复杂校验)和自定义校验
项目中一般使用分组校验或自定义校验,这里更换实体类和优化统一异常处理。
1. 分组校验(多场景的复杂校验)
新增、修改等接口想要校验的入参不同,可以通过分组进行校验。
(1)引入相关依赖 pom.xml
<!-- 数据校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
(2)返回前端数据的相关类
/**
* 返回数据
*
* @author wen
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
@Override
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
(3) 统一处理的全局异常、异常消息枚举类
@Slf4j
@RestControllerAdvice(basePackages = "com.wen.product.controller")
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public R handlerValidException(MethodArgumentNotValidException e){
log.error("数据校验异常:{},异常类型:{}",e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError)->{
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(), BizCodeEnum.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
}
@ExceptionHandler(Throwable.class)
public R handlerException(Throwable throwable){
return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMsg());
}
}
public enum BizCodeEnum {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验异常");
private int code;
private String msg;
BizCodeEnum(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
(4) 定义分组接口
这边定义新增校验分组接口AddGroup、修改校验分组接口UpdateGroup、修改状态分组接口UpdateStatusGroup。
/**
* 新增数据 Group
*
* @author wen
*/
public interface AddGroup {
}
/**
* 更新数据 Group
*
* @author wen
*/
public interface UpdateGroup {
}
/**
* @author wen
* @createDate 2023/3/24 15:04
* @description 修改状态 Group
*/
public interface UpdateStatusGroup {
}
(5)实体类 BrandEntity
观察JSR303校验注解的groups属性的值就是不同分组想要校验的实体变量。
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@Null(message = "新增品牌id必须为空",groups = {AddGroup.class})
@NotNull(message = "修改品牌id不能为空",groups = {UpdateGroup.class, UpdateStatusGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(message = "品牌logo地址不能为空",groups = {AddGroup.class})
@URL(message = "品牌logo地址必须是一个合法的URL",groups = {AddGroup.class,UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "显示状态不能为空", groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(message = "检索首字母不能为空",groups = {AddGroup.class})
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序不能为空",groups = {AddGroup.class})
@Min(value = 0, message = "排序必须大于等于0的整数",groups = {AddGroup.class,UpdateGroup.class})
private Integer sort;
}
(6)控制层使用@Validated注解开启分组校验
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
brandService.updateDetail(brand);
return R.ok();
}
/**
* 修改状态
*/
@RequestMapping("/update/status")
public R updateStatus(@Validated(UpdateStatusGroup.class) @RequestBody BrandEntity brand){
brandService.updateById(brand);
//brandService.updateBrand(brand);
return R.ok();
}
(7)测试
2. 自定义校验
(1)引入依赖 pom.xml
在基础校验和分组校验的基础上添加validation-api依赖,该依赖中包含自定义校验所需的校验器等。
<!-- 数据校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 自定义JSR303校验 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
(2)和(3)同分组校验的(2)和(3)
(4)自定义校验注解(默认消息提示)、自定义校验器
1)编写一个自定义的校验注解 默认message定义在src/main/resources/ValidationMessages.properties文件中 2)编写一个自定义的校验器 3)关联自定义的校验注解和自定义的校验器,使用@Constraint注解
注意:一个自定义注解可以指定多个不同的校验器,适配不同的校验。
/**
* @author wen
* @createDate 2023/3/24 14:17
* @description 自定义校验注解
*/
@Documented
@Constraint(
validatedBy = {ListValueConstraintValidator.class}
) // 指定校验器
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
String message() default "{com.wen.common.validator.anno.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] vals() default {};
}
ValidationMessages.properties
com.wen.common.validator.anno.ListValue.message=必须提交指定的值
/**
* @author WangWenwen
* @createDate 2023/3/24 14:20
* @description 自定义ListValue校验器
* ConstraintValidator<A,T>:A-指定注解;T-指定什么类型的数据
*
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
if(vals!=null && vals.length>0){
for (int val : vals) {
set.add(val);
}
}
}
/**
*
* @param integer 需要校验的值
* @param constraintValidatorContext
* @return
*/
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(integer);
}
}
(5)实体类 BrandEntity
实体类中的showStatus变量使用@ListValue注解。
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@Null(message = "新增品牌id必须为空",groups = {AddGroup.class})
@NotNull(message = "修改品牌id不能为空",groups = {UpdateGroup.class, UpdateStatusGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(message = "品牌logo地址不能为空",groups = {AddGroup.class})
@URL(message = "品牌logo地址必须是一个合法的URL",groups = {AddGroup.class,UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "显示状态不能为空", groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals = {0,1}, groups = {AddGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(message = "检索首字母不能为空",groups = {AddGroup.class})
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序不能为空",groups = {AddGroup.class})
@Min(value = 0, message = "排序必须大于等于0的整数",groups = {AddGroup.class,UpdateGroup.class})
private Integer sort;
}
(6)控制层在分组的基础上
@ListValue指定的分组是新增。
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
}
(7)测试