由於本身寫過基於react的前端應用,所以一看到GraphQL就被深深吸引,真是直擊痛點啊!
服務端開發一直是基於java, Spring的,所以開始研究如何在現有工程框架下加入graphql的支持。
本文屬於隨筆性質,學到哪裏,用到哪裏,就寫到哪裏,觀點爲我的理解,僅供參考。javascript
GraphQL基本概念
- Schema: 指一個特定GraphQL類型系統的定義,也指具體的包含類型系統定義的文本文件。在類型定義中,schema {...} 這樣的代碼塊定義的是入口類型,入口類型有三種,即查詢,變動和訂閱。值得說明的是,查詢,變動和訂閱也都是普通的類型而已,和其它對象類型語法上沒有任何區別,只不過它們做爲入口類型被定義在schema代碼塊中。
- 查詢(query):定義爲入口的對象類型;和變動、訂閱語法上並沒有不一樣,不過語義上對應的是讀操做。
- 變動(mutation): 定義和語法同上,但語義上對應增/刪/改操做。
- 訂閱(subscription): 定義和語法同上,語義上對應的是一個訂閱操做以及隨後服務器對客戶端的0~N次主動推送操做。
- 內省(introspection): 能夠經過特殊的graphql查詢獲取到整個類型系統的詳細定義。這可能帶來數據模型過分暴露的問題,之後會專門說明。
- 類型(type): 沒什麼好說,就是對象類型,和標量類型相對應。
- 標量(scalar): 非對象的簡單數據類型,好比內置的String, Int, ID等。能夠本身定義新的標量類型,只要爲它編寫序列化/反序列化方法便可,具體在graphql-java中對應的類是Coercing。
- 字段(field): 對象類型的成員,能夠是對象類型或者標量類型。和java類裏的field不一樣的是,GraphQL的field都是能夠有參數的,所以有參數的field也能夠理解成java中有特定類型返回值的方法。
- 接口(interface): 和java裏的接口差很少,定義類型的公共字段,java實現中能夠直接對應寫一個interface。有點麻煩的是在每一個interface的實現類中都必須重複書寫公共字段。
- 聯合(union): 和接口相似,可是不要求任何公共字段。爲了方即可以在java實現中使用無方法的interface實現。
- 片斷(fragment): 這是個查詢時的概念,和schema定義無關,用於預約義類型上的若干個字段組合,後面的查詢語句中能夠反覆引用,可避免重複書寫這些字段組合。
- 內聯片斷(inline fragment):片斷還只是個簡化查詢的無關緊要的東西,但內聯片斷則更重要,對於返回interface或union類型的字段,須要使用內聯片斷來根據結果的實際類型獲取不一樣的字段。
- 別名(alias): 在查詢中可爲特定字段的查詢增長別名,用來在返回的結果中加以區分,好比一次查詢了兩個特定用戶,由於類型相同,字段也相同,若是不用別名,則沒法在結果中區分彼此。
- 類型擴展(extend): 在schema中,可使用extend給任意類型(包括interface/union)增長字段;這看似自找麻煩的機制實際上有很大用處,能夠把高權限角色的特定字段使用extend寫在另外的schema文件中,運行時可合併解析,不一樣角色的用戶使用不一樣的schema。這樣能夠經過加法來控制類型系統的可見性,避免內省機制過分暴露類型系統。
- DataLoader: 用於批量查詢,見後文介紹。
- Relay: Facebook的另外一個框架,應該是基於GraphQL的,解決一些更高層的實際應用問題,好比通用的分頁機制等。
graphql-java特定術語
- DataFetcher: 數據獲取器,即用以獲取Field實際值的對象。
-
Data Class: 數據類,這是graphql-java-tools中的概念,對應schema中的同名對象類型。前端
- 能夠在數據類上按照約定格式編寫DataFetcher方法用於獲取簡單字段值(好比無需另外查詢數據庫的字段)。
- 我在工程實踐中直接使用數據庫實體類做爲數據類。
-
GraphQLResolver: 這是graphql-java-tools中的接口,帶有一個數據類的類型參數。java
- 對該數據類定義部分或全部字段值的獲取方法,須要基於約定命名方法。
- 注意Resolver中的DataFetcher方法的優先級高於DataClass中的方法。
- 我在工程實踐中直接使用Dao類做爲對應實體類的GraphQLResolver。
-
ExecutionInput: graphql-java中用來包裝一個完整查詢輸入的類,包括:node
- query - 查詢字符串;
- operationName: 操做名; 可選;可用於在查詢中的多個操做中僅選擇特定名稱的予以執行。
- variables: 變量; 可選;一個Map,用於替換查詢字符串中形如'$value'的變量。
- context - 上下文; 可選;任意Object類型,會被傳遞給DataFetcher;可用於傳遞當前登陸用戶等。
- root - 根對象; 可選;任意Object類型,會被傳遞給DataFetcher,語義上是被查詢的根對象。
-
ExecutionStrategy(執行策略): 定義查詢的具體執行策略。react
- 好比是否異步執行,多個子查詢是依次執行,仍是用線程池併發執行等。
-
Instrumentation(攔截器): 比較像Servlet容器中的Filter,在查詢執行先後各有一次執行機會。git
- 可用於對輸入和結果進行額外處理;
- 支持鏈式執行;
- 須要指出的是DataLoader使用攔截器與核心系統耦合。
-
GraphqlFieldVisibility: 能夠編程控制schema中各個字段的可見性。github
- 和extend對應,至關於用減法來控制類型系統的可見性。
技術選型
github上graphql-java名下的庫很多,若是但願瞭解各自簡介的,能夠看下awesome-graphql-java這個項目。
我本身評估瞭如下幾個:spring
- graphql-java: 這個是核心庫,徹底符合Facebook的spec,能夠直接解析schema文件,可是類型綁定須要使用RuntimeWiring來編程方式添加,用起來仍是比較麻煩的。
- graphql-java-annotations: 這是數據驅動的流派,使用註解直接在java類型上標註GraphQL類型以及DataFetcher等,不用寫schema文件。評估了一陣,我的感受很是麻煩,好比:對每一個字段都會建立新的DataFetcher實例來進行解析,十分低效;要編寫不少類來訪問不一樣字段;過多的對象直接建立,難以託管到Spring容器;等等。所以個人結論是,此庫並不適用於個人工程實踐。
- graphql-java-tools: 這是Schema驅動的流派,這個庫使用Antlr本身重寫了Schema解析器,使用GraphQLResolver實例和Data Class;基於方法名和參數的約定來定義DataFetching,使用起來很方便。這是我最終選定使用的庫。不太爽的地方有兩點:1) 當前版本基於graphql-java 7.0,遲滯於核心庫 2) 使用Kotlin編寫,我在MyEclipse裏面沒法正常設置斷點進行跟蹤調試……
- graphql-java-servlet: GraphQL不像傳統的REST,須要寫一堆Controller,提供惟一的api接口便可,這個servlet就是幫你連這個都包辦的,不過我沒有用,本身基於SpringMVC寫一個也很簡單。
批量數據查詢(解決N+1問題)
graphql-java提供了兩種批量數據查詢的方案:數據庫
- BatchedDataFetcher: 用起來挺簡單的,普通的DataFetcher是給你一個ID讓你返回一個對象,批量版是給你一個ID列表,讓你返回對應的對象列表。不過這個不是Facebook推薦的方式,在新版本中會廢棄掉。
- DataLoader: 這個是Facebook官方推薦的方式,nodejs中的實現是基於js的異步機制延遲查詢,把最近一個週期產生的多個查詢集中執行(沒詳細瞭解,看文檔大概如此),java版實現方式則略有不一樣,下面詳細介紹。
關於DataLoader
graphql-java的dataloader是基於java8中新增的CompletableFeature類(大概至關於javascript裏面的Promise),實現異步延遲批量獲取查詢結果。編程
大概原理(我的理解):
- 在DataFetcher方法中,並不直接返回實體類T,而是調用DataLoader.load()方法,返回一個CompletionStage<T>,這時並不當即進行實際查詢,而是把這些異步階段對象緩存起來。
- 在查詢告一段落後(即可以當即獲取的Field值都已取得,只剩下異步查詢未完成了),graphql-java會經過DataLoaderDispatcherInstrumentation.dispatch方法通知全部當前註冊的DataLoader去執行當前積壓的全部異步階段對象,具體就是會使用DataLoader對應的BatchLoader一次性查詢一批對象。
- 這時候又有一批Field的值已經實際取得,繼續按查詢的請求向下層展開,若是有新的異步階段對象產生,就繼續步驟2,直到全部異步階段對象都得到最終值。
工程實踐中對其應用方式的考慮:
在graphql-java的官方教程中建議針對每請求建立新的DataLoader實例,查詢請求結束則DataLoader實例們的生命週期結束。
這個實現方式比較簡單,不用考慮緩存的更新問題,也不用考慮多個不一樣請求的緩存對象是否可共用。
舉個例子,張三和李四併發查詢張三的信息,他們獲取的"張三"用戶實例的結構多是不一樣的,這種狀況這兩個併發請求就不能共用緩存,而應該各自有獨立的DataLoader實例。
不過在個人工程實踐中,服務端內存中的數據實體類都是客觀一致的,其結構可見性應在更上一層即DataFetcher甚至Schema級別中進行過濾。
所以個人想法是爲每種實體類維護單例的DataLoader,和Dao對象一一對應。
這種狀況下,就不能簡單的使用DataLoader內部默認的簡單內存緩存了,由於此緩存是不會自動定時清理的。
graphql-java是容許開發者提供本身的緩存實現的,下一步我會結合項目中使用的Spring緩存管理器來具體實現。
查詢的緩存
graphql的查詢自己是有必定語法結構的特殊文本,對該文本進行解析也是有性能開銷的,所以graphql-java提供了緩存機制方便開發者把查詢文本的解析後數據結構緩存起來。
如下代碼引自官方教程,我準備結合咱們項目裏的EhCache來實做一下。
Cache<String, PreparsedDocumentEntry> cache = Caffeine.newBuilder().maximumSize(10_000).build();
GraphQL graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
.preparsedDocumentProvider(cache::get)
.build();
關於訂閱的實現
工程實踐中使用WebSocket實現訂閱。
不管是graphql仍是graphql-java都未指定訂閱的具體實現機制,但WebSocket是現代瀏覽器廣泛支持的,高性能低限制的服務器推送機制。
SpringMVC支持WebSocket,同時支持在低版本瀏覽器中使用Sock.js做爲兼容備選方案。
另外,graphql-java體驗性支持的Defer數據獲取也可基於WebSocket實現。
未完待續
參考資料
基於spring和graphql-java-tools的寵物店例程
簡單的TODO例程,使用relay的思路解決分頁問題
基於WebSocket實現GraphQL訂閱