<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
上次給大家講述了 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!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45