构建 GraphQL 服务

Spring for GraphQL 为基于 GraphQL Java 构建的 Spring 应用提供支持。

本指南将引导您了解如何使用 Spring for GraphQL 在 Java 中创建 GraphQL 服务。

您将构建什么

您将构建一个服务,该服务将在 http://localhost:8080/graphql 接收 GraphQL 请求。

您需要什么

如何完成本指南

与大多数 Spring 入门指南一样,您可以从头开始完成每个步骤,也可以跳过您已熟悉的基本设置步骤。无论哪种方式,您最终都会得到可工作的代码。

从头开始,请转到从 Spring Initializr 开始

跳过基础部分,请执行以下操作

完成后,您可以对照 gs-graphql-server/complete 中的代码检查您的结果。

从 Spring Initializr 开始

如果您愿意,可以使用此预设 Spring Initializr 链接来加载正确的设置。否则,请继续手动设置 Initializr。

手动初始化项目

  1. 导航到 https://start.spring.io。此服务会自动引入应用程序所需的所有依赖项,并为您完成大部分设置。

  2. 选择 Gradle 或 Maven,以及您想要使用的语言。本指南假设您选择 Java。

  3. 点击 Dependencies(依赖),然后选择 Spring for GraphQLSpring Web

  4. 点击 Generate(生成)。

  5. 下载生成的 ZIP 文件,这是一个根据您的选择配置好的 GraphQL 应用程序归档。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成此过程。
您也可以从 GitHub Fork 项目,并在您的 IDE 或其他编辑器中打开它。

GraphQL 极简介绍

GraphQL 是一种从服务器检索数据的查询语言。它是 REST、SOAP 或 gRPC 的替代方案。在本教程中,我们将从在线商店后端查询特定图书的详细信息。

这是您可以发送到 GraphQL 服务器以检索图书详细信息的请求示例

query bookDetails {
  bookById(id: "book-1") {
    id
    name
    pageCount
    author {
      firstName
      lastName
    }
  }
}

此 GraphQL 请求表示

  • 查询 ID 为 "book-1" 的图书

  • 对于该图书,返回 ID、名称、页数和作者

  • 对于作者,返回 firstName(名)和 lastName(姓)

响应为 JSON 格式。例如

{
  "bookById": {
    "id":"book-1",
    "name":"Effective Java",
    "pageCount":416,
    "author": {
      "firstName":"Joshua",
      "lastName":"Bloch"
    }
  }
}

GraphQL 的一个重要特性是它定义了一种模式语言,并且是静态类型的。服务器清楚地知道请求可以查询哪些类型的对象以及这些对象包含哪些字段。此外,客户端可以自省(introspect)服务器以获取模式详情。

本教程中的“模式”(schema)一词指的是“GraphQL 模式”,它与“JSON 模式”或“数据库模式”等其他模式无关。

上述查询的模式为

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 中使用此模式实现 GraphQL 服务器。

我们只是触及了 GraphQL 可能性的皮毛。更多信息可以在GraphQL 官方页面找到。

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

使用 Spring for GraphQL 创建服务器的主要步骤如下

  1. 定义 GraphQL 模式

  2. 实现获取查询实际数据的逻辑

我们的示例应用程序将是一个简单的 API,用于获取特定图书的详细信息。它并非旨在成为一个全面的 API。

模式

在您之前准备好的 Spring for GraphQL 应用程序中,在 src/main/resources/graphql 文件夹下添加一个新文件 schema.graphqls,内容如下

type Query {
    bookById(id: ID): Book
}

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

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

每个 GraphQL 模式都有一个顶级的 Query 类型,其下的字段是应用程序公开的查询操作。这里模式定义了一个名为 bookById 的查询,用于返回特定图书的详细信息。

它还定义了包含 idnamepageCountauthor 字段的 Book 类型,以及包含 firstNamelastName 字段的 Author 类型。

