Light
Light
目录
  1. 开始使用GraphQL Java和Spring Boot
    1. 三分钟介绍GraphQL
    2. GraphQL Java 预览
    3. 我们的示例API:获取图书详细信息
    4. 创建一个Spring Boot应用程序
    5. Schema
    6. DataFetchers
      1. 数据源
      2. Book DataFetcher
      3. Author DataFetcher
      4. Default DataFetchers
    7. 试用API
    8. 完整的示例源代码和更多信息

开始使用GraphQL Java和Spring Boot

开始使用GraphQL Java和Spring Boot

这是一篇为想要用Java搭建GraphQL服务器的小伙伴们准备的教程。需要你有一定的Spring Boot和Java开发相关知识,虽然我们简要介绍了GraphQL,但是本教程的重点是用Java开发一个GraphQL服务器。

三分钟介绍GraphQL

GraphQL是一门从服务器检索数据的查询语言。在某些场景下可以替换REST、SOAP和gRPC。让我们假设我们想要从一个在线商城的后端获取某一个本书的详情。

你使用GraphQL往服务器发送如下查询去获取id为”123”的那本书的详情:

1
2
3
4
5
6
7
8
9
10
11
{
bookById(id: "book-1"){
id
name
pageCount
author {
firstName
lastName
}
}
}

这不是一段JSON(尽管它看起来非常像),而是一条GraphQL查询。它基本上表示:

  • 查询某个特定id的书
  • 给我那本书的id、name、pageCount、author
  • 对于author我想知道firstName和lastName

响应是一段普通JSON:

1
2
3
4
5
6
7
8
9
10
11
12
{ 
"bookById":
{
"id":"book-1",
"name":"Harry Potter and the Philosopher's Stone",
"pageCount":223,
"author": {
"firstName":"Joanne",
"lastName":"Rowling"
}
}
}

静态类型是GraphQL最重要的特性之一:服务器明确地知道你想要查询的每个对象都是什么样子的并且任何client都可以”内省”于服务器并请求”schema”。schema描述的是查询可能是哪些情况并且你可以拿到哪些字段。(注意:当提及schema时,我们经常指的是”GraphQL Schema”,而不是像”JSON Schema”或者”Database Schema”)

上面提及的查询的schema是这样描述的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Query {
bookById(id: ID): Book
}

type Book {
id: ID
name: String
pageCount: Int
author: Author
}

type Author {
id: ID
firstName: String
lastName: String
}

这篇教程将关注于如何用Java实现一个有着这种schema的GraphQL服务器。

我们仅仅触及了GraphQL的一些基本功能。更多内容可以去官网查看
https://graphql.github.io/learn/

GraphQL Java 预览

GraphQL Java是GraphQL的Java(服务器)实现。GraphQL Java Github org中有几个Git仓库。其中最重要的一个是GraphQL Java 引擎,它是其他所有东西的基础。

GraphQL Java引擎本身只关心执行查询。它不处理任何HTTP或JSON相关主题。因此,我们将使用GraphQL Java Spring Boot adapter,它通过Spring Boot在HTTP上暴露API。

创建GraphQL Java服务器的主要步骤如下:

  1. 定义GraphQL Schema。
  2. 决定如何获取需要查询的实际数据。

我们的示例API:获取图书详细信息

我们的示例应用程序将是一个简单的API,用于获取特定书籍的详细信息。这个API并不是很全面,但对于本教程来说已经足够了。

创建一个Spring Boot应用程序

创建Spring应用程序的最简单方法是使用https://start.spring.io/上的“Spring Initializr”。

选择:

  • Gradle Project
  • Java
  • Spring Boot 2.1.x

对于我们使用的项目元数据:

  • Group: com.graphql-java.tutorial
  • Artifact: book-details

至于dependency(依赖项),我们只选择Web。

点击Generate Project,你就可以使用Spring Boot app了。所有后面提到的文件和路径都是与这个Generate Project相关的。

我们在build.gradledependencies部分为我们的项目添加了三个依赖项:

前两个是GraphQL Java和GraphQL Java Spring,然后我们还添加了Google Guava。Guava并不是必须的,但它会让我们的生活更容易一点。

依赖项看起来是这样的:

1
2
3
4
5
6
7
dependencies {
implementation 'com.graphql-java:graphql-java:11.0' // NEW
implementation 'com.graphql-java:graphql-java-spring-boot-starter-webmvc:1.0' // NEW
implementation 'com.google.guava:guava:26.0-jre' // NEW
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Schema

我们正在src/main/resources下创建一个新的文件schema.graphqls,它包含以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Query {
bookById(id: ID): Book
}

type Book {
id: ID
name: String
pageCount: Int
author: Author
}

type Author {
id: ID
firstName: String
lastName: String
}

此schema定义了一个顶层字段(在Query类型中):bookById,它返回特定图书的详细信息。

它还定义了类型Book,它包含了:idnamepageCountauthorauthor属于Author类型,在Book之后定义。

上面显示的用于描述模式的特定于域的语言称为模式定义语言或SDL。更多细节可以在这里找到。

一旦我们有了这个文件,我们就需要通过读取文件并解析它,然后添加代码来为它获取数据,从而“让它活起来”。

我们在com.graphqljava.tutorial.bookdetails包中创建了一个新的GraphQLProvider类。init方法将创建一个GraphQL实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class GraphQLProvider {

private GraphQL graphQL;

@Bean
public GraphQL graphQL() {
return graphQL;
}

@PostConstruct
public void init() throws IOException {
URL url = Resources.getResource("schema.graphqls");
String sdl = Resources.toString(url, Charsets.UTF_8);
GraphQLSchema graphQLSchema = buildSchema(sdl);
this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
}

private GraphQLSchema buildSchema(String sdl) {
// TODO: we will create the schema here later
}
}

我们使用Guava资源从类路径读取文件,然后创GraphQLSchemaGraphQL实例。这个GraphQL实例通过使用@Bean注解的GraphQL()方法作为Spring Bean暴露出去。GraphQL Java Spring适配器将使用该GraphQL实例,使我们的schema可以通过默认url/GraphQL进行HTTP访问。

我们还需要做的是实现buildSchema方法,它创建GraphQLSchema实例,并连接代码来获取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Autowired
GraphQLDataFetchers graphQLDataFetchers;

private GraphQLSchema buildSchema(String sdl) {
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
RuntimeWiring runtimeWiring = buildWiring();
SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}

private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query")
.dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
.type(newTypeWiring("Book")
.dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
.build();
}

TypeDefinitionRegistry是schema文件的解析版本。SchemaGenerator将TypeDefinitionRegistryRuntimeWiring结合起来,实际生成GraphQLSchema

buildRuntimeWiring使用graphQLDataFetchersbean来注册两个Datafetchers:

  • 一个是检索具有特定ID的图书。
  • 一个是为特定的书找到作者。

下一节将解释DataFetcher以及如何实现GraphQLDataFetchersbean。

总的来说,创建GraphQLGraphQLSchema实例的过程是这样的:

explain

DataFetchers

GraphQL Java服务器最重要的概念可能是Datafetcher:在执行查询时,Datafetcher获取一个字段的数据。

当GraphQL Java执行查询时,它为查询中遇到的每个字段调用适当的DatafetcherDataFetcher是一个只有一个方法的接口,带有一个类型的参数DataFetcherEnvironment:

1
2
3
public interface DataFetcher<T> {
T get(DataFetchingEnvironment dataFetchingEnvironment) throws Exception;
}

重要提示:模式中的每个字段都有一个与之关联的DataFetcher。如果没有为特定字段指定任何DataFetcher,则使用默认的PropertyDataFetcher。我们稍后将更详细地讨论这个问题。

我们正在创建一个新的类GraphQLDataFetchers,其中包含图书和作者的示例列表。

完整的实现是这样的,我们将很快详细研究它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Component
public class GraphQLDataFetchers {

private static List<Map<String, String>> books = Arrays.asList(
ImmutableMap.of("id", "book-1",
"name", "Harry Potter and the Philosopher's Stone",
"pageCount", "223",
"authorId", "author-1"),
ImmutableMap.of("id", "book-2",
"name", "Moby Dick",
"pageCount", "635",
"authorId", "author-2"),
ImmutableMap.of("id", "book-3",
"name", "Interview with the vampire",
"pageCount", "371",
"authorId", "author-3")
);

private static List<Map<String, String>> authors = Arrays.asList(
ImmutableMap.of("id", "author-1",
"firstName", "Joanne",
"lastName", "Rowling"),
ImmutableMap.of("id", "author-2",
"firstName", "Herman",
"lastName", "Melville"),
ImmutableMap.of("id", "author-3",
"firstName", "Anne",
"lastName", "Rice")
);

public DataFetcher getBookByIdDataFetcher() {
return dataFetchingEnvironment -> {
String bookId = dataFetchingEnvironment.getArgument("id");
return books
.stream()
.filter(book -> book.get("id").equals(bookId))
.findFirst()
.orElse(null);
};
}

public DataFetcher getAuthorDataFetcher() {
return dataFetchingEnvironment -> {
Map<String,String> book = dataFetchingEnvironment.getSource();
String authorId = book.get("authorId");
return authors
.stream()
.filter(author -> author.get("id").equals(authorId))
.findFirst()
.orElse(null);
};
}
}

数据源

我们从类中的静态列表中获取图书和作者。这只是为了本教程而做的。理解GraphQL并不指定数据来自何处是非常重要的。这就是GraphQL的强大之处:它可以来自内存中的静态列表、数据库或外部服务。

Book DataFetcher

我们的第一个方法getBookByIdDataFetcher返回一个DataFetcher实现,该实现接受一个DataFetcherEnvironment并返回一本书。在本例中,这意味着我们需要从bookById字段获取id参数,并找到具有此特定id的图书。

String bookId = dataFetchingEnvironment.getArgument("id");中的”id”为schema中bookById查询字段中的“id”:

1
2
3
4
type Query {
bookById(id: ID): Book
}
...

Author DataFetcher

第二个方法getAuthorDataFetcher返回一个Datafetcher,用于获取特定书籍的作者。与前面描述的book DataFetcher相比,我们没有参数,但是有一个book实例。来自父字段的DataFetcher的结果可以通过getSource获得。这是一个需要理解的重要概念:GraphQL中每个字段的Datafetcher都是以自顶向下的方式调用的,父字段的结果是子Datafetcherenvironmentsource属性。

然后,我们使用先前获取的图书获取authorId,并以查找特定图书的相同方式查找特定的作者。

Default DataFetchers

我们只实现了两个Datafetcher。如上所述,如果不指定一个,则使用默认的PropertyDataFetcher。在我们的例子中,它指的是Book.idBook.nameBook.pageCountAuthor.idAuthor.firstNameAuthor.lastName都有一个默认的PropertyDataFetcher与之关联。

PropertyDataFetcher尝试以多种方式查找Java对象上的属性。以java.util.Map为例, 它只是按键查找属性。这对我们来说非常好,因为book和author映射的键与schema中指定的字段相同。例如,在我们为图书类型定义的schema中,字段pageCount和book DataFetcher返回一个带有键pageCountMap。因为字段名与Map中的键相同(“pageCount”),PropertyDateFetcher正常工作。

让我们假设我们有一个不匹配,book Map有一个键是totalPages而不是pageCount。这将导致每本书的pageCount值为null,因为PropertyDataFetcher无法获取正确的值。为了解决这个问题,你必须为Book.pageCount注册一个新的DataFetcher。它看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    // In the GraphQLProvider class
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query")
.dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
.type(newTypeWiring("Book")
.dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher())
// This line is new: we need to register the additional DataFetcher
.dataFetcher("pageCount", graphQLDataFetchers.getPageCountDataFetcher()))
.build();
}

