手寫一個(gè)@Valid字段校驗(yàn)器的示例代碼
上次給大家講述了 Springboot 中的 @Valid 注解 和 @Validated 注解的詳細(xì)用法:
詳解Spring中@Valid和@Validated注解用法
當(dāng)我們用上面這兩個(gè)注解的時(shí)候,需要首先在對(duì)應(yīng)的字段上打上規(guī)則注解,類似如下。
@Data
public class Employee {
/** 姓名 */
@NotBlank(message = "請(qǐng)輸入名稱")
@Length(message = "名稱不能超過(guò)個(gè) {max} 字符", max = 10)
public String name;
/** 年齡 */
@NotNull(message = "請(qǐng)輸入年齡")
@Range(message = "年齡范圍為 {min} 到 {max} 之間", min = 1, max = 100)
public Integer age;
}
其實(shí),在使用這些規(guī)則注解時(shí),我覺(jué)得不夠好用,比如我列舉幾個(gè)點(diǎn):
(1)針對(duì)每個(gè)字段時(shí),如果有多個(gè)校驗(yàn)規(guī)則,需要打多個(gè)對(duì)應(yīng)的規(guī)則注解,這時(shí)看上去,就會(huì)顯得較為臃腫。
(2)某些字段的類型根本不能校驗(yàn),比如在校驗(yàn) Double 類型的字段規(guī)則時(shí),打上任何校驗(yàn)注解,都會(huì)提示報(bào)錯(cuò),說(shuō)不支持 Double 類型的數(shù)據(jù);
(3)每打一個(gè)規(guī)則注解時(shí),都需要寫上對(duì)應(yīng)的 message 提示信息,這不但使得寫起來(lái)麻煩,而且代碼看起來(lái)又不雅觀,按理說(shuō),我們的一類規(guī)則提示應(yīng)該都是相同的,比如 "xxx不能為空",所以,按理來(lái)說(shuō),我只要配置一次提示格式,就可以不用再寫了,只需要配置每個(gè)字段的名稱xxx即可。
(4)一般來(lái)說(shuō),我們通常進(jìn)行字段校驗(yàn)時(shí),可能還需要一些額外的數(shù)據(jù)處理,比如去掉字符串前后的空格,某些數(shù)據(jù)可以為空的時(shí)候,我們還可以設(shè)置默認(rèn)值這些等。
(5)不能進(jìn)行擴(kuò)展,如果時(shí)自己寫的校驗(yàn)器,還可以進(jìn)行需求擴(kuò)展。
(6)他們?cè)龠M(jìn)行校驗(yàn)的時(shí)候,都需要再方法參數(shù)上打上一個(gè) @Valid 注解或者 @Validate 注解,如果我們采用 AOP 去切所有 controller 中的方法的話,那么我們寫的自定義規(guī)則校驗(yàn)器,甚至連方法參數(shù)注解都可以不用打,是不是又更加簡(jiǎn)潔了呢。
于是,介于上述點(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í),則對(duì)所有數(shù)據(jù)類型有效,不會(huì)設(shè)置默認(rèn)值)
*/
String defaultValue() default "";
/**
* 【String】是否在原來(lái)值的基礎(chǔ)上,去掉前后空格
*/
boolean trim() default true;
/**
* 【String】最小長(zhǎng)度
*/
int minLength() default 0;
/**
* 【String】最大長(zhǎng)度
*/
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)對(duì)象,獲取校驗(yàn)結(jié)果(單個(gè)提示)
*
* @param obj 待校驗(yàn)對(duì)象
* @return null-校驗(yàn)通過(guò),非null-校驗(yàn)未通過(guò)
*/
public static <T> String getMsg(T obj) {
List<String> msgList = getMsgList(obj);
return msgList.isEmpty() ? null : msgList.get(0);
}
/**
* 校驗(yàn)對(duì)象,獲取校驗(yàn)結(jié)果(所有提示)
*
* @param obj 待校驗(yàn)對(duì)象
* @return null-校驗(yàn)通過(guò),非null-校驗(yàn)未通過(guò)
*/
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) {
// 沒(méi)有打校驗(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();
}
// 最小長(zhǎng)度校驗(yàn)
int length = 0;
if (Objects.nonNull(val)) {
length = val.length();
}
if (length < valid.minLength()) {
return minLengthMsg(name, valid);
}
// 最大長(zhǎng)度校驗(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)通過(guò)后,則返回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)通過(guò)后,則返回null
return null;
}
private static <T> String validDouble(T obj, Field field, Valid valid) {
return null;
}
/**
* 獲取對(duì)象指定字段的值
*
* @param obj 原對(duì)象
* @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;
}
}
/**
* 給對(duì)象指定字段設(shè)值,一般校驗(yàn)后值可能有變化(生成默認(rèn)值/去掉前后空格等),需要新的值重新設(shè)置到對(duì)象中
*
* @param obj 原對(duì)象
* @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 字段對(duì)象
* @param valid 校驗(yàn)注解
* @return 字段名稱(如果注解有寫名稱,則取注解名稱;如果沒(méi)有注解名稱,則取字段)
*/
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 類型字段少于最小長(zhǎng)度提示
*
* @param name 字段名稱
* @param valid 校驗(yàn)注解
* @return 提示信息
*/
private static String minLengthMsg(String name, Valid valid) {
return msg(name, "不能少于" + valid.minLength() + "個(gè)字符");
}
/**
* String 類型字段超過(guò)最大長(zhǎng)度提示
*
* @param name 字段名稱
* @param valid 校驗(yàn)注解
* @return 提示信息
*/
private static String maxLengthMsg(String name, Valid valid) {
return msg(name, "不能超過(guò)" + 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 {
// 獲取所有請(qǐng)求對(duì)象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 獲取請(qǐng)求類型
String contentType = request.getHeader("Content-Type");
String json = null;
if (contentType != null && contentType.startsWith(APPLICATION_JSON)) {
// JSON請(qǐng)求體
json = JSON.toJSONString(jp.getArgs()[0]);
} else {
// 鍵值對(duì)參數(shù)
json = getParams(request);
}
// 獲取請(qǐng)求類對(duì)象
String validClassName = getParamClassName(jp);
String msg = valid(json, validClassName);
if (!isEmpty(msg)) {
throw new ValidException(msg);
}
}
/**
* 獲取方法參數(shù)對(duì)象名稱
*/
private String getParamClassName(JoinPoint jp) {
// 獲取參數(shù)對(duì)象
MethodSignature signature = (MethodSignature) jp.getSignature();
Class<?>[] types = signature.getParameterTypes();
// 沒(méi)有參數(shù)則不進(jìn)行校驗(yàn)
if (types == null || types.length == 0) {
return null;
}
// 返回項(xiàng)目中的對(duì)象類名
for (Class<?> clazz : types) {
if (clazz.getName().startsWith("com.unisoc.outsource")) {
return clazz.getName();
}
}
return null;
}
/**
* 獲取請(qǐng)求對(duì)象
*/
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)請(qǐng)求值合規(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ò)誤提示,所以我們可以采用拋異常的方式,最后對(duì)異常進(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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
你知道在Java中Integer和int的這些區(qū)別嗎?
最近面試,突然被問(wèn)道,說(shuō)一下Integer和int的區(qū)別.額…可能平時(shí)就知道寫一些業(yè)務(wù)代碼,包括面試的一些Spring源碼等,對(duì)于這種特別基礎(chǔ)的反而忽略了,導(dǎo)致面試的時(shí)候突然被問(wèn)到反而不知道怎么回答了.哎,還是乖乖再看看底層基礎(chǔ),順帶記錄一下把 ,需要的朋友可以參考下2021-06-06
SpringMVC?HttpMessageConverter報(bào)文信息轉(zhuǎn)換器
這篇文章主要為大家介紹了SpringMVC?HttpMessageConverter報(bào)文信息轉(zhuǎn)換器,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Java中增強(qiáng)for循環(huán)在一維數(shù)組和二維數(shù)組中的使用方法
下面小編就為大家?guī)?lái)一篇Java中增強(qiáng)for循環(huán)在一維數(shù)組和二維數(shù)組中的使用方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-10-10
java webApp異步上傳圖片實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了java webApp異步上傳圖片實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
基于javassist進(jìn)行動(dòng)態(tài)編程過(guò)程解析
這篇文章主要介紹了基于javassist進(jìn)行動(dòng)態(tài)編程過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
java計(jì)算兩個(gè)日期之前的天數(shù)實(shí)例(排除節(jié)假日和周末)
下面小編就為大家?guī)?lái)一篇java計(jì)算兩個(gè)日期之前的天數(shù)實(shí)例(排除節(jié)假日和周末)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07

