開始使用GraphQL Java和Spring Boot

這是一篇爲想要用Java搭建GraphQL服務器的小夥伴們準備的教程。須要你有必定的Spring Boot和Java開發相關知識,雖然咱們簡要介紹了GraphQL,可是本教程的重點是用Java開發一個GraphQL服務器。java

三分鐘介紹GraphQL

GraphQL是一門從服務器檢索數據的查詢語言。在某些場景下能夠替換REST、SOAP和gRPC。讓咱們假設咱們想要從一個在線商城的後端獲取某一個本書的詳情。git

你使用GraphQL往服務器發送以下查詢去獲取id爲"123"的那本書的詳情:github

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

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

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

響應是一段普通JSON:spring

{ 
  "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是這樣描述的: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實現一個有着這種schema的GraphQL服務器。後端

咱們僅僅觸及了GraphQL的一些基本功能。更多內容能夠去官網查看 graphql.github.io/learn/服務器

GraphQL Java 預覽

GraphQL Java是GraphQL的Java(服務器)實現。GraphQL Java Github org中有幾個Git倉庫。其中最重要的一個是GraphQL Java 引擎,它是其餘全部東西的基礎。mvc

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

相關文章
相關標籤/搜索