GraphQl 使用 - java

去年在做内容库的时候,涉及到了多系统多数据源互相查询的情景,当时就想要将内容库的所有数据,增加统一的数据接入层,通过graphql的方式提供给上层业务使用,后面受限于人力没有实施。目前从头开始做站群相关的业务,顺带着浅尝了一下graphql,感觉还不错。

GraphQL 是什么

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

背景

参看: https://www.youtube.com/watch?v=Ah6GSCK5Rfs

graphql 和 rest api 是同一层的东西,都是一种基于 http 之上的数据接口协议,两者设计理念完全不同。目前来说 graphql 在很多大小厂也都开始使用,确实会方便很多。 但是 rest api 还是最广泛应用的协议,比如 AMP (T T)。 事实上任何东西都有2面性,对于graphql 来说,优缺点都有

与 REST API 相比的好处

  • 优点
    • 一次提供,多次使用
      • 作为后端,我们只需要关心数据字段的提供,不需要关心前端是怎么用这些字段,页面结构是怎样的
      • 真的不需要频繁的对接口了
    • 动态可扩展,无冗余查询,所见即所得
      • 因为前端只会请求需要的字段,增加新的字段后,不用担心老的业务请求这些冗余数据,很好的支撑了可扩展性
      • 所见即所得,能够帮助客户端代码不易出错
    • 多个字段,一次请求
      • 对于前端来说,面向schema编程,无需知道后端是多少个服务在支撑需要的这些字段,也无需频繁的更改接口去从新服务获取想要的字段,只需要在请求的时候增加一个 field 就好了
    • 代码即文档
      • 直接通过graphqli 等工具查看schema协议,不需要文档,写注释就好了
    • 类型校验
      • 在定义好schema的同时,就约束了接口类型,graphql支持强类型校验
  • 缺点
    • 基于post 请求
      • 传统方式无法监控(nginx)
      • 不能利用 http 自身的缓存机制
    • 没有充分利用http
      • 只使用了 post 请求,没有其他http的方法:方法的幂等性
      • 缺失了http状态码
    • 配套还不完善
      • 微服务
      • 监控
      • 分流

怎么用

几个基本概念

最好参看官方文档:https://graphql.github.io/graphql-spec/June2018/

中文版的可以查看:https://spec.graphql.cn/#sec-Overview-

概念很多,摘了几个出来,在实际业务场景中使用的话,还是有很多需要探的地方,这里只是大概的一个介绍

  • schema
    • 相当于协议文件,定义了所有对象的类型和查询接口
    • 一般定义的操作是2个: query/ mutation , 一个用于读,一个用于写,也存在subscription的操作
  • type
    • graphql 有自己的一套类型系统,有8种类型
      • scalars/标量
      • objects/对象
      • interfaces/接口
      • unions/联合
      • Enums/枚举
      • Input objects/输入类型
      • lists/列表
      • Non-null/非空型
    • graphql 有自己的基本类型,也可以自定义一些类型,但是需要相应的解释器
  • 内省 -> 验证 -> 执行 -> 响应
  • datafetcher
    • 实现过程中,定义了字段的获取方法

实操

