簡介: 希望本文可以幫助到大家,可以用一種優雅方式接入參數校驗,保護系統解放自身,從你我做起!
作者 | 中野
來源 | 阿里技術公眾號
一 不厭其煩的 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后,做普通的參數校驗簡單到不行:
Hibernate Validator支持了一系列的如非空, 有效郵箱,正則表達式是否匹配等一系列基礎校驗支持:
- @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 Spring
Spring 沒有使用任何的參數校驗框架,使用其維護的Assert工具類+常用的參數異常來做參數校驗。所有的參數校驗都是在編碼時候書寫的。
都是使用Assert.notNull , Assert.notEmpty等來做參數校驗。
Spring主要還是面向開發的框架,出現參數異常其信息是面向開發者的,與我們這種面向用戶的校驗存在區別。不會出現業務參數校驗失敗的情況,人肉校驗簡單參數也無可厚非。
而且Spring是作用在應用啟動時候的框架,對用戶理論上無影響。
2 Feign
看了一些如Feign的基礎框架,都是手動校驗的參數,不復雜, 這里不一一列舉了。
基礎框架和業務系統有根本上的差異,基礎框架是面向開發人員的框架,大部分都是在系統部署時候啟動,校驗有異常的話都是直接拋出,開發人員可以根據錯誤信息及時排查。
而業務系統,敲代碼嗖嗖嗖的敲完了業務邏輯,如果參數傳的有誤,可能會直接導致系統不可用。
這種情況可能由於接口調用方沒有使用准確參數,前端沒有做參數校驗等等,但無論如何,我們必須保證自身系統是穩定可靠的,尤其需要使我們的系統遠離“外部”的無效數據,留意每一個參數的可靠性及邊界情況。
五 總結
最后分享一下防御性編程的一些原則,希望你我一起嚴格按照原則來保護線上系統。
引自網絡:
Steve McConnell 的經典編程之書——《Code Complete》,用一個短篇解釋了防御性編程的一些基本規則:
- 保護你的代碼遠離來自“外部”的無效數據,無論這個“外部”的概念被定位為什么。它可以是來自於外部系統、用戶、文件的數據,也可以是模塊/組件以外的數據,由你決定。樹立“路障”、“安全區”或“信任邊界”——在邊界之外的一切都是危險的,界限之內的所有都是安全的。關於“路障”代碼,需要驗證所有的輸入數據:檢查所有輸入參數的類型、長度和值域是否正確。還要加倍檢查限制和界限。
- 當我們檢查出錯誤數據后,還需要決定如何處理它。防御性編程不會掩蓋錯誤,也不會隱藏bug。這需要在健壯性(如果問題可以處理那就繼續運行)和正確性(不返回不准確的結果)之間做權衡。選擇好策略來應對錯誤數據:返回錯誤就馬上停止,返回中性值就替換數據值……確保策略明確且一貫。
- 不要將代碼外部的函數調用或方法調用想得太過美好。請確保你調用外部的API和庫之前理解並測試了錯誤。
- 至少在開發和測試階段,要使用斷言記錄假設,並高亮“不可能”的條件。這在大型系統中顯得尤為重要,因為隨着時間的推移,將會有不同的程序員用高度可靠的代碼來維護這些大型系統。
- 添加診斷代碼,智能地記錄和跟蹤以幫助解釋在運行時發生的事情,尤其是當你遇到問題的時候。
- 標准化的錯誤處理。想好如何處理“正常錯誤”、“預期錯誤”以及警告,並對此習以為常。
- 只有當你真的需要的時候,才使用異常處理,並確保你得徹底理解該編程語言的異常處理程序。
搞起來,希望本文可以幫助到大家,可以用一種優雅方式接入參數校驗,保護系統解放自身,從你我做起!
原文鏈接
本文為阿里雲原創內容,未經允許不得轉載。