Java后端自顶向下方法——JSON与HTTP
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日
更多推荐
所有评论(0)