五分鐘帶你搞懂GraphQL
當下,前后端分離是互聯(lián)網(wǎng)應用程序開發(fā)的主流做法,如何設(shè)計合理且高效的前后端交互 Web API 是前端和后臺開發(fā)人員日常開發(fā)工作的一大難點和痛點?;叵胛覀冊谌粘i_發(fā)過程中經(jīng)常會碰到的幾個場景:
- 后臺開發(fā)人員調(diào)整了返回值的類型和數(shù)量而沒有通知到前端;
- 后臺開發(fā)人員修改了某一個字段的名稱沒有通知到前端;
- 前端開發(fā)人員需要通過多個接口的拼接才能獲取頁面暫時所需要的所有字段;
- 前端開發(fā)人員無論想要多獲取還是少獲取目標字段都需要與后臺開發(fā)人員進行協(xié)商;
- …
相信你對上面這個場景非常熟悉,因為我們每天都可能在反復經(jīng)歷著類似的場景。那么,如何解決這些問題呢?這就是今天我們要介紹的內(nèi)容,我們將引入 GraphQL 這套全新的技術(shù)體系,它為我們解決上述問題提供了方案。
GraphQL 的基本概念
相比 REST,GraphQL 可以說是一項比較新的技術(shù)體系,2012 年誕生在 Facebook 內(nèi)部,并于 2015 年正式開源。顧名思義,GraphQL 是一種基于圖(Graph)的查詢語言(Query Language,QL),從根本上改變了前后端交互 API 的定義和實現(xiàn)方式。
要想使用 GraphQL,我們首先需要關(guān)注它發(fā)送請求的方式。這里我們可以舉一個例子。假設(shè)系統(tǒng)中存在一個獲取用戶信息的場景,那么一個典型的請求示例如下所示:
{
user (id: "1") {
name
address
}
}
可以看到基于 GraphQL 的請求方式與使用傳統(tǒng)的 RESTful API 有很大的不同。除了在請求體中指定了目標 User 對象的參數(shù) id 值之外,我們還額外指定了“name”和“address”這兩個參數(shù),也就是告訴服務器端這次請求所希望獲取的數(shù)據(jù)字段。
顯然,這種請求方式完美地解決了前端無法預判響應的數(shù)據(jù)格式問題,因為前端在請求的同時已經(jīng)知道從服務端返回的數(shù)據(jù)字段就是請求中指定的字段,因此前端就不需要再對響應結(jié)果進行專門的判斷和處理。
針對傳統(tǒng)交互方式中存在的多次請求問題,GraphQL 可以把多次請求合并成一次。例如,我們可以發(fā)送這樣一個請求。
{
users {
name
address
family{
count
}
}
}
在該請求中,我們一方面指定了想要獲取的 User 對象中的“name”和“address”字段,同時也指定了需要獲取用戶對應的家庭字段“family”以及它的子字段“count”。這樣,通過一次請求,我們就可以同時獲取用戶信息和家庭信息,而不需要發(fā)送兩次請求。
講到這里,你可能已經(jīng)注意到,通過 GraphQL 發(fā)起請求實際上只需要指定一個 HTTP 端點地址即可,因為我們可以基于同一個端點通過傳入不同的參數(shù)而獲取不同的結(jié)果,也就不需要專門設(shè)計一批 HTTP 端點來分別處理不同的請求了。
現(xiàn)在,我們已經(jīng)了解了 GraphQL 的功能特性。那么,如何實施 GraphQL 呢?
GraphQL 的實施方法
首先明確,我們并不推薦在任何場景下都使用 GraphQL。對于那些 API 定義與資源概念匹配度較高、也不需要實現(xiàn)類似用戶信息內(nèi)部嵌套家庭成員信息的復雜查詢場景,傳統(tǒng)的 RESTful API 仍然是首選,各個 HTTP 端點之間相互獨立,職責非常明確。但對有些場景而言,GraphQL 則更有優(yōu)勢。包括:
- 業(yè)務復雜度高
- 需求變化快
- 弱文檔化管理
對于大多數(shù)系統(tǒng)而言,GraphQL 的推行需要前后端進行緊密的配合,在這個配合過程中,前端的痛點往往是大于服務端的。所以,一般場景下前端對于引入 GraphQL 的訴求要大于服務端。另一方面,如果采用和 RESTful API 一樣的開發(fā)模式,那么實現(xiàn) GraphQL 的工作量主要是在服務端。服務端同學需要基于 GraphQL 規(guī)范重新設(shè)計并實現(xiàn) API,通常都建議單獨構(gòu)建一個數(shù)據(jù)層來對外暴露 GraphQL API。
圖 1 在后端服務中構(gòu)建數(shù)據(jù)層示意圖
在實施 GraphQL 的策略上,我們也可以總結(jié)幾條最佳實踐。首先,如果你已經(jīng)實現(xiàn)了一部分 RESTful 服務,那么可以讓 GraphQL 與這部分 RESTful API 并存發(fā)展。尤其是對于那些單一的 RESTful 服務,可以把 GraphQL 直接連接到已有的 RESTful 服務上。通過這種策略,RESTful 服務中已經(jīng)實現(xiàn)的業(yè)務邏輯層、數(shù)據(jù)訪問層組件都可以得到復用,我們要做的只是開放一個新的 GraphQL 訪問入口而已。而且,作為一項新技術(shù)的引入過程,GraphQL 和 RESTful 服務在一段時間內(nèi)并存發(fā)展也符合平滑過渡的客觀需求。
圖 2 RESTful 和 GraphQL 并存發(fā)展示意圖
如果你正在采用微服務架構(gòu),那么引入 GraphQL 的策略就是在所有后端微服務之前架設(shè)一層由 GraphQL 構(gòu)建的數(shù)據(jù)層,并對后端服務提供的數(shù)據(jù)進行自由的組裝之后開放給前端。這種實施策略類似于微服務架構(gòu)中的 API 網(wǎng)關(guān),一方面對前端請求進行適配和路由,一方面完成前后端之間的解耦。
GraphQL Java 框架
我們知道,GraphQL 是一種理念和規(guī)范,它并不直接提供開發(fā)工具和框架。而 GraphQL Java 是基于 Java 開發(fā)的一個 GraphQL 實現(xiàn)庫,在實際的應用開發(fā)中,開發(fā)人員可以創(chuàng)建自己的 Controller 層組件來與 GraphQL 完成整合。當然,我們也可以對 GraphQL Java 進行一定的封裝和擴展。因此,我們需要首先掌握 GraphQL Java 中所包含的核心編程組件。GraphQL Java 中內(nèi)置了一組核心的技術(shù)組件。
圖 3 GraphQL Java 中的核心組件
首先,我們需要引入一個核心組件,即 Schema。所謂 Schema,簡單講就是一種前后端交互的協(xié)議和規(guī)范,或者可以把它類比成 RESTful API 中的接口定義文檔。在 Schema 中,開發(fā)人員需要指定兩部分內(nèi)容。一方面,我們需要明確定義前后端交互的數(shù)據(jù)結(jié)構(gòu),包括具體的字段名稱、類型、是否為空等屬性。另一方面,GraphQL 規(guī)定每一個 Schema 中可以存在一個根 Query 和根 Mutation,分別用于執(zhí)行查詢和更新操作。我們來看一個典型的 Schema 定義。
schema {
query: Query,
mutation: Mutation
}
type Query {
users(filter: InputUser): [User!]
user(id: ID!): User
...
}
type Mutation {
addUser(user: InputUser!): User
....
}
type User{
id: ID!
name: String!
}
input InputUser {
name: String
}
在上述 Schema 中,我們看到了根 Query 和根 Mutation,也看到了 ID、String 等基本數(shù)據(jù)類型和 User 這個自定義復雜數(shù)據(jù)結(jié)構(gòu),以及用來表明是否為空的!和數(shù)組的[]。
第二個要介紹的組件是 DataFetcher。從命名上看,DataFetcher 組件的作用就是在執(zhí)行查詢時獲取字段對應的數(shù)據(jù)。DataFetcher 是一個接口,只定義了一個方法,如下所示:
public interface DataFetcher<T> {
T get(DataFetchingEnvironment dataFetchingEnvironment) throws Exception;
}
開發(fā)人員可以從 DataFetchingEnvironment 中獲取傳入的參數(shù),并根據(jù)該參數(shù)來執(zhí)行具體的數(shù)據(jù)查詢操作。至于數(shù)據(jù)查詢操作的具體實現(xiàn)過程,DataFetcher 并不關(guān)心。
創(chuàng)建 DataFetcher 只是開始,我們還要將它們應用在 GraphQL 服務器上,這就需要借助 RuntimeWiring 組件。通過 Runtime Wiring(運行時組裝)機制,我們可以把 DataFetcher 整合在 GraphQL 的運行環(huán)境中。創(chuàng)建 RuntimeWiring 的典型代碼如下所示:
private UsersDataFetcher usersDataFetcher;
private UserDataFetcher userDataFetcher;
private RuntimeWiring buildRuntimeWiring() {
return RuntimeWiring.newRuntimeWiring()
.type("Query", typeWiring -> typeWiring
.dataFetcher("users", usersDataFetcher)
.dataFetcher("user", userDataFetcher))
.build();
}
可以看到,這里通過 RuntimeWiring 的 type 方法將各個 DataFetcher 與對應的數(shù)據(jù)結(jié)構(gòu)關(guān)聯(lián)起來。
最后,基于 Schema 和 RuntimeWiring,我們就可以創(chuàng)建 GraphQL 對象,如下所示:
File schemas = schemeResource.getFile();
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemas);
RuntimeWiring wiring = buildRuntimeWiring();
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiring);
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
基于這個 GraphQL 對象,我們就可以使用它來完成具體的查詢操作,最終面向業(yè)務層的代碼如下所示:
String query = …;
ExecutionResult result = graphQL.execute(query);
可以看到,使用 GraphQL Java 進行開發(fā)的難度并不大,流程也比較固化。這對我們開發(fā)人員來說無疑減輕了很多學習的壓力和負擔。
總結(jié)
在日常開發(fā)過程中,基于 HTTP 協(xié)議的 RESTful API 是我們目前主流的前后端開發(fā)模式,但并不一定是最合理的開發(fā)模式。今天我們所介紹的 GraphQL 具備的功能特性能夠在一定程度上解決傳統(tǒng)開發(fā)模式中所存在的一些問題。GraphQL 更加具有靈活性和擴展性,并能顯著減少前后端交互所需要的溝通和開發(fā)成本。在日常開發(fā)過程中,建議你根據(jù)自身業(yè)務發(fā)展的需求和變化,在合適的場景中引入 GraphQL 相關(guān)技術(shù)體系。