Java中的數據驗證

来源:https://www.cnblogs.com/cubacn/archive/2018/12/12/cuba-validate.html
-Advertisement-
Play Games

本文介紹了Java驗證的幾種機制,包括JPA驗證,Bean驗證,實體監聽器和事務監聽器。通過介紹希望可以在Java項目整體的驗證方面提供一些參考。 ...


 

原文鏈接:https://www.cuba-platform.com/blog/2018-10-09/945

翻譯:CUBA China

CUBA-Platform 官網 : https://www.cuba-platform.com

CUBA China 官網 : http://cuba-platform.cn

  我經常看見很多項目沒有數據驗證的策略和意識。他們的團隊在交付日期的重壓下,面對不清楚的需求,沒有時間去考慮用合適並且統一的方法對數據進行驗證。所以在這樣的項目中,到處能看見數據驗證的代碼:在前端JS中,在後端頁面控制器中,在業務邏輯的bean中,在數據模型實體中,在資料庫的約束和觸發器中。這些代碼都是一些 if-else 的語句,拋出一些不同的未檢查的異常,所以有時會很難找到這些該死的數據到底是在哪裡做的驗證。因此,一段時間之後,當項目成長到足夠大的時候就很難並且需要耗費很多精力來統一這些驗證,並且後面的需求也一樣模糊不清。

  那有沒有做數據驗證比較標準、優雅而且還簡潔的方法呢?這個方法不會導致代碼的不可讀,這個方法能幫我們將大部分數據驗證的代碼維護在統一的地方,而且有沒有可能一些流行框架的開發者已經替我們做了大部分的工作呢?

  當然有!

 

  作為我們CUBA平臺的開發者來說,讓我們的用戶也遵循最佳實踐非常重要。我們認為,數據驗證的代碼應該是:

    1. 可重用但不重覆,遵循DRY原則(Don’t Repeat Yourself)。

    2. 用乾凈和自然的方式表達出來。

    3. 放在開發人員期望看到的地方。

    4. 能對不同數據來源的數據進行檢查:用戶輸入,SOAP或者REST 調用等。

    5. 能處理併發。

    6. 由應用程式隱式統一調用而不需要手動調用這些檢查代碼。

    7. 能用簡潔的彈窗為用戶展示清晰,本地語言的消息。

    8. 遵循標準。

  這篇文章里,我將使用基於CUBA平臺開發的應用程式來演示所有的例子。由於CUBA是基於Spring和EclipseLink的,所以這些例子對於使用JPA和bean驗證的其他Java框架也適用。

 

資料庫約束驗證

  也許,最常用最直接的數據驗證方法就是使用資料庫級別的約束,比如非空,字元串長度,唯一索引等。對於企業級應用來說,這個方法很自然,因為這種類型的軟體通常都是以數據為中心。但是,即便是這種情況,開發者也經常出錯,在應用程式的各個數據層級分別定義了約束。這個問題主要是由於開發人員的不同責任分工引起的。

  我們看一個幾乎大家都會面對的例子,有的人甚至乾過這樣的事 :)。 假設有個規定要求護照號碼欄位需要有10個數字,很可能到處都會做這個規則檢查:資料庫設計者用DDL檢查,後臺開發人員在相應的實體和REST服務中檢查,最後前端工程師在客戶端代碼中檢查。之後這個需求變了,要求護照欄位升到15個數字。技術支持人員可能只修改了資料庫約束,但是這樣對於用戶來說等於什麼都沒改,因為後臺和前臺的檢查還沒修改呢。

  大家都知道避免這個問題的方法,驗證需要中心化。在CUBA,這種驗證的中心點在是實體的JPA註解。基於這個元數據信息,CUBA Studio可以生成正確的DDL腳本並且能在客戶端採用相應的驗證器。

 

  此時,如果JPA註解改變的話,CUBA會自動更新DDL腳本以及生成資料庫遷移腳本,所以下次部署項目的時候,新的基於JPA的限制將會在UI和DB生效。

  這種方式簡單、也能實施到底層資料庫級別,因此能完全防破解。但是JPA註解的局限性在於,只能使用在最簡單、可以用標準的DDL表述、而不需要引入特定資料庫的觸發器或者存儲過程的情況。所以基於JPA的約束可以用來保證實體欄位是唯一的,或者必須的,抑或也能定義varchar欄位的最大長度。還有,可以使用 @UniqueConstraint 註解來為一組欄位定義唯一性約束。但也就這些了。

  如果在需要更加複雜的驗證邏輯的的時候,比如檢查某個欄位的最大最小值或者對一個欄位使用正則表達式進行驗證,此時我們就需要使用眾所周知的叫做 “bean 驗證” 的方法了。

 

