GraphQL java工程化实践

2023-10-29

因为自己写过基于react的前端应用,因此一看到GraphQL就被深深吸引,真是直击痛点啊!
服务端开发一直是基于java, Spring的,因此开始研究如何在现有工程框架下加入graphql的支持。
本文属于随笔性质,学到哪里,用到哪里,就写到哪里,观点为个人理解,仅供参考。

GraphQL基本概念

  • Schema: 指一个特定GraphQL类型系统的定义,也指具体的包含类型系统定义的文本文件。在类型定义中,schema {...} 这样的代码块定义的是入口类型,入口类型有三种,即查询,变更和订阅。值得说明的是,查询,变更和订阅也都是普通的类型而已,和其它对象类型语法上没有任何区别,只不过它们作为入口类型被定义在schema代码块中。
  • 查询(query):定义为入口的对象类型;和变更、订阅语法上并无不同,不过语义上对应的是读操作。
  • 变更(mutation): 定义和语法同上,但语义上对应增/删/改操作。
  • 订阅(subscription): 定义和语法同上,语义上对应的是一个订阅操作以及随后服务器对客户端的0~N次主动推送操作。
  • 内省(introspection): 可以通过特殊的graphql查询获取到整个类型系统的详细定义。这可能带来数据模型过度暴露的问题,以后会专门说明。
  • 类型(type): 没什么好说,就是对象类型,和标量类型相对应。
  • 标量(scalar): 非对象的简单数据类型,比如内置的String, Int, ID等。可以自己定义新的标量类型,只要为它编写序列化/反序列化方法即可,具体在graphql-java中对应的类是Coercing。
  • 字段(field): 对象类型的成员,可以是对象类型或者标量类型。和java类里的field不同的是,GraphQL的field都是可以有参数的,因此有参数的field也可以理解成java中有特定类型返回值的方法。
  • 接口(interface): 和java里的接口差不多,定义类型的公共字段,java实现中可以直接对应写一个interface。有点麻烦的是在每个interface的实现类中都必须重复书写公共字段。
  • 联合(union): 和接口类似,但是不要求任何公共字段。为了方便可以在java实现中使用无方法的interface实现。
  • 片段(fragment): 这是个查询时的概念,和schema定义无关,用于预定义类型上的若干个字段组合,后面的查询语句中可以反复引用,可避免重复书写这些字段组合。
  • 内联片段(inline fragment):片段还只是个简化查询的可有可无的东西,但内联片段则更重要,对于返回interface或union类型的字段,需要使用内联片段来根据结果的实际类型获取不同的字段。
  • 别名(alias): 在查询中可为特定字段的查询增加别名,用来在返回的结果中加以区分,比如一次查询了两个特定用户,因为类型相同,字段也相同,如果不用别名,则无法在结果中区分彼此。
  • 类型扩展(extend): 在schema中,可以使用extend给任意类型(包括interface/union)增加字段;这看似自找麻烦的机制实际上有很大用处,可以把高权限角色的特定字段使用extend写在另外的schema文件中,运行时可合并解析,不同角色的用户使用不同的schema。这样可以通过加法来控制类型系统的可见性,避免内省机制过度暴露类型系统。
  • DataLoader: 用于批量查询,见后文介绍。
  • Relay: Facebook的另一个框架,应该是基于GraphQL的,解决一些更高层的实际应用问题,比如通用的分页机制等。

