后端基础之API、JSON、序列化

介绍API、JSON、序列化、启动后端、JSON库

Posted by BlackDn on May 17, 2023

“你搅散一池星光,成为我此刻的月亮。”

后端基础之 API、JSON、序列化

前言

比较浅显的扫盲向文章,适合像我一样不怎么接触后端的小白观赏=。=
作为一个不关心后端的前端开发,那么这篇文章看看就差不多了
主要是一些基础概念,API、JSON、序列化啥的

API

什么是 API

(这一段是很早之前写的,那时候自己啥也不懂所以说了很多现在看起来没啥用的话。删是舍不得删的,如果已经知道 API 的概念建议直接跳过=▵=)

API(Application Programming Interface,应用编程接口,接口),其实每个框架/字典/规范对于它的定义都不尽相同,我也懒去看,所以在这我们呢就简单理解为它是一个函数或方法(或者url的形式),用于前后端沟通(数据传递)的。

前后端分离的情况下,通常情况下前端是不需要知道 API 是如何实现的,只需要知道后端提供 API 要接受什么参数,返回什么内容,并对返回的内容进行处理就好。
比如用户登录,前端得到用户输入的账号密码,并用后端提供的 API 将账号密码传给后端。而返回的结果可能是登录成功或者失败,而前端就需要根据结果进行进一步操作,比如成功跳转页面,失败弹出提示。
而后端是 API 的缔造者,他们决定自己的 API 接受什么参数,进行什么操作,返回什么内容。比如我这个用户登陆的 API 接收账号和密码两个参数,去数据库中进行比对,正确或错误要返回什么内呢等等。

如果我们将前端看作是用户操作的图形界面,那么后端则是系统和数据库交流的桥梁。这个时候就会有个问题:用户需要保存或想要看到的数据在数据库中,但是用户没法直接操作数据库;后端可以操作数据库,但是它不知道用户什么时候进行了操作,进行了什么操作(因为用户看到的是前端页面)。
于是,API 出现了,后端提供 API,在某个操作下对数据库进行操作;前端调用 API,告诉后端现在进行的是哪个操作。
所以,虽然我们常说 API 用于前端和后端的沟通,但从实际操作上,也可以说是用户和数据库的沟通——即便用户不知道有这么个数据库的存在。

就好比去点奶茶,门口的店员小姐姐给你点单(前端),门后的小哥哥在手摇柠檬茶(后端)。真正给我们做奶茶的是小哥哥但是我们没法直接和他说话,所以我们和小姐姐点单。这时候小哥哥不知道我们点了什么,为了让他知道,小姐姐吼了一嗓子,“一杯 QQ ㄋㄟㄋㄟ好喝到咩噗茶!”,小哥哥听到后赶紧给你做。这吼的一嗓子,就是前后端交互的API

调用 API

请出我们的好朋友 REQRES,利用这个假接口网站我们来自己调用一下 API。
纠结了一会决定用Pyhton来写例子,requests库用起来很方便,如果要用 JS 的话可以自己改写一下,JS 的网络链接参考:Web:异步方法 & 网络请求 (GET 请求可以直接在浏览器里访问)
获取单个用户信息的 GET 请求 API:

response = requests.get('https://reqres.in/api/users/2')
print(response.text)
#{

#	"data": {

#		"id": 2,

#		"email": "janet.weaver@reqres.in",

#		"first_name": "Janet",

#		"last_name": "Weaver",

#		"avatar": "https://reqres.in/img/faces/2-image.jpg"

#	},

#}

你看,我们在前端(这里是 Python 程序),通过 API('https://reqres.in/api/users/2')获得了所要的信息,这下就可以直接把信息展示在页面上给用户看了。
作为后端,我们会根据前端发来的请求去数据库里找到用户信息并发回给前端(比如这里找的是id=2的用户信息);
作为前端,我不关心后端是如何从数据库中找到信息的,我只知道后端暴露了个 API 给我,而我调用这个 API 就可以获得我想要的用户信息。

后端返回给我们的数据格式是固定的,这种以键值对为基础的数据格式就是 JSON:

{
	"data": {
		"key1": value1,
		"key2": value2,
		···
	}
}

JSON

什么是 JSON

其实用 JSON 很久了,但是一直没仔细看过,连他是什么的缩写也不知道,所以这里回过头来重新认识一下这个数据格式。

