Java中的GraphQL服务器:第二部分:了解解析器

简介: 在第一部分中,我们开发了一个非常简单的GraphQL服务器。该解决方案有一个严重的缺陷:所有字段都急切地加载到后端,即使前端未要求也是如此。通过不给客户任何选择,我们通过RESTful服务来接受这种情况。RESTful API始终返回所有内容,这意味着始终加载所有内容。另一方面,如果将RESTful API分为多个较小的资源,则可能会面临N + 1问题和多次网络往返的风险。

第二部分:了解解析器


在第一部分中,我们开发了一个非常简单的GraphQL服务器。该解决方案有一个严重的缺陷:所有字段都急切地加载到后端,即使前端未要求也是如此。通过不给客户任何选择,我们通过RESTful服务来接受这种情况。RESTful API始终返回所有内容,这意味着始终加载所有内容。另一方面,如果将RESTful API分为多个较小的资源,则可能会面临N + 1问题和多次网络往返的风险。GraphQL是专门为解决以下问题而设计的:

  • 仅获取必需的数据,以避免额外的网络流量以及后端不必要的工作
  • 允许在单个请求中获取客户端所需的尽可能多的数据,以减少总体延迟

RESTful API可以任意决定要返回多少数据,因此几乎无法解决上述问题。它要么获取过多,要么获取不足。好的,这是理论,但是我们对GraphQL服务器的实现不能以这种方式工作。无论是否请求,它仍将获取所有数据。伤心。


不断发展您的API


回顾一下我们的API返回一个实例PlayerDTO:

@Value
class Player {
    UUID id;
    String name;
    int points;
    ImmutableList<Item> inventory;
    Billing billing;
}

与此GraphQL模式匹配的:

type Player {
    id: String!
    name: String!
    points: Int!
    inventory: [Item!]!
    billing: Billing!
}

通过仔细地分析我们的应用程序,我意识到很少有客户billing在他们的查询中提出要求,但是我们必须始终提出要求billingRepository才能创建Player实例。许多急切的,不需要的工作:

private final BillingRepository billingRepository;
private final InventoryClient inventoryClient;
private final PlayerMetadata playerMetadata;
private final PointsCalculator pointsCalculator;
//...
@NotNull
private Player somePlayer() {
    UUID playerId = UUID.randomUUID();
    return new Player(
            playerId,
            playerMetadata.lookupName(playerId),
            pointsCalculator.pointsOf(playerId),
            inventoryClient.loadInventory(playerId),
            billingRepository.forUser(playerId)
    );
}


像这样的字段billing只能在请求时加载!为了了解如何使我们的对象的某些部分_图_(Graph-QL是也)懒加载,让我们添加一个名为新特性trustworthinessPlayer上:

type Player {
    id: String!
    name: String!
    points: Int!
    inventory: [Item!]!
    billing: Billing!
    trustworthiness: Float!
}


此更改是向后兼容的。事实上,GraphQL实际上没有API版本控制的概念。那么迁移路径是什么?有以下几种情况:

  • 您错误地将新架构提供给客户端,而没有实现服务器。在这种情况下,客户端会快速失败,因为它请求trustworthiness服务器尚未交付的字段。好。另一方面,使用RESTful API,客户端认为服务器将返回一些数据。这可能会导致意外错误或服务器故意返回的假设null(丢失字段)
  • 您添加了trustworthiness字段,但没有分发新的架构。还行吧。客户不知道,trustworthiness所以他们不要求它。
  • 服务器准备就绪后,便将新架构分发给客户端。客户端可能会或可能不会使用新数据。没关系。

但是,如果您犯了一个错误并向所有客户端宣布服务器的新版本支持某些架构而实际上却不支持该架构,该怎么办?换句话说,服务器假装为support trustworthiness,但是在被询问时它不知道如何计算。这有可能吗?

Caused by: [...]FieldResolverError: No method or field found as defined in schema [...] with any of the following signatures [...], in priority order:
  com.nurkiewicz.graphql.Player.trustworthiness()
  com.nurkiewicz.graphql.Player.getTrustworthiness()
  com.nurkiewicz.graphql.Player.trustworthiness

