開始使用 GraphQL 和 Spring Boot

1. 介紹

GraphQL是一個(gè)來自于 Facebook 的相當(dāng)新的概念,它讓我們?cè)趯?Web API 的時(shí)候作為 REST 接口風(fēng)格的另一種選擇。
這篇文章將會(huì)介紹如何通過 Spring Boot 來搭建我們的 GraphQL 服務(wù),這樣無論在現(xiàn)有項(xiàng)目或者新項(xiàng)目里都可以很方便地使用。

2. 什么是 GraphQL ?

傳統(tǒng)的 REST API 是依據(jù)服務(wù)器管理資源的概念來編寫的。這些資源可以通過 Http 請(qǐng)求以規(guī)定的幾個(gè) verb(GET厉膀、POST溶耘、PUT、DELETE) 來進(jìn)行訪問服鹅。當(dāng)我們的接口和我們的資源概念相符合時(shí)工作沒什么問題凳兵,但如果我們稍有變化,事情就開始變的麻煩了企软。

這同時(shí)還會(huì)發(fā)生在我們的客戶端請(qǐng)求多個(gè)不同數(shù)據(jù)的時(shí)候:比方說我們請(qǐng)求博客的文章以及對(duì)應(yīng)的評(píng)論庐扫。通常我們只能讓客戶端發(fā)多個(gè)請(qǐng)求,或者讓服務(wù)端在同一個(gè)接口里提供這些額外的數(shù)據(jù)仗哨,但這些數(shù)據(jù)并不總是需要的形庭,這就違背了 REST 的設(shè)計(jì),同時(shí)還導(dǎo)致了服務(wù)端的響應(yīng)包體變大厌漂,造成網(wǎng)絡(luò)傳輸?shù)睦速M(fèi)萨醒。

GraphQL 提供了一個(gè)能夠同時(shí)解決這兩個(gè)問題的方案. 它允許客戶端在一個(gè)請(qǐng)求里指明所需要的數(shù)據(jù),還可以實(shí)現(xiàn)在一個(gè)請(qǐng)求里發(fā)送多個(gè)查詢苇倡。

它工作起來更像是 RPC 服務(wù)富纸,它不用固定的 verb,而是使用 命名查詢(Query)命名修改(Mutation) 的方式旨椒。這就讓業(yè)務(wù)編寫的控制權(quán)回到了它應(yīng)有的地方晓褪,API 接口的開發(fā)者來確定哪些接口行為是被允許的, API 接口的使用者在運(yùn)行時(shí)動(dòng)態(tài)指明他們想要什么數(shù)據(jù)综慎。

舉個(gè)例子涣仿,一個(gè) blog 可能會(huì)有如下的查詢請(qǐng)求:

query {
    recentPosts(count: 10示惊,offset: 0) {
        id
        title
        category
        author {
            id
            name
            thumbnail
        }
    }
}

這個(gè)查詢請(qǐng)求將會(huì):

  • 請(qǐng)求10篇最新的文章
  • 每一篇文章會(huì)返回 ID涝涤,title阔拳,category 字段
  • 對(duì)于每一篇文章還會(huì)返回作者糊肠,其中作者信息里包含了id货裹,name弧圆,thumbnail

在傳統(tǒng)的 REST API 里搔预,這要么需要發(fā)送11個(gè)請(qǐng)求 —— 1個(gè)接口用來請(qǐng)求文章列表拯田,另10個(gè)接口請(qǐng)求相對(duì)應(yīng)的作者】圆或者需要在服務(wù)端 /posts 的接口里把作者的信息都包含進(jìn)去臣淤。

2.1. GraphQL Schemas

GraphQL 服務(wù)會(huì)提供一個(gè) schema 來完整描述所有的 API 接口张弛。這個(gè) schema 文件包含了具體的數(shù)據(jù)類型定義(type)。每個(gè)數(shù)據(jù)類型會(huì)有一個(gè)或多個(gè)字段(field)寺董,每個(gè)字段會(huì)有0到多個(gè)參數(shù)(parameter)遮咖,以及對(duì)應(yīng)的返回類型(type)御吞。

通過這些字段的嵌套組合形成了一個(gè)圖數(shù)據(jù)結(jié)構(gòu)(也就是 GraphQL 里 Graph 的含義)陶珠。整個(gè)圖不需要避免環(huán)揍诽,出現(xiàn)環(huán)也是完全可以接受的暑脆,但一定是有向圖。也就是說碟联,客戶端可以通過一個(gè)類型節(jié)點(diǎn)的字段找到它的子節(jié)點(diǎn),但是無法通過子節(jié)點(diǎn)直接反向找到父親節(jié)點(diǎn)(除非在 schema 里單獨(dú)定義出來)部脚。

