Spring GraphQL 是今年 Spring 新发布的一个集成 Graphql 的 Spring Application,凭借着 Spiring boot 开箱即用的特性,能够非常快地构建出一个可用的 Graphql 服务。然而尽管可用,但其文档 实在是写得一言难尽,尤其是对于一些进一步的功能来说,需要靠自己看 API 文档来摸索。
以下记录使用的相关情况
Setup可以通过构建 Spring boot 项目来进行初始化
使用 Gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// Spring GraphQL Boot starter
implementation 'org.springframework.experimental:graphql-spring-boot-starter:1.0.0-SNAPSHOT'
// ...
}
repositories {
mavenCentral ()
maven { url 'https://repo.spring.io/milestone' } // Spring milestones
maven { url 'https://repo.spring.io/snapshot' } // Spring snapshots
}
或者使用 Maven
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
<dependencies>
<dependency>
<groupId> org.springframework.boot</groupId>
<artifactId> spring-boot-starter-web</artifactId>
</dependency>
// Spring GraphQL Boot starter
<dependency>
<groupId> org.springframework.experimental</groupId>
<artifactId> graphql-spring-boot-starter</artifactId>
<version> 1.0.0-SNAPSHOT</version>
</dependency>
<!-- ... -->
</dependencies>
<!-- For Spring project milestones or snapshot releases -->
<repositories>
<repository>
<id> spring-milestones</id>
<name> Spring Milestones</name>
<url> https://repo.spring.io/milestone</url>
</repository>
<repository>
<id> spring-snapshots</id>
<name> Spring Snapshots</name>
<url> https://repo.spring.io/snapshot</url>
<snapshots>
<enabled> true</enabled>
</snapshots>
</repository>
</repositories>
导入依赖后,将 Graphql Schema 文件放到 resources/graphql
目录下并进行相应的配置即可运行,运行后在设定的路径可以看到网页版的 playground
1
2
3
4
5
6
7
8
9
spring :
graphql :
graphiql : # 提供辅助用的网页端 playground
enabled : true
path : /
schema : # 提供 schema
printer :
enabled : true
path : /query # Graphql endpoint
Query and MutationSchema 中提供的 query 和 mutation 通过注解映射到 Controller 中方法进行处理,并返回相应的数据。此处默认使用的 convention 的方式,只要方法的名字及其参数的名字与 Schema 声明的一致则可自动映射,否则也可以通过注解的参数进行设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class TodoController {
@QueryMapping
public List < TodoItem > todoItems ( GraphQLContext cxt ) {
var u = getUserInContext ( cxt );
return todoService . getTodoItemsByUser ( u );
}
@MutationMapping
public TodoItem createTodoItem ( @Argument TodoItemInput todo , GraphQLContext cxt ) {
var user = getUserInContext ( cxt );
return todoService . createTodoItem ( todo , user );
}
}
方法的参数支持
DataFetchingEnvironment : 包含本次查询当中丰富的相关信息,包括查询的变量,context等信息通过注解 @Argument
可以为参数绑定传入的相应变量 GraphQLContext: 用于存储访问上下文中相应数据内容 Authorization怎样进行权限认证,文档中只是简单地给了段文字介绍说可以用 Spring Security 来进行权限认证。然而我的需求也没有这么复杂,只需要简单地拦截请求,查看有无携带 token 访问,如果带 token 的访问则将其转换成相应的用户并给后续交给后续的 Controller 来进行处理。
此处我使用的是 WebInterceptor 来进行请求的拦截处理,主要的思路为:
拦截每个请求,尝试取请求中 header 中的 Authorization 字段 如果没有这个字段,则跳过 如果有这个字段,则尝试根据 token 取出相应的 User,如果找不到,则抛出无效 token 的错误 找到后将该 User 对象加入到 graphQLContext 当中供 Controller 使用。 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
@Component
public class MyInterceptor implements WebInterceptor {
private final UserService userService ;
@Autowired
public MyInterceptor ( UserService userService ) {
this . userService = userService ;
}
@Override
public Mono < WebOutput > intercept ( WebInput webInput , WebGraphQlHandler next ) {
var authHeader = webInput . getHeaders (). get ( "Authorization" );
var q = webInput . toExecutionInput ();
if ( Objects . isNull ( authHeader )|| authHeader . size ()== 0 ) {
return next . handle ( webInput );
}
var user = userService . getUserByToken ( authHeader . get ( 0 ));
if ( user . isEmpty ()) {
return Mono . just ( new WebOutput ( webInput ,
new ExecutionResultImpl ( GraphqlExceptionBuilder . genError ( GraphqlExceptionBuilder . ErrorType . INVALID_TOKEN ))));
}
webInput . configureExecutionInput ((( executionInput , builder ) -> {
Map < String , Object > context = new HashMap <>() {{
put ( "user" , user . get ());
}};
return builder . graphQLContext ( context ). build ();
}));
return next . handle ( webInput );
}
}
在后续的 controller 方法当中,如果需要使用到用户信息或者需要用户权限,则直接从 graphQLContext 当中取出相应的对象,如果取不到则抛出相应的 error
1
2
3
4
private User getUserInContext ( GraphQLContext cxt ) {
return ( User ) cxt . getOrEmpty ( "user" )
. orElseThrow (()-> GraphqlExceptionBuilder . genError ( GraphqlExceptionBuilder . ErrorType . FORBIDDEN ));
}
Error Handler在应用运行过程所有抛出的异常最终都会以 GraphQL 的形式返回给客户端。一开始我还以为是会直接将抛出的 GraphQLError 直接返回给用户,所以直接在代码内部抛出错误,企图通过 Controller 最终返回给用户。不过发现这样做的话,我在 GraphlQLErrorException 当中设置的 extensions 总会没有效果,而是全被归类为 INTERNAL_ERROR。
后来发现是所有的异常都会跑去一个 ErrorResolver 的东西处理,会重新把 Error 重新封装一次。所以我们必须自己写一个 ExceptionHandler 来将其覆盖
1
2
3
4
5
6
7
@Component
public class DataFetcherExceptionHandler extends DataFetcherExceptionResolverAdapter {
@Override
public GraphQLError resolveToSingleError ( Throwable ex , DataFetchingEnvironment env ) {
return ( GraphQLError ) ex ;
}
}
另外,GraphQLError 的生成我是通过自定义了一个 Builder 工具类来辅助生成,通过枚举类型来生成包含 message,error 的 Error
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
public class GraphqlExceptionBuilder {
public enum ErrorType {
DUPLICATED_USER ,
NO_USER ,
WRONG_PASSWORD ,
INVALID_TOKEN ,
FORBIDDEN ,
NO_ITEM ,
}
public static Map < String , Object > updateErrorCode ( ErrorType errorType ) {
return new HashMap <>() {{
put ( "code" , errorType . name ());
}};
}
public static GraphqlErrorException genError ( ErrorType errorType ) {
var err = GraphqlErrorException . newErrorException ();
switch ( errorType ) {
case DUPLICATED_USER :
err . message ( "existed username" );
break ;
case NO_USER :
err . message ( "not exist username" );
break ;
case WRONG_PASSWORD :
err . message ( "wrong password" );
break ;
case INVALID_TOKEN :
err . message ( "invalid token" );
break ;
case FORBIDDEN :
err . message ( "access forbidden" );
break ;
case NO_ITEM :
err . message ( "no such query items" );
break ;
}
err . extensions ( updateErrorCode ( errorType ));
return err . build ();
}
}