Spring Boot 搭建 Graphql 服务

3,204 阅读4分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

介绍

GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。1

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。2

服务端提供定义好的 schema (数据类型,代表能够提供什么格式的数据),客户端则通过查询语句来主动决定想要的数据格式,数据格式的主动权掌握在了前端,类似数据库中的表和 SQL 的关系,可以查询单表的某个或多个字段,或者多表的联合字段

  • GraphQL 能使得前端在开发过程中不在强依赖后端服务的接口和数据格式,前后端的开发是解耦的,能够极大地减少前后端开发人员的沟通成本
  • GraphQL 会根据 schema 自动生成 API 文档

实践

本示例基于 Spring Boot 2.2.6 与 Graphql Java 16.1

Spring Boot 2.2 以下版本搭配 GraphQL Java Tools 5.4.x 以上的版本时需要指定 kotlin 的版本

WARNING: NoClassDefFoundError when using GraphQL Java Tools > 5.4.x

引入依赖

<!-- com.graphql-java-kickstart 为最新的 groupId, com.graphql-java 已经停止更新 -->
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>11.0.0</version>
</dependency>
<!-- 可视化调试工具 altair -->
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>altair-spring-boot-starter</artifactId>
    <version>11.0.0</version>
    <scope>runtime</scope>
</dependency>

从 maven 仓库可以对比看到 com.graphql-java 的更新时间还是 2018 年

创建 schema 文件

根据不同的业务对象创建不同的 graphqls 文件,并将 Query 和 Mutation 相关 schema 置于 root.graphqls 文件中

# 指定自定义 graphqls 文件路径, 默认为 classpath 下的所以 **/*.graphqls 文件
# 其它配置都是默认开箱即用,无必要无需调整
graphql:
  tools:
    schemaLocationPattern: graphqls/*.graphqls

JS Graphql 插件支持识别 *.graphql/*.graphqls 文件,可以在声明的 query 和 type 之间的快速跳转,该插件支持 WebStorm、IDEA 等多个相关系列产品

Query(读)

schema

# root.graphqls
schema {
    query: Query
}

type Query {
    # 需要与 GraphQLQueryResolver 方法签名对应
    listAuthors(query: AuthorQuery): [AuthorVO]
    getAuthor(id: Int): AuthorVO
}

# author.graphqls
type AuthorVO {
    id: Int
    name: String
    title: String
    school: String
}

input AuthorQuery {
    name: String
    title: String
    school: String
}

Code

@Service
public class AuthorQueryResolver implements GraphQLQueryResolver {

    private final AuthorRepository repository;

    public AuthorQueryResolver(AuthorRepository repository) {
        this.repository = repository;
    }

    public List<AuthorVO> listAuthors(AuthorQuery query) {
        List<AuthorDO> list = repository.select(query);
        return convertAll(list, AuthorVO.class);
    }

    public AuthorVO getAuthor(Integer id) {
        AuthorDO entity = repository.selectById(id);
        return convert(entity, AuthorVO.class);
    }
}

Run

# 可以通过 Docs 侧边栏的 ADD QUERY 按钮 快速生成 Query 语句
query{
  getAuthor(id: 1){
    id
    name
    title
    school
  }
}

query.gif

Mutation(写)

schema

mutation{
  saveAuthor(author: {
    name: "Tomy"
    title: "Teacher"
    school: "BU"  
  })
}

Code

@Service
public class AuthorMutationResolver implements GraphQLMutationResolver {

    private final AuthorRepository repository;

    public AuthorMutationResolver(AuthorRepository repository) {
        this.repository = repository;
    }

    public Integer saveAuthor(AuthorDTO dto) {
        AuthorDO entity = convert(dto, AuthorDO.class);
        repository.insert(entity);
        return entity.getId();
    }

    public Boolean updateAuthor(Integer id, AuthorDTO dto) {
        AuthorDO entity = convert(dto, AuthorDO.class);
        entity.setId(id);
        return repository.update(entity);
    }

}

image.png

分页查询

schema

# root.graphqls
type Query {
    # .. other query
    pageBook(query: BookQuery): BookConnection
}


type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
}

# book.graphqls
type BookEdge {
    node: BookVO!
    cursor: String!
}

type BookConnection {
    edges: [BookEdge!]!
    pageInfo: PageInfo!
}

Code

// com.jingwu.example.graphql.BookQueryResolver#pageBook
public Connection<BookVO> pageBook(BookQuery query) {
    IPage<BookDO> page = repository.selectPage(query);

    List<Edge<BookVO>> edges = page.getRecords()
            .stream()
            .map(book -> new DefaultEdge<>(
                    convert(book, BookVO.class),
                    new DefaultConnectionCursor(String.valueOf(book.getId())))
            )
            .collect(Collectors.toList());

    PageInfo pageInfo =
            new DefaultPageInfo(
                    GraphqlUtils.getStartCursorFrom(edges),
                    GraphqlUtils.getEndCursorFrom(edges),
                    page.getCurrent() > 1 && page.getCurrent() <= page.getPages(),
                    page.getPages() > page.getCurrent());

    return new DefaultConnection<>(edges, pageInfo);
}

这里用法被当成了普通的分页查询,好像也没啥问题,只要是返回了 Connection 对象就可

没有太理解 Connection 这一种方式,也并没找到使用它原因,官方的说明比较简单;除此仍然可以自定义 Page Response 对象返回分页数据

其它参考:

其它

在 WebMVC 中的拦截器/过滤器、全局异常处理、Validation 校验等等使用方式基本上是一样的

认证过滤器

@Component
public class AuthFilter extends OncePerRequestFilter {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (doAuth(request)) {
            response.setContentType("application/json;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            ExecutionResultImpl build = ExecutionResultImpl.newExecutionResult()
                    .addError(new GenericGraphQLError("认证失败"))
                    .data(null)
                    .build();
            response.getWriter().write(objectMapper.writeValueAsString(build.toSpecification()));
            return;
        }
        filterChain.doFilter(request, response);
    }

    private boolean doAuth(HttpServletRequest request) {
        // mock auth result
        return RandomUtil.randomBoolean();
    }
}

调试工具

Altair 除了 Maven 依赖引入,还有对应的 Chrome 插件,个人感觉比 Maven 依赖稳定好用

Altair GraphQL Client

除了 Altair,还有以下两种

<!-- to embed GraphiQL tool -->
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>11.0.0</version>
    <scope>runtime</scope>
</dependency>

<!-- to embed Voyager tool -->
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>voyager-spring-boot-starter</artifactId>
    <version>11.0.0</version>
    <scope>runtime</scope>
</dependency>

项目地址:gitee.com/jingwua/spr…

参考

Footnotes

  1. GraphQL 入门

  2. 一种用于 API 的查询语言