JSON(JavaScript Object Notation),是一种轻量级的数据交换格式,功能和XML类似,都是用文本方式保存数据对象。所以他们并非一种语言,而是一种格式。JSON 文件后缀一般为.json
在古早时期,人们喜欢用 XML 来存储对象,但其标签繁多,格式复杂,偷懒的程序猿们很快就厌倦了这种格式。2001 年 4 月,State Software 公司的联合创始人 Douglas Crockford 和 Chip Morningstar 发出了第一条 JSON 格式的消息(也许 JSON 名字的由来是因为当时他们用的是 JavaScript 程序,如果用的 Python 可能就要改名叫 PyON 了),宣告了 JSON 的诞生。

举个例子,我构造一个关于博客文章对象的 JSON 对象:

{
    "id": 1,
    "title": "Web:JavaScript基础",
    "author": {
        "firstName": "Black",
        "lastName": "Dawn"
    },
    "date": "2022-08-16",
    "tags": ["Web", "JavaScript"]
}

可以发现,这种形式和 JS 中的对象不能说十分相像,可以说是一毛一样,基本上就是{ "key" : "value" }的键值对形式。这也是为啥 Web 开发中基本都用 JSON——它非常符合 JS 开发者的使用习惯。
有许多在线的 JSON 格式转换的工具(JSON 格式化工具),可以将 JSON 字符串以树的形式展现出来,可以拿上面的栗子 🌰 试试。

好不容易出现了 XML 的平替,JSON 自然大受欢迎,雅虎和谷歌开始广泛地使用 JSON 格式,Twitter 也表示其对 XML 格式的支持到 2013 年结束(“以后都给劳资用 JSON!”)。
JSON 的主流地位至今仍然延续,不过也不是说 XML 就不用了。比如 Android 中的 manifest 配置文件就是 XML 格式。

JSON 作为数据传输的格式,有几个显著的优点:

  • JSON 只支持 UTF-8 编码,不存在编码问题;
  • JSON 只支持双引号,特殊字符用\转义,格式简单;
  • 浏览器内置 JSON 支持,如果把数据用 JSON 发送给浏览器,可以直接用 JavaScript 处理。

当然,不止浏览器,Java 也有很多开源库能直接提供方法进行 JavaBean(对应 JSON 对象的 Java 对象)和 JSON 的转换,比如 Google 提供的Gson。若是在Android Studio中开发,javax包中还有JSONObjectJSONArray类可以直接用。

JSON 格式简单,它仅支持以下几种数据类型:

类型 样例
键值对 {"key": value}
数组 [1, 2, 3]
字符串 "abc"
数值(整数和浮点数) 12.34
布尔值 true / false
空值 null

JSON 字符串和 JSON 对象

我曾一度以为 JSON 是一个独立的数据类型,只不过输出的时候会自动变成字符串的形式。不过现在看来,JSON 其实就是字符串,一种特定格式的字符串
不仅因为字符串可以直接被序列化用于前后端交互,而且基本所有编程语言都支持字符串——这意味着基本所有编程语言都可以支持 JSON。

但是如果单纯地用字符串,是很不方便的,比如我不能像对象那样直接通过obj.key来获取某个值(除非不厌其烦地find然后slice)。为了解决这个问题,很多语言/框架/库都内置了JSON 对象,允许我们轻易地将 JSON 字符串转变 JSON 对象,从而可以直接通过key来获取value;或者反过来,将对象变成 JSON 字符串方便进行数据传输
比如JS中,JSON.parse()用于将 JSON 字符串变为对象,JSON.stringify()则相反;在Python中,json.loads()将 JSON 字符串变为对象,json.dumps()则相反
Python举个例子 🌰:

# 对象变JSON字符串

py_obj = {
    "id": 1,
    "title": "Web:JavaScript基础",
    "author": {
        "firstName": "Black",
        "lastName": "Dawn"
    },
    "date": "2022-08-16",
    "tags": ["Web", "JavaScript"]
}
print(type(py_obj))  # <class 'dict'>

print(py_obj['title'])  # Web:JavaScript基础


json_obj = json.dumps(py_obj)
print(type(json_obj))  # <class 'str'>


# JSON字符串变对象

py_str = '''
{
    "id": 1,
    "title": "Web:JavaScript基础",
    "author": {
        "firstName": "Black",
        "lastName": "Dawn"
    },
    "date": "2022-08-16",
    "tags": ["Web", "JavaScript"]
}
'''

