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

Map传参优雅检验,试试json schema validator的更多相关文章
- restTemplate getForObject中map传参问题
		在使用restTemplate中getForObject的map传参形式时: 开始时我是这么调用的: RestTemplate rest = new RestTemplate(); Map<St ... 
- RestTemplate post请求使用map传参 Controller 接收不到值的解决方案 postForObject方法源码解析.md
		结论 post方法中如果使用map传参,需要使用MultiValueMap来传递 RestTemplate 的 postForObject 方法有四个参数 String url => 顾名思义 ... 
- python接口测试(post,get)-传参(data和json之间的区别)
		python接口测试如何正确传参: POST 传data:data是python字典格式:传参data=json.dumps(data)是字符串类型传参 #!/usr/bin/env python3 ... 
- 02基于注解开发SpringMVC项目(jar包,异步,request,参数传递,多选的接收,Model传参,map传参,model传参,ajax,重定向,时间日期转换)
		 1 所需jar包 项目结构如下: 2 web.xml配置文件的内容如下: <?xmlversion="1.0"encoding="UTF-8"?&g ... 
- map传参上下文赋值的问题
		今天开发遇到一个问题就是声明一个map<String,String> param ,给param赋值,明明有结果但是就是返回为空:下面附上代码: 因为在一个大的循环中,param是公用赋值 ... 
- HttpClient调用doGet、doPost、JSON传参及获得返回值
		调用 doPost:map传参 Map<String,Object> map = new HashMap<>(); map.put("test"," ... 
- post参数的方法 json data 和特别的传参
		json格式传参: 那么久使用json的方式传参: json=payload data格式传参: 其他方式传参: 在webFormes里 value 的值不是普通的字符 要把value值先序列化在放入 ... 
- jdbcTemplate传参使用Map或List
		List传参方式 举个例子 sql = "select * from table where id=? and param=?": sql中的参数要用?形式,然后使用list.ad ... 
- WebApi传参总动员(四)
		前文介绍了Form Data 形式传参,本文介绍json传参. WebApi及Model: public class ValuesController : ApiController { [HttpP ... 
随机推荐
- Redis 集群如何选择数据库?
			Redis 集群目前无法做数据库选择,默认在 0 数据库. 
- java-与文件相关
			java.nio.file 表示non-blocking 非阻塞io(输入和输出) 一个 Path 对象表示一个文件或者目录的路径,是一个跨操作系统(OS)和文件系统的抽象 java.nio.file ... 
- Spring 应用程序有哪些不同组件?
			Spring 应用一般有以下组件:接口 - 定义功能.Bean 类 - 它包含属性,setter 和 getter 方法,函数等.Spring 面向切面编程(AOP) - 提供面向切面编程的功能.Be ... 
- Creating a File Mapping Object
			创建一个文件映射对象 映射一个文件的第一步是通过调用CreateFile函数来打开一个文件.为了确保其他的进程不能对文件已经被映射的那一部分进行写操作,你应该以唯一访问(exclusive acces ... 
- 原生ES6写的Web游戏:ES6-Mario,小美女,小帅哥快来玩啊~~
			? ES6-Mario 这是一个用原生ES6语法和HTML5新特性写成的Web 游戏. 通过这个项目,你可以在实践中对ES6的主要内容.HTML Canvas 相关API以及Webpack的基础配置有 ... 
- leetcode1753. 移除石子的最大得分
			题目描述: 你正在玩一个单人游戏,面前放置着大小分别为 a.b 和 c 的 三堆 石子. 每回合你都要从两个 不同的非空堆 中取出一颗石子,并在得分上加 1 分.当存在 两个或 ... 
- JavaScript 中 empty、remove 和 detach的区别
			内容 empty.remove 和 detach的区别 jQuery 操作 DOM 之删除节点 方法名 元素所绑定的事件及数据是否也被移除 作用 $(selector).empty() 是 从被选元素 ... 
- js手机端判断滑动还是点击
			网上的代码杂七杂八, 我搞个简单明了的!! 你们直接复制粘贴, 手机上 电脑上 可以直接测试!!! 上图: 上代码: <!DOCTYPE html> <html lang=&q ... 
- 解决vscode卡顿,CPU占用过高的问题
			打开vscode之后,点击文件==>首选项==>设置 搜索设置 search.followSymlinks 然后将这个值改为false 
- (动态模型类,我的独创)Django的原生ORM框架如何支持MongoDB,同时应对客户使用时随时变动字段
			1.背景知识 需要开发一个系统,处理大量EXCEL表格信息,各种类别.表格标题多变,因此使用不需要预先设计数据表结构的MongoDB,即NoSQL.一是字段不固定,二是同名字段可以存储不同的字段类型. ... 
