開始使用GraphQL Java和Spring Boot

TomorJM的小窩發表於2019-03-13

這是一篇為想要用Java搭建GraphQL伺服器的小夥伴們準備的教程。需要你有一定的Spring Boot和Java開發相關知識,雖然我們簡要介紹了GraphQL,但是本教程的重點是用Java開發一個GraphQL伺服器。

三分鐘介紹GraphQL

GraphQL是一門從伺服器檢索資料的查詢語言。在某些場景下可以替換REST、SOAP和gRPC。讓我們假設我們想要從一個線上商城的後端獲取某一個本書的詳情。

你使用GraphQL往伺服器傳送如下查詢去獲取id為"123"的那本書的詳情:

{
  bookById(id: "book-1"){
    id
    name
    pageCount
    author {
      firstName
      lastName
    }
  }
}
複製程式碼

這不是一段JSON(儘管它看起來非常像),而是一條GraphQL查詢。它基本上表示:

  • 查詢某個特定id的書
  • 給我那本書的id、name、pageCount、author
  • 對於author我想知道firstName和lastName

響應是一段普通JSON:

{ 
  "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是這樣描述的:

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的一些基本功能。更多內容可以去官網檢視 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應用程式的最簡單方法是使用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並不是必須的,但它會讓我們的生活更容易一點。

依賴項看起來是這樣的:

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,它包含以下內容:

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例項:

@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例項,並連線程式碼來獲取資料:

@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:

public interface DataFetcher<T> {
    T get(DataFetchingEnvironment dataFetchingEnvironment) throws Exception;
}
複製程式碼

重要提示:模式中的每個欄位都有一個與之關聯的DataFetcher。如果沒有為特定欄位指定任何DataFetcher,則使用預設的PropertyDataFetcher。我們稍後將更詳細地討論這個問題。

我們正在建立一個新的類GraphQLDataFetchers,其中包含圖書和作者的示例列表。

完整的實現是這樣的,我們將很快詳細研究它:

@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”:

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。它看起來像這樣:

    // 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

完整的示例原始碼和更多資訊

完整的專案和完整的原始碼可以在這裡找到:github.com/graphql-jav…

有關GraphQL Java的更多資訊可以在文件中找到。

對於任何問題, 我們也有spectrum chat 接受討論。

對於直接的反饋,您也可以在我們的GraphQL Java Twitter account帳戶上找到我們。

原文連結:Getting started with GraphQL Java and Spring Boot

譯文連線:開始使用GraphQL Java和Spring Boot

翻譯:TomorJM

相關文章