自定义Spring Boot参数校验注解


0

在某些情况下javax.validation.constraints自带的校验注解(比如常用的@NotNull,@NotEmpty,@Size等)已经不满足我们的需求,这时候就需要创建自定义的Validator来完成入参的校验。
这里我们假设有一个需要根据入参来校验值是否符合枚举的需求(这在实际开发偶尔会遇到),可选的两种方式为根据枚举名称或者枚举值来做校验,基本没区别。这里我们用枚举值来做示例。

首先新建springboot项目名为validator-demo,依赖spring-boot-starter-web和lombok。

自定义参数校验注解

创建自定义校验注解

package com.github.liuzhuoming23.validatordemo.annotation;

import com.github.liuzhuoming23.validatordemo.validator.EnumValueValidator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

/**
* 校验是否为合法的枚举值
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.class)
public @interface IsLegalEnumValue {

String message() default "入参对应的枚举值不存在";

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

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

/**
* 枚举类Class
*/
Class<? extends Enum<?>> value();
}

其中前三个属性是校验注解必需有的,最后一个属性是我们自定义的参数,用来指定需要校验的枚举类。

创建自定义校验注解验证类

package com.github.liuzhuoming23.validatordemo.validator;

import com.github.liuzhuoming23.validatordemo.annotation.IsLegalEnumValue;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import lombok.extern.slf4j.Slf4j;

/**
* 注解校验是否为合法的枚举值
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Slf4j
public class EnumValueValidator implements ConstraintValidator<IsLegalEnumValue, Integer> {

/**
* 存放枚举的全部值
*/
private Set<Integer> values = new HashSet<>();

@Override
public void initialize(IsLegalEnumValue isLegalEnumValue) {
Class<?> clazz = isLegalEnumValue.value();
Object[] objects = clazz.getEnumConstants();
try {
//获取全部枚举值
Method method = clazz.getMethod("getValue");
for (Object obj : objects) {
values.add((Integer) method.invoke(obj));
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.warn(e.getMessage());
}
}

@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
return values.contains(value);
}
}

其中initialize方法只会在相同的枚举类第一次做校验的时候调用。isValid方法的第一个参数是我们要校验的参数类型,和ConstraintValidator的第二个泛型类型相符,ConstraintValidator的第一个泛型类型是我们的自定义注解。
根据代码很容易看懂,整个类的作用就是将枚举的值放入set并判断入参是否存在set中。

创建自定义校验注解验证异常处理类

package com.github.liuzhuoming23.validatordemo.handler;

import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* 异常处理
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
public class ParamExceptionHandler {

/**
* 统一处理请求参数校验(自定义参数校验)
*
* @param e ConstraintViolationException
* @return String
*/
@ResponseBody
@ExceptionHandler(ConstraintViolationException.class)
public String handleConstraintViolationException(ConstraintViolationException e) {
StringBuilder message = new StringBuilder();
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
for (ConstraintViolation<?> violation : violations) {
Path path = violation.getPropertyPath();
String[] pathArr = path.toString().split(".");
message.append(pathArr[1]).append(": ").append(violation.getMessage()).append(",");
}
return message.substring(0, message.length() - 1);
}
}

其中ConstraintViolationException是PARAMETER(方法属性)校验抛出的异常,要是在FIELD(类属性)或者别的地方做校验抛出的类型是不一样的,自行添加尝试。

测试

新增测试枚举类:

package com.github.liuzhuoming23.validatordemo.em;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* 测试
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@AllArgsConstructor
@Getter
public enum TestEm {

FIRST(1), SECOND(2);
private int value;

}

其中value属性是必需的,和EnumValueValidator里面的反射方法相对应。@AllArgsConstructor@Getter注解是为了简化get方法和全参构造器。
在controller中加入测试接口:

package com.github.liuzhuoming23.validatordemo.controller;

import com.github.liuzhuoming23.validatordemo.annotation.IsLegalEnumValue;
import com.github.liuzhuoming23.validatordemo.em.TestEm;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 测试
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@RestController
@Validated
public class TestController {

@RequestMapping("/test")
public Object test(@IsLegalEnumValue(TestEm.class) Integer i) {
return "success";
}
}

完成后启动项目,首先访问http://localhost:8080/test?i=1,参数值为1存在于枚举值中,返回:

success

然后访问http://localhost:8080/test?i=3,参数值为3不存在于枚举值中,返回:

{
"timestamp": "2019-10-11T08:57:10.263+0000",
"status": 500,
"error": "Internal Server Error",
"message": "test.i: 入参对应的枚举值不存在",
"path": "/test"
}

说明验证是ok的。