开发背景:相信大家在实际开发中基本都接触过前后端分离的开发模式,大致流程为前端携带请求参数向后端发起请求然后获取响应结果进行页面渲染。那么这里问题来了,前端发起的请求会携带众多不同格式的请求参数,这些请求参数字段如果没有进行必要的校验很可能对导致后端服务出现一系列报错。因此一般来说需要在后端对前端的请求参数进行一些必要的参数校验,当全部参数校验通过之后才进行服务的调用返回数据,当参数存在不通过的情况时则以友好的方式直接返回给调用方提示进行参数修改。基于此背景,下面介绍一下这样一个需求可以怎么去实现,并且考虑将代码进行必要的设计和重构,在完成功能的前提下也考虑方便其他开发者在使用时也可以非常方便地自定义一个符合自己需求的参数校验器。
使用核心技术点:Spring、SpringBoot、JSR303、Spring Validator、全局异常处理、自定义注解、Spring反射工具、设计模式(单例,模板方法,适配器模式)、Postman等
整体实现功能和流程图:目前版本可以实现对单个实体属性的非空,数值,枚举校验,也支持对内部实体和内部List集合的数据校验
实现思路:可以考虑使用Spring自带的Validator作为主体代码框架,基于Validator提供的特性功能进行逻辑的自定义。建议可以先了解Spring Validator怎么去自定义一个validator注解并在容器中生效。下面展示整体的代码结构:
Application.java:SpringBoot工程启动类
ValidController.java:后台控制类,接收前端请求到赋值到VO中,使用Spring Validator框架的@Validated注解开启参数校验功能:
@RestController
public class ValidController {
/**
* 使用Spring Validator框架的@Validated注解开启参数校验
* @param personVO
* @return
* @throws Exception
*/
@RequestMapping(name="/validVO", method = RequestMethod.POST)
public String valid(@Validated @RequestBody PersonVO personVO) throws Exception {
System.out.println(personVO.toString());
return null;
}
}
checker层:自定义相关的参数校验注解标识,用于修饰在字段上。如下非空校验,枚举校验等注解,要特别注意@Constraint注解链接的就是对应该字段的校验处理器Adapter实现类。
@Constraint(validatedBy = MyNotNullCheckAdapter.class)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyNotNullCheck {
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String message() default "输入的数据不能为空。";
}
@Constraint(validatedBy = MyEnumCheckAdapter.class)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyEnumCheck{
//groups【必须有】
Class<?>[] groups() default { };
//payload【必须有】
Class<? extends Payload>[] payload() default { };
//对应枚举类名称
String enumClass();
//校验消息
String message() default "请输入正确的枚举数值。";
//是否允许为空
boolean allowNull() default false;
@Constraint(validatedBy = MyNumberCheckAdapter.class)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyNumberCheck {
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String message() default "输入的数据必须为数值类型。";
}
上面几个MyEnumCheck,MyNotNullCheck,MyNumberCheck都是使用在请求参数最外层VO的字段上。但是由于目前Spring对于VO里还有其它实体vo或集合list的校验尚没有完善的使用方案,因此进行了自定义实现,使用下面校验注解MyVoCheck:
/**
* 对内部实体或集合对象的数据校验
*/
@Constraint(validatedBy = MyVoCheck.Validator.class)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyVoCheck {
//groups【必须有】
Class<?>[] groups() default { };
//payload【必须有】
Class<? extends Payload>[] payload() default { };
//校验消息
String message() default "校验实体不能为空。";
//是否允许为空
boolean allowNull() default true;
/**
* 内部校验类
*/
class Validator implements ConstraintValidator<MyVoCheck, Object> {
private boolean allowNull;
private String message;
@Override
public void initialize(MyVoCheck enumValue) {
this.allowNull = enumValue.allowNull();
this.message = enumValue.message();
}
/**
* 校验对象:获取该对象内所有添加了自定义注解的参数,
* 获取参数值,调用不同的适配器进行处理
* @param obj
* @param context
* @return boolean
*/
@Override
public boolean isValid(Object obj, ConstraintValidatorContext context) {
//是否允许为空,不能为空时直接提示
if(!allowNull){
if (obj == null) {
throw new MyValueCheckException(ExceptionCode.CHECK_FAILURE_CODE,
String.valueOf(String.format("%s", this.message)));
}
}
return MyAdapterManager.getMyAdapterManager().checkObjOrList(obj);
}
}
}
model层:定义相关的参