这是在服务器启动时发生的!如果您在不实施底层服务器的情况下更改了架构,它甚至将无法启动!这真是个好消息。如果您宣布支持某些架构,则不可能发布不支持该架构的应用程序。改进API时,这是一个安全网。仅当服务器支持架构时,才将架构交付给客户端。并且,当服务器宣布某些架构时,您可以100%确保其正常工作并正确格式化。响应中没有其他缺少的字段,因为您正在询问服务器的旧版本。不再有假装支持某些API版本的损坏服务器,而实际上,您忘记了将字段添加到响应对象。


用懒加载的Resolver代替贪婪加载值


好了,那么我该如何添加trustworthiness以遵守新的架构?在_不那么聪明的_技巧是正确的,在该阻止我们的应用程序启动异常。它说它正在尝试寻找方法,获取器或字段trustworthiness。如果我们盲目地将其添加到Player类中,API将会起作用。那是什么问题 请记住,在更改架构时,旧客户端不知道trustworthiness。即使知道这一点的新客户,也可能永远不会或很少要求它。换句话说,trustworthiness只需要为一小部分请求计算出值。不幸的是,因为trustworthiness是现场上Player课,我们必须总是热切计算。否则,无法实例化并返回响应对象。有趣的是,使用RESTful API,这通常不是问题。只需加载并返回所有内容,让客户决定忽略什么。但是我们可以做得更好。

首先,trustworthinessPlayerDTO中删除字段。我们必须更深入,我是说懒惰。而是,创建以下组件:

import com.coxautodev.graphql.tools.GraphQLResolver;
import org.springframework.stereotype.Component;
@Component
class PlayerResolver implements GraphQLResolver<Player> {
}

保持空白,GraphQL引擎将指导我们。当尝试再次运行该应用程序时,该异常很熟悉,但并不相同:

FieldResolverError: No method or field found as defined in schema [...] with any of the following signatures [...], in priority order:
  com.nurkiewicz.graphql.PlayerResolver.trustworthiness(com.nurkiewicz.graphql.Player)
  com.nurkiewicz.graphql.PlayerResolver.getTrustworthiness(com.nurkiewicz.graphql.Player)
  com.nurkiewicz.graphql.PlayerResolver.trustworthiness
  com.nurkiewicz.graphql.Player.trustworthiness()
  com.nurkiewicz.graphql.Player.getTrustworthiness()
  com.nurkiewicz.graphql.Player.trustworthiness

trustworthiness不仅在Player类上,而且还在PlayerResolver我们刚刚创建的上寻找。您能发现这些签名之间的区别吗?

  • PlayerResolver.getTrustworthiness(Player)
  • Player.getTrustworthiness()

前一种方法Player作为参数,而后一种方法Player本身就是实例方法(getter)。目的是PlayerResolver什么?默认情况下,GraphQL模式中的每种类型都使用默认解析器。该解析器基本上采用例如的实例, Player并检查吸气剂,方法和字段。但是,您可以使用更复杂的默认解析器来装饰该默认解析器。一种可以懒惰地计算给定名称的字段。尤其是在Player课堂上没有这样的领域时。最重要的是,仅当客户端实际请求该字段时才调用该解析程序。否则,我们将退回到默认解析器,该解析器期望所有字段都属于Player对象本身。那么,您如何实现自定义解析器trustworthiness呢?该异常将指导您:

@Component
class PlayerResolver implements GraphQLResolver<Player> {
    float trustworthiness(Player player) {
        //slow and painful business logic here...
        return 42;
    }
}

当然,在现实世界中,实现会做一些聪明的事情。采取Player,应用一些业务逻辑,等等。真正重要的是,如果客户不想知道trustworthiness,永远不会调用此方法。懒!通过添加一些日志或指标来自己查看。是的,指标!这种方法还使您可以深入了解自己的API。客户非常明确,只询问必填字段。因此,您可以为每个解析器获取指标,并快速找出哪些字段已使用,哪些字段无效以及可以弃用或删除。另外,您可以轻松地发现哪个特定字段的加载成本很高。使用RESTful API的全有或全无的方法,这种细粒度的控制是不可能的。为了使用RESTful API停用字段,您必须创建资源的新版本并鼓励所有客户端迁移。


懒惰的一切


如果您想变得更加懒惰并消耗尽可能少的资源,则Player可以将的每个单个字段委派给解析器。模式保持不变,但Player类变为空心:

@Value
class Player {
    UUID id;
}

那么,如何GraphQL知道如何计算namepointsinventorybillingtrustworthiness?好吧,解析器上有一个方法可以解决以下每个问题:

@Component
class PlayerResolver implements GraphQLResolver<Player> {
    String name(Player player) {
        //...
    }
    int points(Player player) {
        //...
    }
    ImmutableList<Item> inventory(Player player) {
        //...
    }
    Billing billing(Player player) {
        //...
    }
    float trustworthiness(Player player) {
        //...
    }
}

实现并不重要。重要的是惰性:只有在请求某些字段时才调用这些方法。这些方法中的每一个都可以分别进行监视,优化和测试。从性能角度来看,这很棒。


性能问题


您是否注意到inventorybilling字段互不相关?即,获取inventory可能需要调用某些下游服务,而billing需要SQL查询。不幸的是,GraphQL引擎将响应组合成一个顺序。我们将在下一期中解决此问题,敬请期待!

相关文章
|
1月前
|
机器学习/深度学习 JSON Java
Java调用Python的5种实用方案:从简单到进阶的全场景解析
在机器学习与大数据融合背景下,Java与Python协同开发成为企业常见需求。本文通过真实案例解析5种主流调用方案,涵盖脚本调用到微服务架构,助力开发者根据业务场景选择最优方案,提升开发效率与系统性能。
405 0
|
1月前
|
Java
Java的CAS机制深度解析
CAS(Compare-And-Swap)是并发编程中的原子操作,用于实现多线程环境下的无锁数据同步。它通过比较内存值与预期值,决定是否更新值,从而避免锁的使用。CAS广泛应用于Java的原子类和并发包中,如AtomicInteger和ConcurrentHashMap,提升了并发性能。尽管CAS具有高性能、无死锁等优点,但也存在ABA问题、循环开销大及仅支持单变量原子操作等缺点。合理使用CAS,结合实际场景选择同步机制,能有效提升程序性能。
|
1月前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
366 100
|
2月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
2月前
|
缓存 安全 Java
Java并发性能优化|读写锁与互斥锁解析
本文深入解析Java中两种核心锁机制——互斥锁与读写锁,通过概念对比、代码示例及性能测试,揭示其适用场景。互斥锁适用于写多或强一致性场景,读写锁则在读多写少时显著提升并发性能。结合锁降级、公平模式等高级特性,助你编写高效稳定的并发程序。
178 0
|
12天前
|
存储 安全 Java
《数据之美》:Java集合框架全景解析
Java集合框架是数据管理的核心工具,涵盖List、Set、Map等体系,提供丰富接口与实现类,支持高效的数据操作与算法处理。
|
1月前
|
Java 开发者
Java 函数式编程全解析:静态方法引用、实例方法引用、特定类型方法引用与构造器引用实战教程
本文介绍Java 8函数式编程中的四种方法引用:静态、实例、特定类型及构造器引用,通过简洁示例演示其用法,帮助开发者提升代码可读性与简洁性。
|
11天前
|
存储 人工智能 算法
从零掌握贪心算法Java版:LeetCode 10题实战解析(上)
在算法世界里,有一种思想如同生活中的"见好就收"——每次做出当前看来最优的选择,寄希望于通过局部最优达成全局最优。这种思想就是贪心算法,它以其简洁高效的特点,成为解决最优问题的利器。今天我们就来系统学习贪心算法的核心思想,并通过10道LeetCode经典题目实战演练,带你掌握这种"步步为营"的解题思维。
|
1月前
|
安全 Java API
Java SE 与 Java EE 区别解析及应用场景对比
在Java编程世界中,Java SE(Java Standard Edition)和Java EE(Java Enterprise Edition)是两个重要的平台版本,它们各自有着独特的定位和应用场景。理解它们之间的差异,对于开发者选择合适的技术栈进行项目开发至关重要。
215 1
|
1月前
|
存储 域名解析 弹性计算
阿里云上云流程参考:云服务器+域名+备案+域名解析绑定,全流程图文详解
对于初次通过阿里云完成上云的企业和个人用户来说,很多用户不仅是需要选购云服务器,同时还需要注册域名以及完成备案和域名的解析相关流程,从而实现网站的上线。本文将以上云操作流程为核心,结合阿里云的活动政策与用户系统梳理云服务器选购、域名注册、备案申请及域名绑定四大关键环节,以供用户完成线上业务部署做出参考。

热门文章

最新文章

推荐镜像

更多
  • DNS