文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-04-02 19:55

关注

上次给大家讲述了 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;
 

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Valid {
 
    
    String name() default "";
 
    
    boolean required() default true;
 
    
    String defaultValue() default "";
 
    
    boolean trim() default true;
 
    
    int minLength() default 0;
 
    
    int maxLength() default 255;
 
    
    String regex() default "";
 
    
    String min() default "";
 
    
    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;
 
 

public class ValidUtils {
 
    
    public static <T> String getMsg(T obj) {
        List<String> msgList = getMsgList(obj);
        return msgList.isEmpty() ? null : msgList.get(0);
    }
 
    
    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;
    }
 
    
    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;
    }
 
    
    private static <T> Object getValue(T obj, Field field) {
        try {
            return field.get(obj);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    
    private static <T> void setValue(T obj, Field field, Object val) {
        try {
            field.set(obj, val);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
 
    
    private static String getFieldName(Field field, Valid valid) {
        return valid.name().isEmpty() ? field.getName() : valid.name();
    }
 
    
    private static boolean isDefaultNull(Valid valid) {
        return "null".equals(valid.defaultValue());
    }
 
    
    private static String msg(String name, String msg) {
        return "【" + name + "】" + msg;
    }
 
    
    private static String requiredMsg(String name) {
        return msg(name, "不能为空");
    }
 
    
    private static String minLengthMsg(String name, Valid valid) {
        return msg(name, "不能少于" + valid.minLength() + "个字符");
    }
 
    
    private static String maxLengthMsg(String name, Valid valid) {
        return msg(name, "不能超过" + valid.maxLength() + "个字符");
    }
 
    
    private static String regexMsg(String name) {
        return msg(name, "填写格式不正确");
    }
 
    
    private static String minMsg(String name, Valid valid) {
        return msg(name, "不能小于" + valid.min());
    }
 
    
    private static String maxMsg(String name, Valid valid) {
        return msg(name, "不能大于" + valid.max());
    }
 
    
    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;
 

@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;
 

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


@Data
public class EntryApplyCancelReq {
 
    @Valid
    private Integer id;
 
    @Valid(name = "取消原因", maxLength = 50)
    private String reason;
 
}

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

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