首頁 > 軟體

手寫一個@Valid欄位校驗器的範例程式碼

2022-07-22 10:01:17

上次給大家講述了 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欄位校驗器內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com