具体的代码可以参看: https://github.com/xtestw/graphql-demo

  • 引入 graphql 的依赖包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>13.0</version>
    </dependency>
  • 定义 schema 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    schema {
    query: Query
    mutation: Mutation
    }
    scalar Date

    type Query {
    student(id:Int!):Student
    students(pagination:Pagination):[Student]
    }

    type Mutation {
    add(newStudent:NewStudent):Student
    }

    input Pagination{
    index: Int!
    size: Int!
    }

    input NewStudent{
    name:String!
    sex:Sex
    }

    type Student {
    id: Int
    name: String
    sex: Sex
    creation: Date
    }

    enum Sex{
    MALE,FEMALE
    }
  • 实现 schema 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    package com.xtestw.graphql.demo.schema.wiring;

    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.ImmutableMap;
    import com.xtestw.graphql.demo.schema.model.NewStudent;
    import com.xtestw.graphql.demo.schema.model.Pagination;
    import com.xtestw.graphql.demo.storage.Student;
    import com.xtestw.graphql.demo.storage.Student.Sex;
    import com.xtestw.graphql.demo.storage.repository.StudentRepository;
    import graphql.schema.DataFetchingEnvironment;
    import graphql.schema.idl.MapEnumValuesProvider;
    import graphql.schema.idl.TypeRuntimeWiring;
    import graphql.schema.idl.TypeRuntimeWiring.Builder;
    import java.util.Collections;
    import java.util.List;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;

    /**
    * Create by xuwei on 2019/8/4
    */
    @Component
    public class StudentWiring implements Wiring {

    @Autowired
    StudentRepository studentRepository;
    ObjectMapper mapper = new ObjectMapper();

    @Override
    public List<TypeRuntimeWiring> wireTypes() {
    return Collections.singletonList(
    TypeRuntimeWiring.newTypeWiring("Sex")
    .enumValues(new MapEnumValuesProvider(
    ImmutableMap.of(
    "MALE", Sex.MALE,
    "FEMALE", Sex.FEMALE
    )))
    .build());
    }

    @Override
    public void wireQueries(Builder queryBuilder) {
    queryBuilder.dataFetcher("student", this::fetchStudentById)
    .dataFetcher("students", this::fetchStudents);
    }

    private List<Student> fetchStudents(DataFetchingEnvironment dataFetchingEnvironment) {
    Pagination pagination = mapper
    .convertValue(dataFetchingEnvironment.getArgument("pagination"), Pagination.class);
    if (pagination == null) {
    pagination = Pagination.create(0, 20);
    }
    return studentRepository.findAll(pagination.toPageable()).getContent();
    }

    private Student fetchStudentById(DataFetchingEnvironment dataFetchingEnvironment) {
    Integer id = dataFetchingEnvironment.getArgument("id");
    return studentRepository.findById(id).orElse(null);
    }

    @Override
    public void wireMutations(Builder mutationBuilder) {
    mutationBuilder.dataFetcher("add", this::addNewStudent);
    }

    private Student addNewStudent(DataFetchingEnvironment dataFetchingEnvironment) {
    NewStudent newStudent = mapper
    .convertValue(dataFetchingEnvironment.getArgument("newStudent"), NewStudent.class);
    return studentRepository.save(newStudent.toStudent());
    }
    }

  • 构建 GraphqQL 对象实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package com.xtestw.graphql.demo.config;

import com.xtestw.graphql.demo.schema.ExtendedScalars;
import com.xtestw.graphql.demo.schema.wiring.Wiring;
import graphql.GraphQL;
import graphql.GraphQLException;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import graphql.schema.idl.TypeRuntimeWiring;
import graphql.schema.idl.errors.SchemaProblem;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;

/**
* Create by xuwei on 2019/8/3
*/
@Configuration
@Slf4j
public class GraphQLConfig {

@Value("classpath*:schemas/*.graphqls")
private Resource[] files;

@Bean
GraphQL graphQL(@Autowired GraphQLSchema graphQLSchema) {

return GraphQL.newGraphQL(graphQLSchema)
.build();
}


@Bean
GraphQLSchema graphQLSchema(@Autowired RuntimeWiring wiring) {
SchemaParser parser = new SchemaParser();
TypeDefinitionRegistry typeDefinitionRegistry = Arrays.stream(files).map(file -> {
try {
return file.getInputStream();
} catch (IOException e) {
log.error("Load graphql file error: {} - {}", file, e);
}
return null;
}).filter(Objects::nonNull)
.map(inputStream -> {
try {
return parser.parse(new InputStreamReader(inputStream));
} catch (SchemaProblem e) {
throw new GraphQLException(
String.format("Compile schema '%s' failed: %s", inputStream,
e.getErrors().stream().map(Object::toString).collect(Collectors.toList())), e);
}
}).reduce(new TypeDefinitionRegistry(), (all, cur) -> {
all.merge(cur);
return all;
});
return new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, wiring);
}

