背景

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

    在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机 器可以共享这个结果,这样可以大大减少重复计算,提高性能,于是就需要进行 leader 选举.

  2. 创建Maven web工程

    ---恢复内容开始--- 第一步,启动Eclipse,依次打开菜单[File][New][Other] 找到目录Maven,选择Maven Project, 选择一个Archetype.这里创建Web ...

  3. C语言类型转换原理

    C语言类型转换 int a; a=1.23 这里把1.23赋值给a发生了隐式转换,原理如下: int a; float b=3.14; a=b; b赋值给a的过程:首先找一个中间变量是a的类型(该例中 ...

  4. 小程序刷新webview小结

    场景 在小程序其它页面做了操作,数据发生改变,回到webview页面时需要更新webview里面的数据.由于小程序没有提供与webview的实时通信能力,因此刷新页面是个可考虑的做法. 方法一 最常见 ...

  5. HTML5 Audio & Video 属性解析

    一.HTML 音频/视频 方法 play() play() 方法开始播放当前的音频或视频. var myVideo=document.getElementById("video1" ...

  6. Muse UI遇到的坑

    写在前面:我只是一个前端小白,文章中的提到可能会有不足之处,仅提供一个参考.若有不完善的地方,欢迎各位大佬指出,希望对你有帮助! 故事背景是这样的,最近做一个Vue项目,使用到 Muse UI 组件库 ...

  7. 关于根据数据反选checkbox

    前两天完成了一个连接hbase数据库的mis系统,mis系统中经常需要修改功能,复选框.多选框等等的自动勾选,感觉很麻烦,在此记录一下修改功能checkbox自动选中. 例子: <div cla ...

  8. 谈谈Spring中都用到了哪些设计模式?

    谈谈Spring中都用到了哪些设计模式? JDK 中用到了那些设计模式?Spring 中用到了那些设计模式?这两个问题,在面试中比较常见.我在网上搜索了一下关于 Spring 中设计模式的讲解几乎都是 ...

  9. Vue Element ui密码框校验

    <el-form-item prop="repeat_Password" class="userName_color"> <el-input ...

  10. 在keil中加入DSP库并且使用arm_math.h

    如果不开启硬件FPU,代码设置和编译控制建议二选一,否则会出现宏定义重复定义的报错