源碼解析
GraphQL Java 從Schema文件到GraphQL實(shí)例
GraphQL Java 一次完整的執(zhí)行歷程
補(bǔ)充:GraphQL相關(guān)資料
一、GraphQL是什么
GraphQL 是一種協(xié)議和一種查詢(xún)語(yǔ)言另假。2012年像屋,GraphQL由Facebook內(nèi)部開(kāi)發(fā),2015年開(kāi)源边篮。
- 應(yīng)用場(chǎng)景:
- 針對(duì)統(tǒng)一需求己莺,后端需要適配多個(gè)端的數(shù)據(jù)需求,此時(shí)使用GraphQL可以提供大而全的接口戈轿,各個(gè)端根據(jù)自己的需求對(duì)數(shù)據(jù)進(jìn)行裁剪獲取
- 遺留 REST API 數(shù)量暴增凌受,變得十分復(fù)雜,使用GrapQL可以提供統(tǒng)一的接口入口
- 優(yōu)點(diǎn):
- 按需請(qǐng)求所要的數(shù)據(jù)
- 獲取多個(gè)資源只用一個(gè)請(qǐng)求
- 提供統(tǒng)一的API入口
- 缺點(diǎn):
- N+1問(wèn)題
- 引入了復(fù)雜性
- 單點(diǎn)問(wèn)題凶杖、性能問(wèn)題胁艰、安全問(wèn)題
二、GraphQL Java入門(mén)
GraphQL的服務(wù)端在多個(gè)語(yǔ)言都有實(shí)現(xiàn)包括Haskell, JavaScript, Python, Ruby, Java, C#, Scala, Go, Elixir, Erlang, PHP, R,和 Clojure。
GraphQL Java是GraphQL規(guī)范的Java原生實(shí)現(xiàn)腾么,也是Java實(shí)現(xiàn)的基本核心奈梳,所以本文主要講解GraphQL Java的基本使用以及GraphQL的一些基本概念。如果需要在公司實(shí)施GraphQL的話(huà)解虱,建議使用針對(duì)GraphQL Java封裝后的GraphQL Java Tools來(lái)實(shí)現(xiàn)服務(wù)器端的接口改造攘须,因?yàn)樗仍鷮?shí)現(xiàn)更簡(jiǎn)單高效,更加符合面向?qū)ο缶幊趟季S習(xí)慣殴泰,當(dāng)然這是后話(huà)了于宙,只要把本文的基本概念弄清楚了,使用它也就是分分鐘的事情了悍汛。
萬(wàn)丈高樓平地起捞魁,接下來(lái)還是先從從GraphQL Java實(shí)現(xiàn)Hello World開(kāi)始吧。
- 環(huán)境準(zhǔn)備离咐,引入GraphQL的Maven依賴(lài)
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>11.0</version>
</dependency>
<!--用于解析schema文件-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0-jre</version>
</dependency>
- 從最簡(jiǎn)單的hello world開(kāi)始谱俭,所有的核心邏輯也都在這個(gè)這里了
public static void main(String[] args) {
// 1\. 定義Schema, 一般會(huì)定義在一個(gè)schema文件中
String schema = "type Query{hello: String}";
// 2\. 解析Schema
SchemaParser schemaParser = new SchemaParser();
TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema);
// 為Schema 中hello方法綁定獲取數(shù)據(jù)的方法
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
// 這里綁定的是最簡(jiǎn)單的靜態(tài)數(shù)據(jù)數(shù)據(jù)獲取器, 正常使用時(shí),獲取數(shù)據(jù)的方法返回一個(gè)DataFetcher實(shí)現(xiàn)即可
.type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("world")))
.build();
// 將TypeDefinitionRegistry與RuntimeWiring結(jié)合起來(lái)生成GraphQLSchema
SchemaGenerator schemaGenerator = new SchemaGenerator();
GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);
// 實(shí)例化GraphQL, GraphQL為執(zhí)行GraphQL語(yǔ)言的入口
GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build();
// 執(zhí)行查詢(xún)
ExecutionResult executionResult = graphQL.execute("{hello}");
// 打印執(zhí)行結(jié)果
System.out.println(executionResult.getData().toString());
}
三、GraphQL Java服務(wù)端改造
1. 定義schema
- schema是什么
- 通俗點(diǎn)說(shuō)宵蛀,schema就是協(xié)議昆著,規(guī)范,或者可以當(dāng)他是接口文檔术陶。就跟我們平時(shí)生成的swagger文檔一樣凑懂,定義接口是什么,參數(shù)是什么梧宫,返回值有哪些接谨,類(lèi)型是什么,哪些值不能為空等等塘匣。
- GraphQL規(guī)定疤坝,每一個(gè)
schema
有一個(gè)根(root)query
和根(root)mutation
,還有一種subscription
類(lèi)型馆铁,我們暫時(shí)用不上。-
query
即定義查詢(xún)接口锅睛,當(dāng)然這只是一種語(yǔ)義規(guī)范埠巨,在接口里寫(xiě)更改操作也是可以的,但是不推薦现拒。 -
mutation
即定義更改接口辣垒,同上也是一種語(yǔ)義規(guī)范。 - 在目前的GraphQL實(shí)現(xiàn)中印蔬,只能定義一個(gè)schema文件勋桶,一個(gè)文件中只能定義一個(gè)
query
和mutation
,如果要定義多個(gè)會(huì)報(bào)錯(cuò)。
-
- 數(shù)據(jù)類(lèi)型
- GraphQL定義了ID【相當(dāng)于String類(lèi)型例驹,GraphQL用來(lái)自己實(shí)現(xiàn)緩存】捐韩,Int(整型), Float(浮點(diǎn)型)【Java中實(shí)現(xiàn)為Double類(lèi)型】, String(字符串), Boolean(布爾型)和ID(唯一標(biāo)識(shí)符類(lèi)型)五個(gè)基本類(lèi)型,在GraphQL中他們統(tǒng)稱(chēng)叫標(biāo)量類(lèi)型(Scalar Type)鹃锈,java實(shí)現(xiàn)中實(shí)現(xiàn)了更多的類(lèi)型都定義在
graphql.Scalars
類(lèi)中荤胁,比如BigInteger、BigDecimal等屎债。GraphQL允許我們自定義標(biāo)量類(lèi)型仅政,比如Data類(lèi)型,只需實(shí)現(xiàn)相關(guān)的序列化盆驹,反序列化和驗(yàn)證的功能即可圆丹。已有實(shí)現(xiàn)參看這里:https://github.com/graphql-java/graphql-java-extended-scalars -
!
用來(lái)表示這個(gè)參數(shù)是非空的。[]
表示查詢(xún)這個(gè)字段返回的是數(shù)組(List)
躯喇,[]
里面是數(shù)組的類(lèi)型辫封。
- GraphQL定義了ID【相當(dāng)于String類(lèi)型例驹,GraphQL用來(lái)自己實(shí)現(xiàn)緩存】捐韩,Int(整型), Float(浮點(diǎn)型)【Java中實(shí)現(xiàn)為Double類(lèi)型】, String(字符串), Boolean(布爾型)和ID(唯一標(biāo)識(shí)符類(lèi)型)五個(gè)基本類(lèi)型,在GraphQL中他們統(tǒng)稱(chēng)叫標(biāo)量類(lèi)型(Scalar Type)鹃锈,java實(shí)現(xiàn)中實(shí)現(xiàn)了更多的類(lèi)型都定義在
- 對(duì)象類(lèi)型
- 用
type
來(lái)定義對(duì)象類(lèi)型,就跟Java用class來(lái)定義一個(gè)類(lèi)一樣玖瘸。 - 用
input
來(lái)定義接口輸入類(lèi)型秸讹,即接口中的輸入對(duì)象。
- 用
- 基本概念差不多就這么多雅倒,下面我們?cè)陧?xiàng)目的
resources
目錄中定義一個(gè)名schema.graphqls
為Schema文件
# 定義查詢(xún)接口, 一個(gè)schema文件中只能定義一個(gè)Query對(duì)象
type Query {
# 無(wú)參, 返回字符串
hello: String
# 字段參數(shù)且不能為空, 返回普通對(duì)象
bookById(id: ID!): Book
# 對(duì)象參數(shù), 返回列表
books(book: BookInput): [Book]
listOrgTrucks(orgTruck: OrgTruckInput):[OrgTruck]
}
# 定義修改接口
type Mutation {
hello: String
}
# 定義入?yún)?duì)象
input BookInput {
id: ID
name: String
}
#定義普通對(duì)象
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
2. 定義DataFetcher
DataFetcher
在GraphQL Java服務(wù)器中是一個(gè)非常重要的概念璃诀,在執(zhí)行查詢(xún)時(shí),通過(guò)Datafetcher
獲取一個(gè)字段的數(shù)據(jù)蔑匣。也就是說(shuō)我們需要為query
或mutation
中定義的方法劣欢,以及對(duì)定義的對(duì)象中的字段綁定一個(gè)DataFetcher實(shí)現(xiàn),這樣在GraphQL執(zhí)行語(yǔ)法后才能通過(guò)綁定的DataFetcher執(zhí)行相應(yīng)的邏輯裁良。也因此GraphQL是一個(gè)執(zhí)行引擎凿将,解析語(yǔ)法后具體要執(zhí)行什么邏輯,GraphQL并不關(guān)心价脾,你只需要在DataFetcher接口并綁定到字段上即可牧抵。
當(dāng)GraphQL Java執(zhí)行查詢(xún)時(shí),它為查詢(xún)中遇到的每個(gè)字段調(diào)用適當(dāng)?shù)腄atafetcher侨把。DataFetcher是一個(gè)只有一個(gè)方法的接口犀变,帶有一個(gè)類(lèi)型的參數(shù)DataFetcherEnvironment:
public interface DataFetcher<T> {
T get(DataFetchingEnvironment dataFetchingEnvironment) throws Exception;
}
- 重要提示:模式中的每個(gè)字段都有一個(gè)與之關(guān)聯(lián)的DataFetcher。如果沒(méi)有為特定字段指定任何DataFetcher秋柄,則使用默認(rèn)的PropertyDataFetcher获枝。
- 現(xiàn)在創(chuàng)建一個(gè)新的類(lèi)GraphQLDataFetchers,其中包含圖書(shū)和作者的示例列表骇笔,此處在靜態(tài)數(shù)據(jù)中獲取數(shù)據(jù)省店,但GraphQL并不需要指定數(shù)據(jù)來(lái)自何處嚣崭,數(shù)據(jù)可以來(lái)自任何你定義的地方,數(shù)據(jù)庫(kù)懦傍,RPC都可雹舀,這也是GraphQL強(qiáng)大的地方。
@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 getAllBooks() {
return environment -> {
Map<String, Object> arguments = environment.getArgument("book");
Book book = JSON.parseObject(JSON.toJSONString(arguments), Book.class);
return books;
};
}
public DataFetcher getBookByIdDataFetcher() {
// dataFetchingEnvironment 封裝了查詢(xún)中帶有的參數(shù)
return dataFetchingEnvironment -> {
String bookId = dataFetchingEnvironment.getArgument("id");
return books
.stream()
.filter(book -> book.get("id").equals(bookId))
.findFirst()
.orElse(null);
};
}
public DataFetcher getAuthorDataFetcher() {
// 這里因?yàn)槭峭ㄟ^(guò)Book查詢(xún)Author數(shù)據(jù)的子查詢(xún)谎脯,所以dataFetchingEnvironment.getSource()中封裝了Book對(duì)象的全部信息
//即GraphQL中每個(gè)字段的Datafetcher都是以自頂向下的方式調(diào)用的葱跋,父字段的結(jié)果是子Datafetcherenvironment的source屬性。
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);
};
}
}
- 上面實(shí)現(xiàn)了兩個(gè)Datafetcher源梭,他們會(huì)綁定到Schema文件中bookById方法娱俺,和Book對(duì)象的author字段上,這樣在執(zhí)行bookById方法和獲取author字段信息時(shí)废麻,就會(huì)條用對(duì)應(yīng)的DataFetcher方法荠卷。此外,沒(méi)有綁定的指定DataFetcher的字段烛愧,會(huì)使用默認(rèn)的PropertyDataFetcher油宜,即DataFetcher中返回的對(duì)象屬性如果跟Schema中定義的屬性名相同的話(huà),會(huì)自動(dòng)賦值給對(duì)應(yīng)的屬性怜姿,否則定義的字段值為null慎冤。
3.解析Schema并綁定DataFetcher
- 定義一個(gè)
GraphQLProvider
類(lèi),來(lái)初始化GraphQL
類(lèi)
@Component
public class GraphQLProvider {
@Autowired
GraphQLDataFetchers graphQLDataFetchers;
private GraphQL 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) {
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
RuntimeWiring runtimeWiring = buildWiring();
SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
// 僅僅是體驗(yàn)Mutation這個(gè)功能,返回一個(gè)字符串
.type("Mutation", builder -> builder.dataFetcher("hello", new StaticDataFetcher("Mutation hello world")))
// 返回字符串
.type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("Query hello world")))
// 通過(guò)id查詢(xún)book
.type(newTypeWiring("Query").dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
// 查詢(xún)所有的book
.type(newTypeWiring("Query").dataFetcher("books", graphQLDataFetchers.getAllBooks()))
// 查詢(xún)book中的author信息
.type(newTypeWiring("Book").dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
.build();
}
// 執(zhí)行GraphQL語(yǔ)言的入口
@Bean
public GraphQL graphQL() {
return graphQL;
}
4. 定義controller通過(guò)GraphQL查詢(xún)數(shù)據(jù)
- 在Spring Boot中不需要定義這個(gè)沧卢,默認(rèn)會(huì)定義一個(gè)host/graphql的Servlet
@RestController
public class GraphQLController {
@Autowired
private GraphQL graphQL;
@RequestMapping(value = "/graphql")
// 這里定義的一個(gè)字符串接口所有的參數(shù)蚁堤,定義對(duì)象也是可以的
public Map<String, Object> graphql(@RequestBody String request) {
JSONObject req = JSON.parseObject(request);
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
// 需要執(zhí)行的查詢(xún)語(yǔ)言
.query(req.getString("query"))
// 執(zhí)行操作的名稱(chēng),默認(rèn)為null
.operationName(req.getString("operationName"))
// 獲取query語(yǔ)句中定義的變量的值
.variables(req.getJSONObject("variables"))
.build();
// 執(zhí)行并返回結(jié)果
return this.graphQL.execute(executionInput).toSpecification();
}
}
5. 演示上訴代碼并說(shuō)明一些概念
本文使用GraphQLPlayground演示但狭,下載地址:https://github.com/prisma/graphql-playground/releases
當(dāng)然用官方的graphiql:https://github.com/graphql/graphiql披诗, 或者postman也都是可以的。
-
查詢(xún)所有的book
-
通過(guò)id查詢(xún)book
- 分別解釋一下上圖中的概念
- 1.query立磁、mutation對(duì)應(yīng)上面說(shuō)的查詢(xún)和修改規(guī)范呈队,也是schema中定義的類(lèi)型,默認(rèn)類(lèi)型為query如第一個(gè)圖唱歧。
- 2.bookByIds就是上面定義Controller中獲取的
operationName
名稱(chēng)宪摧,這個(gè)由查詢(xún)方自行定義,對(duì)后端沒(méi)有特別的意義颅崩。 - 3.查詢(xún)變量的定義绍刮,相當(dāng)于query查詢(xún)接口的入?yún)ⅲ梢栽趒uery里面的接口中引用,7初就是定義查詢(xún)的實(shí)參。
- 4.定義bookById接口的別名说莫,即可以對(duì)接口定義別名姑蓝,在同一個(gè)查詢(xún)中多次請(qǐng)求同一個(gè)接口時(shí)膝蜈,必須為接口定義不同的別名,否則會(huì)報(bào)錯(cuò)熔掺,無(wú)法請(qǐng)求饱搏。看返回?cái)?shù)據(jù)中別名為key置逻,接口返回的數(shù)據(jù)為value推沸。
- 5.對(duì)應(yīng)bookById的另一個(gè)別名,這里相當(dāng)于對(duì)bookById用不同的參數(shù)進(jìn)行了第二次查詢(xún)券坞,這也是GraphQL重要特性之一的合并不同查詢(xún)?yōu)橐淮尾樵?xún)節(jié)約傳輸成本鬓催。
- 6.對(duì)Schema中query中定義的hello查詢(xún),返回一個(gè)字符串恨锚,是為了區(qū)別6下面mutation 中hello的定義宇驾,即在GraphQL中通過(guò)查詢(xún)或變更類(lèi)型+里面定義的接口確定一個(gè)唯一執(zhí)行入口。
- 7.定義3中參數(shù)的實(shí)際入?yún)⒑锪妫稍贑ontroller接口中的
variables
參數(shù)接收课舍。 - 8.為查詢(xún)的所有接口的返回值,默認(rèn)為接口別名(或接口名)為key他挎,接口返回的數(shù)據(jù)為value的Json數(shù)據(jù)筝尾。
- 9.點(diǎn)擊9可以查看服務(wù)端所有定義的接口信息,也是在GraphQL存在的問(wèn)題之一办桨,會(huì)想客戶(hù)端暴露所有的接口信息筹淫。
- 10.點(diǎn)擊10可以查看服務(wù)端定義的Schema信息,對(duì)服務(wù)端定義的Schema信息一覽無(wú)余崔挖。
6. 此GraphQL Java(原生實(shí)現(xiàn))服務(wù)器端搭建存在的問(wèn)題
- 在服務(wù)端只能定義一個(gè)Schema文件贸街,隨著接口越來(lái)越多這個(gè)文件會(huì)超級(jí)龐大。
- Schema中定義的接口需要手動(dòng)跟對(duì)應(yīng)的DataFetcher綁定狸相,無(wú)法根據(jù)Schema定義自動(dòng)綁定對(duì)應(yīng)的解析方法薛匪。
- 解決方案:
- 自行擴(kuò)展GraphQL Java項(xiàng)目(成本太大)
- 使用GraphQL Java Tool,很好的封裝了GraphQL Java脓鹃,實(shí)現(xiàn)了面向?qū)ο蟮拈_(kāi)發(fā)開(kāi)發(fā)模式逸尖,具體參看官方介紹
- 使用Spring Boot搭建,GraphQL針對(duì)Spring Boot添加了起步依賴(lài)瘸右,同時(shí)使用GraphQL Java Tool娇跟,使得開(kāi)發(fā)GraphQL更加的簡(jiǎn)單高效
- Spring Boot GraphQL Demo