graphql-java特定术语

  • DataFetcher: 数据获取器,即用以获取Field实际值的对象。
  • Data Class: 数据类,这是graphql-java-tools中的概念,对应schema中的同名对象类型。

    • 可以在数据类上按照约定格式编写DataFetcher方法用于获取简单字段值(比如无需另外查询数据库的字段)。
    • 我在工程实践中直接使用数据库实体类作为数据类。
  • GraphQLResolver: 这是graphql-java-tools中的接口,带有一个数据类的类型参数。

    • 对该数据类定义部分或所有字段值的获取方法,需要基于约定命名方法。
    • 注意Resolver中的DataFetcher方法的优先级高于DataClass中的方法。
    • 我在工程实践中直接使用Dao类作为对应实体类的GraphQLResolver。
  • ExecutionInput: graphql-java中用来包装一个完整查询输入的类,包括:

    • query - 查询字符串;
    • operationName: 操作名; 可选;可用于在查询中的多个操作中仅选择特定名称的予以执行。
    • variables: 变量; 可选;一个Map,用于替换查询字符串中形如'$value'的变量。
    • context - 上下文; 可选;任意Object类型,会被传递给DataFetcher;可用于传递当前登录用户等。
    • root - 根对象; 可选;任意Object类型,会被传递给DataFetcher,语义上是被查询的根对象。
  • ExecutionStrategy(执行策略): 定义查询的具体执行策略。

    • 比如是否异步执行,多个子查询是依次执行,还是用线程池并发执行等。
  • Instrumentation(拦截器): 比较像Servlet容器中的Filter,在查询执行前后各有一次执行机会。

    • 可用于对输入和结果进行额外处理;
    • 支持链式执行;
    • 需要指出的是DataLoader使用拦截器与核心系统耦合。
  • GraphqlFieldVisibility: 可以编程控制schema中各个字段的可见性。

    • 和extend对应,相当于用减法来控制类型系统的可见性。

技术选型

github上graphql-java名下的库不少,如果希望了解各自简介的,可以看下awesome-graphql-java这个项目。
我自己评估了以下几个:

  • graphql-java: 这个是核心库,完全符合Facebook的spec,可以直接解析schema文件,但是类型绑定需要使用RuntimeWiring来编程方式添加,用起来还是比较麻烦的。
  • graphql-java-annotations: 这是数据驱动的流派,使用注解直接在java类型上标注GraphQL类型以及DataFetcher等,不用写schema文件。评估了一阵,个人感觉非常麻烦,比如:对每个字段都会创建新的DataFetcher实例来进行解析,十分低效;要编写很多类来访问不同字段;过多的对象直接创建,难以托管到Spring容器;等等。因此我的结论是,此库并不适用于我的工程实践。
  • graphql-java-tools: 这是Schema驱动的流派,这个库使用Antlr自己重写了Schema解析器,使用GraphQLResolver实例和Data Class;基于方法名和参数的约定来定义DataFetching,使用起来很方便。这是我最终选定使用的库。不太爽的地方有两点:1) 当前版本基于graphql-java 7.0,迟滞于核心库 2) 使用Kotlin编写,我在MyEclipse里面无法正常设置断点进行跟踪调试……
  • graphql-java-servlet: GraphQL不像传统的REST,需要写一堆Controller,提供唯一的api接口即可,这个servlet就是帮你连这个都包办的,不过我没有用,自己基于SpringMVC写一个也很简单。

批量数据查询(解决N+1问题)

graphql-java提供了两种批量数据查询的方案:

  1. BatchedDataFetcher: 用起来挺简单的,普通的DataFetcher是给你一个ID让你返回一个对象,批量版是给你一个ID列表,让你返回对应的对象列表。不过这个不是Facebook推荐的方式,在新版本中会废弃掉。
  2. DataLoader: 这个是Facebook官方推荐的方式,nodejs中的实现是基于js的异步机制延迟查询,把最近一个周期产生的多个查询集中执行(没详细了解,看文档大概如此),java版实现方式则略有不同,下面详细介绍。

关于DataLoader

graphql-java的dataloader是基于java8中新增的CompletableFeature类(大概相当于javascript里面的Promise),实现异步延迟批量获取查询结果。

大概原理(个人理解):

  1. 在DataFetcher方法中,并不直接返回实体类T,而是调用DataLoader.load()方法,返回一个CompletionStage<T>,这时并不立即进行实际查询,而是把这些异步阶段对象缓存起来。
  2. 在查询告一段落后(即能够立即获取的Field值都已取得,只剩下异步查询未完成了),graphql-java会通过DataLoaderDispatcherInstrumentation.dispatch方法通知所有当前注册的DataLoader去执行当前积压的所有异步阶段对象,具体就是会使用DataLoader对应的BatchLoader一次性查询一批对象。
  3. 这时候又有一批Field的值已经实际取得,继续按查询的请求向下层展开,如果有新的异步阶段对象产生,就继续步骤2,直到所有异步阶段对象都获得最终值。