// In the GraphQLDataFetchers class
// Implement the DataFetcher
public DataFetcher getPageCountDataFetcher() {
return dataFetchingEnvironment -> {
Map<String,String> book = dataFetchingEnvironment.getSource();
return book.get("totalPages");
};
}
...

这个DataFetcher将通过在book Map中查找正确的键来解决这个问题。(同样:在我们的示例中不需要这个,因为我们没有命名不匹配)

试用API

这就是构建一个可工作的GraphQL API所需的全部内容。在启动Spring Boot应用程序之后,可以在http://localhost:8080/graphql上使用API。

尝试和探索GraphQL API的最简单方法是使用GraphQL Playground的工具。下载并运行它。

启动之后,你将被要求输入一个URL,输入http://localhost:8080/graphql

之后,你可以查询我们的示例API,您应该会得到我们在开始时提到的结果。它应该是这样的:

demo

完整的示例源代码和更多信息

完整的项目和完整的源代码可以在这里找到:https://github.com/graphql-java/tutorials/tree/master/book-details

有关GraphQL Java的更多信息可以在文档中找到。

对于任何问题, 我们也有spectrum chat 接受讨论。

对于直接的反馈,您也可以在我们的GraphQL Java Twitter account帐户上找到我们。

原文链接:Getting started with GraphQL Java and Spring Boot

译文连接:开始使用GraphQL Java和Spring Boot

翻译:TomorJM