@Bean
RuntimeWiring wiring(@Autowired List<GraphQLScalarType> scalarTypes,
@Autowired List<TypeRuntimeWiring> types) {
RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();
if (scalarTypes != null) {
scalarTypes.forEach(builder::scalar);
}
if (types != null) {
types.forEach(builder::type);
}
return builder.build();
}

@Bean
List<GraphQLScalarType> scalarTypes() {
return Collections.singletonList(ExtendedScalars.GraphQLDate);
}

@Bean
List<TypeRuntimeWiring> types(@Autowired List<Wiring> wirings) {

TypeRuntimeWiring.Builder queryBuilder = TypeRuntimeWiring.newTypeWiring("Query");
TypeRuntimeWiring.Builder mutationBuilder = TypeRuntimeWiring.newTypeWiring("Mutation");
return wirings.stream().map(wiring -> {
wiring.wireQueries(queryBuilder);
wiring.wireMutations(mutationBuilder);
return wiring.wireTypes();
})
.reduce(new ArrayList<>(Arrays.asList(queryBuilder.build(), mutationBuilder.build())),
(all, cur) -> {
all.addAll(cur);
return all;
});
}

}

  • 定义接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    package com.xtestw.graphql.demo.controller;

    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.type.MapType;
    import com.xtestw.graphql.demo.schema.model.Query;
    import graphql.GraphQL;
    import java.io.IOException;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;
    import javax.annotation.Resource;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    /**
    * Create by xuwei on 2019/8/3
    */
    @RestController
    @Slf4j
    @RequestMapping("/graphql")
    @CrossOrigin
    public class GraphQLController {

    @Resource
    GraphQL graphQL;

    @PostMapping(path = {""})
    private Object query(@RequestBody String queryStr) throws IOException {
    Query query = getQuery(queryStr);
    return graphQL.execute(query.toExecutionInput());
    }

    private static ObjectMapper mapper = new ObjectMapper();
    private static final MapType VARIABLES_TYPE = mapper.getTypeFactory()
    .constructMapType(HashMap.class,
    String.class, Object.class);

    private Query getQuery(String queryText) throws IOException {
    String operationName = null;
    String fullQueryText = queryText;
    Map<String, Object> variables = null;
    JsonNode jsonBody = mapper.readTree(queryText);
    if (jsonBody != null) {
    JsonNode queryNode = jsonBody.get("query");
    if (queryNode != null && queryNode.isTextual()) {
    queryText = queryNode.asText();
    }
    JsonNode operationNameNode = jsonBody.get("operationName");
    if (operationNameNode != null && operationNameNode.isTextual()) {
    operationName = operationNameNode.asText();
    }
    JsonNode variablesNode = jsonBody.get("variables");
    if (variablesNode != null) {
    if (variablesNode.isTextual()) {
    String variablesJson = variablesNode.asText();
    variables = mapper.convertValue(mapper.readTree(variablesJson), VARIABLES_TYPE);
    } else if (variablesNode.isObject()) {
    variables = mapper.convertValue(variablesNode, VARIABLES_TYPE);
    }
    }
    }
    if (variables == null) {
    variables = Collections.emptyMap();
    }
    return new Query(fullQueryText, queryText, operationName, variables);
    }
    }

  • 测试

image-20190804164253298

image-20190804164354974

image-20190804164517506

总结

graphql 相比较 restapi 来说,各有优缺点。个人感觉 graphql的前景还是很大的,目前最大的问题其实还是相关的生态和基础设施还不够完善,也存在很大的迁移成本和学习成本。不过单纯从数据获取的角度来说,非常有优势!此处我们只做了一个非常浅的探索。