舉剛才 blog 的例子,它包含了如下的類型定義:一個(gè) Post 結(jié)構(gòu)裤纹,Post 里 author 字段對(duì)應(yīng)的 Author結(jié)構(gòu),以及從根查詢(Root Query)節(jié)點(diǎn)上通過 recentPosts 字段來找出最新的 Post

type Post {
    id: ID!
    title: String!
    text: String!
    category: String
    author: Author!
}
 
type Author {
    id: ID!
    name: String!
    thumbnail: String
    posts: [Post]!
}
 
# 整個(gè)應(yīng)用的根查詢(讀操作)鹰椒,它也是一個(gè)類型
type Query {
    recentPosts(count: Int,offset: Int): [Post]!
}
 
# 整個(gè)應(yīng)用的根修改 (寫操作)
type Mutation {
    writePost(title: String!漆际,text: String!淆珊,category: String) : Post!
}

一些字段類型后面帶有 "!" 意味著這個(gè)字段是非空的奸汇,如果沒有的話就說明是可選的施符。在我們請(qǐng)求接口的時(shí)候,如果對(duì)應(yīng)的可選字段服務(wù)器返回了空對(duì)象戳吝,GraphQL也能夠正確處理后續(xù)的查詢,比方說 recentPosts 接口里幾篇文章的 category 是空。

GraphQL 服務(wù)也通過接口暴露出了 schema塘雳,這樣客戶端就可以提前獲取 schema 定義便于處理。這使得當(dāng) schema 改變了的時(shí)候妻顶,客戶端能夠自動(dòng)發(fā)現(xiàn)并動(dòng)態(tài)調(diào)整數(shù)據(jù)結(jié)構(gòu)。一個(gè)很有用的場(chǎng)景就是可以使用 GraphiQL 工具(注意中間多了一個(gè) i)來和服務(wù)端進(jìn)行交互(類似于 Postman 等 REST Client)醇王。

3. GraphQL Spring Boot Starter 介紹

Spring Boot GraphQL Starter 提供了一個(gè)便捷的方式讓我們快速地運(yùn)行起一個(gè) GraphQL 服務(wù). 它和 GraphQL Java Tools 配合呢燥,讓我們只需要寫很少的代碼就可以啟動(dòng)起來。

3.1. 配置服務(wù)

我們只需要加入如下的依賴:

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>

Spring Boot 就會(huì)自動(dòng)設(shè)置好相應(yīng)的 handler
默認(rèn)情況下寓娩,GraphQL 服務(wù)會(huì)通過 /graphql 接口暴露出來叛氨,通過 POST 該接口就可以發(fā)送對(duì)應(yīng)請(qǐng)求呼渣,接口地址可以在 application.properties 里修改。

3.2. 定義 Schema

GraphQL Tools 可以通過處理 GraphQL Schema 文件來生成正確的結(jié)構(gòu)對(duì)象寞埠,并綁定到對(duì)應(yīng)的 bean 對(duì)象上屁置。這些schema文件只要以 “.graphqls” 擴(kuò)展名結(jié)尾并存在于 classpath里,Spring Boot GraphQL starter 就可以自動(dòng)找到這些 schema 文件仁连,所以我們完全可以把這些文件按模塊劃分管理蓝角。
但我們只能有一個(gè)根查詢,也必須有一個(gè)根查詢的定義饭冬。而 Mutation 定義可以沒有或者有一個(gè)使鹅。這個(gè)限制是源于 GraphQL Schema 規(guī)則,而不是因?yàn)?Java 無法實(shí)現(xiàn)昌抠。

3.3. 根查詢解析器

根查詢需要通過在Spring里定義特殊的 java bean 對(duì)象患朱,從而來處理不同的字段查詢。它不像 schema 的定義文件炊苫,這些bean對(duì)象可以有多個(gè)裁厅。我們只需要實(shí)現(xiàn) GraphQLQueryResolver 接口,然后在 schema 里的每個(gè)字段都有相對(duì)應(yīng)名字的屬性或函數(shù)就可以了侨艾。

public class Query implements GraphQLQueryResolver {
    private PostDao postDao;
    public List<Post> getRecentPosts(int count执虹,int offset) {
        return postsDao.getRecentPosts(count,offset);
    }
}

