GraphQL Java从入门到实践
源码解析
GraphQL Java 从Schema文件到GraphQL实例
GraphQL Java 一次完整的执行历程
补充:GraphQL相关资料
一、GraphQL是什么
GraphQL 是一种协议和一种查询语言。2012年,GraphQL由Facebook内部开发,2015年开源。
- 应用场景:
- 针对统一需求,后端需要适配多个端的数据需求,此时使用GraphQL可以提供大而全的接口,各个端根据自己的需求对数据进行裁剪获取
- 遗留 REST API 数量暴增,变得十分复杂,使用GrapQL可以提供统一的接口入口
- 优点:
- 按需请求所要的数据
- 获取多个资源只用一个请求
- 提供统一的API入口
- 缺点:
- N+1问题
- 引入了复杂性
- 单点问题、性能问题、安全问题
二、GraphQL Java入门
GraphQL的服务端在多个语言都有实现包括Haskell, JavaScript, Python, Ruby, Java, C#, Scala, Go, Elixir, Erlang, PHP, R,和 Clojure。
GraphQL Java是GraphQL规范的Java原生实现,也是Java实现的基本核心,所以本文主要讲解GraphQL Java的基本使用以及GraphQL的一些基本概念。如果需要在公司实施GraphQL的话,建议使用针对GraphQL Java封装后的GraphQL Java Tools来实现服务器端的接口改造,因为它比原生实现更简单高效,更加符合面向对象编程思维习惯,当然这是后话了,只要把本文的基本概念弄清楚了,使用它也就是分分钟的事情了。
万丈高楼平地起,接下来还是先从从GraphQL Java实现Hello World开始吧。
- 环境准备,引入GraphQL的Maven依赖
<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>
- 从最简单的hello world开始,所有的核心逻辑也都在这个这里了
public static void main(String[] args) {
// 1\. 定义Schema, 一般会定义在一个schema文件中
String schema = "type Query{hello: String}";
// 2\. 解析Schema
SchemaParser schemaParser = new SchemaParser();
TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema);
// 为Schema 中hello方法绑定获取数据的方法
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
// 这里绑定的是最简单的静态数据数据获取器, 正常使用时,获取数据的方法返回一个DataFetcher实现即可
.type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("world")))
.build();
// 将TypeDefinitionRegistry与RuntimeWiring结合起来生成GraphQLSchema
SchemaGenerator schemaGenerator = new SchemaGenerator();
GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);
// 实例化GraphQL, GraphQL为执行GraphQL语言的入口
GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build();
// 执行查询
ExecutionResult executionResult = graphQL.execute("{hello}");
// 打印执行结果
System.out.println(executionResult.getData().toString());
}
三、GraphQL Java服务端改造
1. 定义schema
- schema是什么
- 通俗点说,schema就是协议,规范,或者可以当他是接口文档。就跟我们平时生成的swagger文档一样,定义接口是什么,参数是什么,返回值有哪些,类型是什么,哪些值不能为空等等。
- GraphQL规定,每一个
schema
有一个根(root)query
和根(root)mutation
,还有一种subscription
类型,我们暂时用不上。-
query
即定义查询接口,当然这只是一种语义规范,在接口里写更改操作也是可以的,但是不推荐。 -
mutation
即定义更改接口,同上也是一种语义规范。 - 在目前的GraphQL实现中,只能定义一个schema文件,一个文件中只能定义一个
query
和mutation
,如果要定义多个会报错。
-
- 数据类型
- GraphQL定义了ID【相当于String类型,GraphQL用来自己实现缓存】,Int(整型), Float(浮点型)【Java中实现为Double类型】, String(字符串), Boolean(布尔型)和ID(唯一标识符类型)五个基本类型,在GraphQL中他们统称叫标量类型(Scalar Type),java实现中实现了更多的类型都定义在
graphql.Scalars
类中,比如BigInteger、BigDecimal等。GraphQL允许我们自定义标量类型,比如Data类型,只需实现相关的序列化,反序列化和验证的功能即可。已有实现参看这里:https://github.com/graphql-java/graphql-java-extended-scalars -
!
用来表示这个参数是非空的。[]
表示查询这个字段返回的是数组(List)
,[]
里面是数组的类型。
- GraphQL定义了ID【相当于String类型,GraphQL用来自己实现缓存】,Int(整型), Float(浮点型)【Java中实现为Double类型】, String(字符串), Boolean(布尔型)和ID(唯一标识符类型)五个基本类型,在GraphQL中他们统称叫标量类型(Scalar Type),java实现中实现了更多的类型都定义在
- 对象类型
- 用
type
来定义对象类型,就跟Java用class来定义一个类一样。 - 用
input
来定义接口输入类型,即接口中的输入对象。
- 用
- 基本概念差不多就这么多,下面我们在项目的
resources
目录中定义一个名schema.graphqls
为Schema文件
# 定义查询接口, 一个schema文件中只能定义一个Query对象
type Query {
# 无参, 返回字符串
hello: String
# 字段参数且不能为空, 返回普通对象
bookById(id: ID!): Book
# 对象参数, 返回列表
books(book: BookInput): [Book]
listOrgTrucks(orgTruck: OrgTruckInput):[OrgTruck]
}
# 定义修改接口
type Mutation {
hello: String
}
# 定义入参对象
input BookInput {
id: ID
name: String
}
#定义普通对象
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
2. 定义DataFetcher
DataFetcher
在GraphQL Java服务器中是一个非常重要的概念,在执行查询时,通过Datafetcher
获取一个字段的数据。也就是说我们需要为query
或mutation
中定义的方法,以及对定义的对象中的字段绑定一个DataFetcher实现,这样在GraphQL执行语法后才能通过绑定的DataFetcher执行相应的逻辑。也因此GraphQL是一个执行引擎,解析语法后具体要执行什么逻辑,GraphQL并不关心,你只需要在DataFetcher接口并绑定到字段上即可。
当GraphQL Java执行查询时,它为查询中遇到的每个字段调用适当的Datafetcher。DataFetcher是一个只有一个方法的接口,带有一个类型的参数DataFetcherEnvironment:
public interface DataFetcher<T> {
T get(DataFetchingEnvironment dataFetchingEnvironment) throws Exception;
}
- 重要提示:模式中的每个字段都有一个与之关联的DataFetcher。如果没有为特定字段指定任何DataFetcher,则使用默认的PropertyDataFetcher。
- 现在创建一个新的类GraphQLDataFetchers,其中包含图书和作者的示例列表,此处在静态数据中获取数据,但GraphQL并不需要指定数据来自何处,数据可以来自任何你定义的地方,数据库,RPC都可,这也是GraphQL强大的地方。
@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 封装了查询中带有的参数
return dataFetchingEnvironment -> {
String bookId = dataFetchingEnvironment.getArgument("id");
return books
.stream()
.filter(book -> book.get("id").equals(bookId))
.findFirst()
.orElse(null);
};
}
public DataFetcher getAuthorDataFetcher() {
// 这里因为是通过Book查询Author数据的子查询,所以dataFetchingEnvironment.getSource()中封装了Book对象的全部信息
//即GraphQL中每个字段的Datafetcher都是以自顶向下的方式调用的,父字段的结果是子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);
};
}
}
- 上面实现了两个Datafetcher,他们会绑定到Schema文件中bookById方法,和Book对象的author字段上,这样在执行bookById方法和获取author字段信息时,就会条用对应的DataFetcher方法。此外,没有绑定的指定DataFetcher的字段,会使用默认的PropertyDataFetcher,即DataFetcher中返回的对象属性如果跟Schema中定义的属性名相同的话,会自动赋值给对应的属性,否则定义的字段值为null。
3.解析Schema并绑定DataFetcher
- 定义一个
GraphQLProvider
类,来初始化GraphQL
类
@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()
// 仅仅是体验Mutation这个功能,返回一个字符串
.type("Mutation", builder -> builder.dataFetcher("hello", new StaticDataFetcher("Mutation hello world")))
// 返回字符串
.type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("Query hello world")))
// 通过id查询book
.type(newTypeWiring("Query").dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
// 查询所有的book
.type(newTypeWiring("Query").dataFetcher("books", graphQLDataFetchers.getAllBooks()))
// 查询book中的author信息
.type(newTypeWiring("Book").dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
.build();
}
// 执行GraphQL语言的入口
@Bean
public GraphQL graphQL() {
return graphQL;
}
4. 定义controller通过GraphQL查询数据
- 在Spring Boot中不需要定义这个,默认会定义一个host/graphql的Servlet
@RestController
public class GraphQLController {
@Autowired
private GraphQL graphQL;
@RequestMapping(value = "/graphql")
// 这里定义的一个字符串接口所有的参数,定义对象也是可以的
public Map<String, Object> graphql(@RequestBody String request) {
JSONObject req = JSON.parseObject(request);
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
// 需要执行的查询语言
.query(req.getString("query"))
// 执行操作的名称,默认为null
.operationName(req.getString("operationName"))
// 获取query语句中定义的变量的值
.variables(req.getJSONObject("variables"))
.build();
// 执行并返回结果
return this.graphQL.execute(executionInput).toSpecification();
}
}
5. 演示上诉代码并说明一些概念
本文使用GraphQLPlayground演示,下载地址:https://github.com/prisma/graphql-playground/releases
当然用官方的graphiql:https://github.com/graphql/graphiql, 或者postman也都是可以的。
-
查询所有的book
1555855026893.jpg -
通过id查询book
2222.jpg - 分别解释一下上图中的概念
- 1.query、mutation对应上面说的查询和修改规范,也是schema中定义的类型,默认类型为query如第一个图。
- 2.bookByIds就是上面定义Controller中获取的
operationName
名称,这个由查询方自行定义,对后端没有特别的意义。 - 3.查询变量的定义,相当于query查询接口的入参,可以在query里面的接口中引用,7初就是定义查询的实参。
- 4.定义bookById接口的别名,即可以对接口定义别名,在同一个查询中多次请求同一个接口时,必须为接口定义不同的别名,否则会报错,无法请求。看返回数据中别名为key,接口返回的数据为value。
- 5.对应bookById的另一个别名,这里相当于对bookById用不同的参数进行了第二次查询,这也是GraphQL重要特性之一的合并不同查询为一次查询节约传输成本。
- 6.对Schema中query中定义的hello查询,返回一个字符串,是为了区别6下面mutation 中hello的定义,即在GraphQL中通过查询或变更类型+里面定义的接口确定一个唯一执行入口。
- 7.定义3中参数的实际入参,由在Controller接口中的
variables
参数接收。 - 8.为查询的所有接口的返回值,默认为接口别名(或接口名)为key,接口返回的数据为value的Json数据。
- 9.点击9可以查看服务端所有定义的接口信息,也是在GraphQL存在的问题之一,会想客户端暴露所有的接口信息。
- 10.点击10可以查看服务端定义的Schema信息,对服务端定义的Schema信息一览无余。
6. 此GraphQL Java(原生实现)服务器端搭建存在的问题
- 在服务端只能定义一个Schema文件,随着接口越来越多这个文件会超级庞大。
- Schema中定义的接口需要手动跟对应的DataFetcher绑定,无法根据Schema定义自动绑定对应的解析方法。
- 解决方案:
- 自行扩展GraphQL Java项目(成本太大)
- 使用GraphQL Java Tool,很好的封装了GraphQL Java,实现了面向对象的开发开发模式,具体参看官方介绍
- 使用Spring Boot搭建,GraphQL针对Spring Boot添加了起步依赖,同时使用GraphQL Java Tool,使得开发GraphQL更加的简单高效
- Spring Boot GraphQL Demo