工程实践中对其应用方式的考虑:
在graphql-java的官方教程中建议针对每请求创建新的DataLoader实例,查询请求结束则DataLoader实例们的生命周期结束。
这个实现方式比较简单,不用考虑缓存的更新问题,也不用考虑多个不同请求的缓存对象是否可共用。
举个例子,张三和李四并发查询张三的信息,他们获取的"张三"用户实例的结构可能是不同的,这种情况这两个并发请求就不能共用缓存,而应该各自有独立的DataLoader实例。
不过在我的工程实践中,服务端内存中的数据实体类都是客观一致的,其结构可见性应在更上一层即DataFetcher甚至Schema级别中进行过滤。
因此我的想法是为每种实体类维护单例的DataLoader,和Dao对象一一对应。
这种情况下,就不能简单的使用DataLoader内部默认的简单内存缓存了,因为此缓存是不会自动定时清理的。
graphql-java是允许开发者提供自己的缓存实现的,下一步我会结合项目中使用的Spring缓存管理器来具体实现。

查询的缓存

graphql的查询本身是有一定语法结构的特殊文本,对该文本进行解析也是有性能开销的,因此graphql-java提供了缓存机制方便开发者把查询文本的解析后数据结构缓存起来。
以下代码引自官方教程,我准备结合我们项目里的EhCache来实作一下。

Cache<String, PreparsedDocumentEntry> cache = Caffeine.newBuilder().maximumSize(10_000).build();
GraphQL graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
        .preparsedDocumentProvider(cache::get)
        .build();

关于订阅的实现

工程实践中使用WebSocket实现订阅。
无论是graphql还是graphql-java都未指定订阅的具体实现机制,但WebSocket是现代浏览器普遍支持的,高性能低限制的服务器推送机制。
SpringMVC支持WebSocket,同时支持在低版本浏览器中使用Sock.js作为兼容备选方案。
另外,graphql-java体验性支持的Defer数据获取也可基于WebSocket实现。

未完待续

参考资料

基于spring和graphql-java-tools的宠物店例程
简单的TODO例程,使用relay的思路解决分页问题
基于WebSocket实现GraphQL订阅

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

GraphQL java工程化实践 的相关文章