這些屬性或函數(shù)會(huì)按如下的規(guī)則順序查找:

  • <field>
  • is<field> 僅當(dāng)該字段是個(gè) Boolean 變量時(shí)
  • get<field>

如果schema里對(duì)應(yīng)字段有定義參數(shù)的話蒋畜,這些函數(shù)也需要按照對(duì)應(yīng)的順序來定義(像 count 與 offset)声畏,函數(shù)參數(shù)最后可以有一個(gè)可選的 DataFetchingEnvironment 類型的參數(shù),來獲取一些上下文信息姻成。
這些函數(shù)的返回值也需要和 schema 里對(duì)應(yīng)起來插龄,一會(huì)兒我們就會(huì)看到。所有的原生類型 String科展,Int均牢,List,等等 都可以和相應(yīng)的 Java 類型對(duì)應(yīng)起來才睹。

像上面的這個(gè) getRecentPosts 方法就會(huì)對(duì)應(yīng)上 GraphQL schema 里 recentPosts 這個(gè)查詢字段徘跪。

3.4. 通過 Bean 對(duì)象來映射 GraphQL 類型

在 GraphQL 服務(wù)里,無論是在根節(jié)點(diǎn)還是任何一個(gè)結(jié)構(gòu)里琅攘,所有復(fù)雜的類型都可以對(duì)應(yīng)上 Java Bean 對(duì)象垮庐。每一個(gè) GraphQL 類型只能有一個(gè) Java 類來對(duì)應(yīng),但 Java 的類名并不一定要和 GraphQL 里的類型名一樣
Java bean 里的屬性名會(huì)被映射成 GraphQL 返回?cái)?shù)據(jù)的字段名

public class Post {
    private String id;
    private String title;
    private String category;
    private String authorId;
}

在 Java bean 里的屬性和方法如果在 GraphQL schema 找不到相應(yīng)定義的都會(huì)被直接忽略掉而不會(huì)出什么問題坞琴。這個(gè)機(jī)制可以用來處理一些復(fù)雜情況哨查。
舉例來說,這里的 authorId 在 schema 里并沒有任何的定剧辐,所以在接口里就不會(huì)出現(xiàn)寒亥,但它可以在接下來的步驟里使用:

3.5. 復(fù)雜對(duì)象的字段解析

有時(shí)候邮府,一個(gè)字段的數(shù)據(jù)并不能直接訪問,它有可能涉及到數(shù)據(jù)庫查詢溉奕,復(fù)雜的計(jì)算褂傀,或者一些別的什么情況。 GraphQL Tools 有一套機(jī)制來處理這些場(chǎng)景 它可以用 Spring 的 Bean 對(duì)象來為這些普通 Bean 提供數(shù)據(jù)加勤。

我們通過使用普通 Bean 名字后面加上 Resolver 仙辟,然后再實(shí)現(xiàn) GraphQLResolver 接口,就可以使用 Spring Bean 來為普通 Bean 提供額外的字段解析鳄梅,然后在 Spring Bean里的方法都需要遵循上面的命名規(guī)則欺嗤,唯一的區(qū)別是這些方法的第一個(gè)參數(shù)會(huì)是普通Bean對(duì)象。
如果字段同時(shí)存在于普通 Bean 和 Resolver上的話卫枝,Resolver上的會(huì)被優(yōu)先采用煎饼。

@Repository
public class PostResolver implements GraphQLResolver<Post> {

    @Autowired
    private AuthorDao authorDao;
 
    public Author getAuthor(Post post) {
        return authorDao.getAuthorById(post.getAuthorId());
    }
}

這些 Resolver 會(huì)被 Spring 上下文加載,所以這可以使得我們可以使用很多 Spring 的策略校赤,比方說注入 DAO吆玖。

和上面一樣,如果客戶端并沒有請(qǐng)求對(duì)應(yīng)的字段的話马篮,GraphQL 將不會(huì)去獲取相應(yīng)的數(shù)據(jù)沾乘。這就意味著如果客戶端去獲取了一個(gè) Post 但沒有要請(qǐng)求 author 字段,那么 Resolver 里的 getAuthor() 方法將不會(huì)被調(diào)用浑测,從而相應(yīng)的 DAO 請(qǐng)求也不會(huì)發(fā)出翅阵。

3.6. 可選值

