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文件,一个文件中只能定义一个querymutation,如果要定义多个会报错。
  • 数据类型
    • 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)[]里面是数组的类型。
  • 对象类型
    • 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获取一个字段的数据。也就是说我们需要为querymutation中定义的方法,以及对定义的对象中的字段绑定一个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

参考

扩展阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容