GraphQL 是一个来自于 Facebook 的相当新的概念,它让我们在写 Web API 的时候作为 REST 接口风格的另一种选择。 这篇文章将会介绍如何通过 Spring Boot 来搭建我们的 GraphQL 服务,这样无论在现有项目或者新项目里都可以很方便地使用。
传统的 REST API 是依据服务器管理资源的概念来编写的。这些资源可以通过 Http 请求以规定的几个 verb(GET、POST、PUT、DELETE) 来进行访问。当我们的接口和我们的资源概念相符合时工作没什么问题,但如果我们稍有变化,事情就开始变的麻烦了。
这同时还会发生在我们的客户端请求多个不同数据的时候:比方说我们请求博客的文章以及对应的评论。通常我们只能让客户端发多个请求,或者让服务端在同一个接口里提供这些额外的数据,但这些数据并不总是需要的,这就违背了 REST 的设计,同时还导致了服务端的响应包体变大,造成网络传输的浪费。
GraphQL 提供了一个能够同时解决这两个问题的方案. 它允许客户端在一个请求里指明所需要的数据,还可以实现在一个请求里发送多个查询。
它工作起来更像是 RPC 服务,它不用固定的 verb,而是使用 命名查询(Query) 和 命名修改(Mutation) 的方式。 这就让业务编写的控制权回到了它应有的地方,API 接口的开发者来确定哪些接口行为是被允许的, API 接口的使用者在运行时动态指明他们想要什么数据 。
举个例子,一个 blog 可能会有如下的查询请求:
query { recentPosts(count: 10,offset: 0) { id title category author { id name thumbnail } } } 复制代码
这个查询请求将会:
在传统的 REST API 里,这要么需要发送11个请求 —— 1个接口用来请求文章列表,另10个接口请求相对应的作者。或者需要在服务端 /posts 的接口里把作者的信息都包含进去。
GraphQL 服务会提供一个 schema 来完整描述所有的 API 接口。这个 schema 文件包含了具体的数据类型定义(type)。每个数据类型会有一个或多个字段(field),每个字段会有0到多个参数(parameter),以及对应的返回类型(type)。
通过这些字段的嵌套组合形成了一个图数据结构(也就是 GraphQL 里 Graph 的含义)。整个图不需要避免环,出现环也是完全可以接受的,但一定是有向图。也就是说,客户端可以通过一个类型节点的字段找到它的子节点,但是无法通过子节点直接反向找到父亲节点(除非在 schema 里单独定义出来)。
举刚才 blog 的例子,它包含了如下的类型定义:一个 Post 结构,Post 里 author 字段对应的 Author结构,以及从根查询(Root Query)节点上通过 recentPosts 字段来找出最新的 Post
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 { writePost(title: String!,text: String!,category: String) : Post! } 复制代码
一些字段类型后面带有 "!" 意味着这个字段是非空的,如果没有的话就说明是可选的。在我们请求接口的时候,如果对应的可选字段服务器返回了空对象,GraphQL也能够正确处理后续的查询,比方说 recentPosts 接口里几篇文章的 category 是空。
GraphQL 服务也通过接口暴露出了 schema,这样客户端就可以提前获取 schema 定义便于处理。这使得当 schema 改变了的时候,客户端能够自动发现并动态调整数据结构。一个很有用的场景就是可以使用 GraphiQL 工具(注意中间多了一个 i)来和服务端进行交互(类似于 Postman 等 REST Client)。
Spring Boot GraphQL Starter 提供了一个便捷的方式让我们快速地运行起一个 GraphQL 服务 . 它和 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> 复制代码
Spring Boot 就会自动设置好相应的 handler默认情况下,GraphQL 服务会通过 /graphql 接口暴露出来,通过 POST 该接口就可以发送对应请求,接口地址可以在 application.properties 里修改。
GraphQL Tools 可以通过处理 GraphQL Schema 文件来生成正确的结构对象,并绑定到对应的 bean 对象上。这些schema文件只要以 “. graphqls ” 扩展名结尾并存在于 classpath里,Spring Boot GraphQL starter 就可以自动找到这些 schema 文件,所以我们完全可以把这些文件按模块划分管理。 但我们只能有一个根查询,也必须有一个根查询的定义。而 Mutation 定义可以没有或者有一个。这个限制是源于 GraphQL Schema 规则,而不是因为 Java 无法实现。
根查询需要通过在Spring里定义特殊的 java bean 对象,从而来处理不同的字段查询。它不像 schema 的定义文件,这些bean对象可以有多个。我们只需要实现 GraphQLQueryResolver 接口,然后在 schema 里的每个字段都有相对应名字的属性或函数就可以了。
public class Query implements GraphQLQueryResolver { private PostDao postDao; public List<Post> getRecentPosts(int count,int offset) { return postsDao.getRecentPosts(count,offset); } } 复制代码
这些属性或函数会按如下的规则顺序查找:
如果schema里对应字段有定义参数的话,这些函数也需要按照对应的顺序来定义(像 count 与 offset),函数参数最后可以有一个可选的 DataFetchingEnvironment 类型的参数,来获取一些上下文信息。 这些函数的返回值也需要和 schema 里对应起来,一会儿我们就会看到。所有的原生类型 String,Int,List,等等 都可以和相应的 Java 类型对应起来。
像上面的这个 getRecentPosts 方法就会对应上 GraphQL schema 里 recentPosts 这个查询字段。
在 GraphQL 服务里,无论是在根节点还是任何一个结构里,所有复杂的类型都可以对应上 Java Bean 对象。每一个 GraphQL 类型只能有一个 Java 类来对应,但 Java 的类名并不一定要和 GraphQL 里的类型名一样 Java bean 里的属性名会被映射成 GraphQL 返回数据的字段名
public class Post { private String id; private String title; private String category; private String authorId; } 复制代码
在 Java bean 里的属性和方法如果在 GraphQL schema 找不到相应定义的都会被直接忽略掉而不会出什么问题。这个机制可以用来处理一些复杂情况。 举例来说,这里的 authorId 在 schema 里并没有任何的定,所以在接口里就不会出现,但它可以在接下来的步骤里使用:
有时候,一个字段的数据并不能直接访问,它有可能涉及到数据库查询,复杂的计算,或者一些别的什么情况。 GraphQL Tools 有一套机制来处理这些场景 它可以用 Spring 的 Bean 对象来为这些普通 Bean 提供数据。
我们通过使用普通 Bean 名字后面加上 Resolver ,然后再实现 GraphQLResolver 接口,就可以使用 Spring Bean 来为普通 Bean 提供额外的字段解析,然后在 Spring Bean里的方法都需要遵循上面的命名规则,唯一的区别是这些方法的第一个参数会是普通Bean对象。 如果字段同时存在于普通 Bean 和 Resolver上的话,Resolver上的会被优先采用。
@Repository public class PostResolver implements GraphQLResolver<Post> { @Autowired private AuthorDao authorDao; public Author getAuthor(Post post) { return authorDao.getAuthorById(post.getAuthorId()); } } 复制代码
这些 Resolver 会被 Spring 上下文加载,所以这可以使得我们可以使用很多 Spring 的策略,比方说注入 DAO。
和上面一样, 如果客户端并没有请求对应的字段的话,GraphQL 将不会去获取相应的数据 。这就意味着如果客户端去获取了一个 Post 但没有要请求 author 字段,那么 Resolver 里的 getAuthor() 方法将不会被调用,从而相应的 DAO 请求也不会发出。
GraphQL Schema 有 Optional 的概念,一些类型可以是可选为空的,另一些就是非空的。 这在 Java 里可以直接使用 null 来表示和处理,相应的,如果是在 Java 8 环境下,可以使用 Optional 类型来表示可选,无论是哪种方式,系统都能正确处理。这个机制就让我们的 GraphQL schema 能和 Java 代码更好地对应起来。
到现在为止,我们一直在讨论从服务端获取数据,GraphQL 同样还可以更新服务端的数据,在 GraphQL 里就是 Mutation。 从代码的角度来说,一个 Query 请求没理由不能直接修改数据,我们可以很容易用 Query Resolver 来接受一些参数然后修改数据,最后返回给客户端,在这里采用 Mutation 主要是为了更好地规范。
相应地, 修改(Mutation)接口 应该仅用于告知客户端该操作会改变服务端存储数据 在 Java 代码里,我们只需要把 GraphQLQueryResolver 接口换成 GraphQLMutationResolver 就可以定义一个根修改接口,其它的所有规则都和查询接口一样,修改接口的返回值就和查询接口一样,可以嵌套等。
public class Mutation implements GraphQLMutationResolver { private PostDao postDao; public Post writePost(String title,String text,String category) { return postDao.savePost(title,text,category); } } 复制代码
GraphQL 经常会和 GraphiQL 一起使用,GraphiQL 是一个可以直接和 GraphQL 服务交互的 UI 界面,可以执行查询和修改请求,可以从 这里 下载独立的基于 Electron 的 GraphiQL 应用。 在我们的应用里还可以直接集成基于 Web 版本的 GraphiQL,我们只需要加入以下依赖
<dependency> <groupId>com.graphql-java</groupId> <artifactId>graphiql-spring-boot-starter</artifactId> <version>5.0.2</version> </dependency> 复制代码
就可以在 /graphiql 里看到,但这只适用于 graphql 接口在 /graphql 的默认情况,如果有调整就还需要独立客户端。