Bean 驗證

  我們知道,遵循標準是很好的實踐,通常這種方式有更長的生命周期而且有幾千個項目實戰證明過了。Java 的 Bean驗證是早就寫在石頭上的方案了:在JSR 380, 349 和 303也有些成熟的實現:Hibernate Validator Apache BVal

很多開發者都熟悉這個方法,但是這個方法的好處卻總是被低估。用這個方法甚至可以很容易在遺留項目中添加數據驗證,並且還能以清晰、直接、可靠最貼近業務邏輯的方式表達需要做的驗證。

 

  使用Bean驗證能為項目帶來很多好處:

  l  驗證邏輯集中在數據模型附近:使用最自然的方法定義針對值、方法和bean的約束,因此可以將OOP推進到下一個級別(驗證也可以OOP)。

  l Bean驗證的標準提供了幾十種 開箱即用的驗證註解比如 @NotNull, @Size, @Min, @Max, @Pattern, @Email, @Past, 不太標準的比如 @URL, @Length,強大的 @ScriptAssert,另外還有很多其他的。

  l 不會受限於僅使用預定義的約束,還可以自定義約束註解。可以定義一個註解來將其他幾個註解綁定到一起,或者定義一個全新的註解,然後定義一個相應的Java類作為驗證器。比如,之前那個例子中,可以定義一個類級別的註解 @ValidPassportNumber 用來檢查護照號碼是否符合正確的格式,號碼也許還依賴 country 欄位的值。

  l 不止可以在類和欄位上加約束,也可以添加到方法和方法參數上。這個叫做“合同驗證”,後面會介紹。

 

  CUBA平臺(以及一些其他平臺)會在用戶提交數據的時候自動調用這些驗證,所以一旦驗證失敗用戶會馬上看到錯誤消息,不需要考慮手動執行這些bean驗證。

  我們一起再看看護照號碼驗證的例子,但是這次我們還需要在實體添加幾個其他的驗證:

  l 人物姓名(例子中是用的英文名)至少有2個單詞或者可以更多,必須是格式化很好的姓名。檢查的正則表達式很複雜,比如 Charles Ogier de Batz de Castelmore Comte d'Artagnan 能通過檢查,但是 R2D2 卻不能通過。

  l 人物身高的區間:0< height <=300釐米。

  l 郵件地址需要是正確的郵件地址格式。

  因此,帶有所有這些檢查,Person類看起來是這樣:

 

  那些標準的註解,比如 @NotNull, @DecimalMin, @Length, @Pattern 還有其他幾個都是非常清楚的不需要過多解釋。主要看看自定義的 @ValidPassportNumber 是怎麼實現的。

  我們全新的 @ValidPassportNumber 會檢查 Person#passportNumber 是否符合針對每個國家(Person#country)定義的正則表達式。

  首先,按照文檔(CUBA Hibernate文檔是很好的參考)的描述,我們需要使用新的註解來標記實體類,以及將約束分組傳遞給這個註解。CUBA文檔有說,UiCrossFieldChecks.class 應當在所有單獨的欄位檢查完之後,才執行跨欄位的檢查,Default.class 能將約束添加到預設的驗證組。

  註解的定義是這樣的:

 

  @Target(ElementType.TYPE) 定義了註解在運行時生效的對象是一個類,@Constraint(validatedBy = … ) 聲明註解的實現在 ValidPassportNumberValidator 類中,此類需要實現 ConstraintValidator<...> 介面,在isValid(...) 方法中添加驗證代碼,方法也很直接:

 

  好了,足夠了。使用CUBA平臺不需要多寫任何代碼來保證這個驗證的運行,也不需要添加代碼在用戶輸入錯誤的時候給用戶發送消息通知。很簡單吧?

  現在,我們看看這些東西都是怎麼工作的,CUBA還做了一些額外的事情:不但給用戶展示錯誤消息,而且還將有問題的表單欄位高亮出來,這些漂亮的描紅欄位沒有通過單一欄位的bean驗證:

 

  是不是很簡潔?在用戶的瀏覽器顯示漂亮的錯誤提醒,只需要在實體中添加幾個簡單的註解就好了。

  作為本章節的總結,我們再簡單列舉一下實體的Bean驗證有什麼好處:

  1. 清晰可讀
  2. 可以直接在實體模型中定義值的約束
  3. 可擴展、可定製化
  4. 跟很多流行的ORM集成,檢查都是在實體保存在資料庫之前自動調用的
  5. 有些框架也能在用戶從UI提交數據的時候自動運行bean驗證(但是如果不支持的話,很難手動調用 Validator 介面)
  6. Bean驗證是眾所周知的標準,網上能找到很多相關文檔

  但是如果我們需要將驗證放到方法、構造器上或者放到某個REST終端來驗證從外部來的數據呢?或者我們想用聲明式的方法驗證方法參數而不是在每個方法內寫很多if-else這種枯燥的檢查參數的方法?

  答案很簡單,bean驗證也可以作用在方法上!

  

