|
校验参数在项目中是很常见的,在执行业务逻辑层操作之前,都要验证参数的合法性。比如:入参否为空,数据格式、数据长度是否正确等等,一般的写法就是在执行业务逻辑之前写一大推的if-else去进行判断,既不美观也不优雅,更不便于后期维护,影响代码的可读性。
public void userRegister(UserRequestDTO userDTO){
if(Objects.isNull(userDTO)){
throw new ServiceException("参数为空");
}
if(userDTO.getNickName()==null){
throw new ServiceException("昵称不能为空");
}
if(userDTO.getAge()==null){
throw new ServiceException("年龄不能为空");
}
if(userDTO.getEmail()==null){
throw new ServiceException("邮箱不能为空");
}
//校验通过,执行业务逻辑
}这个时候JCP组织站出来了,并且制定了一个标准来规范校验的操作,这个标准就是Java Validation API(JSR 303)
2. 实现方式
Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length、@NotNull、@Max、@Min、@Size等。
Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。接下来我们主要介绍Spring Validation的使用。
3. 引入依赖
如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。如果spring-boot版本大于2.3.x,则需要手动引入依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>或
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency> 4.使用场景
4.1、需求
用户注册接口,名称,年龄,邮箱、不能为空,参数个数较多,使用requestBody传递参数,DTO(Data Transfer Object)数据传输对象接收。
用户修改接口,名称,年龄,邮箱,主键id,不能为空,参数个数较多,使用requestBody传递参数,,DTO(Data Transfer Object)数据传输对象接收。
用户信息接口,入参为单个参数,使用requestParam/PathVariable的方式传递参数。
4.2、编码实现
因为用户注册和用户修改都是使用同一个DTO对象来传输数据,主键ID校验规则不同,因此这里需要使用到spring-validation的分组校验功能
定义新增分组校验接口
/**
* 新增时的验证规则
*
* @author ven
* @since 2023-02-28
*/
public interface ValidAddRules {
}定义修改分组校验接口
/**
* 修改时的验证规则
*
* @author ven
* @since 2023-02-28
*/
public interface ValidUpdateRules {
}定义入参对象UserRequestDTO
/**
* 入参对象UserRequestDTO
*
* @author ven
* @since 2023-02-28
**/
@Data
@Builder
public class UserRequestDTO implements java.io.Serializable {
private static final long serialVersionUID = -2655536314774756670L;
/**
* 主键ID
*/
@NotNull(message = &#34;id不能为空&#34;,groups = {ValidUpdateRules.class})
@Min(1)
private Long userId;
/**
* 年龄
*/
@NotNull(message = &#34;年龄不能为空&#34;,groups = {ValidUpdateRules.class,ValidAddRules.class})
@Min(1)
private Integer age;
/**
* 邮箱
*/
@NotBlank(message = &#34;邮箱不能为空&#34;,groups = {ValidUpdateRules.class,ValidAddRules.class})
@Email(message = &#34;邮箱格式不正确&#34;)
private String email;
/**
* 昵称
*/
@NotBlank(message = &#34;邮箱不能为空&#34;,groups = {ValidUpdateRules.class,ValidAddRules.class})
private String nickName;
}控制器controller
/**
* 用户控制器
*
* @author ven
* @since 2023-02-28
**/
@RestController
@Validated
public class UserController {
/**
* 用户注册
* @param userRequest 用户注册信息
* @return Result<String>
*/
@PostMapping(&#34;/user/register&#34;)
public Result<String> registerUser(@Validated(ValidAddRules.class) @RequestBody UserRequestDTO userRequest){
return Result.data(&#34;新增成功&#34;);
}
/**
* 根据用户ID获取用户信息
* @param userId 用户ID
* @return Result<UserRequestDTO>
*/
@PostMapping(&#34;/user/get&#34;)
public Result<UserRequestDTO> getUser(@NotNull(message = &#34;用户id不能为空&#34;) @Min(1) Long userId){
return Result.data(UserRequestDTO.builder().build());
}
/**
* 用户修改
* @param userRequest 用户信息
* @return Result<String>
*/
@PostMapping(&#34;/user/update&#34;)
public Result<String> updateUser(@Validated(ValidUpdateRules.class) @RequestBody UserRequest userRequest){
return Result.data(&#34;修改成功&#34;);
}
}
05.校验参数快速失败配置
Spring Validation默认会校验完所有字段,然后才抛出异常。我们可以开启Fali Fast模式,一旦校验失败就立即返回。代码如下:
/**
*参数校验快速失败配置
* @author ven
* @since 2023/02/28
**/
@Configuration
public class ValidatedConfiguration {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory();
return validatorFactory.getValidator();
}
}6.校验参数统一异常处理
参数校验失败,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常,在实际的项目开发中,需要获取参数校验失败的具体原因,这个时候就需要统一异常处理来返回异常信息。代码如下:
/**
* 全局统一异常处理,处理可预见的异常,Order 排序优先级高
*
* @author ven
* @since 2023-02-28
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice{
/**
* 业务异常
* @param e 异常信息
* @return Result
*/
@ExceptionHandler(ServiceException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleError(ServiceException e) {
log.error(&#34;业务异常&#34;,e);
return Result.fail(e.getResultCode(), e.getMessage());
}
/**
* 服务器异常
* @param e 500异常
* @return Result
*/
@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Object> handleError(Throwable e) {
log.error(&#34;服务器异常&#34;, e);
return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, (Objects.isNull(e.getMessage()) ? ResultCode.INTERNAL_SERVER_ERROR.getMessage() : e.getMessage()));
}
/**
* 缺少请求参数异常
* @param e 少参异常
* @return Result
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleError(MissingServletRequestParameterException e) {
String message = String.format(&#34;缺少必要的请求参数: %s&#34;, e.getParameterName());
log.error(&#34;缺少请求参数:{}&#34;, e.getParameterName());
return Result.fail(ResultCode.PARAM_MISS, message);
}
/**
* 请求参数格式错误异常
* @param e 异常信息
* @return Result
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleError(MethodArgumentTypeMismatchException e) {
String message = String.format(&#34;请求参数格式错误: %s&#34;, e.getName());
log.error(&#34;请求参数格式错误{}&#34;, e.getMessage());
return Result.fail(ResultCode.PARAM_TYPE_ERROR, message);
}
/**
* 参数验证失败异常
* @param e 非法参数异常
* @return Result
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleError(MethodArgumentNotValidException e) {
return handleError(e.getBindingResult());
}
/**
* 参数绑定失败异常
* @param e 绑定错误异常
* @return Result
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleError(BindException e) {
return handleError(e.getBindingResult());
}
private Result<Object> handleError(BindingResult result) {
FieldError error = result.getFieldError();
String errorMessage = null != error ? error.getDefaultMessage() : &#34;参数异常&#34;;
log.error(&#34;参数验证或绑定异常,ex = {}&#34;,errorMessage);
return Result.fail(ResultCode.PARAM_BIND_ERROR, errorMessage);
}
/**
* 参数验证失败异常
* @param e 400异常
* @return Result
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleError(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
ConstraintViolation<?> violation = violations.iterator().next();
String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
String message = String.format(&#34;%s:%s&#34;, path, violation.getMessage());
log.error(&#34;参数验证失败{}&#34;, message);
return Result.fail(ResultCode.PARAM_VALID_ERROR, message);
}
/**
* 404没找到请求
* @param e 异常信息
* @return Result
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<Object> handleError(NoHandlerFoundException e) {
log.error(&#34;404没找到请求:{}&#34;, e.getMessage());
return Result.fail(ResultCode.NOT_FOUND, e.getMessage());
}
/**
* 消息不能读取
* @param e 异常信息
* @return Result
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleError(HttpMessageNotReadableException e) {
log.error(&#34;消息不能读取:{}&#34;, e.getMessage());
return Result.fail(ResultCode.MSG_NOT_READABLE, e.getMessage());
}
/**
* 不支持当前请求方法
* @param e 异常信息
* @return Result
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public Result<Object> handleError(HttpRequestMethodNotSupportedException e) {
log.error(&#34;不支持当前请求方法:{}&#34;, e.getMessage());
return Result.fail(ResultCode.METHOD_NOT_SUPPORTED, e.getMessage());
}
/**
* 不支持当前媒体类型
* @param e 异常信息
* @return Result
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public Result<Object> handleError(HttpMediaTypeNotSupportedException e) {
log.error(&#34;不支持当前媒体类型:{}&#34;, e.getMessage());
return Result.fail(ResultCode.MEDIA_TYPE_NOT_SUPPORTED, e.getMessage());
}
/**
* 不接受的媒体类型
* @param e 异常信息
* @return Result
*/
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public Result<Object> handleError(HttpMediaTypeNotAcceptableException e) {
log.error(&#34;不接受的媒体类型:{}&#34;, e.getMessage());
return Result.fail(ResultCode.MEDIA_TYPE_NOT_SUPPORTED,e.getMessage());
}
/**
* 空指针异常
* @param ex 异常信息
* @return Result
*/
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Object> handleTypeMismatchException(NullPointerException ex) {
log.error(&#34;空指针异常,{}&#34;, (Object) ex.getStackTrace());
return Result.fail(ResultCode.INTERNAL_SERVER_ERROR,&#34;空指针异常&#34;);
}
}
7.扩展使用
嵌套校验
前面的示例中,UserRequestDTO类里面的字段都是基本数据类型和String类型。但是实际场景中,有可能某个字段也是一个对象,这种情况可以使用嵌套校验。比如,UserRequestDTO类中带有一个工作类型Job字段对象。需要注意的是,此时DTO类的对应字段必须标记@Valid注解,代码如下:
/**
* 入参对象UserRequestDTO
*
* @author ven
* @since 2023-02-28
**/
@Data
@Builder
public class UserRequestDTO implements java.io.Serializable {
private static final long serialVersionUID = -2655536314774756670L;
/**
* 主键ID
*/
@NotNull(message = &#34;id不能为空&#34;,groups = {ValidUpdateRules.class})
@Min(1)
private Long userId;
/**
* 年龄
*/
@NotNull(message = &#34;年龄不能为空&#34;,groups = {ValidUpdateRules.class,ValidAddRules.class})
@Min(1)
private Integer age;
/**
* 邮箱
*/
@NotBlank(message = &#34;邮箱不能为空&#34;,groups = {ValidUpdateRules.class,ValidAddRules.class})
@Email(message = &#34;邮箱格式不正确&#34;)
private String email;
/**
* 昵称
*/
private String nickName;
/**
* 工作对象
*/
@NotNull(groups = {ValidAddRules.class, ValidUpdateRules.class})
@Valid
private Job job;
/**
* 工作对象
*/
@Data
public static class Job {
/**
* 主键ID
*/
@NotNull(message = &#34;id不能为空&#34;,groups = {ValidUpdateRules.class})
private Long jobId;
/**
* 工作名称
*/
@NotNull(message = &#34;工作名称不能为空!&#34;,groups = {ValidAddRules.class, ValidUpdateRules.class})
@Length(min = 2, max = 10, groups = {ValidAddRules.class, ValidUpdateRules.class})
private String jobName;
/**
*职位
*/
@NotNull(message = &#34;职位不能为空!&#34;,groups = {ValidAddRules.class, ValidUpdateRules.class})
@Length(min = 2, max = 10, groups = {ValidAddRules.class, ValidUpdateRules.class})
private String position;
}
}集合校验
如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:包装List类型,并声明@Valid注解
public class ValidationList<E> implements List<E> {
@Delegate // @Delegate是lombok注解
@Valid // 一定要加@Valid注解
public List<E> list = new ArrayList<>();
// 一定要记得重写toString方法
@Override
public String toString() {
return list.toString();
}
}controller写法:
@PostMapping(&#34;/saveList&#34;)
public Result saveList(@RequestBody @Validated(ValidAddRules.class) ValidationList<UserRequestDTO> userList) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}编码式校验
@Autowired
private javax.validation.Validator globalValidator;
// 编程式校验
@PostMapping(&#34;/save&#34;)
public Result save(@RequestBody UserRequestDTOuserDTO) {
Set<ConstraintViolation<UserRequestDTO>> validate = globalValidator.validate(UserRequestDTO, ValidAddRules.class);
// 如果校验通过,validate为空;否则,validate包含未校验通过项
if (validate.isEmpty()) {
// 校验通过,才会执行业务逻辑处理
} else {
for (ConstraintViolation<UserRequestDTO> userDTOConstraintViolation : validate) {
// 校验失败,做其它逻辑
System.out.println(userDTOConstraintViolation);
}
}
return Result.ok();
}自定义校验
现实中项目的业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。
自定义spring validation非常简单,假设我们自定义电话号码是否合法的校验注解,主要分为两步:
第一步:自定义注解
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface CheckPhone {
String message() default &#34;手机格式不合法&#34;;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}第二步:实现ConstraintValidator接口编写约束校验器
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
/**
* 手机号码校验器
*/
public class PhoneValidator implements ConstraintValidator<CheckPhone,String> {
@Override
public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) {
if (phone.length() != 11) {
return false;
}
// 移动号段正则表达式
Pattern pattern = Pattern.compile(&#34;^((13[4-9])|(147)|(15[0-2,7-9])|(178)|(18[2-4,7-8]))\\d{8}|(1705)\\d{7}$&#34;);
if (pattern.matcher(phone).matches()) {
return true;
}
// 联通号段正则表达式
pattern = Pattern.compile(&#34;^((13[0-2])|(145)|(15[5-6])|(176)|(18[5,6]))\\d{8}|(1709)\\d{7}$&#34;);
if (pattern.matcher(phone).matches()) {
return true;
}
// 电信号段正则表达式
pattern = Pattern.compile(&#34;^((133)|(153)|(177)|(18[0,1,9])|(149))\\d{8}$&#34;);
if (pattern.matcher(phone).matches()) {
return true;
}
//虚拟运营商正则表达式
pattern = Pattern.compile(&#34;^((170))\\d{8}|(1718)|(1719)\\d{7}$&#34;);
return pattern.matcher(phone).matches();
}
}直接@CheckPhone 就可以来使用了,代码如下:
@CheckPhone(message = &#34;手机不合法&#34;)
public String mobile;常用注解
@NotNull被注释的元素不能为null
@Null 被注释的元素必须为null
@NotNull 被注释的元素不能为null,可以为空字符串
@AssertTrue 被注释的元素必须为true
@AssertFalse 被注释的元素必须为false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max,min) 被注释的元素的大小必须在指定的范围内。
@Digits(integer,fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式。
@Email 被注释的元素必须是电子邮件地址
@Length 被注释的字符串的大小必须在指定的范围内
@Range 被注释的元素必须在合适的范围内
@NotEmpty:用在集合类上,不能为null,并且长度必须大于0
@NotBlank:只能作用在String上,不能为null,而且调用trim()后,长度必须大于0 |
|