强大的GraphQL-实战篇
实战篇
引言
上篇讲解了GraphQL的基本情况,相信大家对它有了一定的了解,接下来本篇将用Java语言和大家一起开发一个GraphQL的应用,并逐渐深入。
首先,我们回顾一下几个基本概念,以及他们之间的关系。
-
GraphQL的规范 [1] :是一堆文档 -
GraphQL Java [2] :Java语言的GraphQL规范的实现,也是GraphQL的执行引擎。 -
DGS(Domain Graph Service) [3] :是由Netflix基于GraphQL Java开发的编程框架,我们在它里面开发自己的业务逻辑。
注:DGS和Spring GraphQL[4] 都是基于GraphQL Java的开发框架,但是因为在本文编写时Spring GraphQL还没有GA,现在属于不稳定阶段,而DGS是Netflix开发的,而且已经在Netflix生成环境中应用了好多年了,支撑了上千个Netflix内部服务,非常成熟,因此我们还是基于DGS进行开发。
创建一个GraphQL的应用
我们用一个查询商祥的场景做例子,创建一个GraphQL的应用,来提供商祥的查询服务(商品对象包括skuId、商品名称、商品介绍、类目和门店信息)。
创建SpringBoot项目
首先通过spring的initializr创建一个spring boot的项目,JDK用11(用8也可以)为了方便加上lombok。
添加DGS框架依赖
然后添加在pom.xml
添加依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.netflix.graphql.dgs</groupId>
<artifactId>graphql-dgs-platform-dependencies</artifactId>
<!-- The DGS BOM/platform dependency. This is the only place you set version of DGS -->
<version>4.9.16</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>com.netflix.graphql.dgs</groupId>
<artifactId>graphql-dgs-spring-boot-starter</artifactId>
</dependency>
把上面两段添加完毕后刷新maven,让所有的包下载完。
创建Schema文件
接下来要创建schema文件,我们需要先建立一个目录 src/main/resources/schema
在里面创建一个文件schema.graphqls
添加如下内容
type Query {
products(skuIdFilter: String) : [Product]
}
type Product {
skuId: String
name: String
category : Category
storeInfo: StoreInfo
mobiledesc: String
}
type StoreInfo {
storeId: Int
storeName: String
outerId: String
}
type Category {
categoryId: String
categoryName: String
parentCategoryId: String
}
创建对应实体类Product、Category和StoreInfo
@Data
@Builder
public class Category {
private String categoryId;
private String categoryName;
private String parentCategoryId;
}
@Data
@Builder
public class StoreInfo {
private Integer storeId;
private String storeName;
private String outerId;
}
@Data
@Builder
public class Product {
private String skuId;
private String name;
private Category category;
private StoreInfo storeInfo;
private String mobiledesc;
}
实现数据提取器(Data Fetcher)
接下来要实现数据提取器(Data Fetcher),数据提取器就是负责响应查询的,在里面开发真正的查询逻辑。
创建一个类,标记@DgsComponent
注解,代码如下:
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsQuery;
import com.netflix.graphql.dgs.InputArgument;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@DgsComponent
public class ProductDatafetcher {
private final List<Product> products = generateMockData();
private List<Product> generateMockData() {
List<Product> productList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
productList.add(Product.builder()
.skuId("sku" + i).name("name" + i).mobiledesc("desc" + i)
.category(Category.builder().categoryId("category_" + i).categoryName("category" + i).parentCategoryId("parent" + i).build())
.storeInfo(StoreInfo.builder().storeId(Integer.valueOf(i)).storeName("storeName" + i).outerId("outerId" + i).build())
.build());
}
return productList;
}
@DgsQuery
public List<Product> products(@InputArgument String skuIdFilter) {
if (skuIdFilter == null) {
return products;
}
return products.stream().filter(s -> s.getSkuId().contains(skuIdFilter)).collect(Collectors.toList());
}
}
完成后的工程结构如图:
启动程序,用GraphiQL测试
{
products {
skuId
name
}
}
也可以试一下其他的查询条件,对比查询结果。
使用 graphql-voyager[6] 查看我们的schema如图:
Mutation操作
除了查询,GraphQL还支持改变数据的操作,即Mutation
,实现也和查询类似(还有Subscription
也一样),举个Mutation简单的例子,就不详细展开了。
schema文件:
type Mutation {
addRating(title: String, stars: Int):Rating
}
type Rating {
avgStars: Float
}
mutation实现:
@DgsComponent
public class RatingMutation {
@DgsData(parentType = "Mutation", field = "addRating")
public Rating addRating(DataFetchingEnvironment dataFetchingEnvironment) {
int stars = dataFetchingEnvironment.getArgument("stars");
if(stars < 1) {
throw new IllegalArgumentException("Stars must be 1-5");
}
String title = dataFetchingEnvironment.getArgument("title");
System.out.println("Rated " + title + " with " + stars + " stars") ;
return new Rating(stars);
}
}
小结
至此,我们的第一个GraphQL应用开发完成,注意几点:
-
DGS框架以
schema
为核心,围绕它来进行开发,并提供了一系列注解来开发GraphQL请求的响应逻辑。 -
使用DGS框架必须标记
@DgsComponent
,如果需要响应GraphQL的查询请求,则在方法上增加@DgsQuery
注解,DGS框架会默认把它和查询的请求进行匹配,查询参数@InputArgument
也同样。 -
@DgsQuery
是一个便捷注解,也可以用@DgsData(parentType = "Query")
代替。 -
关于DGS框架的更多用法和示例,请见官网:netflix.github.io/dgs[7]
-
为了方便开发,我们也可以在idea里安装DGS的官方插件[8]
GraphQL在生产环境下的架构演进与水平扩展
如果我们的应用只是提供了自己的GraphQL服务,供客户端调用,那么用上面讲的知识入门,然后不断深入丰富接口里的内容,然后把它和我们已有的数据库或者rpc对接起来就好。
随着业务不断地发展,驱动着架构不断变化与之适应,我们以Netflix为研究案例,介绍GraphQL在生产环境中的架构如何演进。
在创建之初,一个客户端访问一个服务,服务连接一个数据库,简单而美好。
随着业务增长,服务越来越多,都集中在一起,形成了一个大单体结构。
为了提高可用性、开发效率和开发更自由,大家把大单体拆成了微服务,各个客户端连接微服务
但是聪明的工程师发现各个客户端连接各自的一堆微服务并不是很好,于是他们提供了api网关,统一收口。
但是随着rest api的使用,人们发现rest的效率不高,于是发明了一种DSL语言来从api中获取数据,即GraphQL。
时间流逝,公司的业务继续增长,随着服务的增多,GraphQL网关要考虑的兼容问题、内存问题等越来越复杂,它又成了一个新的大单体结构。
在虚拟的数字世界中,Graph已如星辰大海,没有人能够理解所有业务逻辑。
为了解决GraphQL网关的大单体,很多公司采用了n个GraphQL网关作为解决方案,一开始效率还挺高,但是随着时间推移,各种重复性代码以及维护成本以及客户端该调用哪个GraphQL网关成了大家的噩梦,于是又逐渐回归到一个Graph的原点。
这种架构是许多使用微服务并且使用GraphQL作为聚合层的公司遇到的共同问题,那么这种架构下如何保证为调用方提供正常GraphQL服务,以及如何进行水平扩展来支持业务的发展呢?
我们先讲一个小故事。
时间回到2018年,当时Netflix API团队的工程师们这样思考,能否把一个巨大的图按领域拆成若干小图,交给各自领域的领域专家来实现业务逻辑并提供微服务?
于是他们进行了实践,并且成功地运行起来了,也证明了API可以水平扩展的可能性。不过那时他们用的并不是GraphQL,而是一个叫“ Falcor[9] ” 的Netflix内部技术框架,它和GraphQL概念非常相似(在2012年GraphQL还没开源呢,Netflix造了一个相似的轮子)。
有意思的是,与此同时,公司里另外一个团队(Netflix Studio),使用GraphQL为他们的客户端提供API聚合服务,并且在短时间内,GraphQL网关就膨胀了数倍,难以维护,studio团队为此非常痛苦。
网飞决定合并两个API团队,构建一个能够进行水平扩展的GraphQL网关,优先解决Studio的问题。在2019年初,就在两个团队在一起头脑风暴新的网关要如何设计时,Apollo公司公布了他们的 GraphQL Federation Specification[10] 技术规范来解决此类问题。网飞对此规范进行了严格的测试,取得了良好的效果,于是把它作为关键元素,纳入到新网关的开发中。
Apollo
(www.apollographql.com[11])公司是业内知名的深耕GraphQL技术的公司,拥有丰富的开源、商用产品线和各类成熟的解决方案,它和GraphQL的关系可以类比elastic
公司和elastic search
的关系。Apollo Federation[12] 就是他们公司的开源产品。
Apollo Federation
的架构如下:
新网关按照这种思路进行设计,把每段子图都提交到一个schema的注册中心进行注册,子图实现逻辑和已有的各种领域服务进行交互,在API聚合层(网关)通过查询注册中心,又把各个子图重新组装成一个巨大的图为客户端提供GraphQL服务。
这种拆分API实现的方法,不仅为客户端保留了统一的API外观层,还在API聚合层(网关)里剥离了业务逻辑,让网关得以简化,就像一个反向代理,而且以后就可以进行水平扩展了。
这就是Apollo Federation
的设计思想,因此,不论从解决方案的设计思想,还是技术延续性,还是时机上来说,Federation
占尽天时地利人和,完美匹配。
在2019年7月,联合开发团队基于Apollo Federation
的参考实现进行了新一代网关的开发,网关采用kotlin实现,以便和网飞内部的java生态打通。
在Netflix解决这个问题的过程中,和Apollo的团队进行了深入合作,并最终取得了成功,也为越来越多的客户端提供了支撑。
Netflix内部使用GraphQL的服务也迎来了爆发式增长。
防坑指南:当schema的数量越来越多时,有可能会有很多不同的组织去创建相似的schema,而schema之间是可以继承的,如果用继承的话,以后就更不好控制了。所以Netflix的做法是有API组去定期审查,尽量避免出现相似的Graph。
开发一个 Apollo Federation 的应用
仿照Demo01新建一个Demo02工程,提供一个简单的影视查询服务,关键代码如下。
type Query {
shows(titleFilter: String): [Show]
}
type Show {
title: String
releaseYear: Int
}
Show.java
public class Show {
private final String title;
private final Integer releaseYear;
public Show(String title, Integer releaseYear) {
this.title = title;
this.releaseYear = releaseYear;
}
public String getTitle() {
return title;
}
public Integer getReleaseYear() {
return releaseYear;
}
}
ShowsDatafetcher.java
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsQuery;
import com.netflix.graphql.dgs.InputArgument;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@DgsComponent
public class ShowsDatafetcher {
private ArrayList<Show> shows = null;
@PostConstruct
public void init(){
shows = new ArrayList<Show>();
shows.add(new Show("Stranger Things", 2016));
shows.add(new Show("Ozark", 2017));
shows.add(new Show("The Crown", 2016));
shows.add(new Show("Dead to Me", 2019));
shows.add(new Show("Orange is the New Black", 2013));
}
@DgsQuery
public List<Show> shows(@InputArgument String titleFilter) {
if(titleFilter == null) {
return shows;
}
return shows.stream().filter(s -> s.getTitle().contains(titleFilter)).collect(Collectors.toList());
}
}
然后改端口为8081,启动,可以在浏览器启动,做一个简单的查询可以看到正常返回结果。
{
shows {
title
releaseYear
}
}
建立网关
采用Apollo Gateway默认的NodeJs版实现,先把两个GraphQL服务组合起来。核心源码如下:
server.js
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('@apollo/gateway');
const {serializeQueryPlan} = require('@apollo/query-planner');
const demo01Host = process.env.DEMO01_HOST != null ? process.env.DEMO01_host : "localhost:8080";
const demo02Host = process.env.DEMO02_HOST != null ? process.env.DEMO02_host : "localhost:8081";
// Initialize an ApolloGateway instance and pass it an array of
// your implementing service names and URLs
const gateway = new ApolloGateway({
serviceList: [
{ name: 'demo01', url: `http://${demo01Host}/graphql/` },
{ name: 'demo02', url: `http://${demo02Host}/graphql/` },
// Define additional services here
],
__exposeQueryPlanExperimental: true,
experimental_didResolveQueryPlan: function(options) {
if (options.requestContext.operationName !== 'IntrospectionQuery') {
console.log(serializeQueryPlan(options.queryPlan));
}
}
});
// Pass the ApolloGateway to the ApolloServer constructor
const server = new ApolloServer({
gateway,
engine: false,
plugins: [
],
tracing: true,
// Disable subscriptions (not currently supported with ApolloGateway)
subscriptions: false,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
package.json
{
"name": "apollo_gateway",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@apollo/gateway": "^0.24.4",
"apollo-server": "^2.21.1",
"graphql": "^15.5.0"
},
"devDependencies": {
"@apollo/query-planner": "^0.6.4"
}
}
以及合并了两个GraphQL服务的文档和schema。
至此,我们一个最简单的Apollo Federation应用搭建完毕。通过这个例子,可以想象,我们可以为某些领域的服务提供一系列子图,作为已有的服务的补充;还可以用它来做前端的BFF实现等等,为客户端的支撑提供了一种新思路。
DGS的核心开发人员Paul Bakker
在一次访谈中也提到这点,确实有不少服务是直接对已有的http或者rpc接口进行了适配,通过编程的方式把它们进行二次封装提供了GraphQL适配(作者注:如果是Java实现,这里需要仔细研究 GraphQL Java[14] 的实现机制和使用),但这并不能解决所有问题,毕竟GraphQL有它的技术特点,二者是不同的,在某些场景下确实需要针对它的特性进行单独的开发。
几个不用GraphQL的理由
对于技术人员来说,一定要对新技术保持敏锐,更重要的是保持一个客观的态度去审查问题,我们不能光看到GraphQL光鲜的一面,也要看到它的不足。好多人用过一段时间GraphQL后又放弃,那么他们遇到了些什么问题呢?经过大量的调查,列举了一些典型的原因,如下所示:
-
学习成本过高。 -
N+1问题。目前是用缓存的方式解决,但是缓存也是有代价的 -
信息安全泄露问题。不小心暴露出不能暴露的信息,权限控制需要额外注意。 -
执行背后的复杂度问题。如果一个大又复杂的查询,那么执行的时候,一般是把查询映射成一个请求id,然后后端根据这个id来进行缓存。 -
执行时,一般是把query内容放到post的body里,但是这样就不像get一样,浏览器会有缓存。
不用GraphQL的原因总结起来就是:Cool! Fast! But HARD! 确实如此,要获得GraphQL的特性,需要做的工作还有很多,好消息是现在开发GraphQL的应用越来越方便了。
总结篇
通过两篇文章,我们比较系统地介绍了GraphQL的历史和现状、技术特点、实现方式、部分核心原理以及在企业中的实践等等,从开发一个简单的实例到开发一个Federation的复杂实例,希望大家能够快速了解并上手。一个技术能够成熟并且能大规模,需要方方面面的支持,成熟可靠的底层框架和完善的生态缺一不可。
Spring GraphQL[15] 项目稳定后,希望能够为Java后端GraphQL开发带来更强有力的支持实现大一统,而前端部分则尽可以使用已有的成熟的Meta
、Apollo
等公司的开源类库来与GraphQL服务进行交互,或者用JS来开发服务器端逻辑。
GraphQL生态越来越丰富和稳定使得我们采用GraphQL的成本变得越来越低,可靠性也越来越高。如果用Java实现,可以考虑使用DGS
或者Spring GraphQl
,如果用Js实现,可以考虑Meta
等公司的开源框架,如果想做Apollo的实现,它网关和server部分也是开源的(JS实现),还对JVM也提供了支持,而且作为网关的核心就是要有更快的执行效率,为此Apollo还为网关提供了Rust的实现[16] 。
对于中小型规模的服务,各种主流的编程语言都可以进行GraphQL服务的开发;对于大型和超大型规模的GraphQL构建,还有像Apollo Federation
这样比较成熟的技术标准和工程实践能对开发工作进行支撑和指导,这项标准在许多公司进行了大规模的应用并且成功落地,也使得我们避免了许多方向性的探索风险,降低了巨大的试错成本。
GraphQL作为一种“比较新颖”的技术方案来说,它对现有的API相关技术可能是一种补充、在某些场景下也许是一种更好的实现方案,但是我们千万不要天真的认为它是一种解决所有问题的终极解决方案,新技术会带来新问题,未来的变化很多,也许会有更多更适合的技术出现,总之:没有银弹
。
本文的编写参考了众多国内外专家提供的公开的视频或文献资料等,也得到了晓东哥、永康哥等架构师的指导,在此一并表达谢意!
参考文献
重要网站:
-
GraphQL官网 (https://graphql.org) -
GraphQL中文网 (https://graphql.cn/) -
GraphQL Java (https://www.graphql-java.com) -
The DGS framework (https://netflix.github.io/dgs/) -
Spring for GraphQL (https://spring.io/projects/spring-graphql) -
Apollo GraphQl (https://www.apollographql.com) -
graphiql (https://github.com/graphql/graphiql) -
graphql-playground (https://github.com/graphql/graphql-playground) -
graphql-voyager (https://apis.guru/graphql-voyager/)
教程:
-
GraphQL Explained in 100 Seconds (https://www.youtube.com/watch?v=eIQh02xuVw4&ab_channel=Fireship) -
Learn GraphQL In 40 Minutes (https://www.youtube.com/watch?v=ZQL7tL2S0oQ&ab_channel=WebDevSimplified) -
GraphQL, gRPC or REST? Resolving the API Developer's Dilemma - Rob Crowley (https://www.youtube.com/watch?v=l_P6m3JTyp0&ab_channel=NDCConferences) -
Building Modern APIs with GraphQL (https://www.youtube.com/watch?v=bRnu7xvU1_Y&ab_channel=AmazonWebServices) -
REST vs. GraphQL: A Critical Review (https://medium.com/good-api/rest-vs-graphql-a-critical-review-5f77392658e7) -
Why GraphQL is the future of APIs (https://medium.com/free-code-camp/why-graphql-is-the-future-of-apis-6a900fb0bc81) -
The Fullstack Tutorial for GraphQL (https://www.howtographql.com/) -
GraphQL in Rust (https://romankudryashov.com/blog/2020/12/graphql-rust/) -
GraphQL是什么,入门了解看这一篇就够了!(https://blog.csdn.net/feiyingwang/article/details/113945807) -
全面解析 GraphQL,携程微服务背景下的前后端数据交互方案 (https://www.infoq.cn/article/xz0ws6_a5jmrj6ztpoz8) -
GraphQL 在微服务架构中的实践 (https://www.infoq.cn/article/WTivQt9u1xFgB*2RybCO?utm_source=related_read_bottom&utm_medium=article) -
GraphQL两年实战避坑经验 (https://baijiahao.baidu.com/s?id=1672496531174214708&wfr=spider&for=pc) -
5个用/不用GraphQL的理由 (https://www.jianshu.com/p/12dff5905cf6) -
是什么让我放弃了restful api?了解清楚后我全面拥抱GraphQL (https://www.toutiao.com/article/6833818331884028419/) -
通过前端工程化将 Apollo 引入现有 React 技术栈 (http://www.45fan.com/article.php?aid=20022765376794155275113152) -
GraphQL 初探—面向未来 API 及其生态圈 (https://juejin.cn/post/6844903508315996167) -
GraphQL + SpringBoot + React应用开发 (https://www.bilibili.com/video/BV1LQ4y1a72k?spm_id_from=333.999.0.0)
GraphQL Java相关:
-
graphql-java (https://github.com/graphql-java/graphql-java) -
graphql-java-examples (https://github.com/graphql-java/graphql-java-examples) -
awesome-graphql-java (https://github.com/graphql-java/awesome-graphql-java)
DGS相关:
-
dgs-framework (https://github.com/netflix/dgs-framework/) -
dgs-examples-java (https://github.com/Netflix/dgs-examples-java) -
The Netflix Domain Graph Service framework - OSS GraphQL for Spring Boot by Paul Bakker (https://www.youtube.com/watch?v=hgA3RrWoZCA) -
How Netflix Scales Its API with GraphQL Federation (https://www.youtube.com/watch?v=QrEOvHdH2Cg&t=1407s&ab_channel=InfoQ) -
How Netflix Scales its API with GraphQL Federation-part1 (https://netflixtechblog.com/how-netflix-scales-its-api-with-graphql-federation-part-1-ae3557c187e2) -
How Netflix Scales its API with GraphQL Federation-part2 (https://netflixtechblog.com/how-netflix-scales-its-api-with-graphql-federation-part-2-bbe71aaec44a) -
Open Sourcing the Netflix Domain Graph Service Framework: GraphQL for Spring Boot (https://netflixtechblog.com/open-sourcing-the-netflix-domain-graph-service-framework-graphql-for-spring-boot-92b9dcecda18) -
Our learnings from adopting GraphQL (https://netflixtechblog.com/our-learnings-from-adopting-graphql-f099de39ae5f) -
GraphQL Search Indexing (https://netflixtechblog.com/graphql-search-indexing-334c92e0d8d5) -
Paul Bakker — The DGS framework by Netflix — GraphQL for Spring Boot made easy (https://www.youtube.com/watch?v=D3r4-Tmv86k&ab_channel=JUG.ru) -
Let's code a Netflix Clone with GraphQL Pagination! Reviewed by a NETFLIX ENGINEER! (https://www.youtube.com/watch?v=g8COh40v2jU) -
干货|Netflix 联邦 GraphQL 平台的过程及教训 (https://baijiahao.baidu.com/s?id=1689581009723561462&wfr=spider&for=pc)
Spring相关:
-
spring-graphql (https://github.com/spring-projects/spring-graphql) -
Spring for GraphQL Documentation (https://docs.spring.io/spring-graphql/docs/1.0.0-SNAPSHOT/reference/html/) -
Spring Tips: Spring GraphQL (https://www.youtube.com/watch?v=kVSYVhmvNCI) -
Spring GraphQL(https://www.youtube.com/watch?v=Kq3UhUQdIO8) -
Spring Boot GraphQL Tutorial (https://www.youtube.com/watch?v=nju6jFW8CVw&list=PLiwhu8iLxKwL1TU0RMM6z7TtkyW-3-5Wi&ab_channel=PhilipStarritt)
Apollo GraphQl相关:
-
Apollo GraphQl (https://github.com/apollographql) -
federation (https://github.com/apollographql/federation) -
federation-jvm (https://github.com/apollographql/federation-jvm) -
GraphQL Tutorials (https://odyssey.apollographql.com) -
Full-stack React + GraphQL Tutorial (https://www.apollographql.com/blog/graphql/examples/full-stack-react-graphql-tutorial/?source=search_post) -
GraphQL Concepts Visualized (https://www.apollographql.com/blog/graphql/basics/the-concepts-of-graphql)
GraphQL的规范: https://spec.graphql.org/
[2]GraphQL Java: https://www.graphql-java.com/
[3]DGS(Domain Graph Service): https://netflix.github.io/dgs/
[4]Spring GraphQL: https://spring.io/projects/spring-graphql
[5]http://localhost:8080/graphiql: http://localhost:8080/graphiql
[6]graphql-voyager: https://apis.guru/graphql-voyager/
[7]netflix.github.io/dgs: https://netflix.github.io/dgs
[8]官方插件: https://plugins.jetbrains.com/plugin/17852-dgs
[9]Falcor: https://github.com/Netflix/falcor
[10]GraphQL Federation Specification: https://www.apollographql.com/docs/apollo-server/federation/federation-spec
[11]www.apollographql.com: https://www.apollographql.com
[12]Apollo Federation: https://github.com/apollographql/federation
[13]http://localhost:4000: http://localhost:4000/
[14]GraphQL Java: https://www.graphql-java.com/
[15]Spring GraphQL: https://spring.io/projects/spring-graphql
[16]实现: https://github.com/apollographql/router