GraphQL 与 Spring Boot 入门指南

更新于 2025-12-30

Graham Cox 2024-12-16

1. 简介

GraphQL 是 Facebook 提出的一种相对较新的技术。它是一种用于从服务器检索数据的查询语言,可作为 REST、SOAP 或 gRPC 的替代方案。

在本教程中,我们将学习如何使用 Spring Boot 搭建一个 GraphQL 服务器,以便将其集成到现有应用中,或用于新项目开发。

2. 什么是 GraphQL?

传统的 REST API 围绕服务器管理的资源构建,通过标准的 HTTP 动词进行操作。虽然在这种框架下非常有效,但当客户端需要同时从多个资源获取数据,或偏离该范式时,REST 就会遇到困难——常常导致响应体过大,包含大量不必要的数据。

GraphQL 通过允许客户端精确请求所需的数据来解决这些问题:

  • 客户端可以在单个查询中获取所需全部数据;
  • 支持对子资源的嵌套导航;
  • 可以一次性执行多个查询。

这种方式类似于 RPC(远程过程调用),强调命名查询(queries)和变更(mutations),使 API 开发者和使用者都能有效控制功能和期望结果。

例如,一个博客系统可能支持如下查询:

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

该查询将:

  • 请求最近的 10 篇文章;
  • 对每篇文章,返回其 ID、标题和分类;
  • 同时为每篇文章请求作者信息,包括作者的 ID、姓名和头像。

在传统 REST API 中,这通常需要 11 次请求(1 次获取文章列表 + 10 次分别获取每位作者信息),或者必须在文章接口中冗余地包含作者详情。

2.1 GraphQL Schema(模式)

GraphQL 服务器通过暴露一个 Schema(模式) 来描述其 API。该 Schema 由类型定义组成,每个类型包含一个或多个字段,每个字段可接受零个或多个参数,并返回特定类型。

“图”(Graph)的概念来源于这些字段之间的嵌套关系。注意:这个图可以包含环(cycles),但它是有向的——客户端可以从一个字段导航到其子字段,但无法自动返回父级,除非 Schema 显式定义了反向关系。

以下是一个博客系统的 GraphQL Schema 示例,定义了 Post(文章)、Author(作者),以及根查询和变更:

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

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

# 应用的根查询
type Query {
    recentPosts(count: Int, offset: Int): [Post]!
}

# 应用的根变更
type Mutation {
    createPost(title: String!, text: String!, category: String, authorId: String!) : Post!
}

字段名末尾的 ! 表示该类型为非空(non-nullable)。没有 ! 的字段在服务器响应中可以为 null。GraphQL 服务能正确处理这些情况,允许我们安全地请求可空类型的子字段。

此外,GraphQL 服务还通过一组标准字段暴露 Schema 本身,使客户端能在运行前查询 Schema 定义。这使得客户端能够自动检测 Schema 的变更,并动态适配其结构。一个非常实用的例子是 GraphiQL 工具,它能与任意 GraphQL API 交互。

3. 引入 GraphQL Spring Boot Starter

Spring Boot GraphQL Starter 提供了一种快速搭建 GraphQL 服务器的绝佳方式。借助自动配置和基于注解的编程模型,我们只需编写业务逻辑代码即可。

3.1 服务设置

要实现这一点,只需添加以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

由于 GraphQL 本身与传输协议无关,我们额外引入了 Web Starter,以便通过 HTTP 暴露 GraphQL API。默认情况下,Spring MVC 会在 /graphql 路径提供服务。若使用响应式编程,也可选择 Spring WebFlux Starter。

你还可以通过 application.properties 自定义该端点路径。

4. 编写 Schema

