首頁 > 軟體

「技術分享」Java項目中參數校驗優雅實踐分享

2021-05-21 14:30:57

一、不厭其煩的 if else?

參數校驗,為了保護自己的程式碼,一般都會在開發中假設所有的參數都是不可靠的。針對所有的參數校驗場景自己一次進行判斷及錯誤資訊的提示。

例如:

if (a.size > 10 && a.size < 100) { Result result = Reuslt.fail("非法參數size , 請檢查輸入!") ; return result; } if (xxx) return xxx ;

還有一種case,重一點的業務參數校驗,有時候也會被不厭其煩地校驗,散落在各個子系統或者系統的各處模組程式碼中。例如

if (!validItem(itemId)) { Result result = Reuslt.fail("不存在的商品id , 請檢查輸入!") ; return result;}private boolean validItem(itemId){ // RPC getItem // 是否空判斷 return item != null;}

針對以上的場景,本文探討一下如何優雅地在業務系統中做參數校驗,分享構建通用校驗模組的一些實踐。

二、業內框架 hibernate validator

1 簡介JSR提供了一套Bean校驗規範的API,維護在包javax.validation.constraints下。該規範使用屬性或者方法參數或者類上的一套簡潔易用的註解來做參數校驗。開發者在開發過程中,僅需在需要校驗的地方加上形如@NotNull, @NotEmpty , @Email的註解,就可以將參數校驗的重任委託給一些第三方校驗框架來處理。引自網路:

JSR-303 是 JAVA EE 6 中的一項子規範,叫做 Bean Validation,官方參考實現是Hibernate Validator。此實現與 Hibernate ORM 沒有任何關係。JSR 303 用於對 Java Bean 中的欄位的值進行驗證。 Spring MVC 3.x 之中也大力支援 JSR-303,可以在控制器中對錶單提交的資料方便地驗證。

注:可以使用註解的方式進行驗證。

接入validation api及hibernate validator後,做普通的參數校驗簡單到不行:

@Email@NotNull@Pattern@AssertFalse......

而像業務系統中常見的校驗,Hibernate Validator是無法支援的,例如校驗訂單號是否有效,訂單上的商品id是否真實有效,這種校驗Hibernate也留了口子,可以自行定義註解,同時自行定義校驗邏輯後依賴SPI機制註冊到Hibernate Validator中即可。

如何自定義業務參數校驗API及其校驗實現,可以參考官方文件,不再贅述Hibernate Validator的用法。2 實現原理可以想下如果自己做一套支援JSR303 bean校驗規範的校驗框架,我們會如何實現。其實無非是讀取class元資料,獲取bean類上的所有帶有校驗註解的屬性,在每次需要校驗物件的時候,拿到物件對應屬性的值來與其上的所有校驗註解來執行校驗實現邏輯,然後收集所有不通過的資訊。hibernate validator的實現核心原理也是如此:

上圖僅展示一些Hibernate validator的核心元件,實際上有非常多的細節,不在此贅述,有興趣瞭解全流程的同學可以自行debug一下,並不是非常複雜。

校驗的過程:

配置Hibernate Validator,把所有相關的非懶載入的核心元件都進行初始化,依賴java的SPI機制支援自定義validator進行校驗,先進行class資料解析,然後獲取對應屬性的對應validator進行校驗, 最後通過MessageInterpolator元件進行校驗錯誤資訊的提取。依賴java的ResourceBundle機制支援校驗資訊多語言。

三、優雅實踐基於hibernate validator,怎麼可以做一些優雅實踐呢?

hibernate validator僅是bean校驗框架, 可能還需要做一些適配才可以讓我們在業務系統開發中,下面分享一下一些開發實踐,核心追求的是業務邏輯與參數校驗邏輯完全解耦合,常用的業務參數校驗邏輯可以在多套業務系統中被複用以及統一維護所有的校驗錯誤資訊。概要圖:

RPC與WEB系統部署架構圖:

攔截所有請求,可以基於RPC filter和Spring MVC的HandlerInterceptor來實現RPC請求,和HTTP請求的攔截 , 攔截器中使用validator校驗參數,失敗的話直接設定失敗資訊,快速返回。統一參數校驗包,純粹的校驗API,所有校驗以註解形式做抽象,支援簡單複用 , 形如@NotNull , @ExistItem , @ExistBarcode的作用於參數上的註解, 這個可以複用JSR校驗規範來實現。統一參數校驗的實現(validator) ,所有的校驗註解對應的校驗邏輯實現以統一maven依賴形式提供 , 和校驗API一一對應。hibernate validator進行擴展,校驗錯誤資訊解析統一維護於配置中心,接入在配置中心可以在運行時動態修改,以本地檔案形式儲存校驗提示資訊也並無不可,只是維護起來複雜麻煩。維護簡單易用的starter,開箱即用,支援所有業務系統快速接入。

一些程式碼實現(需要自取):RPC filter & ResourceBundle

// 使用自定義的配置中心資訊源 初始化validator public static Validator validator; static { HibernateValidatorConfiguration configure = Validation.byProvider(HibernateValidator.class).configure(); ResourceBundleLocator defaultResourceBundleLocator = configure.getDefaultResourceBundleLocator(); ResourceBundleLocator myResourceBundleLocator = new MyResourceBundleLocator(defaultResourceBundleLocator); configure.messageInterpolator( new ResourceBundleMessageInterpolator(myResourceBundleLocator)); configure.enableTraversableResolverResultCache(false); validator = configure.buildValidatorFactory().getValidator(); }// RPC服務:校驗失敗時候直接mockresponse快速返回,response中設定errorMsg String message = collectValidateMessage(args); if (StringUtils.isNotEmpty(message)) { // fail fast RPCResult rpcResult = new RPCResult(); rpcResult.setHsfResponse(new HSFResponse()); rpcResult.setAppResponse(mockResponse(invocation, message)); SettableFuture<RPCResult> defaultRPCFuture = Futures.createSettableFuture(); defaultRPCFuture.set(rpcResult); return defaultRPCFuture; // 配置中心的ResourceBundle public class DiamondResourceBundle extends ResourceBundle { private static final Properties properties = new Properties(); public DiamondResourceBundle() { try { init(); } catch (IOException e) { log.error("初始化diamond資料失敗 ", e); } } private void init() throws IOException { // load once loadConfig(Diamond.getConfig(DATA_ID, GROUP_ID, 5000)); // add listener Diamond.addListener(DATA_ID, GROUP_ID, new ManagerListener() { @Override public Executor getExecutor() { return pushExecutor; } @Override public void receiveConfigInfo(String configInfo) { log.error("receive config : {} ", configInfo); // load config loadConfig(configInfo); clearCache(); } }); } }

四 優秀框架校驗實現

實際上參數校驗是所有coder都會遇到的問題,如何更加優雅地解決參數校驗的問題呢? 列舉一些框架,一起學習一下他們如何做參數校驗:

1 SpringSpring

沒有使用任何的參數校驗框架,使用其維護的Assert工具類+常用的參數異常來做參數校驗。所有的參數校驗都是在編碼時候書寫的。

都是使用Assert.notNull , Assert.notEmpty等來做參數校驗。Spring主要還是面向開發的框架,出現參數異常其資訊是面向開發者的,與我們這種面向使用者的校驗存在區別。不會出現業務參數校驗失敗的情況,人肉校驗簡單參數也無可厚非。而且Spring是作用在應用啟動時候的框架,對使用者理論上無影響。

2 Feign

看了一些如Feign的基礎框架,都是手動校驗的參數,不復雜, 這裡不一一列舉了。

基礎框架和業務系統有根本上的差異,基礎框架是面向開發人員的框架,大部分都是在系統部署時候啟動,校驗有異常的話都是直接拋出,開發人員可以根據錯誤資訊及時排查。而業務系統,敲程式碼嗖嗖嗖的敲完了業務邏輯,如果參數傳的有誤,可能會直接導致系統不可用。這種情況可能由於介面呼叫方沒有使用準確參數,前端沒有做參數校驗等等,但無論如何,我們必須保證自身系統是穩定可靠的,尤其需要使我們的系統遠離「外部」的無效資料,留意每一個參數的可靠性及邊界情況。六 總結最後分享一下防禦性程式設計的一些原則,希望你我一起嚴格按照原則來保護線上系統。

引自網路:Steve McConnell 的經典程式設計之書——《Code Complete》,用一個短篇解釋了防禦性程式設計的一些基本規則:

1. 保護你的程式碼遠離來自「外部」的無效資料,無論這個「外部」的概念被定位為什麼。它可以是來自於外部系統、使用者、檔案的資料,也可以是模組/元件以外的資料,由你決定。樹立「路障」、「安全區」或「信任邊界」——在邊界之外的一切都是危險的,界限之內的所有都是安全的。關於「路障」程式碼,需要驗證所有的輸入資料:檢查所有輸入參數的類型、長度和值域是否正確。還要加倍檢查限制和界限。2. 當我們檢查出錯誤資料後,還需要決定如何處理它。防禦性程式設計不會掩蓋錯誤,也不會隱藏bug。這需要在健壯性(如果問題可以處理那就繼續運行)和正確性(不返回不準確的結果)之間做權衡。選擇好策略來應對錯誤資料:返回錯誤就馬上停止,返回中性值就替換資料值……確保策略明確且一貫。3. 不要將程式碼外部的函數呼叫或方法呼叫想得太過美好。請確保你呼叫外部的API和庫之前理解並測試了錯誤。4. 至少在開發和測試階段,要使用斷言記錄假設,並高亮「不可能」的條件。這在大型系統中顯得尤為重要,因為隨著時間的推移,將會有不同的程式設計師用高度可靠的程式碼來維護這些大型系統。5. 新增診斷程式碼,智慧地記錄和跟蹤以幫助解釋在運行時發生的事情,尤其是當你遇到問題的時候。6. 標準化的錯誤處理。想好如何處理「正常錯誤」、「預期錯誤」以及警告,並對此習以為常。7. 只有當你真的需要的時候,才使用異常處理,並確保你得徹底理解該程式語言的異常處理程式。

搞起來,希望本文可以幫助到大家,可以用一種優雅方式接入參數校驗,保護系統解放自身,從你我做起!


IT145.com E-mail:sddin#qq.com