Spring Validator從零掌握對(duì)象校驗(yàn)的詳細(xì)過(guò)程
Spring Validator 學(xué)習(xí)指南:從零掌握對(duì)象校驗(yàn)
一、Validator 接口的作用:你的數(shù)據(jù)“守門(mén)員”
想象你開(kāi)發(fā)了一個(gè)用戶(hù)注冊(cè)功能,用戶(hù)提交的數(shù)據(jù)可能有各種問(wèn)題:名字沒(méi)填、年齡寫(xiě)成了負(fù)數(shù)……這些錯(cuò)誤如果直接保存到數(shù)據(jù)庫(kù),會(huì)導(dǎo)致后續(xù)流程出錯(cuò)。Validator 就像一位嚴(yán)格的守門(mén)員,在數(shù)據(jù)進(jìn)入系統(tǒng)前,檢查每個(gè)字段是否符合規(guī)則。
核心任務(wù):
- 檢查對(duì)象屬性是否合法(如非空、數(shù)值范圍)。
- 收集錯(cuò)誤信息,方便后續(xù)提示用戶(hù)。
二、Validator 接口的兩大方法:如何工作?
1. supports(Class clazz)
:我能處理這個(gè)對(duì)象嗎?
- 作用:判斷當(dāng)前 Validator 是否支持校驗(yàn)?zāi)硞€(gè)類(lèi)的對(duì)象。
- 關(guān)鍵選擇:
- 精確匹配:
return Person.class.equals(clazz);
→ 只校驗(yàn)Person
類(lèi)。 - 靈活匹配:
return Person.class.isAssignableFrom(clazz);
→ 支持Person
及其子類(lèi)。
- 精確匹配:
示例場(chǎng)景:
- 如果你有一個(gè)
Student extends Person
,使用equals
時(shí),Student
對(duì)象不會(huì)被校驗(yàn);使用isAssignableFrom
則會(huì)校驗(yàn)。
2. validate(Object target, Errors errors)
:執(zhí)行校驗(yàn)!
- 作用:編寫(xiě)具體的校驗(yàn)規(guī)則,發(fā)現(xiàn)錯(cuò)誤時(shí)記錄到
Errors
對(duì)象。 - 常用工具:
ValidationUtils
簡(jiǎn)化非空檢查。
示例代碼:
public void validate(Object target, Errors errors) { // 檢查 name 是否為空 ValidationUtils.rejectIfEmpty(errors, "name", "name.empty"); Person person = (Person) target; // 檢查年齡是否合法 if (person.getAge() < 0) { errors.rejectValue("age", "negative.age", "年齡不能為負(fù)數(shù)!"); } }
三、處理嵌套對(duì)象:如何避免重復(fù)代碼?
假設(shè)你有一個(gè) Customer
類(lèi),包含 Address
對(duì)象:
public class Customer { private String firstName; private String surname; private Address address; // 嵌套對(duì)象 }
問(wèn)題:直接在一個(gè) Validator 中校驗(yàn)所有字段
缺點(diǎn):
- 若其他類(lèi)(如
Order
)也包含Address
,需重復(fù)編寫(xiě)地址校驗(yàn)代碼。 - 維護(hù)困難:修改地址規(guī)則時(shí),需改動(dòng)多處代碼。
正確做法:
拆分 Validator,組合使用!
步驟 1:為每個(gè)類(lèi)創(chuàng)建獨(dú)立的 Validator
- AddressValidator(校驗(yàn)地址):
public class AddressValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Address.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmpty(errors, "street", "street.empty"); ValidationUtils.rejectIfEmpty(errors, "city", "city.empty"); } }
CustomerValidator(校驗(yàn)客戶(hù),并復(fù)用 AddressValidator):
public class CustomerValidator implements Validator { private final Validator addressValidator; // 通過(guò)構(gòu)造函數(shù)注入 AddressValidator public CustomerValidator(Validator addressValidator) { this.addressValidator = addressValidator; } @Override public boolean supports(Class<?> clazz) { return Customer.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { // 1. 校驗(yàn)客戶(hù)的直屬字段(firstName, surname) ValidationUtils.rejectIfEmpty(errors, "firstName", "firstName.empty"); ValidationUtils.rejectIfEmpty(errors, "surname", "surname.empty"); Customer customer = (Customer) target; Address address = customer.getAddress(); // 2. 校驗(yàn)嵌套的 Address 對(duì)象 if (address == null) { errors.rejectValue("address", "address.null"); return; } // 3. 關(guān)鍵:切換錯(cuò)誤路徑到 "address",防止字段名沖突 errors.pushNestedPath("address"); try { ValidationUtils.invokeValidator(addressValidator, address, errors); } finally { errors.popNestedPath(); // 恢復(fù)原始路徑 } } }
步驟 2:實(shí)際使用
// 創(chuàng)建 Validator AddressValidator addressValidator = new AddressValidator(); CustomerValidator customerValidator = new CustomerValidator(addressValidator); // 準(zhǔn)備測(cè)試數(shù)據(jù) Customer customer = new Customer(); customer.setFirstName(""); // 空名字 customer.setAddress(new Address()); // 空地址 // 執(zhí)行校驗(yàn) Errors errors = new BeanPropertyBindingResult(customer, "customer"); customerValidator.validate(customer, errors); // 輸出錯(cuò)誤 if (errors.hasErrors()) { errors.getAllErrors().forEach(error -> { System.out.println("字段:" + error.getObjectName() + "." + error.getCode()); }); } // 輸出結(jié)果: // 字段:customer.firstName.empty // 字段:customer.address.street.empty
四、關(guān)鍵技巧與常見(jiàn)問(wèn)題
1. 錯(cuò)誤路徑管理
pushNestedPath
和 popNestedPath
:
確保嵌套對(duì)象的錯(cuò)誤字段帶上前綴(如 address.street
),避免與主對(duì)象的字段名沖突。
2. 防御性編程
在組合 Validator 時(shí),檢查注入的 Validator 是否支持目標(biāo)類(lèi)型:
public CustomerValidator(Validator addressValidator) { if (!addressValidator.supports(Address.class)) { throw new IllegalArgumentException("必須支持 Address 類(lèi)型!"); } this.addressValidator = addressValidator; }
3. 國(guó)際化支持
錯(cuò)誤代碼(如 firstName.empty
)可對(duì)應(yīng)語(yǔ)言資源文件(如 messages_zh.properties
),實(shí)現(xiàn)多語(yǔ)言提示:
# messages_zh.properties firstName.empty=名字不能為空 address.street.empty=街道地址不能為空
五、總結(jié):為什么這樣設(shè)計(jì)?
- 代碼復(fù)用:
AddressValidator
可被其他需要校驗(yàn)地址的類(lèi)(如Order
、Company
)直接使用。 - 單一職責(zé):每個(gè) Validator 只負(fù)責(zé)一個(gè)類(lèi)的校驗(yàn),邏輯清晰,易于維護(hù)。
- 靈活擴(kuò)展:新增嵌套對(duì)象(如
PaymentInfo
)時(shí),只需創(chuàng)建新的 Validator 并注入,無(wú)需修改已有代碼。
3.2. 將錯(cuò)誤代碼解析為錯(cuò)誤信息:深入解析與實(shí)例演示
一、核心概念:錯(cuò)誤代碼的多層次解析
當(dāng)你在 Spring 中調(diào)用 rejectValue
方法注冊(cè)錯(cuò)誤時(shí)(例如校驗(yàn)用戶(hù)年齡不合法),Spring 不會(huì)只記錄你指定的單一錯(cuò)誤代碼,而是自動(dòng)生成一組層級(jí)化的錯(cuò)誤代碼。這種設(shè)計(jì)允許開(kāi)發(fā)者通過(guò)不同層級(jí)的錯(cuò)誤代碼,靈活定義錯(cuò)誤消息,實(shí)現(xiàn)“從具體到通用”的覆蓋策略。
二、錯(cuò)誤代碼生成規(guī)則
假設(shè)在 PersonValidator
中觸發(fā)以下校驗(yàn)邏輯:
errors.rejectValue("age", "too.darn.old");
生成的錯(cuò)誤代碼(按優(yōu)先級(jí)從高到低):
too.darn.old.age.int
→ 字段名 + 錯(cuò)誤代碼 + 字段類(lèi)型too.darn.old.age
→ 字段名 + 錯(cuò)誤代碼too.darn.old
→ 原始錯(cuò)誤代碼
三、消息資源文件的匹配策略
Spring 的 MessageSource
會(huì)按照錯(cuò)誤代碼的優(yōu)先級(jí)順序,在消息資源文件(如 messages.properties
)中查找對(duì)應(yīng)的消息。一旦找到匹配項(xiàng),立即停止搜索。
示例消息資源文件:
# messages.properties too.darn.old.age.int=年齡必須是整數(shù)且不超過(guò) 120 歲 too.darn.old.age=年齡不能超過(guò) 120 歲 too.darn.old=輸入的值不合理
匹配過(guò)程:
- 優(yōu)先查找
too.darn.old.age.int
→ 若存在則使用。 - 若未找到,查找
too.darn.old.age
→ 若存在則使用。 - 最后查找
too.darn.old
→ 作為兜底消息。
四、實(shí)戰(zhàn)演示:從代碼到錯(cuò)誤消息
步驟 1:創(chuàng)建實(shí)體類(lèi)與校驗(yàn)器
// Person.java public class Person { private String name; private int age; // getters/setters } // PersonValidator.java public class PersonValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Person.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { Person person = (Person) target; if (person.getAge() > 120) { errors.rejectValue("age", "too.darn.old"); } } }
步驟 2:配置消息資源文件
在 src/main/resources/messages.properties
中定義:
# 具體到字段類(lèi)型 too.darn.old.age.int=年齡必須是整數(shù)且不能超過(guò) 120 歲 # 具體到字段 too.darn.old.age=年齡不能超過(guò) 120 歲 # 通用錯(cuò)誤 too.darn.old=輸入的值無(wú)效
步驟 3:編寫(xiě)測(cè)試代碼
@SpringBootTest public class ValidationTest { @Autowired private MessageSource messageSource; @Test public void testAgeValidation() { Person person = new Person(); person.setAge(150); // 觸發(fā)錯(cuò)誤 Errors errors = new BeanPropertyBindingResult(person, "person"); PersonValidator validator = new PersonValidator(); validator.validate(person, errors); // 提取錯(cuò)誤消息 errors.getFieldErrors("age").forEach(error -> { String message = messageSource.getMessage(error.getCode(), null, Locale.getDefault()); System.out.println("錯(cuò)誤消息:" + message); }); } }
輸出結(jié)果:
錯(cuò)誤消息:年齡必須是整數(shù)且不能超過(guò) 120 歲
解析:
因?yàn)?too.darn.old.age.int
在消息文件中存在,優(yōu)先使用該消息。若刪除這行,則會(huì)匹配 too.darn.old.age
,以此類(lèi)推。
五、自定義錯(cuò)誤代碼生成策略
默認(rèn)的 DefaultMessageCodesResolver
生成的代碼格式為:錯(cuò)誤代碼 + 字段名 + 字段類(lèi)型
。
若需修改規(guī)則,可自定義 MessageCodesResolver
。
示例:簡(jiǎn)化錯(cuò)誤代碼
@Configuration public class ValidationConfig { @Bean public MessageCodesResolver messageCodesResolver() { DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver(); resolver.setMessageCodeFormatter(DefaultMessageCodesResolver.Format.POSTFIX_ERROR_CODE); return resolver; } }
效果:
調(diào)用 rejectValue("age", "too.darn.old")
生成的代碼變?yōu)椋?/p>
age.too.darn.old
too.darn.old
六、常見(jiàn)問(wèn)題與解決方案
問(wèn)題 1:如何查看實(shí)際生成的錯(cuò)誤代碼?
在測(cè)試代碼中打印錯(cuò)誤對(duì)象:
errors.getFieldErrors("age").forEach(error -> { System.out.println("錯(cuò)誤代碼列表:" + Arrays.toString(error.getCodes())); });
輸出:
錯(cuò)誤代碼列表:[too.darn.old.age.int, too.darn.old.age, too.darn.old]
問(wèn)題 2:字段類(lèi)型在代碼中如何表示?
Spring 使用字段的簡(jiǎn)單類(lèi)名(如 int
、String
)。對(duì)于自定義類(lèi)型(如 Address
),代碼中會(huì)使用 address
(類(lèi)名小寫(xiě))。
七、總結(jié):為何需要層級(jí)化錯(cuò)誤代碼?
- 靈活覆蓋:允許針對(duì)特定字段或類(lèi)型定制消息,同時(shí)提供通用兜底。
- 國(guó)際化友好:不同語(yǔ)言可定義不同層級(jí)的消息,無(wú)需修改代碼。
- 代碼解耦:校驗(yàn)邏輯與具體錯(cuò)誤消息分離,提高可維護(hù)性。
學(xué)習(xí)建議:
- 通過(guò)調(diào)試觀(guān)察
errors.getCodes()
的輸出,深入理解代碼生成規(guī)則。 - 在項(xiàng)目中優(yōu)先使用字段級(jí)錯(cuò)誤代碼(如
too.darn.old.age
),提高錯(cuò)誤消息的精準(zhǔn)度。
到此這篇關(guān)于Spring Validator 學(xué)習(xí)指南:從零掌握對(duì)象校驗(yàn)的文章就介紹到這了,更多相關(guān)Spring Validator 對(duì)象校驗(yàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot使用Validator進(jìn)行參數(shù)校驗(yàn)實(shí)戰(zhàn)教程(自定義校驗(yàn),分組校驗(yàn))
- 如何通過(guò)自定義spring?invalidator注解校驗(yàn)數(shù)據(jù)合法性
- Spring 校驗(yàn)(validator,JSR-303)簡(jiǎn)單實(shí)現(xiàn)方式
- springboot validator枚舉值校驗(yàn)功能實(shí)現(xiàn)
- SpringBoot 使用hibernate validator校驗(yàn)
- Spring中校驗(yàn)器(Validator)的深入講解
- springboot使用Validator校驗(yàn)方式
- springboot使用hibernate validator校驗(yàn)方式
相關(guān)文章
maven打包zip包含bin下啟動(dòng)腳本的完整代碼
這篇文章主要介紹了maven打包zip包含bin下啟動(dòng)腳本,本文給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-10-10SpringBoot中使用Guava實(shí)現(xiàn)單機(jī)令牌桶限流的示例
本文主要介紹了SpringBoot中使用Guava實(shí)現(xiàn)單機(jī)令牌桶限流的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06教你如何把Eclipse創(chuàng)建的Web項(xiàng)目(非Maven)導(dǎo)入Idea
這篇文章主要介紹了教你如何把Eclipse創(chuàng)建的Web項(xiàng)目(非Maven)導(dǎo)入Idea,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04淺談Java并發(fā)中ReentrantLock鎖應(yīng)該怎么用
本文主要介紹了ava并發(fā)中ReentrantLock鎖的具體使用,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11