GraphQL Spring Boot Starter 会处理 .graphqls.gqls 格式的 Schema 文件,构建对应结构,并将特定的 Spring Bean 与之绑定。这些 Schema 文件需放在 src/main/resources/graphql/** 目录下,Spring Boot 会自动加载。

你也可以通过以下配置自定义位置和文件扩展名:

  • spring.graphql.schema.locations
  • spring.graphql.schema.file-extensions

⚠️ 注意:Schema 中必须且只能有一个根查询(Query),最多一个根变更(Mutation)。这是 GraphQL 规范本身的限制,而非 Java 实现的问题,因此不能将根查询拆分到多个文件中。

5. 实现 Schema

编写完 Schema 后,我们需要用 Java 代码实现其逻辑。主要包括三类组件:

  • 根查询解析器(Root Query Resolver):处理顶层查询字段。
  • 字段解析器(Field Resolver):处理嵌套字段的值。
  • 变更(Mutations):实现数据修改操作。

所有这些都通过在 Spring 应用中定义带特定注解的 Bean 来实现。

5.1 根查询解析器

根查询的处理方法需使用 @QueryMapping 注解,并置于标准的 @Controller 组件中。Spring 会自动将其注册为 GraphQL 数据获取组件:

@Controller
public class PostController {

    private PostDao postDao;

    @QueryMapping
    public List<Post> recentPosts(@Argument int count, @Argument int offset) {
        return postDao.getRecentPosts(count, offset);
    }
}

上述 recentPosts 方法用于处理 Schema 中定义的 recentPosts 查询字段。方法参数需使用 @Argument 注解,以匹配 Schema 中的参数。

此外,方法还可接收 GraphQLContextDataFetchingEnvironment 等上下文对象,用于访问底层执行环境。

返回类型必须与 Schema 中定义的类型一致。对于简单类型(如 StringIntList 等),系统会自动映射对应的 Java 类型。

5.2 使用 Java Bean 表示类型

GraphQL 中的每个复杂类型都由一个 Java Bean 表示,无论它来自根查询还是嵌套结构。同一个 GraphQL 类型必须始终由同一个 Java 类表示(类名无需与 GraphQL 类型名一致)。

Java Bean 中的字段会根据名称自动映射到 GraphQL 响应字段:

public class Post {
    private String id;
    private String title;
    private String category;
    private String authorId; // 注意:此字段不在 Schema 中
}

未在 Schema 中定义的字段(如 authorId)会被忽略,但不会报错——这对字段解析器的实现至关重要。

5.3 复杂值的字段解析器

某些字段的值可能需要通过数据库查询或复杂计算获得。此时可使用 @SchemaMapping 注解,将方法绑定为该字段的 DataFetcher:

@SchemaMapping
public Author author(Post post) {
    return authorDao.getAuthor(post.getAuthorId());
}

关键优势在于:只有当客户端请求了某个字段时,对应的解析方法才会被执行。例如,若查询未包含 author 字段,则上述 author() 方法不会被调用,避免了不必要的数据库访问。

你也可以显式指定类型和字段名:

@SchemaMapping(typeName = "Post", field = "author")
public Author getAuthor(Post post) {
    return authorDao.getAuthor(post.getAuthorId());
}

5.4 可空值处理

GraphQL Schema 中区分可空与非空类型。在 Java 代码中,我们直接使用 null 表示可空值。此外,也可以使用 Java 8 的 Optional 类型,系统会自动正确处理。

这使得 Java 方法签名与 GraphQL Schema 的语义保持高度一致。

5.5 变更(Mutations)

截至目前,我们只讨论了数据查询。GraphQL 还支持通过 变更(Mutations) 修改服务器数据。

💡 虽然从技术上讲,查询方法也可以修改数据,但这违背了 GraphQL 的设计原则,会导致客户端产生意外副作用,属于不良实践。

正确的做法是使用 @MutationMapping 注解定义变更方法。其返回值的处理方式与查询完全相同,支持嵌套字段:

@MutationMapping
public Post createPost(@Argument String title, @Argument String text,
                       @Argument String category, @Argument String authorId) {
    Post post = new Post();
    post.setId(UUID.randomUUID().toString());
    post.setTitle(title);
    post.setText(text);
    post.setCategory(category);
    post.setAuthorId(authorId);

    postDao.savePost(post);
    return post;
}

6. GraphiQL

GraphQL 配套提供了一个名为 GraphiQL 的 UI 工具,可用于与任意 GraphQL 服务器交互,极大简化 API 的开发与测试。

Spring GraphQL 内置了 GraphiQL,但默认禁用。只需在 application.yml 中启用:

spring:
  graphql:
    graphiql:
      enabled: true

启用后,访问应用的 /graphiql 路径即可打开交互式界面,方便地编写和测试查询:

GraphiQL:一个浏览器内的工具,用于编写和测试 GraphQL 查询。

7. 总结

GraphQL 是一项令人兴奋的新技术,有望彻底改变 Web API 的开发方式。

Spring Boot GraphQL Starter 让我们能够轻松地将 GraphQL 集成到任何新的或现有的 Spring Boot 应用中。