obj = json.loads(py_str)
print(type([backen](#backend%20intro)obj))  # <class 'dict'>

print(obj['author']['firstName'])  # Black

序列化和反序列化

序列化 Serialization

序列化:把对象(Object) 转化为可传输的字节序列(Byte stream)

当我们去查找为什么要序列化的时候,会知道当我们的对象进行网络传输存储的时候需要进行序列化。但网络传输的目的好像就是为了跨平台存储(保存到数据库),所以最终我们可以简化为存储持久化存储的时候需要序列化。

那么为什么呢?为什么需要将对象序列化成字节流传输,而不能直接传对象呢?
我们知道,在 OOP(Object Oriented Programming,面向对象编程)中,对象不出意外的话都是引用类型——系统获取的是对象的地址,通过地址来获取其中的值。
显然我们在传输的时候是不能把地址作为对象来传输的。就同一个机器来说,我将一个对象的信息持久化存储到本地(假设保存成一个文件),那么潜台词就是我暂时用不到这个对象了,系统释放资源,下次要用到这个对象的时候再申请新的地址。但是新的地址是由操作系统分配的,和上次地址相同的概率微乎其微,因此靠地址来存储对象是完全不可靠的。正如赫拉克利特所说:“人不能两次踏进同一条河流”,对象也不能两次获得同样的地址。
我连同一个机器都不是相同的地址,更别说涉及多个机器的前后端跨平台了。
为了解决这个问题,人们决定加一个中间商作为过渡。你想要使用对象?我给你这个中间商,想要用自己去转换。于是就有了 XML、JSON 等作为格式作为数据传输的“中间商”。他们格式固定、标准统一,便于转换;他们存储对象的具体信息,而非相对地址;他们易读、数据类型(字符串)便于传输……

对象 -> 序列化 -> 存储    读取存储内容 -> 反序列化 -> 对象
对象 -> 序列化 -> 传输 -> 新平台 -> 反序列化 -> 对象

反序列化 Deserialization

反序列化:把字节序列(Byte stream) 还原为对象(Object)

反序列化的来历就没那么复杂了,我都决定把对象序列化成“中间商”了,那总得把它变回对象吧,不然我怎么用啊。
我们知道反序列化是将“中间商”变回对象,不过这个“对象”是当前系统能用的对象,和序列化之前的对象关系不大。
比如我JS 前端的一个 JS 对象通过序列化传给后端,Java 后端则把它转成了 Java 对象(某个类的对象)。即便前后的对象不同,但仍是一个序列化和反序列化的过程。同理,我也可以选择对象的一个或几个属性来构造新对象——简而言之,我想要的是对象的属性值,而不是对象本身

注意事项 / 缺点

当然有缺点啦,哪有什么是完美的

  1. 首先是序列化和反序列化过程中带来的性能压力。本质上讲其实是字符串对象的互相转换,我们自己也完全可以实现一个序列化和反序列化的工具:序列化的时候遍历对象的全部属性,按格式构建出键值对的字符串;反序列化的时候进行切分,提取出属性值并赋值给新对象。这个过程当然是费时费力、消耗资源的,对象越复杂就越耗时,从而影响整个系统的性能。
  2. 其次是序列化和反序列化的准确性问题。不同语言的序列化和反序列化实现过程可能有所不同,同一个语言的不同库也可能有所不同,这就会导致对同一个对象的序列化或反序列化操作的结果有所不同。尤其是对象特别复杂,各种 Map、List 嵌套的时候,从而出现数据不一致、无法识别等问题。
  3. 还有就是当对象格式变化时带来的不一致问题。比如我前端修改了对象格式,那么序列化的结果也会随之改变。如果后端没有同步进行修改,仍然使用旧的格式,新旧对象不兼容,也会引发一系列无法识别、数据丢失等问题。
  4. 当然还有其他一些问题,包括但不限于恶意数据流带来的安全性问题、某些对象无法序列化(网络套接字 Network Sockets)等

启动后端

使用 HttpServer 启动后端

Java封装好的HttpServer可以允许我们轻松地启动后端(所以用 Java 作为后端很方便捏)
不过当然是以自己的电脑当服务器的,毕竟我没钱买云服务器,也没钱买网址=⋀=

public class Main {
    public static void main(String[] args) throws IOException {
        HttpServer httpServer = HttpServer.create(new InetSocketAddress(8080), 0);
        httpServer.createContext("/api", new MyAPIHandler());
        httpServer.start();
        System.out.println("Server Started.");
    }
}

在启动项中,我们通过HttpServer将本机作为后端服务器启动,其第一个参数接收一个InetSocketAddress对象,用于指定端口(这里指定了 8080 端口);第二参数为backlog,表示 TCP 连接的最大并发数, 0 或负数表示使用默认值。
然后通过createContext()规定路径和 API,表示我们进入/api路径后,会通过MyAPIHandler来处理数据并返回给前端。什么?还没有MyAPIHandler,别急,等下就来写它。
最后通过start()来启动HttpServer,最后那个输出是我用来判断其是否正常启动的,可有可无。

现在来写一下我们的MyAPIHandler

public class MyAPIHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        String responseText = "Hello, World!";
        exchange.sendResponseHeaders(200, responseText.length());
        exchange.getResponseBody().write(responseText.getBytes());
        exchange.getResponseBody().close();
    }
}

这玩意继承HttpHandler,需要重写handle()方法,但这不重要,重要的是其中真正工作的HttpExchange。因为我们是接收前端的请求(请求可以带参数,但我们这没有),然后我们处理完之后再返回给前端(可能是一些计算,可能是从数据库中找东西,但我们这没有),一来一回是一个交换过程所以叫exchange
我们的MyAPIHandler不需要接收任何参数,只要你请求我,我就返回给你字符串"Hello, World!",而返回头的中状态码是200,再带个返回体长度。最后关闭返回体资源(因为是以流形式写数据的)。

这下子我们就可以通过访问这个 API 来获取内容了(获取"Hello, World!"
接口的 url 是localhost:8080/api,即本机的 8080 端口 api 路径,都是咱前面自己设定的。
用 Python 访问一下:

response = requests.get('http://localhost:8080/api')
print(response.text)
# Hello, World!

当然还可以直接从浏览器访问:

url in browser

Jackson

Jackson has been known as “the Java JSON library” or “the best JSON parser for Java”. Or simply as “JSON for Java”.

在各种 JSON 相关的库中,Jackson是比较经典的一个,1.0版本发布于 2009 年。除了 JSON,Jackson还支持对 CSV、XML 等许多格式的处理,但不重要,我们用不到。

导入 Jackson

可以在MVN 的 Jackson Databind中选择导入自己喜欢的版本(最新版 2.15.0 甚至是 4 月 24 日推出的,真新啊)。注意如果搜索 Jackson 会出现Jackson DatabindJackson CoreJackson Annotations三个库,选第一个 Databind 就好了,它依赖另外两个库,所以导入的时候会一起导入的。
进入页面根据自己的构建工具选择导入声明就好了,用的比较多的应该是MavenGradle,我创建工程的时候选成了IntelliJ所以只能自己手动导入了,就不演示了。

使用 Jackson

我们想用Jackson来帮助我们进行序列化和反序列化,即实现Java 类JSON 字符串之间的转换,首先我们得有一个 Java 类:

public class PersonBean {
    private String name;
    private int age;

    public PersonBean(){}

    public PersonBean(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //setter & getter...
}

要注意的是,Jackson要求我们的类必须包含一个空构造方法。

因为我们没有用 Java 来访问后端,所以序列化就简单做个 Demo,把一个PersonBean对象变成JSON 字符串

public static void main(String[] args) throws IOException {
	PersonBean person = new PersonBean("Black", 18);
	ObjectMapper objectMapper = new ObjectMapper();
	String jsonStr = objectMapper.writeValueAsString(person);
	System.out.println(jsonStr);
	// {"name":"Black","age":18}
}

作为前端,我们肯定不能直接传PersonBean对象的,但是通过 Jackson 的ObjectMapper进行序列化之后,我们可以直接传jsonStr("{"name":"Black","age":18}")这个字符串给后端,后端通过反序列化重新得到这个对象,进行一些列操作:

public class MyAPIHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        String requestText = "{\"name\":\"Black\",\"age\":18}";
        ObjectMapper objectMapper = new ObjectMapper();
        PersonBean requestPerson = objectMapper.readValue(requestText, PersonBean.class);
        String responseText = String.format("Hello %s, you're %d years old", requestPerson.getName(), requestPerson.getAge());

        exchange.sendResponseHeaders(200, responseText.length());
        exchange.getResponseBody().write(responseText.getBytes());
        exchange.getResponseBody().close();
    }
}

我们先复制了上面前端序列化的 JSON 字符串在这,假装是前端传过来的(如果真的是前端传来的可以通过exchange.getRequestBody()获取)
然后还是 ObjectMapper 帮助我们进行反序列化,将 JSON 字符串重新变回PersonBean的对象,然后我们就能轻松地调用对象的方法,获取对象的属性了。
这里要注意的是,如果我们要反序列化的对象类(PersonBean)没有空构造方法的话,Jackson 是会报错的InvalidDefinitionException。我们可以理解为在反序列化的时候,Jackson 先帮我们实例化一个空的对象,然后根据 JSON 字符串里的内容将属性填充进去。因此如果没有空构造方法,就无法实例化一个空对象,因此报错。

response = requests.get('http://localhost:8080/api')
print(response.text)
# Hello Black, you're 18 years old

Gson

这是 Google 在 2008 年推出的 JSON 库,使用也很方便,主要就是  toJson()  和  fromJson()  两个方法。和 Jackson 相比,Gson 不依赖其他库或 jar 包,而且精确性较高,能够实现一些复杂的对象转换(比如包含 Map、List 等属性的类)

PersonBean person = new PersonBean("Black", 18);
Gson gson = new Gson();
String jsonStr = gson.toJson(person);
System.out.println(jsonStr);
// {"name":"Black","age":18}

说实话这基本用法和 Jackson 不能说有点相似,只能说一模一样,换个对象换个方法名呗

public class MyAPIHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        String requestText = "{\"name\":\"Black\",\"age\":18}";
        Gson gson = new Gson();
        PersonBean requestPerson = gson.fromJson(requestText, PersonBean.class);
        String responseText = String.format("Gson: Hello %s, you're %d years old", requestPerson.getName(), requestPerson.getAge());

        exchange.sendResponseHeaders(200, responseText.length());
        exchange.getResponseBody().write(responseText.getBytes());
        exchange.getResponseBody().close();
    }
}
// 反正访问后会得到:Gson: Hello Black, you're 18 years old

