手写 @Valid 字段校验器
上次给大家讲述了 Springboot 中的 @Valid 注解 和 @Validated 注解的详细用法:
@Valid 和 @Validated 注解用法详解
当我们用上面这两个注解的时候,需要首先在对应的字段上打上规则注解,类似如下。
@Data
public class Employee {/** 姓名 */@NotBlank(message = "请输入名称")@Length(message = "名称不能超过个 {max} 字符", max = 10)public String name;/** 年龄 */@NotNull(message = "请输入年龄")@Range(message = "年龄范围为 {min} 到 {max} 之间", min = 1, max = 100)public Integer age;}
其实,在使用这些规则注解时,我觉得不够好用,比如我列举几个点:
(1)针对每个字段时,如果有多个校验规则,需要打多个对应的规则注解,这时看上去,就会显得较为臃肿。
(2)某些字段的类型根本不能校验,比如在校验 Double 类型的字段规则时,打上任何校验注解,都会提示报错,说不支持 Double 类型的数据;
(3)每打一个规则注解时,都需要写上对应的 message 提示信息,这不但使得写起来麻烦,而且代码看起来又不雅观,按理说,我们的一类规则提示应该都是相同的,比如 "xxx不能为空",所以,按理来说,我只要配置一次提示格式,就可以不用再写了,只需要配置每个字段的名称xxx即可。
(4)一般来说,我们通常进行字段校验时,可能还需要一些额外的数据处理,比如去掉字符串前后的空格,某些数据可以为空的时候,我们还可以设置默认值这些等。
(5)不能进行扩展,如果时自己写的校验器,还可以进行需求扩展。
(6)他们再进行校验的时候,都需要再方法参数上打上一个 @Valid 注解或者 @Validate 注解,如果我们采用 AOP 去切所有 controller 中的方法的话,那么我们写的自定义规则校验器,甚至连方法参数注解都可以不用打,是不是又更加简洁了呢。
于是,介于上述点,写了一个自定义注解校验器,包括下面几个文件:
Valid
这个注解作用于字段上,用于规则校验。
package com.zyq.utils.valid;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 字段校验注解** @author zyqok* @since 2022/05/06*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Valid {/*** 属性名称*/String name() default "";/*** 是否可为空*/boolean required() default true;/*** 默认值(如果默认值写 null 时,则对所有数据类型有效,不会设置默认值)*/String defaultValue() default "";/*** 【String】是否在原来值的基础上,去掉前后空格*/boolean trim() default true;/*** 【String】最小长度*/int minLength() default 0;/*** 【String】最大长度*/int maxLength() default 255;/*** 【String】自定义正则校验(该配置为空时则不进行正则校验)*/String regex() default "";/*** 【Integer】【Long】【Double】范围校验最小值(该配置为空时则不进行校验)*/String min() default "";/*** 【Integer】【Long】【Double】范围校验最大值(该配置为空时则不进行校验)*/String max() default "";}
ValidUtils
自定义规则校验工具类
package com.zyq.utils.valid;import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;/*** 字段校验注解工具** @author zyqok* @since 2022/05/05*/
public class ValidUtils {/*** 校验对象,获取校验结果(单个提示)** @param obj 待校验对象* @return null-校验通过,非null-校验未通过*/public static String getMsg(T obj) {List msgList = getMsgList(obj);return msgList.isEmpty() ? null : msgList.get(0);}/*** 校验对象,获取校验结果(所有提示)** @param obj 待校验对象* @return null-校验通过,非null-校验未通过*/public static List getMsgList(T obj) {if (Objects.isNull(obj)) {return Collections.emptyList();}Field[] fields = obj.getClass().getDeclaredFields();if (fields.length == 0) {return Collections.emptyList();}List msgList = new ArrayList<>();for (Field field : fields) {// 没有打校验注解的字段则不进行校验Valid valid = field.getAnnotation(Valid.class);if (Objects.isNull(valid)) {continue;}field.setAccessible(true);// String 类型字段校验if (field.getType().isAssignableFrom(String.class)) {String msg = validString(obj, field, valid);if (Objects.nonNull(msg)) {msgList.add(msg);}continue;}// int / Integer 类型字符校验String typeName = field.getType().getTypeName();if (field.getType().isAssignableFrom(Integer.class) || "int".equals(typeName)) {String msg = validInteger(obj, field, valid);if (Objects.nonNull(msg)) {msgList.add(msg);}continue;}// double/Double 类型字段校验if (field.getType().isAssignableFrom(Double.class) || "double".equals(typeName)) {String msg = validDouble(obj, field, valid);if (Objects.nonNull(msg)) {msgList.add(msg);}continue;}}return msgList;}/*** 校验String类型字段*/private static String validString(T obj, Field field, Valid valid) {// 获取属性名称String name = getFieldName(field, valid);// 获取原值Object v = getValue(obj, field);String val = Objects.isNull(v) ? "" : v.toString();// 是否需要去掉前后空格boolean trim = valid.trim();if (trim) {val = val.trim();}// 是否必填boolean required = valid.required();if (required && val.isEmpty()) {return requiredMsg(name);}// 是否有默认值if (val.isEmpty()) {val = isDefaultNull(valid) ? null : valid.defaultValue();}// 最小长度校验int length = 0;if (Objects.nonNull(val)) {length = val.length();}if (length < valid.minLength()) {return minLengthMsg(name, valid);}// 最大长度校验if (length > valid.maxLength()) {return maxLengthMsg(name, valid);}// 正则判断if (!valid.regex().isEmpty()) {boolean isMatch = Pattern.matches(valid.regex(), val);if (!isMatch) {return regexMsg(name);}}// 将值重新写入原字段中setValue(obj, field, val);// 如果所有校验通过后,则返回nullreturn null;}private static String validInteger(T obj, Field field, Valid valid) {// 获取属性名称String name = getFieldName(field, valid);// 获取原值Object v = getValue(obj, field);Integer val = Objects.isNull(v) ? null : (Integer) v;// 是否必填boolean required = valid.required();if (required && Objects.isNull(val)) {return requiredMsg(name);}// 是否有默认值if (Objects.isNull(val)) {boolean defaultNull = isDefaultNull(valid);if (!defaultNull) {val = parseInt(valid.defaultValue());}}// 校验最小值if (!valid.min().isEmpty() && Objects.nonNull(val)) {int min = parseInt(valid.min());if (val < min) {return minMsg(name, valid);}}// 校验最大值if (!valid.max().isEmpty() && Objects.nonNull(val)) {int max = parseInt(valid.max());if (val > max) {return maxMsg(name, valid);}}// 将值重新写入原字段中setValue(obj, field, val);// 如果所有校验通过后,则返回nullreturn null;}private static String validDouble(T obj, Field field, Valid valid) {return null;}/*** 获取对象指定字段的值** @param obj 原对象* @param field 指定字段* @param 泛型* @return 该字段的值*/private static Object getValue(T obj, Field field) {try {return field.get(obj);} catch (IllegalAccessException e) {e.printStackTrace();return null;}}/*** 给对象指定字段设值,一般校验后值可能有变化(生成默认值/去掉前后空格等),需要新的值重新设置到对象中** @param obj 原对象* @param field 指定字段* @param val 新值* @param 泛型*/private static void setValue(T obj, Field field, Object val) {try {field.set(obj, val);} catch (IllegalAccessException e) {e.printStackTrace();}}/*** 获取字段名称(主要用于错误时提示用)** @param field 字段对象* @param valid 校验注解* @return 字段名称(如果注解有写名称,则取注解名称;如果没有注解名称,则取字段)*/private static String getFieldName(Field field, Valid valid) {return valid.name().isEmpty() ? field.getName() : valid.name();}/*** 该字段是否默认为 null** @param valid 校验注解* @return true - 默认为 null; false - 默认不为 null*/private static boolean isDefaultNull(Valid valid) {return "null".equals(valid.defaultValue());}/*** 提示信息(该方法用于统一格式化提示信息样式)** @param name 字段名称* @param msg 提示原因* @return 提示信息*/private static String msg(String name, String msg) {return "【" + name + "】" + msg;}/*** 必填字段提示** @param name 字段名称* @return 提示信息*/private static String requiredMsg(String name) {return msg(name, "不能为空");}/*** String 类型字段少于最小长度提示** @param name 字段名称* @param valid 校验注解* @return 提示信息*/private static String minLengthMsg(String name, Valid valid) {return msg(name, "不能少于" + valid.minLength() + "个字符");}/*** String 类型字段超过最大长度提示** @param name 字段名称* @param valid 校验注解* @return 提示信息*/private static String maxLengthMsg(String name, Valid valid) {return msg(name, "不能超过" + valid.maxLength() + "个字符");}/*** String 类型正则校验提示** @param name 字段名称* @return 提示信息*/private static String regexMsg(String name) {return msg(name, "填写格式不正确");}/*** 数字类型小于最小值的提示** @param name 字段名称* @param valid 校验注解* @return 提示信息*/private static String minMsg(String name, Valid valid) {return msg(name, "不能小于" + valid.min());}/*** 数字类型大于最大值的提示** @param name 字段名称* @param valid 校验注解* @return 提示信息*/private static String maxMsg(String name, Valid valid) {return msg(name, "不能大于" + valid.max());}/*** 将字符串数字转化为 int 类型的数字,转换异常时返回 0** @param intStr 字符串数字* @return int 类型数字*/private static int parseInt(String intStr) {try {return Integer.valueOf(intStr);} catch (NumberFormatException e) {return 0;}}
}
ValidAop
这是一个 controller 拦截切面,写了这个,就不用再 controller 方法参数上打上类似于原@Valid 和 @Validate 注解,还原的方法参数的原始整洁度。
但需要注意的是:类中 controller 的路径需要替换为你的包路径(我这里 controller 包路径为com.zyq.controller)。
package com.zyq.aop;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.unisoc.outsource.config.global.ValidException;
import com.unisoc.outsource.utils.valid.ValidUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;/*** @author zyqok* @since 2022/05/05*/
@Aspect
@Component
public class ValidAop {private static final String APPLICATION_JSON = "application/json";// 这里为你的 controller 包路径@Pointcut("execution(* com.zyqok.controller.*Controller.*(..))")public void pointCut() {}@Before("pointCut()")public void doBefore(JoinPoint jp) throws ValidException {// 获取所有请求对象ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 获取请求类型String contentType = request.getHeader("Content-Type");String json = null;if (contentType != null && contentType.startsWith(APPLICATION_JSON)) {// JSON请求体json = JSON.toJSONString(jp.getArgs()[0]);} else {// 键值对参数json = getParams(request);}// 获取请求类对象String validClassName = getParamClassName(jp);String msg = valid(json, validClassName);if (!isEmpty(msg)) {throw new ValidException(msg);}}/*** 获取方法参数对象名称*/private String getParamClassName(JoinPoint jp) {// 获取参数对象MethodSignature signature = (MethodSignature) jp.getSignature();Class>[] types = signature.getParameterTypes();// 没有参数则不进行校验if (types == null || types.length == 0) {return null;}// 返回项目中的对象类名for (Class> clazz : types) {if (clazz.getName().startsWith("com.zyq.outsource")) {return clazz.getName();}}return null;}/*** 获取请求对象*/private String getParams(HttpServletRequest request) {Map parameterMap = request.getParameterMap();if (Objects.isNull(parameterMap) || parameterMap.isEmpty()) {return "{}";}JSONObject obj = new JSONObject();parameterMap.forEach((k, v) -> {if (Objects.nonNull(v) && v.length == 1) {obj.put(k, v[0]);} else {obj.put(k, v);}});return obj.toString();}/*** 校验请求值合规性*/private String valid(String json, String className) {if (isEmpty(className)) {return null;}System.out.println("json : " + json);System.out.println("className : " + className);try {Class> clazz = Class.forName(className);Object o = JSON.parseObject(json, clazz);return ValidUtils.getMsg(o);} catch (Exception e) {e.printStackTrace();return null;}}/*** 校验字符串是否为空*/private boolean isEmpty(String s) {return Objects.isNull(s) || s.trim().isEmpty();}
}
ValidException
因为 AOP 切面里,不能在前置切面中直接返回校验规则的错误提示,所以我们可以采用抛异常的方式,最后对异常进行捕捉,再提示给用户(原 Springboot 的 @Validate 也是采用类似方式进行处理)。
package com.zyq.valid;/*** 自定义注解异常** @author zyqok* @since 2022/05/06*/
public class ValidException extends RuntimeException {private String msg;public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public ValidException(String msg) {this.msg = msg;}
}
ValidExceptionHandler
这个异常处理器就是用于捕捉上面的异常,最后提示给前端。
@ControllerAdvice
@ResponseBody
public class ValidExceptionHandler {@ExceptionHandler(ValidException.class)public Map validExceptionHandler(ValidException ex) {Map map = new HashMap();map.put("code", 1);map.put("msg", ex.getMsg());return map;}}
当把所有文件复制到文件中后,那么在使用的时候

只需要将方法中的参数打上我们定义的 @Valid 即可,其余不用做任何操作就OK
/*** @author zyqok* @since 2022/05/06*/
@Data
public class EntryApplyCancelReq {@Validprivate Integer id;@Valid(name = "取消原因", maxLength = 50)private String reason;}
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
