GraphQL是来自Facebook的一个相对较新的概念,被宣传为RESTforWebAPI的替代品。

本文将介绍如何使用Spring Boot设置GraphQL服务器,以便将其添加到现有应用程序或用于新应用程序。

什么是GraphQL?

传统的RESTAPI使用服务器管理的资源概念。这些资源可以按照各种HTTP谓词以一些标准方式进行操作。只要我们的API符合资源概念,它就可以很好地工作,但当我们需要偏离它时,它很快就会崩溃。

当客户端同时需要来自多个资源的数据时,这也会受到影响。例如,请求一篇博客文章和评论。通常,这可以通过让客户机发出多个请求或让服务器提供可能并不总是需要的额外数据来解决,从而导致更大的响应大小。

GraphQL为这两个问题提供了解决方案。它允许客户机精确地指定所需的数据,包括在单个请求中导航子资源,并允许在单个请求中进行多个查询。

它还以更加RPC的方式工作,使用命名查询和突变,而不是一组标准的强制操作。这可以将控件放在它所属的位置,API开发人员指定可能的内容,API使用者指定所需的内容。

例如,博客可能允许以下查询:

query {
    recentPosts(count: 10, offset: 0) {
        id
        title
        category
        author {
            id
            name
            thumbnail
        }
    }
}

此查询将:

  • 请求最近的十个帖子
  • 对于每个帖子,请求ID、标题和类别
  • 对于每个post请求,作者将返回ID、名称和缩略图

在传统的RESTAPI中,这要么需要11个请求(1个用于帖子,10个用于作者),要么需要在帖子详细信息中包含作者详细信息。

GraphQL模式

GraphQL服务器公开了一个描述API的模式。此方案由类型定义组成。每个类型都有一个或多个字段,每个字段接受零个或多个参数并返回特定类型。

该图由这些字段相互嵌套的方式组成。请注意,图不需要是非循环的-循环是完全可以接受的-但它是定向的。也就是说,客户机可以从一个字段返回到它的子字段,但是除非模式明确定义,否则它不能自动返回到父字段。

博客的示例GraphQL模式可能包含以下定义,这些定义描述了一篇文章、文章的作者以及获取博客上最新文章的根查询。

type Post {
    id: ID!
    title: String!
    text: String!
    category: String
    author: Author!
}

type Author {
    id: ID!
    name: String!
    thumbnail: String
    posts: [Post]!
}

# The Root Query for the application
type Query {
    recentPosts(count: Int, offset: Int): [Post]!
}

# The Root Mutation for the application
type Mutation {
    writePost(title: String!, text: String!, category: String) : Post!
}

某些名称末尾的“”表示这是不可为空的类型。任何不具有此属性的类型在服务器的响应中都可以为null。GraphQL服务正确地处理这些问题,允许我们安全地请求可为空类型的子字段。

GraphQL服务还使用一组标准字段公开模式本身,允许任何客户端提前查询模式定义。

这允许客户端自动检测模式何时更改,并允许客户端动态地适应模式的工作方式。一个非常有用的例子是GraphiQL工具(稍后讨论),它允许我们与任何GraphQL API交互。

介绍GraphQL Spring Boot

