背景
筆者目前所在團隊的程式碼年代已久,早年規範缺失導致現在維護成本激增,舉一個深惡痛疾的例子就是方法引數使用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。
推薦閱讀