開始使用 GraphQL 和 Spring Boot

1. 介紹

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

2. 什么是 GraphQL ?

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

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

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

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

舉個例子,一個 blog 可能會有如下的查詢請求:

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

這個查詢請求將會:

  • 請求10篇最新的文章
  • 每一篇文章會返回 ID,title,category 字段
  • 對于每一篇文章還會返回作者,其中作者信息里包含了id,name,thumbnail

在傳統(tǒng)的 REST API 里,這要么需要發(fā)送11個請求 —— 1個接口用來請求文章列表,另10個接口請求相對應(yīng)的作者?;蛘咝枰诜?wù)端 /posts 的接口里把作者的信息都包含進(jìn)去。

2.1. GraphQL Schemas

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

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

舉剛才 blog 的例子,它包含了如下的類型定義:一個 Post 結(jié)構(gòu),Post 里 author 字段對應(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]!
}
 
# 整個應(yīng)用的根查詢(讀操作),它也是一個類型
type Query {
    recentPosts(count: Int,offset: Int): [Post]!
}
 
# 整個應(yīng)用的根修改 (寫操作)
type Mutation {
    writePost(title: String!,text: String!,category: String) : Post!
}

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

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

3. GraphQL Spring Boot Starter 介紹

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

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 就會自動設(shè)置好相應(yīng)的 handler
默認(rèn)情況下,GraphQL 服務(wù)會通過 /graphql 接口暴露出來,通過 POST 該接口就可以發(fā)送對應(yīng)請求,接口地址可以在 application.properties 里修改。

3.2. 定義 Schema

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

3.3. 根查詢解析器

根查詢需要通過在Spring里定義特殊的 java bean 對象,從而來處理不同的字段查詢。它不像 schema 的定義文件,這些bean對象可以有多個。我們只需要實(shí)現(xiàn) GraphQLQueryResolver 接口,然后在 schema 里的每個字段都有相對應(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ù)會按如下的規(guī)則順序查找:

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

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

像上面的這個 getRecentPosts 方法就會對應(yīng)上 GraphQL schema 里 recentPosts 這個查詢字段。

3.4. 通過 Bean 對象來映射 GraphQL 類型

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

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

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

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

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

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

@Repository
public class PostResolver implements GraphQLResolver<Post> {

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

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

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

3.6. 可選值

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

3.7. 修改(Mutation)

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

相應(yīng)地, 修改(Mutation)接口 應(yīng)該僅用于告知客戶端該操作會改變服務(wù)端存儲數(shù)據(jù)
在 Java 代碼里,我們只需要把 GraphQLQueryResolver 接口換成 GraphQLMutationResolver 就可以定義一個根修改接口,其它的所有規(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)常會和 GraphiQL 一起使用,GraphiQL 是一個可以直接和 GraphQL 服務(wù)交互的 UI 界面,可以執(zhí)行查詢和修改請求,可以從這里下載獨(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 是個非常令人激動的新技術(shù),它讓我們開發(fā)接口的時候有不一樣的視角,把 Spring Boot GraphQL Starter 和 GraphQL Java Tools 結(jié)合起來非常容易,它可以讓我們輕易地加到現(xiàn)有應(yīng)用里或者干脆創(chuàng)建一個新的應(yīng)用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容