構建 GraphQL 服務

Spring for GraphQL 為基於 GraphQL Java 構建的 Spring 應用提供支援。

本指南將引導您瞭解如何使用 Spring for GraphQL 在 Java 中建立 GraphQL 服務。

您將構建什麼

您將構建一個服務,該服務將在 https://: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 應用程式。導航到 https://: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 許可證釋出,而文字內容則根據 署名-禁止演繹知識共享許可證 釋出。

獲取程式碼