SpringWeb 服务构建轻量级 Web 技术体系:SpringGraphQL
Spring GraphQL
当下,前后端分离是互联网应用程序开发的主流做法,如何设计合理且高效的前后端交互 Web API 是前端和后端开发人员日常工作的一大难点和痛点。我们接下来要引入的这项新技术就为解决这一问题提供了很好的方案,这就是 GraphQL。而随着 Spring GraphQL 正式成为 Spring 家族的顶级项目,我们也迎来了面向 GraphQL 的全新开发模式。
在本节中,我们将深入分析 GraphQL 所具备的功能特性和核心组件,并基于 Spring GraphQL 展示如何引入并使用这款新的开发框架。
GraphQL 与 RESTful API
假设正在开发一个 Web 应用程序,让我们先来回想日常开发过程中的真实场景:服务端开发人员通过 HTTP 暴露了一个 RESTful API,然后前端开发人员尝试对这个 HTTP 端点发起调用。这个 HTTP 端点一开始代码如下所示:
看上去非常简单,对不对?刚开始前后端联调一切正常。不知什么时候,前端开发人员发现响应结果中原来的 address 字段不见了,而是出现了一个 location 字段,原来是后端开发人员觉得 address 这个字段名不合适,偷偷把它改成了 location,但并没有告诉前端开发人员。下图展示了这一过程。
这时候,前后端之间就需要重新明确 API 定义,并再一次进行联调。显然,这个过程实际上是非常浪费时间的。
相信你对上面这个场景非常熟悉,因为我们可能每天都在反复经历着类似的场景。从这些场景中,前后端开发人员已经意识到,传统的 RESTful API 并不能非常好地满足前后端分离场景下的交互需求。我们可以进一步把 RESTful API 存在的问题做一些梳理。
1. RESTful API 存在的问题
RESTful API 的第一个典型问题就是前端无法预判响应的数据格式,正如上图所展示的那样,一旦服务端对数据结构做了任何改变,前端都只能被动接收,而无法在发起请求之前感知到这种改变。
RESTful API 的第二个典型问题是无法根据请求控制对应的返回结果。例如在前面的场景中,前端请求可能只想获取 User 对象中的 name 和 age 字段,而不需要 address 字段。显然,RESTful API 无法满足这种诉求,除非另外开发一个 HTTP 端点。我们知道,数据在网络中的传输是需要成本的,无法按需获取数据同样导致了资源的不必要浪费。
RESTful API 的第三个典型问题就是多次请求。再次回到上述场景中,假设 User 对象中包含了一组家庭成员信息。那么基于 RESTful API,如果想要获取这些数据,就只能再发起一个专门的请求来根据 User 的 id 获取对应的家庭成员列表,例如下图所展示的https://api.example.com/user/family/1。
当然,我们也可以针对该需求专门设计一个能够同时返回用户信息和家庭成员信息的接口。但这又会引出 RESTful API 的第四个典型问题,即请求地址过多的问题。如果针对各个具体场景我们都需要一一暴露专门的 HTTP 端点,那么在一个系统中 HTTP 端点数量会非常庞大,难以维护和管理。
RESTful API 的问题已经暴露得非常清楚了。如何有效解决这些问题呢?
可以引入一个新技术,即 GraphQL。
相比于 REST,GraphQL 可以说是一个比较新的技术,它于 2012 年诞生在 Facebook 内部,并于 2015 年正式开源。顾名思义,GraphQL 是一种基于图(Graph)的查询语言(Query Language,QL),从根本上改变了前后端交互 API 的定义和实现方式。接下来,我们详细分析如何通过 GraphQL 解决 RESTfulAPI 所面临的一系列问题。
2. GraphQL 的解决方案
要想使用 GraphQL,我们首先需要关注它发送请求的方式。针对获取用户信息这个场景,一个典型的请求示例代码如下所示:
可以看到基于 GraphQL 的请求方式与使用 RESTful API 有很大的不同。除了在请求体中指定了目标 User 对象的参数 id 值之外,我们还额外指定了 name 和 age 这两个参数,也就是告诉服务器端这次请求所希望获取的数据字段。
显然,这种请求方式完美解决了 RESTful API 中无法根据请求控制对应返回结果的问题。同时,这种请求方式也解决了前端无法预判响应的数据的格式问题,因为前端在请求的同时已经知道从服务端返回的数据字段就是请求中指定的字段,因此就不需要再对响应结果进行专门的判断和处理。
针对 RESTful API 存在的多次请求问题,GraphQL 可以把多次请求合并成一次。例如,我们可以发送如下所示的请求:
在该请求中,我们指定了想要获取的 User 对象中的 name 和 age 字段,同时也指定了该获取用户对应的家庭成员列表字段 members 以及它的子字段 name。
这样,通过一次请求,我们就可以同时获取用户信息和家庭成员信息,而不需要像 RESTful API 那样发送两次请求。
讲到这里,你可能已经注意到,通过 GraphQL 发起请求实际上只需要指定一个 HTTP 端点地址即可,因为我们可以基于同一个端点传入不同的参数而获取不同的结果,也就不需要专门设计一批 HTTP 端点来分别处理不同的请求了。
总结一下,RESTful API 所存在的核心问题通过 GraphQL 都可以得到解决。
集成 Spring 和 GraphQL
在讨论如何在 Spring 中使用 GraphQL 之前,我们首先来梳理 Java 世界中与 GraphQL 相关的几个开发框架,如图所示:
在上图中,GraphQL Java 是 GraphQL 的 Java 语言实现。这个框架是偏底层的,相当于是一个负责执行 GraphQL 请求的引擎。Spring 在 GraphQL Java 的基础上开发了一个 GraphQL Java Spring 框架,专门用来实现在 Spring 框架中嵌入 GraphQL Java。而最新的 Spring GraphQL 则是 GraphQL Java Spring 的替代框架,该框架的开发工作将由 GraphQL Java 和 Spring 两个团队共同承担,代表了 Spring 和 GraphQL Java 之间最新的合作成果。
1. GraphQL Java 中的核心组件
无论是 GraphQL Java Spring,还是 Spring GraphQL,本质上都是对 GraphQL Java 的封装和扩展。因此,在使用这些框架之前,我们需要首先掌握 GraphQL Java 中的核心编程组件。
(1)Schema
首先,我们需要引入一个核心组件,即 Schema。所谓 Schema,简单讲就是一种前后端交互的协议和规范,或者可以把它类比成 RESTful API 中的接口定义文档。
在 Schema 中,开发人员需要指定两部分内容。一方面,我们需要明确定义前后端交互的数据结构,包括具体的字段名称、类型、是否为空等属性。
另一方面,GraphQL 规定每一个 Schema 中可以存在一个根 Query 和根 Mutation,分别用于执行查询和更新操作。
下面代码所展示的就是一个典型的 Schema 定义。在 Spring Boot 中,我们需要把该文件放置在 classpath 下。
在上述 Schema 中,我们看到了 ID、String、Boolean 等基本数据类型,以及用来表明是否为空的!和数组的[]。
(2)DataFetcher
从命名上看,DataFetcher 组件的作用就是在执行查询时获取字段对应的数据。Data-Fetcher 是一个接口,只定义了一个方法,代码如下所示:
开发人员可以从 DataFetchingEnvironment 中获取传入的参数,并根据该参数来执行具体的数据查询操作。至于数据查询操作的具体实现过程,DataFetcher 并不关心。
(3)RuntimeWiring
创建 DataFetcher 只是开始,我们还要将它们应用在 GraphQL 服务器上,这就需要借助 RuntimeWiring 组件。通过 Runtime Wiring 机制,我们可以把 DataFetcher 整合在 GraphQL 的运行环境中。创建 RuntimeWiring 的典型实现如下所示:
可以看到,这里通过 RuntimeWiring 的 type()方法将各个 DataFetcher 与对应的数据结构关联起来。
(4)GraphQL 对象
最后,基于 Schema 和 RuntimeWiring,我们就可以创建 GraphQL 对象,代码如下所示:
基于这个 GraphQL 对象,我们就可以使用它来完成具体的查询操作,代码如下所示:
2. Spring GraphQL
介绍完 GraphQL Java 之后,让我们来到 Spring Boot,看看如何通过 Spring GraphQL 完成对 GraphQL Java 的集成。
在 2021 年 7 月 6 日,Spring 社区正式宣布 Spring GraphQL 成为 Spring 家族的顶级项目,并发布了该新项目里程碑的 1.0 版本。在 GraphQL Java 诞生 6 周年之际,我们迎来了 Spring 家族的这个新成员,Spring GraphQL 的核心价值是将 GraphQL Java 集成到 Spring 生态。
我们知道,GraphQL 是一种理念和规范,并不直接提供开发工具和框架。
而 GraphQL Java 是基于 Java 开发的一个 GraphQL 实现库,但这个实现库一直都还只是一个执行 GraphQL 请求的引擎。在实际的应用开发中,开发人员还需要创建自己的 HTTP 适配器来将它与业务代码完成整合。
在 Java 世界中,Spring 是目前最主流的开发框架,没有之一。
Spring GraphQL 的诞生,为使用 Spring 框架的开发人员提供了针对 GraphQL 的一站式开发体验。Spring GraphQL 的诞生以及不断发展,将大幅度降低 GraphQL 的开发难度和成本,也将极大促进广大 Spring 框架的开发人员熟悉并掌握 GraphQL,从而推动 GraphQL 在日常开发过程中的落地。
可以说,Spring GraphQL 为开发人员使用 GraphQL Java 提供了最简便的封装。在 Spring GraphQL 代码工程中,主要有 spring-graphql 和 graphqlspring-boot-starter 这两个子工程,其中前者对如何使用 GraphQL 进行了抽象,而后者就是一个 Spring Boot Starter 工程。
关于 spring-graphql,我们不得不提 GraphQlSource。GraphQlSource 是 Spring GraphQL 中的一个核心抽象,用于访问 GraphQL 实例以执行请求。它提供了一个构建器 API 来初始化 GraphQL Java 并创建一个 GraphQL 实例。
请注意,开发人员本身并不需要了解这个 GraphQlSource 对象的构建过程,因为它的职责是在框架内部完成 GraphQL 执行引擎的初始化,这是 SpringGraphQL 框架自动会为我们做的事情。开发人员唯一要做的就是通过 GraphQlSource 获取一个 GraphQL 对象,代码如下所示:
另外,GraphQL 引擎所需要执行的数据查询操作与业务相关,这部分功能需要开发人员根据具体业务场景进行设计并实现,这时候就会使用到 graphql-spring-boot-starter 中的 RuntimeWiringBuilderCustomizer 接口。
RuntimeWiringBuilderCustomizer 接口简化了 Runtime-Wiring 的实现过程,开发人员通过实现这个接口就可以设置一系列的 DataFetcher,示例代码如下所示:
Spring GraphQL 案例分析
到目前为止,关于 GraphQL 以及 Spring GraphQL 的知识点都已经介绍完毕。在本节的最后,我们将通过一个完整的案例来展示 Spring GraphQL 框架的使用方法。该案例是前面所介绍的 Spring WebMVC 案例的升级版。设计并实现一个基于 GraphQL 的完整案例,需要遵循一定的开发流程,如图所示:
在上图中,设计领域对象和实现数据访问层组件这两个步骤和开发 RESTful API 是一致的,而其他 5 个步骤则需要基于前面介绍的 GraphQL Java 和 Spring GraphQL 分别完成。
在明确了开发步骤之后,我们进入到代码演示阶段,首先需要初始化代码环境。我们在 Maven 工程的 pom 文件中添加如下所示的依赖包:
这里有几点值得注意。首先,如果你在公开的 Maven 仓库中搜索 graphqlspring-boot-starter 这个 artifactId,会发现存在多个对应的 groupId,这是因为老版本的 GraphQL Java Spring 框架已经实现了同名的 artifactId。
而我们在这里指定 groupId 为 org.springframework.experimental,这是 SpringGraphQL 框架目前所属的 groupId,可以看到它还属于试验(experimental)阶段,并没有发布到公开的 Maven 仓库中。所以,为了引入这个依赖包,我们需要指定 Spring 官方的 Maven 仓库地址,代码如下所示:
同时,在这个案例中,我们将基于前面介绍的 Spring WebMVC 案例,采用 GraphQL 对其进行重构。因此,案例中所采用的领域对象以及数据存储媒介都保持一致,这里就不再赘述。
有了领域对象,就可以定义 GraphQL Schema 了。我们在代码工程的 resources 目录下创建一个 graphql 文件夹,然后创建一个 schema.graphqls 文件,该文件内容如下所示:
同时,不要忘了在 Spring Boot 的配置文件中指定存放该文件的路径地址,代码如下所示:
第三步实现数据访问层组件的过程就比较简单了,我们直接继承 Spring Data 提供的 PagingAndSortingRepository 接口即可,这部分代码在前面也都介绍过,这里简单回顾,代码如下所示:
第四步定义 DataFetcher,我们需要根据业务场景来设计并实现对应的 DataFetcher 组件。在案例中,一方面需要根据用户 ID 来获取单个 User 对象,以及获取整个 User 列表;另一方面也需要根据用户信息获取该用户所阅读文章的信息。所以,我们将设计并实现三个 DataFetcher,即 UserDataFetcher、AllUsersDataFetcher 和 ArticlesDataFetcher,代码如下所示:
以上三个 DataFetcher 组件的实现过程就是对 UserRepository 和 ArticleRepository 的封装,具体代码可以参考案例的源码,这里不再展开讨论。
在 Spring GraphQL 中,可以通过实现 RuntimeWiringBuilderCustomizer 接口中的 customize()方法来完成 Data Wiring。我们创建一个 UserDataWiring 组件,代码如下所示:
可以看到,这里通过 Builder 的 type()方法把 Schema 中定义的数据结构和 DataFetcher 组件整合在一起。
最后,我们通过构建一个 GraphQLController 来完成对 HTTP 端点的暴露,代码如下所示:
在这个过程中,我们通过注入 Spring GraphQL 提供的 GraphQlSource 来获取 GraphQL 对象,然后使用 GraphQL 对象的 execute()方法实现最终的数据查询操作。
接下来到了案例演示的环节。让我们打开 Postman,在 http://localhost:8080/query 这个 HTTP 端点中输入如下所示的请求信息:
执行这个请求,在返回结果中,我们将得到一组 User 对象,而这些 User 对象将只包含 name 和 age 这两个数据字段。
让我们考虑一个更加复杂的查询场景。我们想要获取一个 User 对象列表,这些 User 对象中包含其好友名称、所阅读文章的标题和阅读时间,那么就可以发起如下所示的请求:
对于这些请求的完整响应结果,你可以自己运行代码来尝试获取。
案例的完整代码位于:https://github.com/tianminzheng/spring-bootexamples/tree/main/SpringGraphQLExample。
另外,GraphQL 的技术体系还为开发人员提供了一个发送请求和获取响应结果的可视化工具,即 GraphiQL。在使用 Spring GraphQL 时,我们可以通过在配置文件中添加如下所示的配置项来启用这个工具。
这时候,访问 http://localhost:8080/graphiql,我们将会得到一个可视化界面,如图所示:
在这个界面中,我们可以输入请求参数、发起调用并获取响应结果。你同样可以自己做一些尝试。
评论