Java后端自顶向下方法——JSON与HTTP

(一)为什么是JSON

为什么JSON会与HTTP有联系?我们来想一想HTTP到底是干嘛的?对,就是传递数据嘛,这个问题是个人都知道。那么问题来了,HTTP是怎么传递数据的?或者说他通过什么方式传递数据?

你一定会说用HTTP参数传递就行了呗,没错,这是一个办法,而且很简洁。比如我要做一个用来登录的API,我们的请求URL就可以这样写:

http://example.com/login?userName=123&password=456

很显然,当我们用GET请求(其他请求方式也行)去请求这个API时,服务器就可以拿到用户名和密码的信息了。但是呢,这个API设计的不够简洁,而且把请求的地址和传递的数据都放在一起,URL会显得臃肿,很不美观。那么我们就有必要着手去改进他。因此,json就是我们的一个很好的解决方案。

(二)什么是JSON

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。 这些特性使JSON成为理想的数据交换语言。

这是JSON官网对他的描述,说了这么多,说了些啥?其实你只要明白,JSON是一个数据交换格式就行了,顺便了解一下JSON来源于JavaScript。知道这些就足够了。

JSON十分简单,他一共就只有两种结构,一种是无序的键值对(对象)的集合,另一种是值的有序列表(数组)。我们先来说说键值对集合,他也通常被描述为对象,哈希表,字典(学过Python的朋友肯定很了解)等等。有序列表很好理解,就是一列有序的字符串或者是对象。仔细想想,JSON有着非常浓厚的面向对象的味道。下面我们就来仔细看看JSON的结构。

(三)JSON的结构

1. 对象

对象是一个无序的键值对的集合。一个对象以“{”(左大括号)开始,以“}”(右大括号)结束。每个“键”后跟一个“:”(冒号),然后跟上对应的“值”,键值对之间使用“,”(逗号)分隔。如下图所示:

在这里插入图片描述
其实非常好理解,拿最开始我们举得那个例子来说,我们如果要把HTTP参数写成JSON的样式,就可以这样来做:

{"userName" : 123, "password" : 456}

是不是十分简单呢,有了JSON之后,我们就可以他放入POST请求的请求体里面发送给服务器了,这样我们的URL中就不再需要带上参数来传递数据了,URL也因此精简了许多。

有一点要说明,就是JSON中的对象支持嵌套,这就好比java中的对象中可以包含对象一样。嵌套的JSON对象可以极大地增加灵活性,方便我们开发。

2. 数组

数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。如下图所示:

在这里插入图片描述
数组就比对象更简单了,他甚至连“键”都没有了,只剩下“值”了。数组的主要作用就是保存一些有序的字符串或者是对象,比如我们可以这样把一个人的爱好全部列出来:

["篮球", "打游戏", "睡觉", "看片"]

非常简洁明了,当然,数组中也可以放对象,在这里就不举例了。

(四)Jackson简介

Java中并没有内置JSON的解析,因此使用JSON需要借助第三方类库。下面是几个常用的 JSON 解析类库:

Gson: 谷歌开发的 JSON 库,功能全面。
FastJson: 阿里巴巴开发的 JSON 库,性能优秀。
Jackson:社区十分活跃且更新速度很快。

在这里我们选取Jackson来使用(Jackson也是springMVC中自带的JSON解析器),下面我们来看看Jackson有哪些特点:

1.容易使用: jackson API提供了一个高层次外观,以简化常用的用例。
2.无需创建映射:API提供了默认的映射大部分对象序列化。
3.性能高:快速,低内存占用,适合大型对象图表或系统。
4.简洁 : jackson创建一个干净和紧凑的JSON结果,这是让人很容易阅读。
5.无依赖: jackson库不需要任何其他的库(除了JDK)。
6.开源: jackson代码是开源的,可以免费使用。

讲了这么多,我们还是来看看Jackson如何使用。

(五)Jackson对象序列化

JSON与面向对象思想息息相关,最常用的功能就是将对象序列化,所谓序列化就是将对象转化为可以持久保存的形式,比如字符串等,其实JSON实际上就是一个字符串。所以我们先来看看Jackson如何将一个java序列化为JSON字符串。

首先我们得有一个类:

public class User {  
    private String name;  
    private Integer age;  
    private String email;  
      
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public Integer getAge() {  
        return age;  
    }  
    public void setAge(Integer age) {  
        this.age = age;  
    }  
    public String getEmail() {  
        return email;  
    }  
    public void setEmail(String email) {  
        this.email = email;  
    }  
}  

Jackson中的ObjectMapper类是将对象进行序列化的核心,ObjectMapper可以把JSON字符串保存File、OutputStream、String对象等不同的介质中。我们来看看ObjectMapper中常用的方法:

writeValue(File arg0, Object arg1)          //把arg1转成json序列,并保存到arg0文件中。 
writeValue(OutputStream arg0, Object arg1)  //把arg1转成json序列,并保存到arg0输出流中。 
writeValueAsBytes(Object arg0)              //把arg0转成json序列,并把结果输出成字节数组。 
writeValueAsString(Object arg0)             //把arg0转成json序列,并把结果输出成字符串。 

为了演示方便,我们就用writeValueAsString方法,将JSON保存在字符串对象中。首先我们先构造对象:

User user = new User();  
user.setName("abc");   
user.setEmail("abc@163.com");  
user.setAge(25); 

然后对他进行序列化:

ObjectMapper mapper = new ObjectMapper();  
String json = mapper.writeValueAsString(user);  
System.out.println(json);  

最后的输出结果为:

