手写一个@Valid字段校验器的示例代码

上次给大家讲述了 Springboot 中的 @Valid 注解 和 @Validated 注解的详细用法:

详解Spring中@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 <T> String getMsg(T obj) { List<String> msgList = getMsgList(obj); return msgList.isEmpty() ? null : msgList.get(0); } /** * 校验对象,获取校验结果(所有提示) * * @param obj 待校验对象 * @return null-校验通过,非null-校验未通过 */ public static <T> List<String> getMsgList(T obj) { if (Objects.isNull(obj)) { return Collections.emptyList(); } Field[] fields = obj.getClass().getDeclaredFields(); if (fields.length == 0) { return Collections.emptyList(); } List<String> 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 <T> 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); // 如果所有校验通过后,则返回null return null; } private static <T> 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); // 如果所有校验通过后,则返回null return null; } private static <T> String validDouble(T obj, Field field, Valid valid) { return null; } /** * 获取对象指定字段的值 * * @param obj 原对象 * @param field 指定字段 * @param <T> 泛型 * @return 该字段的值 */ private static <T> 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 <T> 泛型 */ private static <T> 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.unisoc.outsource")) { return clazz.getName(); } } return null; } /** * 获取请求对象 */ private String getParams(HttpServletRequest request) { Map<String, String[]> 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<String, String> validExceptionHandler(ValidException ex) { Map<String, String> 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 { @Valid private Integer id; @Valid(name = "取消原因", maxLength = 50) private String reason; }

到此这篇关于手写一个@Valid字段校验器的示例代码的文章就介绍到这了,更多相关@Valid字段校验器内容请搜索易知道(ezd.cc)以前的文章或继续浏览下面的相关文章希望大家以后多多支持易知道(ezd.cc)!

推荐阅读