上述用于描述模式的领域特定语言称为 Schema Definition Language(模式定义语言)或 SDL。更多详细信息,请参阅GraphQL 文档

数据源

GraphQL 的一个关键优势在于数据可以来自任何地方。数据可以来自数据库、外部服务,或者静态的内存列表。

为了简化教程,图书和作者数据将来自于它们各自类中的静态列表。

创建图书和作者数据源

现在我们在主应用程序包中创建 BookAuthor 类,紧挨着 GraphQlServerApplication。使用以下内容作为它们的内容

package com.example.graphqlserver;

import java.util.Arrays;
import java.util.List;

public record Book (String id, String name, int pageCount, String authorId) {

    private static List<Book> books = Arrays.asList(
            new Book("book-1", "Effective Java", 416, "author-1"),
            new Book("book-2", "Hitchhiker's Guide to the Galaxy", 208, "author-2"),
            new Book("book-3", "Down Under", 436, "author-3")
    );

    public static Book getById(String id) {
        return books.stream()
				.filter(book -> book.id().equals(id))
				.findFirst()
				.orElse(null);
    }
}
package com.example.graphqlserver;

import java.util.Arrays;
import java.util.List;

public record Author (String id, String firstName, String lastName) {

    private static List<Author> authors = Arrays.asList(
            new Author("author-1", "Joshua", "Bloch"),
            new Author("author-2", "Douglas", "Adams"),
            new Author("author-3", "Bill", "Bryson")
    );

    public static Author getById(String id) {
        return authors.stream()
				.filter(author -> author.id().equals(id))
				.findFirst()
				.orElse(null);
    }
}

添加代码以获取数据

Spring for GraphQL 提供了一种基于注解的编程模型。通过使用控制器注解的方法,我们可以声明如何获取特定 GraphQL 字段的数据。

在主应用程序包中,紧邻 BookAuthor,向 BookController.java 添加以下内容

package com.example.graphqlserver;

import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;

@Controller
public class BookController {
    @QueryMapping
    public Book bookById(@Argument String id) {
        return Book.getById(id);
    }

    @SchemaMapping
    public Author author(Book book) {
        return Author.getById(book.authorId());
    }
}

通过定义一个使用 @QueryMapping 注解的名为 bookById 的方法,此控制器声明了如何获取 Query 类型下定义的 Book。查询字段由方法名确定,但也可以在注解本身上声明。

Spring for GraphQL 使用 RuntimeWiring.Builder 将每个这样的控制器方法注册为 GraphQL Java 的 graphql.schema.DataFetcherDataFetcher 提供了获取查询或任何模式字段数据的逻辑。GraphQL 的 Spring Boot starter 具有自动配置,可以自动完成此注册。

在 GraphQL Java 引擎中,DataFetchingEnvironment 提供对字段特定参数值映射的访问。使用 @Argument 注解可以将参数绑定到目标对象并注入到控制器方法中。默认情况下,方法参数名用于查找参数,但也可以在注解本身上指定。

这个 bookById 方法定义了如何获取特定的 Book,但它不负责获取相关的 Author。如果请求需要作者信息,GraphQL Java 将需要获取此字段。

@SchemaMapping 注解将处理方法映射到 GraphQL 模式中的一个字段,并声明它为该字段的 DataFetcher。字段名默认为方法名,类型名默认为注入到方法中的源/父对象的简单类名。在此示例中,字段默认为 author,类型默认为 Book

这就是我们所需的全部代码!

让我们运行我们的第一个查询。

运行我们的第一个查询

启用 GraphiQL Playground

GraphiQL 是一个用于编写和执行查询等的有用可视化界面。通过将此配置添加到 application.properties 文件来启用 GraphiQL。

spring.graphql.graphiql.enabled=true

启动应用程序

启动您的 Spring 应用程序。导航到 http://localhost:8080/graphiql

运行查询

输入查询,然后点击窗口顶部的运行按钮。

query bookDetails {
  bookById(id: "book-1") {
    id
    name
    pageCount
    author {
      id
      firstName
      lastName
    }
  }
}

