概要
在我们使用Spring mvc的开发Restful风格的接口时,往往需要后端对请求参数进行合法性校验,并且返回包含异常提示信息给到用户提示或接口提示,以满足我们接口的合法性,防止后续的请求业务逻辑出现不可控的异常。
Spring Validated注解作用
在一个正常的POST带body请求体的请求接口当中,@Validated注解可用用来校验请求参数的合法性。@Valid也可以做参数校验,但题主使用了自定义分组校验,所以未使用@Valid注解。
关于@Validated和@Valid注解的区别,请参考这边文章:@Validated和@Valid区别
定义一个通用的枚举校验器
- 首先定义一个枚举校验器,题主这边的枚举校验器只能校验,String类型和Integer类型。如果需要支持其他的类型,请各位coder自行实现。
/**
* 枚举校验注解处理类
*/
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
private Class<? extends Enum> enumClass;
@Override
public void initialize(EnumValue constraintAnnotation) {
enumClass = constraintAnnotation.enumClass();
}
@Override
@SuppressWarnings("unchecked")
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (!(value instanceof Integer || value instanceof String)) {
return false;
}
List<String> strEnumCode;
List<Integer> intEnumCode;
Method enumValidatorList;
try {
enumValidatorList = enumClass.getMethod("enumValidatorList");
} catch (NoSuchMethodException e) {
return false;
}
Object[] objects = enumClass.getEnumConstants();
List<?> result = Lists.newArrayList();
try {
result = (List<?>) enumValidatorList.invoke(objects[0]);
} catch (IllegalAccessException | InvocationTargetException e) {
return false;
}
if (CollectionUtils.isEmpty(result)) {
return false;
}
//可以校验的数据类型实现。暂时我这边的业务只需要支持Integer和String。
//对于大部分的业务场景应该都是满足的。
Object firstElement = result.get(0);
if (firstElement instanceof String) {
strEnumCode = (List<String>) result;
return strEnumCode.contains((String) value);
} else if (firstElement instanceof Integer) {
intEnumCode = (List<Integer>) result;
return intEnumCode.contains((Integer) value);
} else {
return false;
}
}
}
- 定义一个通用的枚举校验注解。
/**
* 枚举校验注解,只能校验为Integer或者String类型的参数
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {
// 默认错误消息
String message() default "必须为指定类型";
//校验的枚举类
Class<? extends Enum> enumClass();
// 分组
Class<?>[] groups() default {};
// 负载
Class<? extends Payload>[] payload() default {};
// 指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
}
- 定义一个供所有的枚举继承的接口,需要将枚举的定义的数组值赋值到
EnumValue
这个累的List
属性当中。
/**
* 公共类,用来做枚举类型校验的基类
* 暂时只支持Integer类型和Sting类型的参数
*/
public interface Enum<T> {
List<T> enumValidatorList();
}
有了上面的这些基础代码,我们就可以实现对枚举类型数据的合法性校验了。
具体的使用方法
- 定义一个枚举类型数据,我们这里拿可用状态来举个例子。
@Getter
public enum EnableStatusEnum implements Enum<Integer> {
//正常
NORMAL(1, "启用"),
//禁用
DISABLE(0, "禁用");
/**
* 状态code
*/
private final Integer status;
/**
* 状态描述
*/
private final String statusName;
EnableStatusEnum(Integer status, String statusName) {
this.status = status;
this.statusName = statusName;
}
@Override
public List<Integer> enumValidatorList() {
List<Integer> codes = Lists.newArrayList();
for (EnableStatusEnum value : EnableStatusEnum.values()) {
codes.add(value.getStatus());
}
return codes;
}
}
- 定义一个请求参数对象reqDto或者数据库生成的entity对象,此处我们假设对新增/修改一个用户举例,定义一个用户对象信息。
@ApiModel(value = "用户信息")
@Data
@Accessors(chain = true)
public class User implements Serializable {
@ApiModelProperty(value = "")
@NotNull(groups = {UpdateGroup.class}, message = "修改时d不能为空")
@Null(groups = {InsertGroup.class}, message = "新增时id不能存在")
private Long id;
@ApiModelProperty(value = "状态:-1 删除 0 禁用 1 启用")
@NotNull(groups = {UpdateGroup.class, InsertGroup.class}, message = "用户状态不能为空")
@EnumValue(enumClass = EnableStatusEnum.class, message = "用户状态不合法")
@LogTag(alias = "是否启用")
private Integer status;
}
- 接口当中去新增或修改这个User对象,这个时候当用户的状态不满足我们定义的枚举当中的状态是,spring就会报一个MethodArgumentNotValidException 参数校验异常的全局错误,我们将指定类型的异常通过全局异常处理器捕获处理就行了。
@ApiOperation(value = "新增用户")
@RequestMapping(value = "/grade", method = RequestMethod.POST)
@ResponseBody
public OperationResultDto insertUser(@RequestBody @Validated
User user) {
return userService.insert(user);
}
/**
* 使用@Valid或@Validated注解时,如果校验失败,会抛出
* MethodArgumentNotValidException异常,这里捕获该异常,返回校验失败的信息
* 此时需要注意,不需要在校验的注解后面跟BindingResult参数,否则不会报异常
*
* @param ex MethodArgumentNotValidException 异常
* @return API端对该参数异常的处理
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Object handleValidationException(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
// 处理校验失败的情况,返回错误信息等
logger.error(ex.getMessage(), ex);
return packMessage(OperationResDto.fail(result.getAllErrors().get(0).getDefaultMessage()), OperationStatusEnum.CUSTOMIZED_ERROR.value());
}
小结
通过上述的处理方式,我们就可以全局的对枚举类型的数据进行通用的有效校验,并且给出用户合理的提示和反馈了。当后台枚举类型新增也不会影响到整个代码的修改,当某个环节忘记处理新增的枚举类型时,整个系统也会给个友好提示,而不是抛出异常,保证了代码的可靠性。其中代码还是有优化的地方的,比如说如何在定义枚举时,不去继承自身定义的枚举类而将属性列表数据能写到注解方法上,大家可以自行优化这部分逻辑。