Spring Boot GraphQL Starter(https://github.com/graphql-java-kickstart/graphql-spring-boot)提供了一种在很短的时间内运行GraphQL服务器的极好方法。结合GraphQL Java工具库(https://github.com/graphql-java/graphql-java-tools),我们只需编写服务所需的代码。

设置服务

我们需要的只是正确的依赖关系:

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>

SpringBoot将自动拾取这些数据,并设置相应的处理程序以自动工作。

默认情况下,这将在应用程序的/GraphQL端点上公开GraphQL服务,并接受包含GraphQL负载的POST请求。如有必要,可以在application.properties文件中自定义此端点。

编写模式

GraphQL工具库通过处理GraphQL模式文件来构建正确的结构,然后将特殊bean连接到此结构。SpringBootGraphQL启动器会自动查找这些模式文件。

这些文件需要以扩展名“.graphqls”保存,并且可以出现在类路径的任何位置。我们还可以根据需要拥有任意数量的这些文件,因此我们可以根据需要将方案拆分为模块。

一个要求是必须有一个根查询,最多一个根变异。与方案的其余部分不同,这不能跨文件拆分。这是GraphQL模式定义本身的限制,而不是Java实现的限制。

根查询解析器

根查询需要在Spring上下文中定义特殊的bean来处理此根查询中的各个字段。与模式定义不同,没有限制根查询字段只能有一个Springbean。

唯一的要求是bean实现GraphQLQueryResolver,并且来自scheme的根查询中的每个字段在其中一个类中都有一个同名的方法。

public class Query implements GraphQLQueryResolver {
    private PostDao postDao;
    public List<Post> getRecentPosts(int count, int offset) {
        return postsDao.getRecentPosts(count, offset);
    }
}

方法的名称必须是以下名称之一,顺序如下:

  • <field>
  • is<field>–仅当字段为布尔类型时
  • get<field>

该方法必须具有与GraphQL模式中的任何参数相对应的参数,并且可以选择采用DataFetchingEnvironment类型的最终参数。

正如我们将要看到的,该方法还必须为GraphQL方案中的类型返回正确的返回类型。任何简单类型——StringIntList等——都可以与等价的Java类型一起使用,系统只是自动映射它们。

上面定义了getRecentPosts方法,该方法将用于处理前面定义的模式中recentPosts字段的任何GraphQL查询。

使用bean表示类型

GraphQL服务器中的每个复杂类型都由一个Java bean表示——无论是从根查询还是从结构中的任何其他地方加载。相同的Java类必须始终表示相同的GraphQL类型,但不需要类的名称。

JavaBean中的字段将根据字段名称直接映射到GraphQL响应中的字段。

public class Post {
    private String id;
    private String title;
    private String category;
    private String authorId;
}

JavaBean上未映射到GraphQL模式的任何字段或方法都将被忽略,但不会导致问题。这对于字段解析程序的工作非常重要。

例如,此处的字段authord与我们前面定义的模式中的任何内容都不对应,但它将可用于下一步。

复杂值的字段解析器

有时,字段的值很难加载。这可能涉及数据库查找、复杂的计算或其他任何操作。GraphQL工具有一个用于此目的的字段解析器的概念。这些是Springbean,可以提供值来代替数据bean。

字段解析器是Spring上下文中与数据bean同名、后缀为resolver并实现GraphQLResolver接口的任何bean。字段解析器bean上的方法遵循与数据bean上相同的所有规则,但也将数据bean本身作为第一个参数提供。

如果字段解析器和数据bean都具有用于相同GraphQL字段的方法,那么字段解析器将优先。

public class PostResolver implements GraphQLResolver<Post> {
    private AuthorDao authorDao;

    public Author getAuthor(Post post) {
        return authorDao.getAuthorById(post.getAuthorId());
    }
}

这些字段解析器是从Spring上下文加载的,这一点很重要。这允许他们使用任何其他Spring管理的bean,例如DAO。

重要的是,如果客户端不请求字段,那么GraphQL服务器将永远不会执行检索该字段的工作。这意味着,如果客户机检索帖子而不请求作者,那么上面的getAuthor()方法将永远不会执行,DAO调用也永远不会进行。

可为空的值

GraphQL模式的概念是,某些类型可以为空,而其他类型则不能为空。

这可以通过直接使用空值在Java代码中处理,但同样,Java 8中的新可选类型可以直接用于可空类型,并且系统将正确处理这些值。

这非常有用,因为这意味着我们的Java代码显然与方法定义中的GraphQL模式相同。

Mutations

到目前为止,我们所做的一切都是关于从服务器检索数据。GraphQL还能够通过变量更新存储在服务器上的数据。

从代码的角度来看,查询没有理由不能更改服务器上的数据。我们可以很容易地编写接受参数、保存新数据并返回这些更改的查询解析器。这样做会给API客户端带来令人惊讶的副作用,被认为是不好的做法。

相反,应该使用突变来通知客户,这将导致存储的数据发生更改。

通过使用实现GraphQLMutationResolver而不是GraphQLQueryResolver的类在Java代码中定义突变。

否则,所有规则都适用于查询。然后,对来自突变字段的返回值的处理与来自查询字段的返回值的处理完全相同,从而也可以检索嵌套值。

public class Mutation implements GraphQLMutationResolver {
    private PostDao postDao;

    public Post writePost(String title, String text, String category) {
        return postDao.savePost(title, text, category);
    }
}

介绍GraphiQL

GraphQL还有一个名为GraphiQL的配套工具。这是一个能够与任何GraphQL服务器通信并对其执行查询和更改的UI。它的一个可下载版本作为电子应用程序存在,可以从这里检索:https://github.com/skevy/graphiql-app

通过添加GraphiQL Spring Boot Starter依赖项,还可以在我们的应用程序中自动包含基于web的GraphiQL版本:

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>

但是,只有当我们在/graphql的默认端点上托管graphqlapi时,这才起作用,因此如果不是这样,则需要独立应用程序。

总结

GraphQL是一项非常令人兴奋的新技术,它可能会彻底改变Web API的开发方式。

SpringBootGraphQlStarter和GraphQlJava工具库的结合使得将此技术添加到任何新的或现有的SpringBoot应用程序中变得非常容易。

可以在GitHub上找到代码:https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-libraries