Java 中的 GraphQL 框架,了解一下?

几乎所有流行的编程语言都有 GraphQL 实现。

微信搜索关注《Java学研大本营》,加入读者群,分享更多精彩

今天,几乎所有流行的编程语言都有 GraphQL 实现。这些解决方案可以安全使用,而且大多是经过时间检验、可用于生产的。

应该首先将注意力集中在 Graphql-java 库上。这是唯一可用的真实 GraphQL 引擎。

该引擎已经能够实现数据获取、处理上下文、处理错误、监控、查询限制、字段可见性,甚至是数据加载器。因此,可以选择按原样使用它,或者对框架进行大胆的更改,看看哪个最适合。Graphql-java 是开源的,由普通人创建,最近一次提交只是几天前。该引擎正在积极开发中。

但是,尽管有这么多优点,还是应该仔细考虑是否值得直接使用它。

除了这个库之外,还有其他三个需要考虑的框架。其他一切主要由非常小的库组成。

模式优先与代码优先

但首先,看一下在后端设计 graphql API 的两种关键方法。有两个对立的阵营——模式优先和代码优先解决方案。

在经典的模式优先方法中,首先描述 graphql 模式,然后在代码中使用它来实现模型和数据获取器。这种方法的优点是不同的人甚至部门都可以设计和开发方案——例如,分析师设计方案,开发人员实施。也可以很方便的写一个方案马上给客户,同时开发一个后端。缺点是需要同时实现模式和代码——开发 API 时可能需要更多时间 + 现在有 2 个源不能相互冲突并完全同步——一个额外的链接可能会中断。

使用代码优先的方法,我们只编写代码并基于注释,框架本身生成模式。在这里,我们只有 1 个事实来源,但是没有代码就无法构建 graphql 图。

域图服务

我们要关注的第一个框架是 DGS (Domain Graph Service)。

Netflix 最初是在 2019 年发明的,并于 2020 年发布在开源上。这是一个成熟的框架——它有助于处理 GraphQL 代码、编写单元测试、提供自己的错误处理、基于模式生成数据获取器的代码生成等等。这是一个模式优先的解决方案。这一切都已准备就绪,Netflix 正在充分利用它。

尽管如此,我们还是选择了不同的解决方案。

首先,DGS 是模式优先的,我们希望使用代码优先的方法,更容易提出,开发速度更快,不需要开发没有代码的模式。

其次,DGS 使用spring boot。没关系!但我们不在公司内部使用它——我们有自己的框架,它使用纯 spring-core。当然,这并不意味着不可能提高它——我们设法开始了,之前与保罗讨论过是否在没有引导的情况下提高规范或作者不推荐(规范) . 但要做到这一点,有必要了解框架本身的代码,手动查找和声明十几个未记录且并不总是可以理解的 bin,这些 bin 在新版本的 DGS 中可能会被破坏。一般来说,不是免费维护的。

第三,即使它是一个成熟的框架,你仍然必须添加它来处理单元测试、错误处理、监控等。这仅仅是因为你的项目正在增长,而你将没有足够的现有解决方案。

尽管如此,它还是很酷的。

DGS:

  • 模式优先

  • 来自 Netflix 的开源

  • 在Spring-boot启动

  • 成熟的框架

Java SPQR

我们将分析的下一个liba是 Java SPQR 。

经过多年验证的开源库。此外,这也是唯一的代码优先解决方案,而且不是一个成熟的框架。liba 所做的所有事情都是实现代码优先的方法,并帮助稍微使用服务 GraphQL 代码。

但是,尽管做出了选择,但目前很难建议使用它,因为它已被放弃。最后一次提交是一年多前,问题没有答案,也没有支持。

为什么这可能很重要,例如,graphql 支持继承,并且在 2020 年,graphql-spec,然后是 graphql-java,获得了使用多接口继承的能力。现在是 2022 年,但在 SPQR 中不能使用这个新功能。

然而,最近该项目有了恢复工作的计划,这不禁让人欣喜若狂。

Spring GraphQL

要谈论的最后一个框架是 Spring GraphQL 。

2021 年 7 月发布。同样是模式优先的方法,与 spring 的集成,稍微重复了 DGS。也有自己的错误处理程序,支持编写单元测试,更方便地使用数据获取器。

Spring GraphQL:

  • 模式优先

  • Spring集成

  • 成熟的框架

  • 最近发布

效果

