「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。
3分钟集成GraphQL
GraphQL是一个从服务端检数据的查询语言。某种程度上,是REST、SOAP、或者gRPC的替代品。
假设我们要从在线商店的后台查询一本特定书记的详细信息。
使用GraphQL,你可以发送以下的查询到服务端来获取id为“book-1”的书籍的详细信息。
{
bookById(id: "book-1"){
id
name
pageCount
author {
firstName
lastName
}
}
}
复制代码
这不是JSON,虽然看起来很像。他是一个GraphQL查询语句。语句的含义是:
- 查询指定id的一本书
- 返回书籍的id、name、pageCount、author
- 对于author字段,还需要包含firstName和lastName
返回结果是一个正常的JSON格式:
{
"bookById":
{
"id":"book-1",
"name":"Harry Potter and the Philosopher's Stone",
"pageCount":223,
"author": {
"firstName":"Joanne",
"lastName":"Rowling"
}
}
}
复制代码
GraphQL一个非常重要的特性--它是静态类型的:服务端明确的知道你可以查询的每个对象的类型,客户端可以从服务端获取“schema”。Schema描述了哪些查询是可能的,哪些字段能被获取到。(注意:我们这里说的Schema指的是GraphQL Schema,他与其他Schema比如JSON Schema获取DataBase Schema无关)
上面查询的Schema如下所示:
type Query {
bookById(id: ID): Book
}
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
复制代码
这个教程将重点介绍如何使用Schema在java中实现一个GraphQL服务。
GraphQL Java概述
GraphQL Java是GraphQL的Java服务端的实现。在GraphQL Java Github org上有几个仓库。最重要的一个是GraphQL Java Engine(github.com/graphql-jav…
GraphQL Java Engine只关心查询的执行。它不处理任何HTTP或者JSON相关的事。基于这些方面,我们将用GraphQL Java Spring Boot适配器,它负责通过SpringBoot基于HTTP暴露我们的API。
创建一个GraphQL Java服务主要的步骤:
- 定义GraphQL Schema
- 决定一个查询如何获取到数据
示例API:获取书的详情
我们的示例项目将会写一个简单的接口:获取指定书记的详细信息。虽然这不是一个全面的API,但对于本教程来说足够了。
创建一个SpringBoot项目
导入3个包:
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>17.3</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-spring-boot-starter-webmvc</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
复制代码
前2个是GraphQL Java和GraphQL Java Spring。Guava不是必要的,但是它能让我们更方便点。
Schema
在src/main/resources下面创建一个新文件schema.graphqls,写入一下内容:
type Query {
bookById(id: ID): Book
}
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
复制代码
这个Schema定义了一个顶级字段(在Query类型中):bookById,它返回指定熟记的详情。
它也定义了类型Book,包含字段:id,name,pageCount,author。author的类型是Author,这个在Book类型之后也定义了。
上面用来描述Schema的专用语言叫做Schema Definition Language 或者叫DSL。更多细节可以查看:graphql.org/learn/schem…
这个文件完成功后,我们就得使用它。
我们创建一个新的类GraphQLProvider,定义一个init方法,这个方法用来创建GraphQL实例:
@Component
public class GraphQLProvider {
private GraphQL graphQL;
@Bean
public GraphQL graphQL() {
return 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) {
// TODO: 我们稍后将在这创建Schema
}
}
复制代码
我们使用Guava的Resources去读取之前创建的文件。接下来创建GraphQLSchema和GraphQL实例。这个GraphQL实例通过带有@Bean注解的graphQL()方法作为一个Spring Bean被暴露。GraphQL Java Spring适配器将使用GraphQL实例让我们的schema生效,通过HTTP,默认url: /graphql。
我们还需要做的就是实现buildSchema方法,该方法创建GraphQLSchema实例,连接sdl和DataFetcher。
@Autowired
GraphQLDataFetchers graphQLDataFetchers;
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()
.type(newTypeWiring("Query")
.dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
.type(newTypeWiring("Book")
.dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
.build();
}
复制代码
TypeDefinitionRegistry是schema文件的解析版本。SchemaGenerator将TypeDefinitionRegistry和RuntimeWiring结合起来,生成GraphQLSchema。
buildWiring方法使用graphQLDataFetchers bean去注册2个DataFetcher
。
- 一个是通过指定id检索book
- 一个是获取指定book的author
DataFetcher
和如何实现GraphQLDataFetchers
bean在下部分内容说明。
创建GraphQL
和GraphQLSchema
实例总体过程如下所示:
DataFetchers
也许GraphQL Java server最重要的概念就是DataFetcher
:在查询执行的时候,一个DataFetcher
获取一个字段的数据。
当GraphQL Java在执行一个查询时,它会为查询语句中的每个字段调用合适的DataFetcher
。一个DataFetcher
是一个接口,其中只有一个方法,发发只有一个DataFetcherEnvironment
参数:
public interface DataFetcher<T> {
T get(DataFetchingEnvironment dataFetchingEnvironment) throws Exception;
}
复制代码
重点:schema中的每个字段都有一个DataFethcer
与之关联。如果你没有给某个字段定义一个DataFetcher
,那么会使用默认的PropertyDataFetcher
。我们稍后会消息讨论这点。
我们接下来创建一个新类GraphQLDataFetchers
,它包含books和authors的样例list。
完整的实现如下所示:
@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 getBookByIdDataFetcher() {
return dataFetchingEnvironment -> {
String bookId = dataFetchingEnvironment.getArgument("id");
return books
.stream()
.filter(book -> book.get("id").equals(bookId))
.findFirst()
.orElse(null);
};
}
public DataFetcher getAuthorDataFetcher() {
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);
};
}
}
复制代码
数据源
我们从这个类中的静态列表里获取books和authors,这会帮助你理解GraphQL对数据来源没有任何要求。这是GraphQL的优势:数据可以来自内存里的静态数据,来自数据库或者外部服务。
Book DataFetcher
我们第一个方法getBookByIdDataFetcher
返回一个DataFetcher
的实现--使用DataFetcherEnvironment
返回指定的book。这个例子的意思是,我们需要从bookById
字段拿到id参数,然后找到这个id对应的book。如果没有找到,返回null。
String bookId = dataFetchingEnvironment.getArgument("id");
中的“id”指的是在schema中的query字段bookById
:
type Query {
bookById(id: ID): Book
}
复制代码
Author DataFetcher
第二个方法getAuthorDataFetcher
,返回一个DataFetcher
的实现--获取指定book的author。和book的DataFetcher方法比较,这个我们没有获取参数,但是我们有一个book的实例。通过getSource
方法可以获取到有效的父类字段,然后得到DataFetcher
结果。这是一个重要的概念:GraphQL中每个字段的DataFetcher
都是以自顶向下的方式被调用,父类的结果是子类DataFetcherEnvironment
的source
属性。
我们接下来使用前面获取的book拿到authorId
,然后使用和查询书相同的方式查询指定的author。
Default DataFetchers
我们只实现了2个DataFetchers
。正如前面提及的,如果你没有实现,那么就会使用默认的PropertyDataFetcher
。按我们的例子来讲,意思就是说Book.id
,Book.name
,Book.pageCount
,Author.id
,Author.firstname
,Author.lastName
都有一个默认的PropertyDataFetcher
分别与之关联。
PropertyDataFetcher
通过多种方式尝试在Java对象上获取属性。例子中java.util.Map
很容易通过key查找属性。这对我们来说很好,因为book和Author的Maps的keys和schema中字段是一模一样的。比如,schema中我们定义Book类型的字段pageCount
,book的DataFetcher
返回的Map中有key pageCount
. 因为字段名字和Map中的key一样,所以PropertyDateFetcher
才能起作用。
让我们假设一下,我们有一个字段不匹配,book Map中的key是totalPages,而不是pageCount。这将会导致所有book的pageCount值为null,因为PropertyDataFetcher
无法获取到正确的值。为了解决这个问题,你需要为Book.pageCount去注册一个新的DataFetcher
,所下所示:
// In the GraphQLProvider class
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query")
.dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
.type(newTypeWiring("Book")
.dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher())
// This line is new: we need to register the additional DataFetcher
.dataFetcher("pageCount", graphQLDataFetchers.getPageCountDataFetcher()))
.build();
}
// In the GraphQLDataFetchers class
// Implement the DataFetcher
public DataFetcher getPageCountDataFetcher() {
return dataFetchingEnvironment -> {
Map<String,String> book = dataFetchingEnvironment.getSource();
return book.get("totalPages");
};
}
复制代码
试用API
这就是构建GraphQL API所需要的全部内容。在运行SpringBoot项目后,使用http://localhost:8080/graphql
地址下能看到API了。
尝试和查看GraphQL API最简单的方式是使用GraphQL Playground(www.graphql-java.com/tutorials/g…
启动之后你会要求输入一个URL,输入:http://localhost:8080/graphql
之后,你就可以查询我们之前写的样例API。
graphql-java-kickstart
上面所讲的,引用了2个jar包,graphql-java是GraphQL的java实现形式。graphql-java-spring-boot-starter-webmvc是为了支持SpringWeb方式请求。
而graphql-java-kickstart是在这之上,不仅包含graphql-java,还有graphql-java-tools、graphql-java-servlet。支持了图形界面,我们只需要引入这一个依赖就可以了。
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>12.0.0</version>
</dependency>
复制代码
开启GraphIQL和PlayGround
这两个都是graphql语法执行图形界面。
开启方式如下:
graphql:
graphiql:
enabled: true
playground:
enabled: true
复制代码
创建schema
和之前所讲一样,需要创建schema文件,后缀必须是.graphqls
。
type Query {
bookById(id: String): Book
}
type Book {
id: String
name: String
pageCount: Int
author: Author
}
type Author {
id: String
firstName: String
lastName: String
}
复制代码
创建对应的Object
Schema中定义的每一个数据都需要有一个对应的Java对象:
@Data
class Book{
private String id;
private String name;
private Integer pageCount;
private Author author;
}
@Data
class Author{
private String id;
private String firstName;
private String lastName;
}
复制代码
创建Resolver
和之前不一样的是,这个不需要我们写详细的解析步骤,只需要写一个Resolver。
@Component
public class BookResolver implements GraphQLQueryResolver {
public Book bookById(String id){
Book book = new Book();
book.setId("book1");
book.setName("test");
return book;
}
}
复制代码
@Component注解是为了被Spring发现。GraphQLQueryResolver接口是为了标记为Schema的解析类。方法名字必须和schema中定义的查询名字一样。
启动SpringBoot
启动SpringBoot后,浏览器访问:http://localhost:8080/playground。就会进入图形界面
左边有2个按钮,DOCS和SCHEMA。DOCS会显示你定义的查询,以及可以获取到的详细信息。SCHEMA会显示你定义的SCHEMA。
PostMan发起请求
如下所示,请求地址为:localhost:8080/graphql 请求方式为POST 参数为{"query":"graphql语句"}
优势
- 前端不需要多次请求,一次请求就可以获取所需数据
- 前端自己决定需要获取的数据
- API文件是及时更新的