合同驗證

  有時候,我們需要前進一步,不只是做到應用的數據模型驗證。如果能做到參數和返回值自動驗證,那麼寫方法的時候就會容易很多。這個需求可能不只是用在檢查REST或者SOAP接入的數據,也會用在針對方法的輸入參數和返回值上。用來做所謂的前置條件和後置條件檢查,確保在方法體執行前對輸入參數的檢查,以及在方法執行後對返回值範圍的檢查,或者只是希望能聲明式的用在參數上限定參數的範圍以達到代碼更好的可讀性。

  使用合同驗證,就可以在任何Java類型的方法、構造器的參數和返回值上使用驗證。相對傳統的檢查參數和返回值的辦法,這個方案的優點是:

  l 不需要以極端的方式執行檢查(比如,拋出類似 illegalArgumentException 這樣的異常)。我們會更願意使用聲明式的約束,這樣會形成可讀性表達性更強的代碼。

  l 約束都是可重用、可配置、可定製化的:不需要每次都寫驗證代碼,更少的代碼意味著更少的bug。

  l 如果類、方法的返回值或參數使用了 @Validated 註解,平臺會在每個方法調用的時候自動執行約束檢查。

  l 如果一個可執行程式使用了 @Documented 註解,那麼它的前置條件和後置條件會自動包含在生成的JavaDoc中。

  因此,使用合同驗證方案,會有清晰、相對少的代碼,更易於維護和理解。

  我們看看在CUBA應用的REST控制器中,使用合同驗證的代碼大概是什麼樣的。通過 PersonApiService 介面的 getPerson() 方法可以從資料庫獲取用戶的列表,使用 addNewPerson(…) 方法可以添加新用戶。需要註意的是,bean驗證是可以繼承的!也就是說,如果用驗證的註解標記了某些類,欄位或者方法,那麼這個類的後代或者介面的實現類都會受到這些驗證的影響。

 

 

  這個代碼片段看起來怎樣,是不是非常清晰,可讀性也不錯?(除了 @RequiredView(“_local”)  註解,這個是CUBA平臺的專有註解,確保返回的Person對象會有 PASSPORTNUMBER_PERSON 表的所有欄位)。

  @Valid 註解指定 getPerson() 方法的返回列表中的每個對象需要使用 Person 類的驗證進行檢查。

  CUBA會自動生成下列路徑用來執行這些API:

  l /app/rest/v2/services/passportnumber_PersonApiService/getPersons

  l /app/rest/v2/services/passportnumber_PersonApiService/addNewPerson

  我們打開Postman試試這些驗證是否都好用:

 

  你可能會註意到,上面的例子沒有驗證護照號碼。這是因為這個需要在 addNewPerson 做跨參數驗證,passportNumber 的驗證正則表達式依賴 country 的值。這種跨參數的驗證跟實體類級別約束是一樣的。

  JSR 349 和 380 支持跨參數驗證, 可以查閱 hibernate 文檔瞭解如何為類/介面方法實施自定義的跨參數驗證。

 

