SpringWeb服务构建轻量级Web技术体系:SpringGraphQL

SpringWeb服务构建轻量级Web技术体系:SpringGraphQL我们接下来要引入的这项新技术就为解决这一问题提供了很好的方案,这就是GraphQL。不知什么时候,前端开发人员发现响应结果中原来的address

大家好,欢迎来到IT知识分享网。

Spring GraphQL

当下,前后端分离是互联网应用程序开发的主流做法,如何设计合理且高效的前后端交互Web API是前端和后端开发人员日常工作的一大难点和痛点。我们接下来要引入的这项新技术就为解决这一问题提供了很好的方案,这就是GraphQL。而随着Spring GraphQL正式成为Spring家族的顶级项目,我们也迎来了面向GraphQL的全新开发模式。

在本节中,我们将深入分析GraphQL所具备的功能特性和核心组件,并基于Spring GraphQL展示如何引入并使用这款新的开发框架。

GraphQL与RESTful API

假设正在开发一个Web应用程序,让我们先来回想日常开发过程中的真实场景:服务端开发人员通过HTTP暴露了一个RESTful API,然后前端开发人员尝试对这个HTTP端点发起调用。这个HTTP端点一开始如代码清单4-56所示。

代码清单4-56 普通HTTP端点示例代码

请求:

GET https://api.example.com/user/1

响应:

{

“id”: “1”,

“name”: “tianyalan”,

“age”: “38”,

“address”: “shanghai”

}

看上去非常简单,对不对?刚开始前后端联调一切正常。不知什么时候,前端开发人员发现响应结果中原来的address字段不见了,而是出现了一个location字段,原来是后端开发人员觉得address这个字段名不合适,偷偷把它改成了location,但并没有告诉前端开发人员。图4-5展示了这一过程。

这时候,前后端之间就需要重新明确API定义,并再一次进行联调。显然,这个过程实际上是非常浪费时间的。

SpringWeb服务构建轻量级Web技术体系:SpringGraphQL

图4-5 Web API中字段名变更示意图

相信你对上面这个场景非常熟悉,因为我们可能每天都在反复经历着类似的场景。从这些场景中,前后端开发人员已经意识到,传统的RESTful API并不能非常好地满足前后端分离场景下的交互需求。我们可以进一步把RESTful API存在的问题做一些梳理。

1. RESTful API存在的问题

RESTful API的第一个典型问题就是前端无法预判响应的数据格式,正如图4-5所展示的那样,一旦服务端对数据结构做了任何改变,前端都只能被动接收,而无法在发起请求之前感知到这种改变。

RESTful API的第二个典型问题是无法根据请求控制对应的返回结果。例如在图4-5的场景中,前端请求可能只想获取User对象中的name和age字段,而不需要address字段。显然,RESTful API无法满足这种诉求,除非另外开发一个HTTP端点。我们知道,数据在网络中的传输是需要成本的,无法按需获取数据同样导致了资源的不必要浪费。

RESTful API的第三个典型问题就是多次请求。再次回到上述场景中,假设User对象中包含了一组家庭成员信息。那么基于RESTful API,如果想要获取这些数据,就只能再发起一个专门的请求来根据User的id获取对应的家庭成员列表,例如图4-6中所展示的https://api.example.com/user/family/1

SpringWeb服务构建轻量级Web技术体系:SpringGraphQL

图4-6 RESTful API的多次请求示意图

当然,我们也可以针对该需求专门设计一个能够同时返回用户信息和家庭成员信息的接口。但这又会引出RESTful API的第四个典型问题,即请求地址过多的问题。如果针对各个具体场景我们都需要一一暴露专门的HTTP端点,那么在一个系统中HTTP端点数量会非常庞大,难以维护和管理。

RESTful API的问题已经暴露得非常清楚了。如何有效解决这些问题呢?

可以引入一个新技术,即GraphQL。

