手寫一個(gè)@Valid字段校驗(yàn)器的示例代碼
上次給大家講述了 Springboot 中的 @Valid 注解 和 @Validated 注解的詳細(xì)用法:
詳解Spring中@Valid和@Validated注解用法
當(dāng)我們用上面這兩個(gè)注解的時(shí)候,需要首先在對應(yīng)的字段上打上規(guī)則注解,類似如下。
@Data
public class Employee {
/** 姓名 */
@NotBlank(message = "請輸入名稱")
@Length(message = "名稱不能超過個(gè) {max} 字符", max = 10)
public String name;
/** 年齡 */
@NotNull(message = "請輸入年齡")
@Range(message = "年齡范圍為 {min} 到 {max} 之間", min = 1, max = 100)
public Integer age;
}
其實(shí),在使用這些規(guī)則注解時(shí),我覺得不夠好用,比如我列舉幾個(gè)點(diǎn):
(1)針對每個(gè)字段時(shí),如果有多個(gè)校驗(yàn)規(guī)則,需要打多個(gè)對應(yīng)的規(guī)則注解,這時(shí)看上去,就會(huì)顯得較為臃腫。
(2)某些字段的類型根本不能校驗(yàn),比如在校驗(yàn) Double 類型的字段規(guī)則時(shí),打上任何校驗(yàn)注解,都會(huì)提示報(bào)錯(cuò),說不支持 Double 類型的數(shù)據(jù);
(3)每打一個(gè)規(guī)則注解時(shí),都需要寫上對應(yīng)的 message 提示信息,這不但使得寫起來麻煩,而且代碼看起來又不雅觀,按理說,我們的一類規(guī)則提示應(yīng)該都是相同的,比如 "xxx不能為空",所以,按理來說,我只要配置一次提示格式,就可以不用再寫了,只需要配置每個(gè)字段的名稱xxx即可。
(4)一般來說,我們通常進(jìn)行字段校驗(yàn)時(shí),可能還需要一些額外的數(shù)據(jù)處理,比如去掉字符串前后的空格,某些數(shù)據(jù)可以為空的時(shí)候,我們還可以設(shè)置默認(rèn)值這些等。
(5)不能進(jìn)行擴(kuò)展,如果時(shí)自己寫的校驗(yàn)器,還可以進(jìn)行需求擴(kuò)展。
(6)他們再進(jìn)行校驗(yàn)的時(shí)候,都需要再方法參數(shù)上打上一個(gè) @Valid 注解或者 @Validate 注解,如果我們采用 AOP 去切所有 controller 中的方法的話,那么我們寫的自定義規(guī)則校驗(yàn)器,甚至連方法參數(shù)注解都可以不用打,是不是又更加簡潔了呢。
于是,介于上述點(diǎn),寫了一個(gè)自定義注解校驗(yàn)器,包括下面幾個(gè)文件:
Valid
這個(gè)注解作用于字段上,用于規(guī)則校驗(yàn)。
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;
/**
* 字段校驗(yàn)注解
*
* @author zyqok
* @since 2022/05/06
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Valid {
/**
* 屬性名稱
*/
String name() default "";
/**
* 是否可為空
*/
boolean required() default true;
/**
* 默認(rèn)值(如果默認(rèn)值寫 null 時(shí),則對所有數(shù)據(jù)類型有效,不會(huì)設(shè)置默認(rèn)值)
*/
String defaultValue() default "";
/**
* 【String】是否在原來值的基礎(chǔ)上,去掉前后空格
*/
boolean trim() default true;
/**
* 【String】最小長度
*/
int minLength() default 0;
/**
* 【String】最大長度
*/
int maxLength() default 255;
/**
* 【String】自定義正則校驗(yàn)(該配置為空時(shí)則不進(jìn)行正則校驗(yàn))
*/
String regex() default "";
/**
* 【Integer】【Long】【Double】范圍校驗(yàn)最小值(該配置為空時(shí)則不進(jìn)行校驗(yàn))
*/
String min() default "";
/**
* 【Integer】【Long】【Double】范圍校驗(yàn)最大值(該配置為空時(shí)則不進(jìn)行校驗(yàn))
*/
String max() default "";
}ValidUtils
自定義規(guī)則校驗(yàn)工具類
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;
/**
* 字段校驗(yàn)注解工具
*
* @author zyqok
* @since 2022/05/05
*/
public class ValidUtils {
/**
* 校驗(yàn)對象,獲取校驗(yàn)結(jié)果(單個(gè)提示)
*
* @param obj 待校驗(yàn)對象
* @return null-校驗(yàn)通過,非null-校驗(yàn)未通過
*/
public static <T> String getMsg(T obj) {
List<String> msgList = getMsgList(obj);
return msgList.isEmpty() ? null : msgList.get(0);
}
/**
* 校驗(yàn)對象,獲取校驗(yàn)結(jié)果(所有提示)
*
* @param obj 待校驗(yàn)對象
* @return null-校驗(yàn)通過,非null-校驗(yàn)未通過
*/
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) {
// 沒有打校驗(yàn)注解的字段則不進(jìn)行校驗(yàn)
Valid valid = field.getAnnotation(Valid.class);
if (Objects.isNull(valid)) {
continue;
}
field.setAccessible(true);
// String 類型字段校驗(yàn)
if (field.getType().isAssignableFrom(String.class)) {
String msg = validString(obj, field, valid);
if (Objects.nonNull(msg)) {
msgList.add(msg);
}
continue;
}
// int / Integer 類型字符校驗(yàn)
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 類型字段校驗(yàn)
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;
}
/**
* 校驗(yàn)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);
}
// 是否有默認(rèn)值
if (val.isEmpty()) {
val = isDefaultNull(valid) ? null : valid.defaultValue();
}
// 最小長度校驗(yàn)
int length = 0;
if (Objects.nonNull(val)) {
length = val.length();
}
if (length < valid.minLength()) {
return minLengthMsg(name, valid);
}
// 最大長度校驗(yàn)
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);
// 如果所有校驗(yàn)通過后,則返回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);
}
// 是否有默認(rèn)值
if (Objects.isNull(val)) {
boolean defaultNull = isDefaultNull(valid);
if (!defaultNull) {
val = parseInt(valid.defaultValue());
}
}
// 校驗(yàn)最小值
if (!valid.min().isEmpty() && Objects.nonNull(val)) {
int min = parseInt(valid.min());
if (val < min) {
return minMsg(name, valid);
}
}
// 校驗(yàn)最大值
if (!valid.max().isEmpty() && Objects.nonNull(val)) {
int max = parseInt(valid.max());
if (val > max) {
return maxMsg(name, valid);
}
}
// 將值重新寫入原字段中
setValue(obj, field, val);
// 如果所有校驗(yàn)通過后,則返回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;
}
}
/**
* 給對象指定字段設(shè)值,一般校驗(yàn)后值可能有變化(生成默認(rèn)值/去掉前后空格等),需要新的值重新設(shè)置到對象中
*
* @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();
}
}
/**
* 獲取字段名稱(主要用于錯(cuò)誤時(shí)提示用)
*
* @param field 字段對象
* @param valid 校驗(yàn)注解
* @return 字段名稱(如果注解有寫名稱,則取注解名稱;如果沒有注解名稱,則取字段)
*/
private static String getFieldName(Field field, Valid valid) {
return valid.name().isEmpty() ? field.getName() : valid.name();
}
/**
* 該字段是否默認(rèn)為 null
*
* @param valid 校驗(yàn)注解
* @return true - 默認(rèn)為 null; false - 默認(rèn)不為 null
*/
private static boolean isDefaultNull(Valid valid) {
return "null".equals(valid.defaultValue());
}
/**
* 提示信息(該方法用于統(tǒng)一格式化提示信息樣式)
*
* @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 校驗(yàn)注解
* @return 提示信息
*/
private static String minLengthMsg(String name, Valid valid) {
return msg(name, "不能少于" + valid.minLength() + "個(gè)字符");
}
/**
* String 類型字段超過最大長度提示
*
* @param name 字段名稱
* @param valid 校驗(yàn)注解
* @return 提示信息
*/
private static String maxLengthMsg(String name, Valid valid) {
return msg(name, "不能超過" + valid.maxLength() + "個(gè)字符");
}
/**
* String 類型正則校驗(yàn)提示
*
* @param name 字段名稱
* @return 提示信息
*/
private static String regexMsg(String name) {
return msg(name, "填寫格式不正確");
}
/**
* 數(shù)字類型小于最小值的提示
*
* @param name 字段名稱
* @param valid 校驗(yàn)注解
* @return 提示信息
*/
private static String minMsg(String name, Valid valid) {
return msg(name, "不能小于" + valid.min());
}
/**
* 數(shù)字類型大于最大值的提示
*
* @param name 字段名稱
* @param valid 校驗(yàn)注解
* @return 提示信息
*/
private static String maxMsg(String name, Valid valid) {
return msg(name, "不能大于" + valid.max());
}
/**
* 將字符串?dāng)?shù)字轉(zhuǎn)化為 int 類型的數(shù)字,轉(zhuǎn)換異常時(shí)返回 0
*
* @param intStr 字符串?dāng)?shù)字
* @return int 類型數(shù)字
*/
private static int parseInt(String intStr) {
try {
return Integer.valueOf(intStr);
} catch (NumberFormatException e) {
return 0;
}
}
}ValidAop
這是一個(gè) controller 攔截切面,寫了這個(gè),就不用再 controller 方法參數(shù)上打上類似于原@Valid 和 @Validate 注解,還原的方法參數(shù)的原始整潔度。
但需要注意的是:類中 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 {
// 鍵值對參數(shù)
json = getParams(request);
}
// 獲取請求類對象
String validClassName = getParamClassName(jp);
String msg = valid(json, validClassName);
if (!isEmpty(msg)) {
throw new ValidException(msg);
}
}
/**
* 獲取方法參數(shù)對象名稱
*/
private String getParamClassName(JoinPoint jp) {
// 獲取參數(shù)對象
MethodSignature signature = (MethodSignature) jp.getSignature();
Class<?>[] types = signature.getParameterTypes();
// 沒有參數(shù)則不進(jìn)行校驗(yàn)
if (types == null || types.length == 0) {
return null;
}
// 返回項(xiàng)目中的對象類名
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();
}
/**
* 校驗(yàn)請求值合規(guī)性
*/
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;
}
}
/**
* 校驗(yàn)字符串是否為空
*/
private boolean isEmpty(String s) {
return Objects.isNull(s) || s.trim().isEmpty();
}
}ValidException
因?yàn)?AOP 切面里,不能在前置切面中直接返回校驗(yàn)規(guī)則的錯(cuò)誤提示,所以我們可以采用拋異常的方式,最后對異常進(jìn)行捕捉,再提示給用戶(原 Springboot 的 @Validate 也是采用類似方式進(jìn)行處理)。
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
這個(gè)異常處理器就是用于捕捉上面的異常,最后提示給前端。
@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;
}
}
當(dāng)把所有文件復(fù)制到文件中后,那么在使用的時(shí)候