超越Bean驗證

  世上沒有什麼是完美的,bean驗證也有局限性:

  l 有時候需要在保存更改之前檢查複雜的對象關係圖的狀態。比如,可能需要檢查客戶在你電商網站的訂單中購買的所有東西是否能裝到一個快遞箱子中。這是個比較繁重的檢查,因此每次客戶訂單的商品變更的時候都做這個檢查不合適。所以這個檢查應該只需要在Order對象和它的OrderItem對象保存到資料庫之前做一次。

  l 有些檢查需要在資料庫的事務中做。比如,電商系統需要在訂單保存到資料庫之前檢查是否有足夠的庫存。這些檢查只能在事務級別,因為系統是併發的,庫存的數量是實時變化的。

  CUBA平臺提供了兩個在數據提交之前做驗證的機制:實體監聽器事務監聽器我們仔細看看。

 

實體監聽器

  CUBA的實體監聽器JPA提供的 PreInsertEvent, PreUpdateEvent 和 PredDeleteEvent 監聽器非常相似。這兩種機制都都可以在實體對象持久化到資料庫之前或者之後做檢查。

  在CUBA中定義和組織實體監聽器不難,只需要兩步:

  1. 創建實現了實體監聽器介面的托管bean。作為數據驗證方面的考慮,其中三個介面比較重要:BeforeDeleteEntityListener,BeforeInsertEntityListener以及BeforeUpdateEntityListener。
  2. 在需要做驗證的實體用 @Listeners 註解標記

  可以了。

  跟JPA標準(JSR 338 3.5)不一樣,CUBA的監聽器介面是帶數據類型的,所以不需要在方法內做類型轉換,可以直接使用實體。CUBA平臺還提供了跟當前實體關聯的實體以及通過EntityManager去載入或者更改其他任何實體的機制。這些改動也會調用相應的實體監聽器。

  另外,CUBA平臺支持“軟刪除(soft deletion”,實體在資料庫只是標記為刪除,但是不會真正刪除資料庫記錄。所以對於軟刪除,CUBA平臺會調用 BeforeDeleteEntityListener / AfterDeleteEntityListener 而標準的實現則會調用 PreUpdate / PostUpdate。

  看看下麵的例子吧。事件監聽器的bean跟實體類連接,只需要一行註解:@Listeners,註解使用的參數是監聽器類的名稱。

 

  實體監聽器的實現是這樣的:

 

  實體監聽器有時候很有用:

  l 在實體持久化到資料庫之前需要在事務內做檢查

  l 需要在驗證的過程中訪問資料庫信息,比如在保存訂單之前先檢查庫存的數量

  l 需要遍歷實體關聯或者組合的實體,比如Order裡面的OrderItem實體

  l 需要跟蹤某些實體的增/刪/改操作,比如希望跟蹤Order和OrderItem的變化情況

 

事務監聽器

  CUBA 事務監聽器也在事務的上下文環境中工作,但是跟實體監聽器不一樣的是,事務監聽器是在事務級別被調用的。

  因此,事務監聽器是終極大殺器,能監管到所有的資料庫交互,但是這樣也帶來了弱點:

  l 不是很好編碼

  l 如果做太多檢查會顯著的降低性能

  l 編碼需要很小心,一個bug可能會導致整個應用都啟動不了

  所以事務監聽器在需要用同一演算法檢查很多不同類型的實體的時候是個好辦法。比如需要給支持所有業務的“欺詐偵探器”填充數據的時候。

 

  我們看看下麵這個例子,檢查是否有實體帶有 @FraudDetectionFlag 註解,如果有的話,調用欺詐偵探器來檢查一下。註意,這個方法會在每次資料庫提交的事務都調用,所以代碼需要儘可能少的檢查數據對象,並且越快越好。

 

  只需要實現 BeforeCommitTransactionListener 介面的 beforeCommit 方法,托管bean就會變成事務監聽器。事務監聽器會在應用啟動的時候自動裝載。CUBA會將所有實現了 BeforeCommitTransactionListener 或者 AfterCompleteTransactionListener 介面的類註冊為事務監聽器。

 

結論

  Bean 驗證(JPA 303 349 980)基本能滿足企業級應用中 95% 的數據驗證的情況。這個方案最大的優點是,大部分驗證的邏輯都集中到了數據模型類中。因此很容易找到代碼,可讀性強還容易維護。Spring,CUBA以及很多類庫都能知道這些標準並且在UI輸入值的時候,調用方法的時候或者做ORM持久化的時候自動調用驗證代碼,從開發者角度來說,這些驗證就像是小魔法。

  有些軟體工程師認為,在數據模型層面做的驗證複雜且帶有侵入性,覺得在UI層做驗證就夠了。但是,我個人覺得,在UI或者UI控制器中寫很多驗證點是很容易出問題的。另外,我們這裡討論的驗證方法在跟平臺集成的時候,並不是侵入性的代碼,因為平臺會感知這些驗證器、監聽器然後將它們自動集成到客戶端層。

  最後,我們制定一個經驗規則來選擇最佳的驗證方法:

  l JPA驗證:功能有限,但是在實體類上做最簡單的約束是最好的選擇。要求這些約束能映射成DDL

  l Bean驗證:靈活、簡潔、聲明式、可重用而且易讀。基本上能覆蓋模型中需要的所有驗證,如果不需要在事務中進行驗證的話,這是最好的選擇

  l 合同驗證:也是一種bean驗證,不過是應用在方法上。如果需要檢查輸入和輸出參數,比如REST調用,可以使用這個方法

  l 實體監聽器:儘管不像bean驗證那樣是使用全部聲明式的方式,但是可以在資料庫事務中對比較複雜的對象關係圖做驗證。比如需要從資料庫載入一些信息來做決定。Hibernate也有類似的監聽器

  l 事務監聽器:危險但是這是事務級別的終極武器。如果需要在運行時對實體進行驗證或者需要對很多不同類型的實體使用同一種驗證方法的時候可以選用

  希望這篇文章能刷新你對於Java企業級應用中驗證方法的記憶,也希望在提升項目架構方面提供一點點參考。

 

 

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 嗷嗷方便的文字轉語音,不過用的時候記得到百度語音上申請key,免費的.之前在網路上看到有人寫了一部分,自己豐富下,以後用也方便 ...
  • 來自:https://blog.csdn.net/m0_37068028/article/details/72898154 侵刪 來自:https://segmentfault.com/a/1190000010378259 侵刪 第一種: //date.jsexport function forma ...
  • tofixed方法 四捨五入 toFixed() 方法可把 Number 四捨五入為指定小數位數的數字。例如將數據Num保留2位小數,則表示為:toFixed(Num);但是其四捨五入的規則與數學中的規則不同,使用的是銀行家舍入規則,銀行家舍入:所謂銀行家舍入法,其實質是一種四舍六入五取偶(又稱四舍 ...
  • 示例html代碼: 示例html代碼: 示例html代碼: 示例html代碼: 示例html代碼: <div id="test"> <span style="color:red">test1</span> test2 </div> 獲得id為test的DOM對象,下麵就不一一獲取了。 var tes ...
  • 面向對象的語言有一個標誌,那就是它們都有類的概念,而通過類可以創建任意多個具有相同屬性和方法的對象。 理解對象 創建自定義對象的最簡單的方法就是創建一個Object的實例,然後再為它添加屬性和方法。例如: 同樣上面的例子可以通過對象字面量語法寫成如下: 屬性類型 ECMAScript中有兩種屬性:數 ...
  • Steps步驟條組件源碼: steps.vue step.vue ...
  • 1、Node.js簡介 簡單的說 Node.js 就是運行在服務端的 JavaScript。Node.js 是一個基於 Chrome V8 引擎的 JavaScript 運行環境。Node.js 使用了一個事件驅動、非阻塞式 I/O 的模型,使其輕量又高效。Node.js 的包管理器 npm,是全球 ...
  • 如何學好面向對象? 面向對象雖然只有三個特性,封裝、繼承、多態,但是真正面向對象卻是說的容易做起來困難。但是,還是有一定的規則可尋的, 要學好面向對象,必須掌握設計模式 。 什麼是設計模式? 設計模式(Design pattern):是一套被反覆使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...