上篇主要复习了HTTP以及POST相关的几种传参形式,这一篇来讲讲和实际开发更为紧密的内容:JSON。
初学者在需求分析阶段通常缺乏以下能力:
- 无法通过看页面原型图分析出大致的请求格式和响应格式
- 即使能分析出请求格式,却不知道用什么样的Java对象接收
- 即使能分析出响应格式,却不知道用什么样的Java对象返回
这三个能力,其实都依赖于你对JSON的理解(本文主要讨论JSON,不涉及表单请求和文件上传类型)。
说得更具体些就是,初学者往往搞不清楚JSON与Java对象的转换关系(图片来自尚硅谷):
- 请求:JSON转Java
- 响应:Java转JSON
JSON
JSON的作用
在我刚入行时写过相关的博客,比较简单,感兴趣的同学可以看看:AJAX与JSON
JSON简单来说就是特定格式的字符串,注意,它的载体是字符串。很多人分不清JSON和JS对象,其实两者没有可比性。实在要说的话,JS对象和Java对象都是正儿八经的对象,存活于内存中(浏览器/服务器),而JSON只是字符串,往往承担的是网络传输的角色:
我们都知道,网页上的动态数据都是服务器那边组装后通过HTTP返回的,但很少有人会思考这样的一个问题:
服务器组装数据时用的是Java对象,比如user.setName("bravo"),但前端得到的却是JS对象,怎么做到的?
一般来说,浏览器被安装在我们的个人电脑中,而提供服务的Java应用则可能运行在阿里云服务器上,两者并不在同一片物理内存。由于对象存活必须依赖于内存(如果不做持久化,只要一断电,内存中的数据就没了),所以就上图而言,Java对象显然没法自己“跳出”服务器进入浏览器中,更别提Java对象如何变成JS对象。
要想完成跨内存的数据传输、对象转换,必须通过网络传输,而且需要一个传递信息的载体,过程中还涉及到序列化和反序列化。
实际上,整个请求响应的过程就是序列化和反序列化的过程:
- 服务器端把Java对象序列化为JSON(中介,特定格式的字符串)
- 网络不能直接传递对象,但可以传输字符串
- 浏览器得到JSON后,反序列化为JS对象,然后设置到页面上
同理,不仅是浏览器和服务器,服务器和服务器之间也需要JSON作为数据交互的中介:
JSON的格式
使用Postman时,我们经常会发这样的请求:
Body就是请求体内容,一般都是字符串格式,所以这是一个JSON,而不是JS对象,尽管它们看起来很像。你可以理解为JSON是多种语言共同协定的一种数据交互格式:
- "field":"xxx" 表示对象的字段,value如果是字符类型需要加"",数值可以不加
- {} 表示对象或map或其他符合key-value格式的结构
- [] 表示一组对象、一组字符串或一组数值
很简单吧?
然后各个语言都会遵守这个协定,转化为自己的对象结构,比如:
- {}可以代表Java对象/Map,[]由于表示一组数据,刚好可以对应Java的数组、List或Set等单列集合
- {}也可以代表Python对象/字典,[]对应Python的元组或list等
- {}还可以代表PHP的对象,[]对应PHP中的Array
- ...
当然,{}也可以代表JS对象,[]则可以转化为JS的数组。
为什么很多初学者会搞混JSON和JS对象呢?本质上还是因为:
- 初学者都“见过”JS对象,而且它往往都是{}的形式出现
- JS对象的格式和JSON确实比较像
但JSON本质是“干瘪瘪”的字符串,当各大语言需要进行反序列化时,就会按照上面的格式转为内存中“圆鼓鼓”的对象。应该把JS对象和Java对象看作一个梯队,而JSON则在另一个梯队,是一个特定格式的字符串。
常见JSON格式与Java对象的转换
这里只演示Java对象与JSON的转换:
如果不信的话,可以自己动手写一下接口,然后用Postman按照图中的格式发送JSON,看看接口能不能顺利接收参数。
上面说过,JSON的{}可以对应Java的对象或者Map,{}两个括号表示对象的边界,其实刚好对应类的{},里面的就是对象的字段。
我们来分析一下第一张图的结构。
接口返回的是一个HashMap,所以很容易想到最终JSON格式是:
{
...
}
接着往Map里put了一些value,我们先不管value是什么类型:
{
"1号男嘉宾" : xxx,
"2号男嘉宾" : xxx,
"3号男嘉宾" : xxx,
}
OK,JSON的大致结构出来了,再深入一层,看看xxx是什么。很明显,是一个Java的User对象,还是对应{}:
{
"1号男嘉宾" : {},
"2号男嘉宾" : {},
"3号男嘉宾" : {},
}
那么,这个User对象有哪些字段呢?填上即可:
{
"1号男嘉宾": {
"name": "雅木茶",
"age": 23
},
"2号男嘉宾": {
"name": "卡卡罗特",
"age": 23
},
"3号男嘉宾": {
"name": "贝吉塔",
"age": 22
}
}
其他两个分析过程同理,就不演示了(思考一下,如果User里面有Department会是什么样)。
刚才是顺着来,现在我们玩一下“逆推”。假设现在前端跑过来告诉你
大佬,这个接口我到时候这样传参给你行吗?
[
{
"name": "张飞",
"age": 18,
"tags": [
"大眼睛",
"大胡子"
]
},
{
"name": "关羽",
"age": 19,
"tags": [
"万人敌",
"长胡子"
]
},
{
"name": "刘备",
"age": 20,
"tags": [
"刘皇叔",
"摔阿斗"
]
}
]
此时你应该如何设计入参才能接受前端这种格式的JSON呢?
jackson的一些操作
之前介绍过,服务器本身没有能力处理JSON和文件上传,都要靠第三方组件。SpringBoot则引入了jackson作为默认的JSON组件,其他常见的还有阿里的fastJson和谷歌的gson。
这里介绍一下jackson常见的几个注解。
@Slf4j
@RestController
public class UserController {
@PostMapping("/addUser")
public UserPOJO addUser(@RequestBody UserPOJO user) {
user.setAge(null);
return user;
}
}
@Data
public class UserPOJO {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 用户类型
*/
private Integer userType;
/**
* 个人标签
*/
private List<String> tags;
}
@JsonInclude
有时我们希望如果字段为null就不要返回给前端,可以使用@JsonInclude,它可以指定很多属性。
@JsonInclude还可以加在类上,那么该对象所有为null的字段都不会参与JSON序列化。
@JsonProperty
对于一些老项目或者其他什么原因,原本传参使用的是下划线,比如user_type,而后端用Java改写时又要符合驼峰命名,此时可以用@JsonProperty做一层“隔离”。
此时出入参都必须叫user_type:
@JsonFormat
有些同学容易把@JsonFormat和@DateTimeFormat搞混,我们单独开一个小节聊一聊。
时间格式
首先和大家说一下,数据库字段无论是datetime还是timestamp,其实都是可以自动对应Java的Date对象,一般讨论的所谓时间格式,都是指前端的显示格式:
刚才我们讨论为什么需要JSON时,提到一个观点:对象是在内存中存活的,无法直接进行网络传输。但是大家有没有想过:
class User {
private String name;
private Date birthday;
}
其实里面的字段也是对象,也要进行序列化。SpringBoot引入了jackson作为JSON序列化的组件,其中必然包括对Date进行序列化/反序列化的方案。
然而,SpringBoot1.x和2.x其实有较大的改动,其中就包括对Date格式化的改动。大家可以沿用刚才的项目,给UserPOJO加上birthday字段,然后在SpringBoot1.5.9和SpringBoot2.3.4环境下实验。
@Slf4j
@RestController
public class UserController {
@PostMapping("/addUser")
public UserPOJO addUser(@RequestBody UserPOJO user) {
return user;
}
}
SpringBoot1.x
SpringBoot2.x
有两个细节:
- SpringBoot1.x的返回值是毫秒数,SpringBoot2.x是另一种格式
- 当入参和出参时间格式不同时,会发生转换,此时会出现时间差
- SpringBoot1.x传递2020-12-07T22:58:11.000+00:00,返回1607381891000(+8)
- SpringBoot2.x传递1607353091000,返回2020-12-07T14:58:11.000+00:00(-8)
可以通过时间戳转换验证一下(注意单位)。
时差的问题可以通过配置解决,比如:
spring.jackson.time-zone=GMT+8
总的来说就是:
如果希望更改出入参的时间格式,可以有局部和全局两种方式:
- 局部:@JsonFormat / @DateTimeFormat
- 全局
- YAML
- Config
@DateTimeFormat只适用于非JSON的POST请求,也就是说,如果项目本身都是JSON请求,你用@DateTimeFormat是无效的。你可以简单理解为:
- @DateTimeFormat,走表单请求时间转换器(适用于GET、POST表单请求)
- @JsonFormat,走JSON请求时间转换器(适用于POST JSON请求、JSON响应)
所以,你在这煞费苦心地调整表单请求的转换格式有啥用?
这里演示一下@JsonFormat的用法:
/**
* 生日(时间格式很容易写错,可以抽取为常量或者使用第三方提供的,比如hutool就有)
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date birthday;
此时出入参都变为指定格式:
也可以在YAML中进行全局配置:
上面那个mvc:date-format是对表单请求的配置。
或者使用Config:
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// 全局配置
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
return objectMapper;
}
}
如果说@JsonInclude/@JsonFormat加到字段上、类上分别是字段级别、类级别,那么上面的配置就是整个项目级别,因为Spring的bean默认单例,而这个唯一的ObjectMapper已经被做了手脚,最终所有接口的序列化/反序列化行为都被改写。
这种全局和局部的思想,后面会很常见,这里先点一下。