手写一个@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字段校验器内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot参数校验之@Valid的使用详解

    目录 简介 依赖 代码 测试 测试1:缺少字段 测试2:不缺少字段 测试3:缺少字段,后端获取BindResult 简介 说明 本文用示例说明SpringBoot的@Valid的用法. 依赖 <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> </dependency> 代码 Contr

  • 参数校验Spring的@Valid注解用法解析

    参数校验Spring的@Valid注解 @Valid 注解通常用于对象属性字段的规则检测. 以新增一个员工为功能切入点,以常规写法为背景,慢慢烘托出 @Valid 注解用法详解. 那么,首先,我们会有一个员工对象 Employee,如下 : public class Employee { /** 姓名 */ public String name; /** 年龄 */ public Integer age; public String getName() { return name; } publ

  • 浅谈spring方法级参数校验(@Validated)

    依赖的jar包: spring相关jar包版本:4.3.1.RELEASE <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.1.3.Final</version> </dependency> 一.配置与注入 MethodValidationPostProce

  • 使用@Valid 校验嵌套对象

    目录 @Valid 校验嵌套对象 @Valid 嵌套对象验证不成功 @Valid 校验嵌套对象 参考网上的博客:ValidList 能校验list. 通过测试发现,@Valid只能校验一层.比如我这里有个person对象,里面有个ValidList<Teacher> 属性,Teacher对象里面有个List<Student> 属性. 如果在Controller层加上@Valid 是校验不到ValidList<Student> 属性的. 需要在List<Teache

  • SpringBoot参数校验之@Validated的使用详解

    目录 简介 依赖 用法1:不分组 代码 测试 用法2:分组 代码 测试 简介 说明 本文用示例说明SpringBoot的@Validated的用法. 依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> 它里边依赖了hibernat

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

    上次给大家讲述了 Springboot 中的 @Valid 注解 和 @Validated 注解的详细用法: 详解Spring中@Valid和@Validated注解用法 当我们用上面这两个注解的时候,需要首先在对应的字段上打上规则注解,类似如下. @Data public class Employee { /** 姓名 */ @NotBlank(message = "请输入名称") @Length(message = "名称不能超过个 {max} 字符", max

  • vue用Object.defineProperty手写一个简单的双向绑定的示例

    前言 上次写了一个Object.defineProperty() 不详解,文末说要写用它来写个双向绑定.说话算话,说来就来 前文链接 Object.defineProperty() 不详解 先看最后效果 model演示.gif 什么是双向绑定? 1.当一个对象(或变量)的属性改变,那么调用这个属性的地方显示也应该改变,模型到视图(model => view) 2.当调用属性的这个地方改变了这个属性(通常是一个表单元素),那么这个对象(或变量)的属性也会改为最新的值 ,即视图到模型(view =>

  • Python实现手写一个类似django的web框架示例

    本文实例讲述了Python实现手写一个类似django的web框架.分享给大家供大家参考,具体如下: 用与django相似结构写一个web框架. 启动文件代码: from wsgiref.simple_server import make_server #导入模块 from views import * import urls def routers(): #这个函数是个元组 URLpattern=urls.URLpattern return URLpattern #这个函数执行后返回这个元组

  • 如何手写一个Spring Boot Starter

    何为 Starter ? 想必大家都使用过 SpringBoot,在 SpringBoot 项目中,使用最多的无非就是各种各样的 Starter 了.那何为 Starter 呢?你可以理解为一个可拔插式的插件(组件).或者理解为场景启动器. 通过 Starter,能够简化以前繁杂的配置,无需过多的配置和依赖,它会帮你合并依赖,并且将其统一集成到一个 Starter 中,我们只需在 Maven 或 Gradle 中引入 Starter 依赖即可.SpringBoot 会自动扫描需要加载的信息并启动

  • Golang 手写一个简单的并发任务 manager

    目录 前言 errgroup 需求拆解 实战代码 Job JobManager 错误处理 及时退出 完整代码 小结 前言 今天也是偏实战的内容,作为一个并发复习课,很简单,我们来看看怎样实现一个并发任务 manager. 在微服务的场景下,我们有很多任务的执行是没有明确的先后顺序的,比如一个接口同时要做到任务 A 和 任务 B,两个任务分别拿到一些数据,最后组装裁剪后通过接口下发. 此时,A 和 B 两个任务没有依赖关系,如果我们串行来执行,会拖慢整个任务的执行节奏,用并发的方式来优化是一个方向

  • 基于Java手写一个好用的FTP操作工具类

    目录 前言 windows服务器搭建FTP服务 工具类方法 代码展示 使用示例 前言 网上百度了很多FTP的java 工具类,发现文章代码都比较久远,且代码臃肿,即使搜到了代码写的还可以的,封装的常用操作方法不全面,于是自己花了半天实现一个好用的工具类.最初想用java自带的FTPClient 的jar 去封装,后来和apache的jar工具包对比后,发现易用性远不如apache,于是决定采用apache的ftp的jar 封装ftp操作类. windows服务器搭建FTP服务 打开控制版面,图示

  • 如何从零开始利用js手写一个Promise库详解

    前言 ECMAScript 是 JavaScript 语言的国际标准,JavaScript 是 ECMAScript 的实现.ES6 的目标,是使得 JavaScript 语言可以用来编写大型的复杂的应用程序,成为企业级开发语言. 概念 ES6 原生提供了 Promise 对象. 所谓 Promise,就是一个对象,用来传递异步操作的消息.它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理. 三道思考题 刚开始写前端的时候,处理异步请求经常用

  • JS手写一个自定义Promise操作示例

    本文实例讲述了JS手写一个自定义Promise操作.分享给大家供大家参考,具体如下: 经常在面试题中会看到,让你实现一个Promsie,或者问你实现Promise的原理,所以今天就尝试利用class类的形式来实现一个Promise 为了不与原生的Promise命名冲突,这里就简单命名为MyPromise. class MyPromise { constructor(executor) { let _this = this this.state = 'pending' // 当前状态 this.v

  • 如何手写一个简易的 Vuex

    前言 本文适合使用过 Vuex 的人阅读,来了解下怎么自己实现一个 Vuex. 基本骨架 这是本项目的src/store/index.js文件,看看一般 vuex 的使用 import Vue from 'vue' import Vuex from './myvuex' // 引入自己写的 vuex import * as getters from './getters' import * as actions from './actions' import state from './stat

  • 模仿Spring手写一个简易的IOC

    这个小项目是我读过一点Spring的源码后,模仿Spring的IOC写的一个简易的IOC,当然Spring的在天上,我写的在马里亚纳海沟,哈哈 感兴趣的小伙伴可以去我的github拉取代码看着玩 地址: https://github.com/zhuchangwu/CIOC 项目中有两种方式实现IOC: 第一种是基于dom4j实现的解析XML配置文件版 第二种是基于自定义注解实现全配置版 全注解版 模仿Spring原生的IOC机制如下: Interface类型的beanDefinition不会被实

随机推荐