GraphQL与Java集成
在了解了客户端如何编写查询请求及如何描述数据之后,接下来为大家介绍在服务端到底该如何开发一个GraphQL的接口。
GraphQL提供了多种语言的实现,其中包括Java版本的实现:graphql-Java,如果我们使用Java来完成BFF的开发工作,graphqlJava可以构建一个GraphQL的后端接口服务。
graphql-Java 的 GitHub 地 址 是 https://github.com/graphqljava/graphql-java,目前在本书编写时最新版本为9.4,所以使用该版本。GraphQL官方的Java示例代码如下。
上述代码的最终目的是要创建一个GraphQL对象,然后通过该对象执行请求的Query或Mutation语句。那么,GraphQL对象该如何创建?
首先需要加载描述的schema,通过SchemaParser类来完成,然后定义具体在运行时schema中的type所对应的Java代码中的数据抓手(DataFetcher),例如,user会出现哪个方法?role又会调用哪个方法?通过RuntimeWiring类可以将DataFetcher与schema关联起来,最后通过相关的样例代码就可以创建GraphQL对象。
在正式的项目中,一般要如何使用?还是使用Gradle,首先在BFF的工程中添加graphql java的依赖,内容如下。
implementation 'com.graphql-java:graphql-java:2019-04-09T05-29-53-bd9240c'
然后在main的resource路径下创建.graphql文件,用于描述数据的schema,内容如下。
和之前的内容一样,我们首先定义用户和角色的数据类型,然后定义创建用户和查询用户的接口,接下来编写一个工厂类来创建GraphQL对象,用于最终执行请求,代码如下。
根据GraphQL类的源码可以得到解释:
Building this object is very cheap and can be done on each execution if necessary. Building the schema is often not as cheap, especially if its parsed from graphql IDL schema format via.
意思是构造一个GraphQL对象是很轻量的,在每次执行时去build,但构造一个schema就不轻量了,尤其是使用SchemaParser,所以我们可以将GraphQL的Build定义为一个Bean,然后在每次执行时去build,代码如下。
接下来需要开发一个GraphQL的接口,用于处理所有的请求,我们首先定义一个值对象用来接收前端的JSON数据,代码如下。
然后创建GraphQLController,代码如下。
这 样,GraphQL的接口开发就完成了,接下来实现具体的DataFetcher来调用后端的服务,GraphQL就可以工作了。
例如,查询用户首先定义一个UserQueryDataFetcher类并实现graphql-java提供的DataFetcher接口,代码如下。
在上述代码中,DataFetcher接口提供了get方法,通过方法的参数environment可以得到请求的参数,然后通过UserService去远程调用后端的服务。
作为示例,我们快速实现一下UserService,代码如下。
定义好DataFetcher以后,需要将DataFetcher与schema关联起来,回到GraphQLFactory,在构建GraphQL的方法中加入DataFetcher的绑定,代码如下。
在上述代码中,整体的构建方式和之前一样,通过runtimeWiringBuilder.type 方法可以绑定 DataFetcher 到 具 体 的schema中的type。例如,此处是绑定userQueryDataFetcher到Query类型的user字段上,这样,一个GraphQL的接口就定义好了。然后启动服务验证一下,发起如下请求。
得到结果如下。
由6.4.2节中了解到,通过编写不同的请求语句,我们可以动态地增加和过滤数据,完成基本数据组合的功能之后,BFF还有一个核心能力,就是组合接口,比如用户信息中包含的角色信息需要查询另一个接口,该如何去组合?其实和单个接口查询的思路一样,GraphQL的设计理念就是开发人员只需定义好DataFetcher,并将其和schema关联上,剩下的接口组合、数据过滤、转换等工作就交给GraphQL,如角色查询,我们可以创建一个UserRoleQueryDataFetcher,代码如下。
其中,UserRoleService的内容如下。
和User的DataFetcher类似,不同的是,查询User时使用的是请求时传入的用户ID作为参数,而查询Role时并没有传入任何参数。如果不明白,可以看schema的代码。
在编写查询语句时,通过Query类型中的user(id:String)方法,我们传入ID作为请求的参数,但在查询User类型中的roles字段时并没有任何参数,通常的做法是通过子类型的父类来获取有用的参数,DataFetcher的get方法参数environment提供了getSource()方法可以获得当前字段的父类对象,此处即为User对象,然后通过User获取用户的ID,来支持后续的查询操作。
定义好DataFetcher后 ,只需将它与 schema绑定即可,修改GraphQLFactory,代码如下。
在上述代码中,添加了User 类型中的 roles 字段使用userRoleQueryDataFetcher,当查询语句中包含了roles字段时,例如:
GraphQL帮助我们自动调用该DataFetcher,反之,如果没有roles字段,Role的DataFetcher就不会执行。
Mutation和Query的用法一样,也是使用DataFetcher来实现和后端服务的绑定,这里不再介绍,下面介绍在GraphQL中一个批量查询数据的方式:DataLoader。
为什么需要DataLoader?其实DataLoader是为了解决在查询中经常会遇到的N+1次查询的问题。例如,我们查询一个用户的详细信息(包括用户的角色信息),需要调用后端服务两次,组合用户接口和角色接口的数据,当批量查询10个用户信息时,假设后端提供了批量查询用户的接口,调用一次接口就可以获取10个用户的信息,然后根据每个用户的信息,需要单独调用10次角色的接口,总共需要11次查询才能获得最终的数据,这就是N+1次查询的问题。
N+1的设计肯定不合理,要解决这个问题,我们应同时提供批量查询用户和批量查询角色的接口,最多需要两次查询,就可以批量地获取到用户和角色的信息,然后将用户和角色组合起来得到最终的结果,graphql-java中提供的DataLoader就用来完成上述操作。例如,我们先来定义一个批量查询用户的schema,代码如下。
接下来创建UserListQueryDataFetcher,代码如下。
然后将DataFetcher与schema绑定,修改GraphQLFactory,代码如下。
可以验证一下,发送如下请求。
得到结果如下。
这里查询了3个用户,在UserRoleService中打印一些日志会发现,它被调用了3次。下面使用DataLoader修改上述代码,首先定义一个RoleDataLoader类并继承graphql-java提供的DataLoader,代码如下。
UserRoleService中添加findRolesByUserIds的方法,代码如下。
在上述代码中,我们需要定义DataLoader的泛型,DataLoader的源码解释如下。
也就是说,K用来表示单个查询时key的类型,如查询角色的key是用户ID,所以类型是String;V用来表示查询单个返回结果的类型,如单个用户拥有多个角色,所以类型是Role[]。然后,我们需要定义最终批量查询的方法,这里传入UserRoleService来完成最终的查询工作,需要注意的是,要返回一个非阻塞式的方法定义,使用java.util.concurrent中的CompletableFuture.supplyAsync来快速定义一个异步的方法。接下来修改原来的UserRole QueryDataFetcher,代码如下。
在上述代码中,不需要UserRoleService去单独查询角色信息,而是使用我们定义好的RoleDataLoader的load方法来查询角色,load并不会立即执行查询方法,而是先将key都收集起来,最后只调用一次批量查询的方法,从而解决N+1次查询的问题。需要注意的是,DataFetcher的泛型要对应地修改为CompletableFuture<Role[]>类型。
最后一步,需要将 DataFetcher 注册到 GraphQL 中,修改GraphQLFactory,代码如下。
通过GraphQL.Builder可以将DataLoader注册到GraphQL的执行对象中,这样就完成了DataLoader的开发工作,再次调用服务,如果在后端服务中有日志输出,就可以发现角色接口只被调用了一次。
GraphQL与WebFlux集成
BFF是所有请求的唯一入口,它的并发量远大于后端的服务,所以对系统的负载能力要求会相对高一些,graphql-java内部采用非阻塞式的设计,使用CompletableFuture等多线程的方式来达到异步执行的目的。
在官方的一些Demo中可以发现,GraphQL更推荐使用异步的方式来执行DataFetcher。例如,用户查询的DataFetcher可以写成如下代码。
除了使用java.util.concurrent中的Future,我们还可以使用WebFlux 来 实 现 非 阻 塞 式 的 接 口 开 发 , Spring 官方也推荐使用WebClient来实现异步的远程调用,那么GraphQL该如何与WebFlux集成?如图6.11所示。
在图6.11中,客户端会请求WebFlux的接口,然后调用GraphQL执行查询,在GraphQL的DataFetcher中调用WebClient完成后端的远程调用,整个过程都是响应式的,这样会大大增加BFF层的负载能力。所以,要完成上述的集成工作,首先需要开发一个WebFlux的接口,代码如下。
在上述代码中,创建一个GraphQLController并注入GraphQL对象,这里并不直接声明为RestController,而是使用@Component注册成Spring的Bean即可。然后增加一个query方法,方法参数类型为ServerRequest,返回类型为Mono<ServerResponse>。
通过serverRequest.bodyToMono能够快速将请求的body转换为Mono<GraphqlVo>对象,然后将参数转换为ExecutionInput,最终通过GraphQL对象执行并返回,大致与之前所示的SpringMVC的GraphQL执行过程相同,只不过全部采用了响应式的写法。
定义完Controller后,需要配置一个POST的接口,代码如下。
完成接口开发后,修改远程的调用方式,定义WebClient的Builder,代码如下。
在第2章中介绍过WebClient的用法,这里不再解释,假设之前UserService的findById方法使用的是RestTemplate,那么可以再增加一个WebClient方式的调用方法,代码如下。
在上述代码中,新增了一个findById2的方法,并且使用了WebClient来调用后端的服务,与RestTemplate不同,WebClient返回了Mono<User>类型的结果,那么如何在DataFetcher中使用Mono类型的对象?graphql-java并不支持Mono类型。
由之前的介绍可知,graphql-java支持Java Concurrent包中的Futrue类型,幸运的是Spring WebFlux的Mono和Flux类型也可以与Future进行相互转换。例如,之前在定义GraphQLController中使用fromFuture的静态方法将CompletableFuture转换为Mono类型,同样也可以将Mono转换成CompletableFuture。WebFlux与GraphQL数据转换示意图如图6.12所示。
通过CompletableFuture 类型的转换 , 将 Spring WebFlux 与GraphQL完美地集成在一起,修改UserQueryDataFetcher的代码,内容如下。
在上述代码中,通过Mono.toFuture就可以方便地将Mono类型转换为CompletableFuture类型,那如果是Flux类型呢?Flux类型也很简单,只不过需要将Flux先转换为Mono类型,然后执行toFuture转换成CompletableFuture类型,代码如下。
由上述代码也可以验证之前介绍Flux时所说的,Flux本质上就是集合版的Mono,所以Flux可以很容易地转换成Mono<List<?>>类型的数据。
运行后可以执行如下请求验证结果。
关于通过GraphQL实现BFF的介绍就到这里,GraphQL采用了一种更加表意的方式来定义数据和接口,像描述方法一样直接定义接口,无论是对调用者还是提供者都更加易于理解,灵活的机制大大减少了后端的工作量,同时也给予前端开发很多的自由度。在不久的将来,在API的定义方式上,GraphQL或许会超越RESTful。
本文给大家讲解的内容是GraphQL与Java集成
- 下篇文章给大家讲解的是领域驱动设计;
- 觉得文章不错的朋友可以转发此文关注小编;
- 感谢大家的支持!