在Netflix DGS上使用Java实现大规模GraphQL服务器

由于Netflix的Tech博客上发布了GraphQL框架的OSS版本,所以我立刻尝试使用了它。

    • How Netflix Scales its API with GraphQL Federation (Part 1)

 

    How Netflix Scales its API with GraphQL Federation (Part 2)

网飞 DGS

DGS(Domain Graph Service)是Netflix开发的用于构建Java/Kotlin的GraphQL服务器的框架。它旨在构建大规模的GraphQL服务,并采用了Apollo Federation。由于在Spring Boot上运行,因此成为非常易于操作的框架。它还支持Spring Security和从模式生成代码。请参阅官方文档以获取更详细的信息。

Netflix开发GraphQL框架的背景是什么?

スクリーンショット 2021-01-17 20.59.42.png

真的很複雜呢。從前端的角度來看,由於有很多查詢服務端點,因此管理起來會很麻煩。
從服務端的角度來看,如果需要身份驗證等前置處理,每個微服務都需要實現相同的處理,可能會產生問題。

在Netflix中,我们决定采用GraphQL。采用GraphQL之后的架构如下所示。

スクリーンショット 2021-01-17 21.06.33.png

前端的查询操作现在被收集到紫色部分,以GraphQL作为统一的结构,使得整体更加清晰简洁。
前端发出的请求通过GraphQL转发到各个微服务进行处理。
如果将前置处理(如认证)也实现在GraphQL部分,看起来就不需要在每个微服务中重复实现相同的处理逻辑了。

然而,这里存在一个问题。负责运营GraphQL的团队承担着巨大的工作负荷。

因此,我们采用了Apollo Federation并将负载分散到了GraphQL开发中。Apollo Federation采用后的情况如下图所示。

スクリーンショット 2021-01-17 21.18.17.png

通过采用Apollo Federation,可以将GraphQL Gateway放置在前端,并将各个GraphQL服务部署在其后端,实现这样的架构是可能的。在上图中,紫色部分代表GraphQL Gateway,右侧的蓝色部分代表各个微服务的GraphQL。

各个微服务的GraphQL由各个服务团队运营管理,GraphQL Gateway则通过仅实现必要的最小限度功能,实现了GraphQL负载的分散。

确定要实施的功能

好的,现在我们将使用框架来进行实际实现。
我决定将之前写过的《使用Java实现GraphQL服务器》这篇文章中的同一功能(查询、变更、订阅)进行实现。以下是这些功能的使用案例。

    • Query: 本の一覧を取得する。

 

    • Query: 本のIDを指定して、特定の本を取得する。

 

    • Mutation: 新しい本を登録する。

 

    Subscription: 新たに登録された本を通知する。

我本来想实现Federation,但目前还没找到有关Federation的文件,尤其是Gateway部分的实现我不太清楚。一旦有了文档,我会尝试着去实现。

构成项目。

该项目将使用Spring Initializer来创建一个适当的模板。由于需要的依赖项稍后添加,因此在这里只添加Lombok。

创造一个应用程序

构建.gradle文件

将会进行如下更改。

plugins {
    id 'org.springframework.boot' version '2.4.1'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
    // Added for "api".
    id 'java-library'
    // Added for code generation from scheme.
    id 'com.netflix.dgs.codegen' version '4.0.10'
}

sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
    // Added to fix error "Could not find com.apollographql.federation:federation-graphql-java-support".
    jcenter()
}

