這是一篇為想要用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伺服器的主要步驟如下:
- 定義GraphQL Schema。
- 決定如何獲取需要查詢的實際資料。
我們的示例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.gradle
的dependencies
部分為我們的專案新增了三個依賴項:
前兩個是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
,它包含了:id
、name
、pageCount
和author
。author
屬於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
資源從類路徑讀取檔案,然後創GraphQLSchema
和GraphQL
例項。這個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將TypeDefinitionRegistry
與RuntimeWiring
結合起來,實際生成GraphQLSchema
。
buildRuntimeWiring
使用graphQLDataFetchers
bean來註冊兩個Datafetcher
s:
- 一個是檢索具有特定ID的圖書。
- 一個是為特定的書找到作者。
下一節將解釋DataFetcher
以及如何實現GraphQLDataFetchers
bean。
總的來說,建立GraphQL
和GraphQLSchema
例項的過程是這樣的:
DataFetchers
GraphQL Java伺服器最重要的概念可能是Datafetcher
:在執行查詢時,Datafetcher
獲取一個欄位的資料。
當GraphQL Java執行查詢時,它為查詢中遇到的每個欄位呼叫適當的Datafetcher
。DataFetcher
是一個只有一個方法的介面,帶有一個型別的引數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
都是以自頂向下的方式呼叫的,父欄位的結果是子Datafetcherenvironment
的source
屬性。
然後,我們使用先前獲取的圖書獲取authorId,並以查詢特定圖書的相同方式查詢特定的作者。
Default DataFetchers
我們只實現了兩個Datafetcher
。如上所述,如果不指定一個,則使用預設的PropertyDataFetcher
。在我們的例子中,它指的是Book.id
、Book.name
、Book.pageCount
、Author.id
、Author.firstName
和Author.lastName
都有一個預設的PropertyDataFetcher
與之關聯。
PropertyDataFetcher
嘗試以多種方式查詢Java物件上的屬性。以java.util.Map
為例, 它只是按鍵查詢屬性。這對我們來說非常好,因為book和author對映的鍵與schema中指定的欄位相同。例如,在我們為圖書型別定義的schema中,欄位pageCount
和book DataFetcher
返回一個帶有鍵pageCount
的Map
。因為欄位名與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,您應該會得到我們在開始時提到的結果。它應該是這樣的:
完整的示例原始碼和更多資訊
完整的專案和完整的原始碼可以在這裡找到:github.com/graphql-jav…
有關GraphQL Java的更多資訊可以在文件中找到。
對於任何問題, 我們也有spectrum chat 接受討論。
對於直接的反饋,您也可以在我們的GraphQL Java Twitter account帳戶上找到我們。
翻譯:TomorJM