现在创建一个简单的 graphql 服务器。作为标准堆栈,我们将使用 Java 和 Spring,作为 GraphQL - SPQR,使用 Graphql-java 引擎。

GraphQL bean

首先,让我们创建将执行所有查询的GraphQL bin。

@Configuration
public class GraphQLConfig {

  private final CandidateResolver candidateResolver;
  private final ResumeResolver resumeResolver;
  public GraphQLConfig(CandidateResolver candidateResolver,
                       ResumeResolver resumeResolver) {
    this.candidateResolver = candidateResolver;
    this.resumeResolver = resumeResolver;
  }
  @Bean
  public GraphQLSchema getGraphQLSchema() {
    return new GraphQLSchemaGenerator()
      .withBasePackages("com.example.graphql.demo.models")
      .withOperationsFromSingletons(candidateResolver, resumeResolver)
      .generate();
  }
  @Bean
  public GraphQL getGraphQL(GraphQLSchema graphQLSchema) {
    return GraphQL.newGraphQL(graphQLSchema)
      .queryExecutionStrategy(new AsyncExecutionStrategy())
      .instrumentation(new CustomTracingInstrumentation())
      .build();
  }
}

要执行它,需要知道模式。但由于 SPQR 是一种代码优先的方法,我们使用一个模式生成器,将从根包中的模型字段构建它。

接下来,我们将定义一个 graphql 查询执行策略。默认情况下,图中的每个节点都是异步执行的,以防万一可以更改。

之后,让我们重新定义工具(我们将分别讨论)并运行 bin.GraphQLSchemaExecutionStrategyAsyncExecutionStrategy。

端点

需要从某个地方获取请求,所以创建一个接受查询的常规 POST 方法。对于所有 graphql 请求,都是相同的,不像 REST,我们为每个请求创建了一个单独的方法。

然后将执行请求传递给 graphql bin。

@RestController
public class DemoController {
  private final GraphQL graphQL;
  @Autowired
  DemoController(GraphQL graphQL) {
    this.graphQL = graphQL;
  }
  @PostMapping(path = "graphql",
    consumes = MediaType.APPLICATION_JSON_VALUE,
    produces = MediaType.APPLICATION_JSON_VALUE)
  public ExecutionResult graphql(@RequestBody EntryPoint entryPoint) {
    ExecutionInput executionInput = ExecutionInput.newExecutionInput()
      .query(entryPoint.query)
      .build();
    return graphQL.execute(executionInput);
  }
  public static class EntryPoint {
    public String query;
  }
}

入口点

已经描述了一个模式,知道如何接受查询。但是在哪里描述这个图的入口点?数据提取器(或解析器)在 graphql 中负责这一点。将在其中描述图节点的 bean。

@GraphQLQuery(name = "candidates") public CompletableFuture<List<Candidate>> getCandidates() { return CompletableFuture.supplyAsync(candidateService::getCandidates); }

在这种情况下,我们创建了一个入口点,它返回一些 .candidatesCandidate 的模型

public class Candidate {
  private Integer id;
  private String firstName;
  private String lastName;
  private String email;
  private String phone;
  // getters and setters are omitted
}

此外,SPQR 将在解析器中的模型上构建方案。

当然,尽可能多的这样的节点是可能的,也是必要的,这样它们才能相互交织,形成一个图。因此,让我们创建另一个节点并使用 . resumes@GraphQLContext。

@GraphQLQuery(name = "resumes")
  public CompletableFuture<List<Resume>> getResumes(@GraphQLContext Candidate candidate) {
    return CompletableFuture.supplyAsync(() -> resumeService.getResumes(candidate));
  }
public class Resume {
  private Integer id;
  private String lastExperience;
  private Salary salary;
    // getters and setters are omitted
}
public class Salary {
  private String currency;
  private Integer amount;
    // getters and setters are omitted
}

它的工作原理是这样的——如果你从 请求某些东西,那么这个解析器才会对.candidatesresumes起作用。

Instrumentation

除此之外,我们当然希望监控查询执行的状态:每个解析器执行了多长时间,完整请求执行了多长时间,可以捕获哪些错误。为此,在注册 graphql-bin 时,可以指定 Instrumentation — 默认和自定义。

从技术上讲,这是一个实现的类(在例子中,继承自一个常规存根,以便不实现所有方法)。

它说明了在请求的某种状态下调用的方法:请求刚开始执行时,解析器被调用时,执行结束时等。

CustomTracingInstrumentation