相比于REST,GraphQL可以说是一个比较新的技术,它于2012年诞生在Facebook内部,并于2015年正式开源。顾名思义,GraphQL是一种基于图(Graph)的查询语言(Query Language,QL),从根本上改变了前后端交互API的定义和实现方式。接下来,我们详细分析如何通过GraphQL解决RESTfulAPI所面临的一系列问题。

2. GraphQL的解决方案

要想使用GraphQL,我们首先需要关注它发送请求的方式。针对获取用户信息这个场景,一个典型的请求示例如代码清单4-57所示。

代码清单4-57 基于GraphQL的请求示例代码{

user (id: “1”) {

name

age

}

}

可以看到基于GraphQL的请求方式与使用RESTful API有很大的不同。除了在请求体中指定了目标User对象的参数id值之外,我们还额外指定了name和age这两个参数,也就是告诉服务器端这次请求所希望获取的数据字段。

显然,这种请求方式完美解决了RESTful API中无法根据请求控制对应返回结果的问题。同时,这种请求方式也解决了前端无法预判响应的数据的格式问题,因为前端在请求的同时已经知道从服务端返回的数据字段就是请求中指定的字段,因此就不需要再对响应结果进行专门的判断和处理。

针对RESTful API存在的多次请求问题,GraphQL可以把多次请求合并成一次。例如,我们可以发送如代码清单4-58所示的请求。

代码清单4-58 基于GraphQL的合并多次请求示例代码

{

users {

name

age

members {

name

}

}

}

在该请求中,我们指定了想要获取的User对象中的name和age字段,同时也指定了该获取用户对应的家庭成员列表字段members以及它的子字段name。

这样,通过一次请求,我们就可以同时获取用户信息和家庭成员信息,而不需要像RESTful API那样发送两次请求。

讲到这里,你可能已经注意到,通过GraphQL发起请求实际上只需要指定一个HTTP端点地址即可,因为我们可以基于同一个端点传入不同的参数而获取不同的结果,也就不需要专门设计一批HTTP端点来分别处理不同的请求了。

总结一下,RESTful API所存在的核心问题通过GraphQL都可以得到解决。

集成Spring和GraphQL

在讨论如何在Spring中使用GraphQL之前,我们首先来梳理Java世界中与GraphQL相关的几个开发框架,如图4-7所示。

SpringWeb服务构建轻量级Web技术体系:SpringGraphQL

图4-7 Java世界中的GraphQL相关开发框架

在图4-7中,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,分别用于执行查询和更新操作。

如代码清单4-59所示的就是一个典型的Schema定义。在Spring Boot中,我们需要把该文件放置在classpath下。

代码清单4-59 GraphQL Schema示例代码

type Query {

employees: [Employee]

}

type Mutation {

updateSalary(input: UpdateSalaryInput!): UpdateSalaryPayload

}

type Employee {

id: ID!

name: String

salary: String}

input UpdateSalaryInput {

employeeId: ID!

salary: String!

}

type UpdateSalaryPayload {

success: Boolean!

employee: Employee

}

在上述Schema中,我们看到了ID、String、Boolean等基本数据类型,以及用来表明是否为空的!和数组的[]。

(2)DataFetcher

从命名上看,DataFetcher组件的作用就是在执行查询时获取字段对应的数据。Data-Fetcher是一个接口,只定义了一个方法,如代码清单4-60所示。

代码清单4-60 DataFetcher接口定义代码

public interface DataFetcher<T> {

T get(DataFetchingEnvironment dataFetchingEnvironment) throws

Exception;

}

开发人员可以从DataFetchingEnvironment中获取传入的参数,并根据该参数来执行具体的数据查询操作。至于数据查询操作的具体实现过程,DataFetcher并不关心。

(3)RuntimeWiring创建DataFetcher只是开始,我们还要将它们应用在GraphQL服务器上,这就需要借助RuntimeWiring组件。通过Runtime Wiring机制,我们可以把DataFetcher整合在GraphQL的运行环境中。创建RuntimeWiring的典型实现如代码清单4-61所示。