您应该会看到如下响应。

GraphQL response

恭喜您,您已经构建了一个 GraphQL 服务并执行了您的第一个查询!借助 Spring for GraphQL,您仅用少量代码就完成了这项工作。

测试

Spring for GraphQL 在 spring-graphql-test artifact 中提供了 GraphQL 测试辅助工具。我们已将此 artifact 作为 Spring Initializr 生成项目的一部分包含在内。

全面测试 GraphQL 服务需要具有不同范围的测试。在本教程中,我们将编写一个 @GraphQlTest 切片测试,它专注于单个控制器。还有其他辅助工具可以帮助进行完整的端到端集成测试和专注于服务器端的测试。有关完整详情,请参阅Spring for GraphQL 测试文档以及 Spring Boot 文档中的自动配置的 Spring for GraphQL 测试

让我们编写一个控制器切片测试,该测试验证刚才在 GraphiQL playground 中请求的相同 bookDetails 查询。

将以下内容添加到测试文件 BookControllerTests.java 中。将此文件保存在 src/test/java/com/example/graphqlserver/ 文件夹内的位置。

package com.example.graphqlserver;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest;
import org.springframework.graphql.test.tester.GraphQlTester;

@GraphQlTest(BookController.class)
public class BookControllerTests {

    @Autowired
    private GraphQlTester graphQlTester;

    @Test
    void shouldGetFirstBook() {
        this.graphQlTester
				.documentName("bookDetails")
				.variable("id", "book-1")
                .execute()
                .path("bookById")
                .matchesJson("""
                    {
                        "id": "book-1",
                        "name": "Effective Java",
                        "pageCount": 416,
                        "author": {
                          "firstName": "Joshua",
                          "lastName": "Bloch"
                        }
                    }
                """);
    }
}

此测试引用了一个类似于我们在 GraphiQL Playground 中使用的 GraphQL 查询。它使用 $id 参数化以使其可重用。将此查询添加到位于 src/test/resources/graphql-testbookDetails.graphql 文件中。

query bookDetails($id: ID) {
    bookById(id: $id) {
        id
        name
        pageCount
        author {
            id
            firstName
            lastName
        }
    }
}

运行测试并验证结果是否与在 GraphiQL Playground 中手动请求的 GraphQL 查询结果一致。

@GraphQlTest 注解对于编写控制器切片测试很有用,它专注于单个控制器。@GraphQlTest 会自动配置 Spring for GraphQL 基础设施,不涉及任何传输或服务器。自动配置通过跳过样板代码使我们能够更快地编写测试。由于这是一个重点切片测试,只会扫描有限数量的 bean,包括 @ControllerRuntimeWiringConfigurer。有关扫描 bean 的列表,请参阅文档

GraphQlTester 是一个契约,它声明了测试 GraphQL 请求的通用工作流程,与传输无关。在我们的测试中,我们提供了一个带有 documentName 和所需变量的文档,然后 execute(执行)请求。然后,我们使用 JSON Path 选择响应的一部分,并断言此位置的 JSON 与预期结果匹配。

恭喜!在本教程中,您构建了一个 GraphQL 服务,运行了您的第一个查询,并编写了您的第一个 GraphQL 测试!

进一步阅读

示例源代码

本指南是与 GraphQL Java 团队合作编写的。非常感谢 Donna ZhouBrad BakerAndreas Marek!本教程的源代码可在 GitHub 上找到。

文档

GraphQL Java 是支持 Spring for GraphQL 的 GraphQL 引擎。阅读GraphQL Java 文档

更多 Spring for GraphQL 示例

1.0.x 分支中查看更多示例,该分支很快将移至一个单独的仓库中。

Stack Overflow 问题

想撰写新指南或为现有指南做贡献?请查看我们的贡献指南

所有指南的代码都根据 ASLv2 许可证发布,而文字内容则根据 署名-禁止演绎知识共享许可证 发布。

获取代码