这是一篇面向那些想用Java创建GraphQL服务的人。它需要一些Spring Boot和Java知识,虽然我们简要介绍了GraphQL,但本教程的重点是用Java开发GraphQL服务器。

3分钟内搭建GraphQL

GraphQL是一种从服务器检索数据的查询语言。在某种程度上,它是REST、SOAP或gRPC的替代品。

假设我们要从在线商店后端查询特定书籍的详细信息。

使用GraphQL,您向服务器发送以下查询,以获取id为“book-1”的书籍的详细信息:

{
  bookById(id: "book-1"){
    id
    name
    pageCount
    author {
      firstName
      lastName
    }
  }
}

这不是JSON(尽管它看起来有意地相似),而是一个GraphQL查询。它基本上说:

  • 查询具有特定id的书籍
  • 给我那本书的id,名字,页数和作者
  • 对于作者,我想知道名字和姓氏

响应为正常JSON:

{
  "bookById":
  {
    "id":"book-1",
    "name":"Harry Potter and the Philosopher's Stone",
    "pageCount":223,
    "author": {
      "firstName":"Joanne",
      "lastName":"Rowling"
    }
  }
}

GraphQL的一个非常重要的特性是它是静态类型的:服务器确切地知道您可以查询的每个对象的形状,任何客户端实际上都可以“内省”服务器并请求所谓的“模式”。模式描述了哪些查询是可能的,哪些字段可以返回。(注意:当我们在这里提到模式时,我们总是提到“GraphQL模式”,它与其他模式(如“JSON模式”或“数据库模式”)无关)

上述查询的架构如下所示:

type Query {
  bookById(id: ID): Book
}

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

type Author {
  id: ID
  firstName: String
  lastName: String
}

本教程将重点介绍如何在Java中使用此模式实现GraphQL服务器。

我们几乎没有触及GraphQL可能实现的功能的表面。更多信息可在官方网页上找到:https://graphql.github.io/learn/

GraphQL Java概述

GraphQL Java是GraphQL的Java(服务器)实现。GraphQL Java Github组织中有几个存储库。最重要的是GraphQL Java引擎,它是其他一切的基础。

GraphQLJava引擎本身只关心执行查询。它不处理任何与HTTP或JSON相关的主题。对于这些方面,我们将使用GraphQLJavaSpringBootAdapter,它负责通过SpringBootoverHTTP公开我们的API。

创建GraphQL Java服务器的主要步骤包括:

1. 定义GraphQL模式。

2. 决定如何获取查询的实际数据。

我们的示例API:获取书籍详细信息

我们的示例应用程序将是一个简单的API,用于获取特定书籍的详细信息。这绝不是一个全面的API,但对于本教程来说已经足够了。

创建一个Spring启动应用程序

创建Spring启动应用程序最简单的方法是在https://start.spring.io/.

选择:

对于我们使用的项目元数据:

  • Group: com.graphql-java.tutorial
  • Artifact: book-details

作为依赖项,我们只选择Web。

点击Generate项目将为您提供一个随时可用的Spring Boot应用程序。所有随后提到的文件和路径都将与此生成的项目相关。

我们在build的dependencies部分向我们的项目添加了三个依赖项build.gradle

前两个是Graphql Java和Graphql Java Spring,然后我们还添加了Google Guava。Guava不是严格需要的,但它会让我们的生活更轻松一点。

依赖项将如下所示:

dependencies {
    implementation 'com.graphql-java:graphql-java:11.0' // NEW
    implementation 'com.graphql-java:graphql-java-spring-boot-starter-webmvc:1.0' // NEW
    implementation 'com.google.guava:guava:26.0-jre' // NEW
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Schema

我们正在创建一个新的文件架构。src/main/resources中包含以下内容的graphqls:

type Query {
  bookById(id: ID): Book
}

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

type Author {
  id: ID
  firstName: String
  lastName: String
}

此模式定义了一个顶级字段(在类型查询中):bookById,它返回特定书籍的详细信息。

它还定义了类型Book,其中包含以下字段:idnamepageCountauthor。author是author类型,在Book之后定义。

上面显示的用于描述模式的特定于域的语言称为模式定义语言或SDL。

一旦我们有了这个文件,我们就需要通过读取文件并对其进行解析和添加代码来获取数据来“激活它”。

我们在com.graphqljava.tutorial.bookdetails包中创建一个新的GraphQLProvider类。其中包含将创建GraphQL实例的init方法:

@Component
public class GraphQLProvider {

    private GraphQL graphQL;

    @Bean
    public GraphQL graphQL() {
        return 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) {
      // TODO: we will create the schema here later
    }
}

我们使用Guava资源从类路径读取文件,然后创建GraphQLSchema和GraphQL实例。这个GraphQL实例通过用@Bean注释的GraphQL()方法作为Springbean公开。GraphQL Java Spring适配器将使用该GraphQL实例通过默认url/GraphQL上的HTTP使我们的模式可用。

我们仍然需要实现buildSchema方法,该方法创建GraphQLSchema实例并连接代码以获取数据:

@Autowired
GraphQLDataFetchers graphQLDataFetchers;

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()
            .type(newTypeWiring("Query")
                    .dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
            .type(newTypeWiring("Book")
                    .dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
            .build();
}

TypeDefinitionRegistry是模式文件的解析版本。SchemaGeneratorTypeDefinitionRegistryRuntimeWiring结合起来,实际生成GraphQLSchema。

buildWiring使用graphQLDataFetchers bean来实际注册两个DataFetcher

  • 检索具有特定ID的书籍的方法
  • 一个是为了得到一本书的作者。

总体而言,创建GraphQL和GraphQLSchema实例的过程如下所示:

DataFetchers

对于GraphQLJava服务器来说,最重要的概念可能是DataFetcher:DataFetcher在执行查询时获取一个字段的数据。

GraphQLJava在执行查询时,会为查询中遇到的每个字段调用相应的DataFetcher。DataFetcher是具有单个方法的接口,采用DataFetcherEnvironment类型的单个参数:

public interface DataFetcher<T> {
    T get(DataFetchingEnvironment dataFetchingEnvironment) throws Exception;
}

要点:模式中的每个字段都有一个与之关联的DataFetcher。如果没有为特定字段指定任何DataFetcher,则使用默认的PropertyDataFetcher。我们稍后将更详细地讨论这一点。

我们正在创建一个新的类GraphQLDataFetchers,它包含书籍和作者的示例列表。

完整的实现如下所示,我们将很快详细介绍:

@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 getBookByIdDataFetcher() {
        return dataFetchingEnvironment -> {
            String bookId = dataFetchingEnvironment.getArgument("id");
            return books
                    .stream()
                    .filter(book -> book.get("id").equals(bookId))
                    .findFirst()
                    .orElse(null);
        };
    }

    public DataFetcher getAuthorDataFetcher() {
        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);
        };
    }
}