随机推荐

  • MySQL数据库-列的完整性约束-调整列的完整约束

    一 主键 外键和唯一键 1 新增 语法如下 alter table table name add constraint constraint name unique key primary key foreign key column na
  • 自动安装第三方库python_Python自动安装第三方类库

    Python在使用过程中会用到大量的第三方库 逐一手工去下载 安装比较繁琐 可以配置第三方镜像源并使用pip进行自动安装 这里推荐选择豆瓣的镜像源 http pypi douban com simple Windows下的安装介绍 我的环境
  • DenseFuse:红外和可见图像的融合方法

    目录 论文下载地址 代码下载地址 论文作者 模型讲解 论文解读 网络结构 损失函数 训练过程 融合策略 相加策略 L1范数策略 结果分析 训练阶段 实验设置 评价指标 融合结果评估 RGB图像与红外图像的测试 传送门 论文下载地址 Dens
  • BLAS&LAPACK数值计算资源

    网络资源 Factoring block tridiagonal symmetric positive definite matrices Solving a system of linear equations with a block
  • datetime时间格式化中间为什么有个T

    如时间为 2020 01 12T15 17 21 国际标准化组织的国际标准ISO 8601是日期和时间的表示方法 全称为 数据存储和交换形式 信息交换 日期和时间的表示方法 原文如下 日期和时间的组合表示法编辑 合并表示时 要在时间前面加一
  • [ESP][驱动]GT911 ESP系列驱动

    GT911ForESP GT911在ESP系列上的驱动 基于IDF5 0 ESP32S3编写 本库使用面向对象思想编写 可创建多设备多实例 Github Gitee同步更新 Gitee仅作为下载仓库 提交Issue和Pull request
  • 求解 org.elasticsearch.index.mapper.MapperParsingException

    Exception in thread main org elasticsearch index mapper MapperParsingException Root type mapping not empty after parsing
  • Qt GUI编程 基础入门

    一 Qt简介 Qt是挪威Trolltech公司的旗舰产品 作为跨平台开发框架 是开源KDE桌面的基石 Google Earch Skype Opera Adobe Photoshop Element等著名软件都是基于Qt编写的 和java的
  • 代码自动生成,给程序员带来的是“春天”还是“寒冬”?

    CodeGeeX受邀参与由AI大模型领域的青年中坚力量组织的活动 在计算机编程领域 基于大模型能力的代码生成工具 探讨给程序员带来的各种机会与挑战 近期CodeGeeX 2 0大版本上线 用对话的方式直接操作代码 开发提效 推荐体验 活动背
  • elementui生产环境图标加载时偶而乱码

    elementui 打包后图标加载偶尔会乱码 preface 错误现象 猜想 解决方案 1 elementui 源码使用的是 node sass 2 我本地的 是 dart sass 3 查看了 打包后的css 文件 4 卸载 dart s
  • QT qint64转int 的使用记录(小白笔记)

    起因 使用Qt Creator 5 7 版本时警报没那么明显 新使用 Qt Creator 5 13 2 版本后报错和警告都异常明显 留着查看的时候感觉很不舒服 也是知识面不够充足 记录下自己的对 qint64 强转为 int的做法 以免忘
  • ocr表格识别(四)——文本检测DBnet原理及其实现

    文本检测DBnet原理及其实现 文本检测之DBnet 文本检测之DBnet模型构建 Backbone选择与构建 构建FPN特征金字塔 DBhead 可微二值化 文本检测之DBnet DB DifferenttiableBinarizatio
  • 通过分析Ajax请求抓取【今日头条】“街拍”美图

    20119 3 25更新 今日头条的 图集 模块已经改为 视频 了 可能是被人爬多了 分割线 有一些网页直接请求得到的HTML代码并没有在网页中看到的内容 因为一些信息是通过Ajax加载 并通过js渲染生成的 这时就需要通过分析网页的请求来
  • JAVA之Mysql应用|记一次通过mysql表中的三个字段对应一个前端组合状态字段查询场景的解决方案

    多个后端字段对应前端单一字段的思考与解决方案 一 需求背景 二 需求可行性 1 现状描述 2 可行性分析 三 细节分析 四 解决方案 1 前端逻辑 2 后端处理逻辑 3 后端sql 4 sql小细节 1 1 五 结论 一 需求背景 最近项目
  • Pyotorch自定义损失函数

    作者简介 大数据专业硕士在读 CSDN人工智能领域博客专家 阿里云专家博主 专注大数据与人工智能知识分享 公众号 GoAI的学习小屋 免费分享书籍 简历 导图等资料 更有交流群分享AI和大数据 加群方式公众号回复 加群 或 点击链接 专栏推
  • Spring Boot 教程:使用 Spring Boot 实现 SSE 服务端实时单向消息推送

    Spring Boot 教程 使用 Spring Boot 实现 SSE 服务端实时单向消息推送 在本教程中 我们将探讨如何使用 Spring Boot 框架来实现 SSE Server Sent Events 服务端 以实现实时的单向消息
  • APP自动化测试-6.断言处理assert与hamcrest

    APP自动化测试 6 断言处理assert与hamcrest 文章目录 APP自动化测试 6 断言处理assert与hamcrest 前言 一 assert断言 二 hamcrest断言 总结 前言 主要记录一下常规断言assert的常用场
  • 全志h3文件服务器,全志H3 NFS使用手册介绍

    1 功能介绍 1 1 文件传输 支持 NFS 服务器向本地进行文件传输 支持本地项 NFS 服务器进行文件传输 1 2 视频播放 盒子播放 NFS 服务器上的视频如同播放本地视频 只要是支持解码的视频都可以正常播放 测试过的视频格式 3pg
  • C++&QT-模仿string类

    目录 1 mystring h 2 mystring cpp 3 main cpp 4 运行结果 1 mystring h ifndef MYSTRING H define MYSTRING H include
  • GraphQL java工程化实践

    因为自己写过基于react的前端应用 因此一看到GraphQL就被深深吸引 真是直击痛点啊 服务端开发一直是基于java Spring的 因此开始研究如何在现有工程框架下加入graphql的支持 本文属于随笔性质 学到哪里 用到哪里 就写到