public class CustomTracingInstrumentation extends SimpleInstrumentation {
    Logger logger = LoggerFactory.getLogger(CustomTracingInstrumentation.class);
    static class TracingState implements InstrumentationState {
        long startTime;
    }
    // Cоздаём контекст трэйсинга для конкретного запроса
    @Override
    public InstrumentationState createState() {
        return new TracingState();
    }
    // Выполняется перед каждым запросом. Инициализируем контекст трейсинга для замеров времени выполнения
    @Override
    public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
        TracingState tracingState = parameters.getInstrumentationState();
        tracingState.startTime = System.currentTimeMillis();
        return super.beginExecution(parameters);
    }
    // Выполняется при завершении запроса. С помощью totalTime мерим время выполнения всего запроса
    @Override
    public CompletableFuture<ExecutionResult> instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) {
        TracingState tracingState = parameters.getInstrumentationState();
        long totalTime = System.currentTimeMillis() - tracingState.startTime;
        logger.info("Total execution time: {} ms", totalTime);
        return super.instrumentExecutionResult(executionResult, parameters);
    }

    // Выполняется при каждом вызове DataFetcher/Resolver. С помощью него будем мерить время выполнения каждого резолвера
    @Override
    public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters) {
        // Так как любое поле в графе потенциально может быть резолвером, оставим только те, которые хотя бы что-то делают
        if (parameters.isTrivialDataFetcher()) {
        return dataFetcher;
        }
        return environment {
        long startTime = System.currentTimeMillis();
        Object result = dataFetcher.get(environment);
        // Так как все ноды в нашем случае выполняются асинхронно, замерим время только для них
        if(result instanceof CompletableFuture) {
            ((CompletableFuture<?>) result).whenComplete((r, ex); {
            long totalTime = System.currentTimeMillis() - startTime;
            logger.info("Resolver {} took {} ms", findResolverTag(parameters), totalTime);
            });
        }
        return result;
        };
    }

    // Ветьеватая логика получения имени резолвера и его родителя (для лучшего понимания откуда вызывалась нода)
    private String findResolverTag(InstrumentationFieldFetchParameters parameters) {
        GraphQLOutputType type = parameters.getExecutionStepInfo().getParent().getType();
        GraphQLObjectType parent;
        if (type instanceof GraphQLNonNull) {
            parent = (GraphQLObjectType) ((GraphQLNonNull) type).getWrappedType();
        } else {
            parent = (GraphQLObjectType) type;
        }
        return parent.getName() + "." + parameters.getExecutionStepInfo().getPath().getSegmentName();
    }
}

事实上,Instrumentation 是非常强大的功能,不仅可以用于监控。例如,已经从 graphql-java 实现的 graphql-java 测量查询的最大深度并在超过时取消查询,并且在帮助下您可以将权重分配给特定节点并控制查询的复杂性,但存在细微差别。

这足以启动我们的服务。

请求本身。

{
    candidates {
        id,
        firstName,
        lastName,
        phone,
        email,
        resumes {
            id,
            lastExperience,
            salary {
                currency,
                amount
            }
        }
    }
}

响应将采用服务的标准 json 格式。

结论

这是 java 中 graphql 的情况。我们查看了不同的框架,评估了它们的优缺点,然后用 Java 实现了一个简单的 graphql 服务。

推荐书单

《Java编程讲义》

购买链接:https://item.jd.com/13495830.html

《Java编程讲义》根据目前Java开发领域的实际需求,从初学者角度出发,详细讲解了Java技术的基础知识。

全书共15章,包括Java开发入门,Java语言基础,Java控制结构,数组,面向对象编程,继承和多态,抽象类、接口和内部类,异常处理,Java常用类库,集合与泛型,Lambda表达式,输入-输出流,多线程,JDBC数据库技术,网络编程等内容。内容全面覆盖.1ava开发必备的基础知识点,结合生活化案例展开讲解,程序代码给出了详细的注释,能够使初学者轻松领会Java技术精髓,快速掌握Java开发技能。

《Java编程讲义》适合作为高等院校相关专业的教材及教学参考书,也适合作为Java开发入门者的自学用书,还可供开发人员查阅、参考。

精彩回顾

想要代码干净又整洁?这里有十大原则

详细&全面的RxJava架构原理与设计讲解

深入理解Docker网络通信原理

微信搜索关注《Java学研大本营》

访问【IT今日热榜】,发现每日技术热点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值