数据来源​

我们从课堂上的静态列表中获取书籍和作者。这是专为本教程制作的。理解GraphQL并不指示数据来自何处是非常重要的。这就是GraphQL的威力:它可以来自静态内存列表、数据库或外部服务

图书DataFetcher

我们的第一个方法getBookByIdDataFetcher返回一个DataFetcher实现,它接受DataFetcherEnvironment并返回一本书。在我们的例子中,这意味着我们需要从bookById字段中获取id参数,并找到具有此特定id的书籍。如果找不到它,我们只返回null。

String bookId = dataFetchingEnvironment.getArgument("id")中的“id”是架构中bookById查询字段的“id”:

type Query {
  bookById(id: ID): Book
}
...

Author DataFetcher​

我们的第二个方法getAuthorDataFetcher返回一个DataFetcher,用于获取特定书籍的作者。与前面描述的book DataFetcher相比,我们没有参数,但有一个book实例。父字段的DataFetcher的结果可通过getSource获得。这是一个需要理解的重要概念:GraphQL中每个字段的DataFetcher是以自顶向下的方式调用的,父级的结果是子DataFetcherEnvironment的源属性。

然后,我们使用先前获取的书来获取authord,并以与查找特定书籍相同的方式查找特定的作者。

默认DataFetchers

我们只实现了两个数据获取程序。如上所述,如果不指定,则使用默认的PropertyDataFetcher。在我们的例子中,它的意思是书。身份证,书。名字,书。页数,作者。身份证,作者。名字和作者。lastName都有一个与之关联的默认PropertyDataFetcher

PropertyDataFetcher尝试以多种方式查找Java对象上的属性。如果是java.util.Map它只需按键查找属性。这对我们来说非常好,因为图书和作者Map的键与模式中指定的字段相同。例如,在我们为图书类型定义的模式中,pageCount字段和图书数据获取程序返回一个带有键pageCount的Map。因为字段名与Map("pageCount")相同,所以PropertyDateFetcher适用于我们。

让我们假设一秒钟我们有一个不匹配,图书地图有一个键totalPages而不是pageCount。这将导致每本书的pageCount值为空,因为PropertyDataFetcher无法获取正确的值。为了解决这个问题,您必须为Book注册一个新的DataFetcher。页面计数,如下所示:

// In the GraphQLProvider class
private RuntimeWiring buildWiring() {
    return RuntimeWiring.newRuntimeWiring()
            .type(newTypeWiring("Query")
                    .dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
            .type(newTypeWiring("Book")
                    .dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher())
                    // This line is new: we need to register the additional DataFetcher
                    .dataFetcher("pageCount", graphQLDataFetchers.getPageCountDataFetcher()))
            .build();
}

// In the GraphQLDataFetchers class
// Implement the DataFetcher
public DataFetcher getPageCountDataFetcher() {
    return dataFetchingEnvironment -> {
        Map<String,String> book = dataFetchingEnvironment.getSource();
        return book.get("totalPages");
    };
}

这个数据获取程序可以通过在图书地图中查找正确的键来解决这个问题。(同样:我们的示例不需要这个,因为我们没有命名不匹配)

试试这个API

这就是构建工作GraphQLAPI所需的全部内容。启动Spring Boot应用程序后,API在上可用http://localhost:8080/graphql.

尝试和探索GraphQLAPI最简单的方法是使用GraphQLAphy之类的工具。下载并运行它。

启动后,系统会要求您输入URL,请输入“http://localhost:8080/graphql".

在那之后,您可以查询我们的示例API,您应该会得到前面提到的结果。它应该是这样的:

包含完整源代码的完整项目可在此处找到:https://github.com/graphql-java/tutorials/tree/master/book-details