{"name":"abc", "age":25, "email":"abc@163.com"}

序列化对象就是这么简单,嗯对,只是看起来很简单,一些复杂的情况我们还没遇到呢,过一会再讲。下面我们先来看看反序列化对象。

(六)Jackson对象反序列化

ObjectMapper支持从byte数组、File、InputStream、字符串对象等数据的JSON反序列化。反序列化,和序列化相反,是将JSON转化为对象的过程。

String json = "{\"name\":\"abc\", \"age\":25, \"email\":\"abc@163.com\"}";    
ObjectMapper mapper = new ObjectMapper();  
User user = mapper.readValue(json, User.class);  

通过ObjectMapper对象的readValue方法我们可以快速的将JSON转化为java对象,反序列化好像比序列化还要简单哈。慢着,下面我们就来看一个非常常见的错误,可能会把新手搞的怀疑人生。

(七)Jackson之无限递归

这个问题几乎每个人都会遇到,当看见控制台“栈溢出”的报错时,你可能会盯着自己的代码仔细地看上好久,然后说一句:“我这代码哪错了???”

我先来举个例子,然后大家可以思考这个问题出在哪里。

老规矩,我们先来写一个准备序列化对象的类:

public class User {
    private String name;
    private User[] friends;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public User[] getFriends() {
        return friends;
    }
    public void setFriends(User[] friends) {
        this.friends = friends;
    }
}

然后我们来写个主函数试一试:

public static void main(String[] args) throws JsonProcessingException {
        User u1 = new User();
        User u2 = new User();
        u1.setName("u1");
        u2.setName("u2");
        User[] u1friends = {u2};
        User[] u2friends = {u1};
        u1.setFriends(u1friends);
        u2.setFriends(u2friends);

        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(u1);
        System.out.println(json);
}

是的,运行这段代码百分之百会报一个“Infinite recursion”的异常,那么问题来了,为什么这段代码会陷入死递归?这段代码与上面的示例有什么本质的区别呢?

我们先来仔细看看这段代码,这个User中有个特殊的变量,他是一个User对象的数组,我在写测试函数的时候,我在u1对象的数组中填入了u2对象,在u2对象的数组中填入了u1对象。当Jackson在解析对象时,就会发生循环嵌套,就像这样:

{
	"name":"u1", 
	"friends":[{
		"name":"u2", 
		"friends":[{
			"name":"u1", 
			"friends":[{
				"name":"u2", 
				"friends":[{
					"name":"u1", 
					"friends":[{
						"name":"u2", 
						"friends":[......]
					}]
				}]
			}]
		}]
	}]
}

于是程序就会陷入死递归。我们往深层次想一想,为什么这种对象的结构就会发生死递归呢?其实很简单,因为这是一个双向关联的关系,说直白一点就是一个对象里的参数直接或间接地又包含了这个对象(上面这个例子就是间接包含,看似u1对象的数组中没有u1对象只有u2对象,但是u2对象中又包含了u1对象,相当于是数组中间接包含了u1对象。这种情况在我们写代码时要绝对避免,因为这种错误在对象的结构变得复杂之后极难被发现),这样在解析的时候就会陷入一个“圈”永远出不来了。那么这种问题如何去解决呢?我们接下来就会讲到通过注解来解决的方案。

(八)Jackson高级注解

在讲Jackson注解之前,我们先要了解他的注解的分类,通过作用的对象的不同我们可以把注解分为:实体类可用、属性可用、实体类和属性均可用。下面我来分类列举一下常用的几个注解:

1. 实体类可用

注解功能
@JsonIdentityInfo指定在序列化/反序列化值时使用对象标识
@JsonInclude排除值为empty/null/default的属性
@JsonIgnoreProperties在类上指定要忽略的属性

2. 属性可用

注解功能
@JsonProperty指定JSON中属性的名称
@JsonFormat序列化时指定格式
@JsonManagedReference, @JsonBackReference处理父/子关系并解决循环问题
@JsonIgnore忽略属性

3. 实体类和属性均可用

注解功能
@JsonSerialize指定序列化时使用的JsonSerialize类

Jackson中的注解有几十个,这里只是列举了一些常用的,如果有没有列举到的可以根据你的具体需求去查看官方文档。下面我们回到上面那个死递归的问题,来尝试解决他。

很显然,我们要用@JsonManagedReference和@JsonBackReference注解,这两个标注通常配对使用,用在父子关系中。@JsonBackReference标注的属性在序列化时,会被忽略(即结果中的json数据不包含该属性的内容)。@JsonManagedReference标注的属性则会被序列化。在序列化时,@JsonBackReference的作用相当于@JsonIgnore,此时可以没有@JsonManagedReference。但在反序列化(deserialization,即json数据转换为对象)时,如果没有@JsonManagedReference,则不会自动注入@JsonBackReference标注的属性(被忽略的父或子);如果有@JsonManagedReference,则会自动注入自动注入@JsonBackReference标注的属性。

于是我们给User数组添加@JsonBackReference注解,发现问题已经被解决,执行后结果为:

{"name":"u1"}

很显然,原本的“friends”属性不见了,这样就达到了断开死递归的目的。但是,这样子的结果不一定满足我们的实际需求,因此,我们在设计数据结构的时候,就应该把这种死递归的情况提前考虑并且尽量避免,而不是出现问题之后再通过加注解来解决。

2020年5月5日

GitHub 加速计划 / js / json
18
5
下载
适用于现代 C++ 的 JSON。
最近提交(Master分支:2 个月前 )
960b763e 5 个月前
8c391e04 8 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