代码清单4-61 创建RuntimeWiring示例代码

private AllUsersDataFetcher allUsersDataFetcher;

private UserDataFetcher userDataFetcher;

private ArticlesDataFetcher articlesDataFetcher;

private RuntimeWiring buildRuntimeWiring() {

return RuntimeWiring.newRuntimeWiring()

.type(“Query”, typeWiring -> typeWiring

.dataFetcher(“users”, allUsersDataFetcher)

.dataFetcher(“user”, userDataFetcher))

.type(“User”, typeWiring -> typeWiring

.dataFetcher(“articles”, articlesDataFetcher)

.dataFetcher(“friends”, allUsersDataFetcher))

.build();

}

可以看到,这里通过RuntimeWiring的type()方法将各个DataFetcher与对应的数据结构关联起来。

(4)GraphQL对象

最后,基于Schema和RuntimeWiring,我们就可以创建GraphQL对象,如代码清单4-62所示。

代码清单4-62 创建GraphQL对象示例代码

File schemas = schemeResource.getFile();

TypeDefinitionRegistry typeRegistry = newSchemaParser().parse(schemas);

RuntimeWiring wiring = buildRuntimeWiring();

GraphQLSchema schema = new

SchemaGenerator().makeExecutableSchema(typeRegistry, wiring);

GraphQL graphQL = GraphQL.newGraphQL(schema).build();

基于这个GraphQL对象,我们就可以使用它来完成具体的查询操作,如代码清单4-63所示。

代码清单4-63 基于GraphQL的查询操作示例代码

String query = …;

ExecutionResult result = graphQL.execute(query);

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对象,如代码清单4-64所示。

代码清单4-64 通过GraphQlSource获取GraphQL对象示例代码

GraphQL graphQL = graphQlSource.graphQl();

另外,GraphQL引擎所需要执行的数据查询操作与业务相关,这部分功能需要开发人员根据具体业务场景进行设计并实现,这时候就会使用到graphql-spring-boot-starter中的RuntimeWiringBuilderCustomizer接口。

RuntimeWiringBuilderCustomizer接口简化了Runtime-Wiring的实现过程,开发人员通过实现这个接口就可以设置一系列的DataFetcher,示例代码如代

码清单4-65所示。

代码清单4-65 ProjectDataWiring类代码

public class ProjectDataWiring implements

RuntimeWiringBuilderCustomizer {

@Override

public void customize(RuntimeWiring.Builder builder) {

builder.type(“Query”, typeWiring ->

typeWiring.dataFetcher(“project”, env -> {

String slug = env.getArgument(“slug”);

})).type(“Project”, typeWiring ->

typeWiring.dataFetcher(“releases”, env -> {

Project project = env.getSource();

}));

}

}

Spring GraphQL案例分析

到目前为止,关于GraphQL以及Spring GraphQL的知识点都已经介绍完毕。在本节的最后,我们将通过一个完整的案例来展示Spring GraphQL框架的使用方法。该案例是4.1节中所介绍的Spring WebMVC案例的升级版。设计并实现一个基于GraphQL的完整案例,需要遵循一定的开发流程,如图4-8所示。

SpringWeb服务构建轻量级Web技术体系:SpringGraphQL

图4-8 Spring GraphQL开发流程

在图4-8中,设计领域对象和实现数据访问层组件这两个步骤和开发RESTful API是一致的,而其他5个步骤则需要基于前面介绍的GraphQL Java和Spring GraphQL分别完成。

在明确了开发步骤之后,我们进入到代码演示阶段,首先需要初始化代码环境。我们在Maven工程的pom文件中添加如代码清单4-66所示的依赖包。

代码清单4-66 Spring GraphQL依赖包定义代码

<dependencies>

<dependency>

