背景
筆者目前所在團隊的代碼年代已久,早年規範缺失導致現在維護成本激增,舉一個深惡痛疾的例子就是方法參數使用Map“一擼到底“,說多了都是淚,我常常在團隊內自嘲“咱硬是把java寫成了JavaScript、php”,代碼靈活的讓人懷疑人生,你根本不知道方法需要什麼、返回什麼,新人來了想快速上手不可能的,老老實實debug吧,另一方面,以往的校驗大多數都是放在前端做的,後端幾乎沒有校驗,所幸業務量沒上來,沒有引起不速之客的造訪,要不程序員早被拉去祭天多少回了。
恰逢接到一個任務在團隊內推廣參數校驗,希望能帶來一些業內的最佳實踐,開始我內心是拒絕的:“這麼成熟的東西還需要普及什麼呢,網上一搜一大篇”,罷了罷了,拿人錢財,從開始的牴觸到後來的坦然,還是有不少收穫,待我娓娓道來。
業內實踐
1.簡單粗暴的if else
if(a == null){
return Result.failure(400,"a不能爲空);
}
if(StringUtil.isEmpty(b)){
return Result.failure(400,"b不能爲空);
}
通俗易懂的校驗方式,不使用框架,代碼重複度會比較高,參數較少的簡單場景可以這麼用。
2.JSR規範+hibernate validator框架【成熟體系】
JSR提供了一套Bean校驗規範的API,維護在包javax.validation.constraints下。該規範使用屬性或者方法參數或者類上的一套簡潔易用的註解來做參數校驗。開發者在開發過程中,僅需在需要校驗的地方加上形如@NotNull, @NotEmpty , @Email的註解,就可以將參數校驗的重任委託給一些第三方校驗框架來處理。
接入validation api及hibernate validator後,做校驗就很easy了
@Entity public class Blog { public Blog() { } @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @NotNull @Size(min = 2, message = "Blog Title must have at least 2 characters") private String blogTitle; @NotBlank(message = "Blog Editor cannot be blank") private String blogEditor; @Email(message = "Email should be valid") private String blogEmail; // Getters and Setters } @RestController @RequestMapping("api/v1") public class BlogController { @PostMapping("/blog") public Blog saveBlog(@Valid @RequestBody Blog savedBlog,BindingResult result) { if(result.hasErrors()){ // 獲取異常信息對象 List<ObjectError> errors = result.getAllErrors(); // 將異常信息輸出 for (ObjectError error : errors) { //執行自己的邏輯 } } }
場景複雜,參數多,這時我們就需要藉助框架來助力,減少重複工作量,框架久經驗證,bug相對來講較少。
想深入瞭解的,可以參考官方文檔
Getting Started | Validating Form Input (spring.io)
3.json schema+json schema validator【新寵】
json schema 是用於驗證 JSON 數據結構的強大工具,適用於表單靈活變動、controller層沒有定義對象數據綁定情況下(我們現在的場景就是大量使用Map接收前端數據,沒法使用JSR規範+hibernate validator框架)
@RestController @RequestMapping("api/v1") public class BlogController { @PostMapping("/saveChnl") public void saveChnl(HttpServletRequest request) { Map<String,Object> chnl = JsonUtils.toMap(request.getParameter("data")); }
鑑於此我們需要引入正統的json schema標準來解決歷史問題,json schema已經有成熟的規範,不需要我們自己造輪子,後面重點介紹json schema這種方式。
json schema瞭解
1.認識json schema
json schema 是用於驗證 JSON 數據結構的強大工具,簡單來說就是通過定義一些規則來約束json數據的合法性,比如類型、是否必填、最大值、最小值、正則等,看一個具體的例子:
{ "$schema":"http://json-schema.org/draft-07/schema#", "$id": "http://com.公司名.項目名.模塊名.子模塊/schemas/channel_add.json", "title":"門戶模塊-欄目編輯", "description":"門戶模塊-欄目編輯-json schema 配置信息", "type":"object", "properties":{ "chnlcode":{ "description":"門戶編碼", "type":"string" }, "chnlid":{ "type":"string", "description":"門戶編碼id" }, "data": { "type": "object", "properties":{ "disname":{ "description":"顯示名稱", "type":"string" }, "chnlorder":{ "description":"排序", "type":"string" } }, "required": [ "vmuri" ] } }, "message": { "required": "必填" }, "required":[ "chnlid", "chnlcode" ] }
$schema
關鍵字來聲明將使用哪個版本的 JSON 架構規範,我們統一使用draft-07;$id:唯一標識符,格式爲url格式,我們約定格式爲http://代碼包標識/schemas/有意義的名稱.json,比如http://com.公司名.模塊名.ec/schemas/channel_add.json 代表ec工程下頻道添加json對象的schema;
title: 有意義的名稱;
description:對title的補充;
type:類型,object代表對象,還可以爲string,integer,array等
properties:對象的屬性(鍵值對)是使用 properties
關鍵字定義的,properties
是一個對象,其中每個鍵是屬性的名稱,每個值是用於驗證該屬性的模式;
message:自定義的錯誤信息;
required:必填字段;
具體解釋請參考:
1.1 json-schema 版本選擇
根據一些社區的統計,draft-7是目前使用最廣泛的版本,以史爲鑑,我們也選擇draft-07即可。
2.定義json schema
這一步我們開始定義符合自己要求的json schema,我們需要限制
1.chnlorder是一個數字;
2.indexCount是一個數字而且需要大於0。
要校驗對象的數據結構如下(有刪減):
{ "disname": "優質供應商", "chnlcode": "exsupplier", "chnltype": "0", "chnldesc": "優質供應商", "chnlorder": "99", "extdata": { "indexCount": "12" } }
chnlorder是參數對象的屬性,indexCount是參數對象中嵌套對象extdata的屬性。
最終形成一份這樣的json schema
{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://com.公司名.項目名.模塊名.子模塊/schemas/channel_add.json", "title": "門戶模塊-欄目編輯", "description": "門戶模塊-欄目編輯-json schema 配置信息", "type": "object", "properties": { "disname": { "description": "顯示名稱", "type": "string" }, "chnlorder": { "description": "排序,正整數", "type": "string", "pattern": "^[1-9]\\d*$" }, "extdata": { "type": "object", "properties": { "indexCount": { "description": "顯示條數,空串或者正整數", "type": "string", "pattern": "^$|[1-9][0-9]*" } } } }, "message": { "required": "必填" }, "required": [ "chnlcode" ] }
3.選擇一個趁手的json schema validator
第2步我們已經定義好了json schema,相當於制訂了規範,現在還需要找到一個validator來識別規範,根據官方的介紹有以下備選項:
https://json-schema.org/implementations.html#validator-java
結合以下考量點:
1.受歡迎程度
start 過百的有everit-org/json-schema和networknt/json-schema-validator,當然還有官方未提到的https://github.com/java-json-tools/json-schema-validator(start超過1.5k)
2.依賴的json庫
everit-org/json-schema底層基於 org.json API ,意味着還需要引入新json庫,而networknt/json-schema-validator和https://github.com/java-json-tools/json-schema-validator,底層基於jackson,正好項目中的JsonUtils也是基於jackson實現,不需要引入其他json庫;
3.性能
根據性能測試networknt最優
4.近期是否有更新
https://github.com/java-json-tools/json-schema-validator最後一次更新在2020年,networknt最近還有更新;
5.json-schema的支持程度
https://github.com/java-json-tools/json-schema-validator只支持到draft4,而networknt支持到draft-2019-09-formerly-known-as-draft-8;
綜合對比,最終選擇了networknt/json-schema-validator。
實踐
經過前面的準備工作,我們已經定義了schema,選擇了validator,現在開始實踐到我們的代碼中
1.JsonUtils工具類擴展原來的轉換方法,增加驗證邏輯
/** * json string convert to map,有校驗邏輯,如果校驗不通過拋出異常 */ @SuppressWarnings("unchecked") public static <T> Map<String, Object> toMapValid(String jsonStr,String schemaPath) { if (StringUtil.isBlank(jsonStr)) { return null; } Assert.hasLength(schemaPath,"schemaPath不能爲空"); try { JsonNode jsonNode = objectMapper.readTree(jsonStr); Set<ValidationMessage> validationMessageSet = JsonSchemaValidatorUtil.validate(jsonNode,schemaPath); if(!CollectionUtils.isEmpty(validationMessageSet)){ for(ValidationMessage validationMessage : validationMessageSet){ throw new IllegalArgumentException("參數不合法:"+validationMessage.getMessage()); } } return objectMapper.convertValue(jsonNode,Map.class); } catch (JsonMappingException e) { e.printStackTrace(); } catch (JsonProcessingException e) { e.printStackTrace(); } return null; } public static Set<ValidationMessage> validate(JsonNode checkData,String schemaPath){ schemaPath = "conf/validation/json/schema/"+schemaPath; JsonNode schemaJson = null; try { schemaJson = getJsonNodeFromClasspath(schemaPath); } catch (IOException e) { throw new IllegalArgumentException("查找schema失敗,請檢查"+schemaPath+"是否存在"); } JsonSchema schema = getJsonSchemaFromJsonNodeAutomaticVersion(schemaJson); Set<ValidationMessage> errors = schema.validate(checkData); return errors; }
2.編寫json schema文件
位置:src\main\resources\conf\validation\json\schema\jc-ec\xxx.json
{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://公司名.項目名.模塊名.子模塊/schemas/channel_add_or_update.json", "title": "門戶模塊-欄目編輯", "description": "門戶模塊-欄目編輯-json schema配置信息,校驗前端傳遞的data是否合法", "type": "object", "properties": { "disname": { "description": "顯示名稱", "type": "string" }, "chnlorder": { "description": "排序,正整數", "type": "string", "pattern": "^[1-9]\\d*$" }, "extdata": { "type": "object", "properties": { "indexCount": { "description": "顯示條數,空串或者正整數", "type": "string", "pattern": "^$|^[1-9]\\d*$" } } } }, "required": [ "disname", "chnlcode" ] }
3.業務代碼中json轉換方法切換爲帶有驗證邏輯的
Map<String,Object> chnl = JsonUtils.toMapValid("jsonStr","ec/channel_add_or_update.json");
4.效果
總結
目前大量的校驗集中在前端,後臺代碼鮮有校驗,長此下去對系統的安全問題是很大的挑戰,鑑於此開發應該加強後臺代碼的校驗,推薦使用“JSR規範+hibernate validator框架“來實現校驗功能,因爲規則在java對象上可讀性相對於json schema更高,新人的接受度也更高,如果是老代碼,開發可以根據實際情況去抉擇:
如果定義了對象接收參數,推薦使用JSR規範+hibernate validator框架。
如果採用Map接受json格式參數,推薦使用json schema validator。
推薦閱讀