背景
我们在进行接口开发的时候经常都会对参数进行校验,比如非空,数据长度,email等等的校验。有一个专门的参数校验工具包javax.validation用于校验我们的参数形式。看一下这个包下的注解:
发现提供的注解还是有很多的,当然有些不符合我们业务的场景我们也是可以自定义校验格式来完成的。自定义校验格式我们最后再说,我们先一点点看看怎么使用吧。
`
一、使用方式
我的环境是springboot项目,已经集成了javax.validation 引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.7.2</version>
</dependency>
假如我们要对参数name进行非空验证,如下:
准备一个参数实体类:
@Data
public class TestDTO {
@NotNull
private String name;
}
准备一个测试接口:
@RestController
public class TestController {
@PostMapping("/test")
public ApiResponse test(@Validated @RequestBody TestDTO testDTO) {
return ApiResponse.buildSuccess();
}
}
然后启动项目访问测试接口,返回结果:
控制台:抛出MethodArgumentNotValidException异常
我们想要的返回结果是类似name不得为null,但是这个我们看不懂是什么错误。我们来看一下 @NotNull的源码:
二、使用步骤
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
// 默认消息:不得为null
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();
}
}
通过源码我们可以看到有一个默认的消息指向了{javax.validation.constraints.NotNull.message},我们全局搜索javax.validation.constraints.NotNull.message:
使用unicode转中文工具来转换一下,下边红框的意思就是:不得为null.
那么我们怎么才可以拿到这个消息呢?如果这个信息不符合业务要求呢,是不是可以换呢,答案是肯定的,接下来就来看一下:
实体类:
@Data
public class TestDTO {
@NotNull(message = "姓名不能为null") // 使用我们自定义的信息替换原有的信息
private String name;
}
拿到这个错误提示的办法有两个,第一个:在接口接收参数的后边紧跟一个BindingResult类型的参数,这样错误信息会自动放在这个参数里:
@Data
public class TestDTO {
@NotNull(message = "姓名不能为null")
private String name;
}
接口:
@PostMapping("/test")
public ApiResponse test(@Validated @RequestBody TestDTO testDTO, BindingResult result) {
// 判断是否有错误信息
if (result.hasErrors()) {
// 获取错误信息集合
List<FieldError> fieldErrors = result.getFieldErrors();
HashMap<Object, Object> map = new HashMap<>();
fieldErrors.forEach(i ->{
// 错误信息封装到map
map.put(i.getField(), i.getDefaultMessage());
});
return ApiResponse.buildFailure(400, "参数错误", map);
} else {
return ApiResponse.buildSuccess();
}
}
启动项目查看返回值:
到这里返回结果也是我们想要的。但是会有一个问题,就是我们在实际的开发中会有很多的接口,那是不是每一个接口都要判断是不是有错误啊,然后再进行业务处理?这不就有很多冗余代码了么?我们上边说过参数校验失败会抛出MethodArgumentNotValidException异常,我们可以写一个全局异常处理类来解决这个问题,只要抛了这个异常就会被这个全局异常处理类拦截进行处理,那我们写一个全局异常处理类:
package com.chunqiu.demo.config;
import com.chunqiu.demo.common.ApiResponse;
import com.chunqiu.demo.common.BizException;
import com.chunqiu.demo.common.BizExceptionEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
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.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.validation.ValidationException;
import java.nio.file.AccessDeniedException;
import java.util.HashMap;
import java.util.List;
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
// 请求参数未通过验证错误
// 方法参数校验错误
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ApiResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException", e);
BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
HashMap<Object, Object> map = new HashMap<>();
fieldErrors.forEach(i ->{
// 错误信息封装到map
map.put(i.getField(), i.getDefaultMessage());
});
return ApiResponse.buildFailure(400, "参数错误", map);
}
// 处理以上没有处理的异常
@ResponseBody
@ExceptionHandler(value = Exception.class)
public ApiResponse handleMethodArgumentTypeMismatchException(Exception e) {
log.error("Exception", e);
return ApiResponse.buildFailure("未知异常");
}
}
接口恢复:
@PostMapping("/test")
public ApiResponse test(@Validated @RequestBody TestDTO testDTO) {
return ApiResponse.buildSuccess();
}
再来测试一下:
也是可以控制住的。
接下来我们加一个年龄的校验,不能为负数:
@Data
public class TestDTO {
@NotNull(message = "姓名不能为null")
private String name;
@Min(message = "年龄不得小于1", value = 1)
private String age;
}
再来测试:
已经可以了。
三、分组校验
分组校验就是给某些参数设置一个组别,并在接口层声明是调用的哪几个组,那这几个组的参数校验才会生效,如果在参数实体类中设置了组别,但是在接口层没有传递任何组别,那么只会校验没有声明组别的参数。 我们来通过代码验证一下、首先创建两个接口,一个添加组,一个更新组:
public interface AddGroup {
}
public interface UpdateGroup {
}
实体类:addField在添加组,updateField在更新组,test没有任何组别
@Data
public class TestDTO {
@NotNull(message = "addField不能为null", groups = {AddGroup.class})
private String addField;
@NotNull(message = "updateField不能为null", groups = {UpdateGroup.class})
private String updateField;
@NotNull(message = "test不能为null")
private String test;
}
接口层:
@RestController
public class TestController {
@PostMapping("/add")
public ApiResponse add(@Validated(AddGroup.class) @RequestBody TestDTO testDTO) {
return ApiResponse.buildSuccess();
}
@PostMapping("/update")
public ApiResponse update(@Validated({UpdateGroup.class}) @RequestBody TestDTO testDTO) {
return ApiResponse.buildSuccess();
}
@PostMapping("/test")
public ApiResponse test(@Validated @RequestBody TestDTO testDTO) {
return ApiResponse.buildSuccess();
}
}
我们先访问test接口:
添加组和更新组没有生效
访问add接口
测试和更新组没有生效
访问更新接口:
测试和添加组没有生效
四、自定义校验规则
正则
我们可以使用@Patterns声明一个正则表达式规则,只有符合才会校验成功:
@Data
public class TestDTO {
@Pattern(regexp = "/^[a-zA-Z$]/", message = "必须字母开头")
private String test;
}
返回值:
自定义检验器
假如我们有一个属性sex性别,只能是1,2两个值,我们根据这个需求来完成自定义校验 第一步:自定义校验注解(参考已经写好的,比如NotBlank):
@Documented
@Constraint(validatedBy = { }) // 和检验器相关联
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface SexValue {
String message() default "{com.chunqiu.demo.SexValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default { };
}
resource目录下创建错误信息映射ValidationMessages.properties:
# 性别值不对
com.chunqiu.demo.SexValue.message=\u6027\u522b\u503c\u4e0d\u5bf9
编写自定义校验器:
public class SexValueConstraintValidator implements ConstraintValidator<SexValue, Integer> {
private Set<Integer> set = new HashSet<>();
@Override
public void initialize(SexValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int i = 0; i < vals.length; i++) {
set.add(i);
}
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
关联自定义检验器和自定义注解:
在刚才的自定义注解上@Constraint注解进行关联:
测试的实体类:
@Data
public class TestDTO {
@SexValue(vals = {1,2})
private Integer sex;
}
测试接口:
我们自定义的校验规则就结束了。