dependencies {
    api 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:latest.release'
    // Added for subscription.
    implementation 'com.netflix.graphql.dgs:graphql-dgs-subscriptions-websockets-autoconfigure:latest.release'

    implementation 'org.springframework.boot:spring-boot-starter-web'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

// Added for code generation from scheme.
generateJava {
    schemaPaths = ["${projectDir}/src/main/resources/schema"]
    // Set package name of generated code.
    packageName = 'sandbox.dgs'
    // Set "false" not to generate client code.
    generateClient = false
}

schema.graphqls 的中文表述

架构.graphqls

我們要定義GraphQL的模式。
將schema.graphqls檔案註冊在src/main/resources/schema資料夾中。

type Query {
    bookById(id: ID): [Book]!
    books: [Book]!
}

type Book {
    id: ID
    name: String
    pageCount: Int
}

type Mutation {
    registerBook (
        id: ID
        name: String
        pageCount: Int
    ): Book
}

type Subscription {
    subscribeBooks: Book
}

Java模型

使用代码生成插件,从模式中创建Java类。

$ ./gradlew generateJava 
$ ls -l build/generated/sandbox/dgs
total 8
-rw-r--r--  1 xxxxxxx  yyyyy  942 Jan 11 19:48 DgsConstants.java
drwxr-xr-x  4 xxxxxxx  yyyyy  128 Jan 11 19:48 types

$ ls -l build/generated/sandbox/dgs/types
total 16
-rw-r--r--  1 xxxxxxx  yyyyy  2223 Jan 11 19:48 Book.java
-rw-r--r--  1 xxxxxxx  yyyyy  1487 Jan 11 19:48 Subscription.java

我成功创建了一个Book类。

DGS组件

然后,我们创建了一个实现了Query、Mutation和Subscription的Service类(DGS组件)。我们做了以下事情:

    • クラスに@DgsComponentを付与します。

 

    • メソッドに@DgsDataを付与し、parentTypeにはスキーマのtype name、fieldにはfield nameを設定します。

 

    • fieldの引数は、@InputArgumentもしくはDataFetchingEnvironmentを使用して取得します。

 

    subscriptionのreturn値はreactive-streamsのPublisherを返します。

DataProvider 和 IBookProcessor 是我們自己建立的類別。執行以下的處理步驟:

    • DataProvider : データ提供クラス。すべてのBookのList、または指定されたbookIdのBookを返す。

 

    IBookProcessor : 本の登録をイベントとして登録(emit)し、イベントのPublisherを発行(publish)する。
@AllArgsConstructor
@DgsComponent
public class BookService {
    private final DataProvider dataProvider;
    private final IBookProcessor bookProcessor;

    @DgsData(parentType = "Query", field = "books")
    public List<Book> books() {
        return dataProvider.books();
    }

    @DgsData(parentType = "Query", field = "bookById")
    public List<Book> books(@InputArgument("id") String id) {
        if (id == null || id.isEmpty()) {
            return dataProvider.books();
        }
        return List.of(dataProvider.bookById(id));
    }

    @DgsData(parentType = "Mutation", field = "registerBook")
    public Book registerBook(DataFetchingEnvironment dataFetchingEnvironment) {
        final String id = dataFetchingEnvironment.getArgument("id");
        final String name = dataFetchingEnvironment.getArgument("name");
        final int pageCount = dataFetchingEnvironment.getArgument("pageCount");

        final Book book = new Book(id, name, pageCount);
        dataProvider.books().add(book);
        // Emit an event for subscription.
        bookProcessor.emit(book);
        return book;
    }

    @DgsData(parentType = "Subscription", field = "subscribeBooks")
    public Publisher<Book> subscribeBooks() {
        return bookProcessor.publish();
    }
}

执行应用程序

执行创建的应用程序。由于附带了GraphiQL,因此启动SpringBoot应用程序,并在浏览器中访问端点(http://localhost:8080/graphiql)。

subscription.png

只有订阅出现错误。检查了调试日志后,发现以下错误已被输出。

2021-01-11 21:05:52.305 DEBUG 70696 --- [nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing ["Trying to execute subscription on /graphql. Use /subscriptions instead!"]
2021-01-11 21:05:52.305 DEBUG 70696 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed 400 BAD_REQUEST

对于Subscription,请发送请求到/subscriptions。
由于不知道如何更改GraphiQL的请求目标,所以我尝试实施如下客户端代码,并成功运行。

import { WebSocketLink } from "@apollo/client/link/ws";
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { ApolloClient, InMemoryCache } from "@apollo/client";
import gql from 'graphql-tag';
import * as $ from 'jquery';

const GRAPHQL_ENDPOINT = 'ws://localhost:8080/subscriptions';

const client = new SubscriptionClient(GRAPHQL_ENDPOINT, {
  reconnect: true,
});

const link = new WebSocketLink(client);

const apolloClient = new ApolloClient({
  link: link,
  cache: new InMemoryCache()
});

const asGql = gql`
subscription BookSubscription {
  subscribeBooks {
    id,
    name
  }
}
`

const s = apolloClient.subscribe({
  query: asGql
})
s.subscribe({
  next: ({ data }) => {
    const result = document.getElementById("result");
    $("#result").append(JSON.stringify(data));
    $("#result").append("<br>");
  }
});

摘要

(mǎ

我已经描述了使用Netflix DGS实现GraphQL服务器的方法。
尽管我只是尝试了GraphQL的基本功能,但使用起来感觉还不错。
我很想尽快尝试Federation的部分。

我已经把编写好的源代码注册到了GitHub上。希望您可以参考。

    Simple application of GraphQL server with DGS
广告
将在 10 秒后关闭
bannerAds