Fastjson

最后来看一个国产库,阿里巴巴的开源 JSON 库,Fastjson
Fastjson 主打一个高性能,无依赖,其有着独创的算法,有效提高了 JSON 字符串和 Java 对象之间的转换速度。虽然其精确性赶不上 Gson,但是运行速度是所有 JSON 库中数一数二的。

现在阿里已经推出了Fastjson v2,提升性能,扩展功能,修复问题,但在某些方面不兼容Fastjson 1,所以官方建议原项目进行迁移升级。Fastjson v2 宣称“目标是为下一个十年提供一个高性能的JSON库”,可以说是次时代 JSON 库了。

Fastjson提供了一些静态方法允许 JSON 字符串和 Java 对象之间进行转换:

	// Object to JSON String
	PersonBean person = new PersonBean("Black", 18);
	byte[] jsonBytes = JSON.toJSONBytes(person);
	String jsonStr = JSON.toJSONString(person);
	// {"age":18,"name":"Black"}

	// JSON String to Object
	String requestText = "{\"name\":\"Black\",\"age\":18}";
	PersonBean requestPerson = JSON.parseObject(requestText, PersonBean.class);
	System.out.println(requestPerson.getName() + " is " + requestPerson.getAge());
	// Black is 18

此外,Fastjson 还提供了中间类型JSONObjectJSONArray供我们使用,如果不指定parseObject()方法的类参数,那么其就会返回一个JSONObject
即使我们本地没有定义这个类,也可以获取其中的属性:

