背景

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

2.json-shcema规范

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-schemanetworknt/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-validatorhttps://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。

推荐阅读

1.什么是json schema

2.json-shcema规范

Map传参优雅检验,试试json schema validator的更多相关文章

  1. restTemplate getForObject中map传参问题

    在使用restTemplate中getForObject的map传参形式时: 开始时我是这么调用的: RestTemplate rest = new RestTemplate(); Map<St ...

  2. RestTemplate post请求使用map传参 Controller 接收不到值的解决方案 postForObject方法源码解析.md

    结论 post方法中如果使用map传参,需要使用MultiValueMap来传递 RestTemplate 的 postForObject 方法有四个参数 String url => 顾名思义 ...

  3. python接口测试(post,get)-传参(data和json之间的区别)

    python接口测试如何正确传参: POST 传data:data是python字典格式:传参data=json.dumps(data)是字符串类型传参 #!/usr/bin/env python3 ...

  4. 02基于注解开发SpringMVC项目(jar包,异步,request,参数传递,多选的接收,Model传参,map传参,model传参,ajax,重定向,时间日期转换)

     1 所需jar包 项目结构如下: 2 web.xml配置文件的内容如下: <?xmlversion="1.0"encoding="UTF-8"?&g ...

  5. map传参上下文赋值的问题

    今天开发遇到一个问题就是声明一个map<String,String> param ,给param赋值,明明有结果但是就是返回为空:下面附上代码: 因为在一个大的循环中,param是公用赋值 ...

  6. HttpClient调用doGet、doPost、JSON传参及获得返回值

    调用 doPost:map传参 Map<String,Object> map = new HashMap<>(); map.put("test"," ...

  7. post参数的方法 json data 和特别的传参

    json格式传参: 那么久使用json的方式传参: json=payload data格式传参: 其他方式传参: 在webFormes里 value 的值不是普通的字符 要把value值先序列化在放入 ...

  8. jdbcTemplate传参使用Map或List

    List传参方式 举个例子 sql = "select * from table where id=? and param=?": sql中的参数要用?形式,然后使用list.ad ...

  9. WebApi传参总动员(四)

    前文介绍了Form Data 形式传参,本文介绍json传参. WebApi及Model: public class ValuesController : ApiController { [HttpP ...

随机推荐

  1. SpringDataJpa 实体类过滤伪删除

    当需要过滤实体类的数据时,根据伪删除字段进行过滤,需要使用Hibernate提供的@Where注解 使用方式: @Entity(name = "Account") @Where( ...

  2. 在并发情况下,Elasticsearch 如果保证读写一致?

    1.可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用 层来处理具体的冲突: 2.另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只 有当大多数 ...

  3. elasticsearch 了解多少,说说你们公司 es 的集群架构,索 引数据大小,分片有多少,以及一些调优手段 。

    面试官:想了解应聘者之前公司接触的 ES 使用场景.规模,有没有做过比较大 规模的索引设计.规划.调优. 解答: 如实结合自己的实践场景回答即可. 比如:ES 集群架构 13 个节点,索引根据通道不同 ...

  4. pandas学习总结

    什么是pandas pandas数据读取 03. Pandas数据结构 Pandas查询数据的几种方法

  5. DOS、DOS攻击、DDOS攻击、DRDOS攻击

    https://baike.baidu.com/item/dos%E6%94%BB%E5%87%BB/3792374?fr=aladdin DOS:中文名称是拒绝服务,一切能引起DOS行为的攻击都被称 ...

  6. C#内置委托类型Func和Action对比及用法

    C#的内置委托类型 Func Action 返回值 有(只有一个Tresult) 无 重载 17个(0参-16参) 17个(0参-16参) 泛型 支持 支持 系统内置 是 是 是否需要声明 否 否 c ...

  7. hbase增删查

    代码: package cn.idcast.hbase; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.h ...

  8. 移动端input输入框把页面顶起, 收起键盘页面复原不了问题

    我相信大家平时也会遇到这种问题, 移动端 input 或者 textarea获取光标, 整个页面被顶起来, 键盘收起, 页面不复原的问题 ====>>>>  我这边提供两种解决 ...

  9. FastAPI(六十七)实战开发《在线课程学习系统》接口开发--用户登陆接口开发

    接上一篇文章FastAPI(六十六)实战开发<在线课程学习系统>接口开发--用户注册接口开发.这次我们分享实际开发--用户登陆接口开发. 我们先来梳理下逻辑 1.查询用户是否存在2.校验密 ...

  10. PyQt5 基本语法(四)

    目录 2. 输入控件(一) 2.1 纯键盘 2.1.1 QLineEdit 2.1.1.1 描述 2.1.1.2 控件创建 2.1.1.3 输出模式 2.1.1.4 提示字符串 2.1.1.5 清空按 ...