<groupId>org.springframework.experimental</groupId>

<artifactId>graphql-spring-boot-starter</artifactId>

<version>1.0.0-SNAPSHOT</version>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

</dependencies>

这里有几点值得注意。首先,如果你在公开的Maven仓库中搜索graphqlspring-boot-starter这个artifactId,会发现存在多个对应的groupId,这是因为老版本的GraphQL Java Spring框架已经实现了同名的artifactId。而我们在这里指定groupId为org.springframework.experimental,这是SpringGraphQL框架目前所属的groupId,可以看到它还属于试验(experimental)阶段,并没有发布到公开的Maven仓库中。所以,为了引入这个依赖包,我们需要指定Spring官方的Maven仓库地址,如代码清单4-67所示。

代码清单4-67 Spring官方Maven仓库配置代码

<repositories>

<repository>

<id>spring-milestones</id>

<name>Spring Milestones</name>

<url>https://repo.spring.io/milestone</url>

</repository>

<repository>

<id>spring-snapshots</id>

<name>Spring Snapshots</name>

<url>https://repo.spring.io/snapshot</url>

<snapshots>

<enabled>true</enabled>

</snapshots>

</repository>

</repositories>

同时,在这个案例中,我们将基于4.1.4节中介绍的Spring WebMVC案例,采用GraphQL对其进行重构。因此,案例中所采用的领域对象以及数据存储媒介都保持一致,这里就不再赘述。

有了领域对象,就可以定义GraphQL Schema了。我们在代码工程的resources目录下创建一个graphql文件夹,然后创建一个schema.graphqls文件,该文件内容如代码清单4-68所示。

代码清单4-68 GraphQL Schema示例代码

schema {

query : Query

}

type Query {

users: [User]

user(id: String): User

}

type User {

id: String

name: String

age: Int

nationality: String

createdAt: String

friends: [User]

articles: [Article]

}

type Article {

id: String

title: String

minutesRead: Int

}

同时,不要忘了在Spring Boot的配置文件中指定存放该文件的路径地址,代码清单4-69所示。

代码清单4-69 GraphQL Schema文件地址配置代码

spring.graphql.schema.locations=classpath:graphql/

如图4-8所示的第三步实现数据访问层组件的过程就比较简单了,我们直接继承Spring Data提供的PagingAndSortingRepository接口即可,这部分代码在4.1.4节中也都介绍过,这里简单回顾,如代码清单4-70所示。

代码清单4-70 UserRepository和ArticleRepository接口定义代码

public interface UserRepository extends

PagingAndSortingRepository<User, String> {

User findUserById(String id);

}

public interface ArticleRepository extends

PagingAndSortingRepository<Article, String> {

Article findArticleById(String id);

}

至于图4-8中的第四步定义DataFetcher,我们需要根据业务场景来设计并实现对应的DataFetcher组件。在案例中,一方面需要根据用户ID来获取单个User对象,以及获取整个User列表;另一方面也需要根据用户信息获取该用户所阅读文章的信息。所以,我们将设计并实现三个DataFetcher,即UserDataFetcher、AllUsersDataFetcher和ArticlesDataFetcher,如代码清单4-71所示。

代码清单4-71 DataFetcher类定义代码

public class UserDataFetcher implements DataFetcher<User>

public class AllUsersDataFetcher implements DataFetcher<List<User>>

public class ArticlesDataFetcher implements DataFetcher<List<Article>>

以上三个DataFetcher组件的实现过程就是对UserRepository和ArticleRepository的封装,具体代码可以参考案例的源码,这里不再展开讨论。

在Spring GraphQL中,可以通过实现RuntimeWiringBuilderCustomizer接口中的customize()方法来完成Data Wiring。我们创建一个UserDataWiring组件,如代码清单4-72所示。

代码清单4-72 UserDataWiring类定义代码

@Component

public class UserDataWiring implements RuntimeWiringBuilderCustomizer

