背景
笔者目前所在团队的代码年代已久,早年规范缺失导致现在维护成本激增,举一个深恶痛疾的例子就是方法参数使用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。
推荐阅读