只需要將方法中的參數(shù)打上我們定義的 @Valid 即可,其余不用做任何操作就OK
/**
* @author zyqok
* @since 2022/05/06
*/
@Data
public class EntryApplyCancelReq {
@Valid
private Integer id;
@Valid(name = "取消原因", maxLength = 50)
private String reason;
}到此這篇關(guān)于手寫一個(gè)@Valid字段校驗(yàn)器的示例代碼的文章就介紹到這了,更多相關(guān)@Valid字段校驗(yàn)器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA啟動(dòng)服務(wù)提示端口被占用,Web?server?failed?to?start.Port?was?al
這篇文章主要介紹了IDEA啟動(dòng)服務(wù)提示端口被占用,Web?server?failed?to?start.Port?was?already?in?use.,本文給大家分享解決方案,分為linux系統(tǒng)和windows系統(tǒng)解決方案,需要的朋友可以參考下2023-07-07
詳解Java中AC自動(dòng)機(jī)的原理與實(shí)現(xiàn)
AC自動(dòng)機(jī)是一個(gè)多模式匹配算法,在模式匹配領(lǐng)域被廣泛應(yīng)用。本文將詳細(xì)為大家介紹AC自動(dòng)機(jī)的原理與實(shí)現(xiàn)方法,感興趣的可以了解一下2022-05-05
Java concurrency集合之LinkedBlockingDeque_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
LinkedBlockingDeque是雙向鏈表實(shí)現(xiàn)的雙向并發(fā)阻塞隊(duì)列。該阻塞隊(duì)列同時(shí)支持FIFO和FILO兩種操作方式,即可以從隊(duì)列的頭和尾同時(shí)操作(插入/刪除);并且,該阻塞隊(duì)列是支持線程安全。2017-06-06
Maven報(bào)錯(cuò)之導(dǎo)入Junit包來實(shí)現(xiàn)@Test注解問題
這篇文章主要介紹了Maven報(bào)錯(cuò)之導(dǎo)入Junit包來實(shí)現(xiàn)@Test注解問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
SpringBoot中Shiro緩存使用Redis、Ehcache的方法
這篇文章主要介紹了SpringBoot中Shiro緩存使用Redis、Ehcache的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
如何將Java枚舉名稱作為注解的屬性值實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了如何將Java枚舉名稱作為注解的屬性值實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05