String requestText = "{\"name\":\"Black\",\"age\":18}";
JSONObject object = JSON.parseObject(requestText);
String name = object.getString("name");

String requestText = "[{\"name\":\"Black\",\"age\":18}, {\"name\":\"White\",\"age\":8}]";
JSONArray array = JSON.parseArray(requestText);
// get Object from Array
JSONObject secondObj = array.getJSONObject(1);
String secondName = secondObj.getString("name"); // White
// get Java Obj from Array
PersonBean firstPerson = array.getObject(0, PersonBean.class);
String firstName = firstPerson.getName(); // Black

后话

因为偷懒,上面的例子都是用 Python 来充当前端,去访问我们的 Java 后端。实际上还有很多其他的工具能够帮我们调用 API / 访问接口,比如Postman,比如 命令行工具 Curl

这篇文章比较粗浅,是好久以前学习安卓开发的时候给自己扫盲后端用的,但是一直没写完。一方面是因为懒,另一方面…这篇之后我想在后端更进一步,那么就要去看 Spring 框架了,不愿面对 QAQ
所以关于 Spring 的文章,有空再说吧,能拖一天是一天嗷

参考

  1. MDN: API
  2. JSON 的兴起与崛起
  3. 使用 JSON
  4. What Are Serialization and Deserialization in Programming?
  5. MVN:Jackson Databind/ Jackson Project Home @github
  6. MVN:Gson / Github:Gson
  7. Github:fastjson2