GraphQL Schema 有 Optional 的概念,一些類型可以是可選為空的迁央,另一些就是非空的掷匠。
這在 Java 里可以直接使用 null 來表示和處理,相應(yīng)的岖圈,如果是在 Java 8 環(huán)境下讹语,可以使用 Optional 類型來表示可選,無論是哪種方式蜂科,系統(tǒng)都能正確處理顽决。這個(gè)機(jī)制就讓我們的 GraphQL schema 能和 Java 代碼更好地對(duì)應(yīng)起來。

3.7. 修改(Mutation)

到現(xiàn)在為止导匣,我們一直在討論從服務(wù)端獲取數(shù)據(jù)才菠,GraphQL 同樣還可以更新服務(wù)端的數(shù)據(jù),在 GraphQL 里就是 Mutation贡定。
從代碼的角度來說赋访,一個(gè) Query 請(qǐng)求沒理由不能直接修改數(shù)據(jù),我們可以很容易用 Query Resolver 來接受一些參數(shù)然后修改數(shù)據(jù),最后返回給客戶端进每,在這里采用 Mutation 主要是為了更好地規(guī)范。

相應(yīng)地命斧, 修改(Mutation)接口 應(yīng)該僅用于告知客戶端該操作會(huì)改變服務(wù)端存儲(chǔ)數(shù)據(jù)
在 Java 代碼里田晚,我們只需要把 GraphQLQueryResolver 接口換成 GraphQLMutationResolver 就可以定義一個(gè)根修改接口,其它的所有規(guī)則都和查詢接口一樣国葬,修改接口的返回值就和查詢接口一樣贤徒,可以嵌套等。

public class Mutation implements GraphQLMutationResolver {
    private PostDao postDao;
 
    public Post writePost(String title汇四,String text接奈,String category) {
        return postDao.savePost(title,text通孽,category);
    }
}

4. 有關(guān) GraphiQL

GraphQL 經(jīng)常會(huì)和 GraphiQL 一起使用序宦,GraphiQL 是一個(gè)可以直接和 GraphQL 服務(wù)交互的 UI 界面,可以執(zhí)行查詢和修改請(qǐng)求背苦,可以從這里下載獨(dú)立的基于 Electron 的 GraphiQL 應(yīng)用互捌。
在我們的應(yīng)用里還可以直接集成基于 Web 版本的 GraphiQL,我們只需要加入以下依賴

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>

就可以在 /graphiql 里看到行剂,但這只適用于 graphql 接口在 /graphql 的默認(rèn)情況秕噪,如果有調(diào)整就還需要獨(dú)立客戶端。

5. 小結(jié)

GraphQL 是個(gè)非常令人激動(dòng)的新技術(shù)厚宰,它讓我們開發(fā)接口的時(shí)候有不一樣的視角腌巾,把 Spring Boot GraphQL Starter 和 GraphQL Java Tools 結(jié)合起來非常容易,它可以讓我們輕易地加到現(xiàn)有應(yīng)用里或者干脆創(chuàng)建一個(gè)新的應(yīng)用铲觉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末澈蝙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子撵幽,更是在濱河造成了極大的恐慌碉克,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件并齐,死亡現(xiàn)場(chǎng)離奇詭異漏麦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)况褪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門撕贞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人测垛,你說我怎么就攤上這事捏膨。” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵号涯,是天一觀的道長(zhǎng)目胡。 經(jīng)常有香客問我,道長(zhǎng)链快,這世上最難降的妖魔是什么誉己? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮域蜗,結(jié)果婚禮上巨双,老公的妹妹穿的比我還像新娘。我一直安慰自己霉祸,他們只是感情好筑累,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著丝蹭,像睡著了一般慢宗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上奔穿,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天婆廊,我揣著相機(jī)與錄音,去河邊找鬼巫橄。 笑死淘邻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的湘换。 我是一名探鬼主播宾舅,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼彩倚!你這毒婦竟也來了筹我?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤帆离,失蹤者是張志新(化名)和其女友劉穎蔬蕊,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哥谷,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岸夯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了们妥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猜扮。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖监婶,靈堂內(nèi)的尸體忽然破棺而出旅赢,到底是詐尸還是另有隱情齿桃,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布煮盼,位于F島的核電站短纵,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏僵控。R本人自食惡果不足惜香到,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望喉祭。 院中可真熱鬧,春花似錦雷绢、人聲如沸泛烙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔽氨。三九已至,卻和暖如春帆疟,著一層夾襖步出監(jiān)牢的瞬間鹉究,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工扇救, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鄙信,地道東北人怀伦。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像绍妨,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子柬脸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容