{

private AllUsersDataFetcher allUsersDataFetcher;

private UserDataFetcher userDataFetcher;

private ArticlesDataFetcher articlesDataFetcher;

@Autowired

public UserDataWiring(AllUsersDataFetcher allUsersDataFetcher,

UserDataFetcher userDataFetcher, ArticlesDataFetcher

articlesDataFetcher) {

this.allUsersDataFetcher = allUsersDataFetcher;

this.userDataFetcher = userDataFetcher;

this.articlesDataFetcher = articlesDataFetcher;

}

@Override

public void customize(Builder builder) {

builder.type(“Query”, typeWiring ->

typeWiring.dataFetcher(“users”, allUsersDataFetcher) .dataFetcher(“user”, userDataFetcher))

.type(“User”, typeWiring ->

typeWiring.dataFetcher(“articles”, articlesDataFetcher)

.dataFetcher(“friends”, allUsersDataFetcher));

}

}

可以看到,这里通过Builder的type()方法把Schema中定义的数据结构和DataFetcher组件整合在一起。

最后,我们通过构建一个GraphQLController来完成对HTTP端点的暴露,如代码清单4-73所示。

代码清单4-73 GraphQLController类定义代码

@RestController

public class GraphQLController {

private GraphQL graphQL;

@Autowired

public GraphQLController(GraphQlSource graphQlSource) throws

IOException {

graphQL = graphQlSource.graphQl();

}

@PostMapping(value = “/query”)

public ResponseEntity<Object> query(@RequestBody String query){

ExecutionResult result = graphQL.execute(query);

System.out.println(“errors: ” + result.getErrors());

return ResponseEntity.ok(result.getData());

}

}

在这个过程中,我们通过注入Spring GraphQL提供的GraphQlSource来获取GraphQL对象,然后使用GraphQL对象的execute()方法实现最终的数据查询操作。

接下来到了案例演示的环节。让我们打开Postman,在http://localhost:8080/query这个HTTP端点中输入如代码清单4-74所示的请求信息。

代码清单4-74 通过GraphQL发起请求示例代码

{

users {

name

age

}

}

执行这个请求,在返回结果中,我们将得到一组User对象,而这些User对象将只包含name和age这两个数据字段。

让我们考虑一个更加复杂的查询场景。我们想要获取一个User对象列表,这些User对象中包含其好友名称、所阅读文章的标题和阅读时间,那么就可以发起如代码清单4-75所示的请求。

代码清单4-75 通过GraphQL发起复杂查询请求示例代码

{

users {

age

name

friends {

name

}

articles {

title

minutesRead }

}

}

对于这些请求的完整响应结果,你可以自己运行代码来尝试获取。

案例的完整代码位于:https://github.com/tianminzheng/spring-bootexamples/tree/main/SpringGraphQLExample。

另外,GraphQL的技术体系还为开发人员提供了一个发送请求和获取响应结果的可视化工具,即GraphiQL。在使用Spring GraphQL时,我们可以通过在配置文件中添加如代码清单4-76所示的配置项来启用这个工具。

代码清单4-76 GraphiQL配置代码

spring.graphql.graphiql.enabled=true

spring.graphql.graphiql.path=/graphiql

这时候,访问http://localhost:8080/graphiql,我们将会得到一个可视化界面,如图4-9所示。

SpringWeb服务构建轻量级Web技术体系:SpringGraphQL

图4-9 GraphiQL可视化界面效果

在这个界面中,我们可以输入请求参数、发起调用并获取响应结果。你同样可以自己做一些尝试。

本文给大家讲解的内容是springweb服务构建轻量级Web技术体系: Spring GraphQL

  • 下文给大家讲解的是springweb服务构建轻量级Web技术体系:轻量级Web实战经验

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/43036.html

(0)
上一篇 2024-04-14 19:45
下一篇 2024-04-19 19:00

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信