全面涵盖!彻底攻略Spring for GraphQL的错误处理!!
首先
我想介绍一下在使用Spring Boot实现GraphQL服务器时如何处理错误。
从GraphQL的一般错误处理方法开始,我将解释在Spring Boot中根据该策略实施的方法。
希望吸引的读者
本文旨在针对符合以下条件的人群作为预期读者:
-
- GraphQLサーバでエラーハンドリングを行いたい人
- GraphQLの基礎知識を理解している人
-
- Spring Bootでサーバを構築した経験がある人
- Spring BootでGraphQLサーバを構築した経験がある人
示例代码(参考)
本篇文章不着重于已实施的内容,但在阅读时运行示例代码可以加深理解。这次我们是通过修改和扩展Spring for GraphQL官方提供的代码进行实施。官方代码用于构建一个从GraphQL查询中获取书籍信息和作者信息的服务器。
GitHub存储库中的示例代码
你可以在GitHub上查看已经实施的代码。有关如何运行的详细方法,请参考GitHub存储库。
变更点如下所示。
-
- Controller, Service, Repository層の3層アーキテクチャによる実装に変更
-
- 各層のデータのやり取りをDTOによって行うように変更
- エラーに関するコードの追加
环境
环境的现状如下。
-
- JDK: OpenJDK 17
-
- Spring Boot: 3.1.5
- Gradle: 8.4
GraphQL的错误
简要说明
错误可以大致分为两类。
-
- 業務エラー
- システムエラー
希望对于每个错误都能相应地改变错误处理的方式。在接下来的章节中,会详细讨论每个错误。
业务错误 (yè wù cuò wù)
業務錯誤是指能夠通過使用者端操作的更改來恢復的錯誤。下列情況屬於業務錯誤。
-
- 必須項目に項目が指定されていない場合に発生するエラー
-
- 入力された項目の型が不正な場合に発生するエラー
- すでに登録されている項目を入力したときに発生するエラー
如果出现业务错误,客户端可以再次提示用户进行输入,并通过访问服务器来解决。
系统错误
系统错误指的是用户无论进行何种操作都无法恢复的错误。以下情况属于系统错误。
-
- サーバがダウンしている際のエラー
- サーバがデータベースへのアクセスに失敗したときに発生するエラー
当系统发生错误时,需要服务器管理者通过查看错误日志等来解决。
错误响应的方针
在GraphQL中,除了将要获取的数据放在data部分之外,当发生错误时还会将错误信息存储在error部分。GraphQL推荐使用HTTP状态码200(OK),原因如下。
-
- error部にエラーメッセージを入れることができるので、ステータスコードを指定するメリットがほとんどないため
- 一つのリクエストで複数の操作が可能で、複数のステータスコードが異なるエラーが発生する可能性があるため
GraphQL 的标准错误格式。
GraphQL错误部分有一个标准格式。
格式如下:
{
"errors": [
"message": "error message"
"locations": {
"line": "15"
"column": "10"
}
"path": []
"extensions": {
"classification": "errorType"
...
}
]
}
如果将各项目简单总结到表格中,如下所示:
message
エラーメッセージ
locations
line
エラーが起きたクエリの行数locations
column
エラーが起きたクエリの列数path
エラーが起きたフィールドのパス情報
extensions
ユーザが定義できる拡張項目
在默认错误消息中,extensions的子条目classification会显示为错误类型。
在业务错误中设计错误响应。
GraphQL的错误部分无法进行模式定义,因此客户端很难根据错误部分的数据灵活应对。因此,在可恢复的业务错误中,有一个使用数据部分而不使用错误部分的策略。这种策略的优点包括以下几点:
–
-
- クライアントとサーバの干渉が少なくエラーハンドリングの実装ができる
-
- クライアントは欲しいエラー情報のみを指定することができる
-
- GraphQLのdata部だけを見てエラーハンドリングできる
-
- 各エラーに対して柔軟に定義できる
- 警告レスポンスも定義できる
有关GraphQL错误的模式定义
GraphQL的错误定义如下所示。由于可能发生多个业务错误,错误会被集合起来。
type SelectBookPayload {
book: Book!
errors: [GraphQLError]
}
interface Error {
message: String!
}
type InputValidationError implements Error {
message: String!
validationType: String
}
type NotFoundError implements Error {
message: String!
resourceId: Int
}
union GraphQLError = InputValidationError | NotFoundError
您可以通过定义union类型的错误来定义错误列表。有关union的详细信息,请参阅以下文档。
关于业务错误的查询
根据先前定义的模式,基于这个模式的查询如下。根据每个错误类型选择获取的数据。由于errors是一个联合类型的集合,因此需要根据每个具体类型选择要获取的字段(…在on后面指定)。
query GetBook {
bookById(id: "book-1") {
book {
name
pageCount
author {
firstName
lastName
}
}
errors {
... on InputValidationError {
message
validationType
}
... on NotFoundError {
message
resourceId
}
}
}
}
有關業務錯誤的回應
根据先前定义的模式,来自GraphQL服务器的响应如下。
{
"data": {
"bookById": {
"book": null,
"errors": [
{
"message": "Custom Not Found Error",
"resourceId": "book-1"
},
{
"message": "Custom Input Validation Error",
"validationType": "miss type input: book-1"
}
]
}
}
}
在系统错误中设计错误响应。
如果发生系统错误,我们将在error部分记载错误的详细信息。将系统错误记录在error部分的好处如下:
-
- extensionsエントリにサーバ側が自由に設定したデータを入れることができる
- クライアント側のエラーハンドリングは基本的にdata部だけ見て処理すればよくなる
关于系统错误的查询
系统错误不需要特定的模式定义。因此,有关查询的信息也不包含特定的错误,如下所示。
query GetBook {
bookById(id: "book-1") {
book {
name
pageCount
author {
firstName
lastName
}
}
}
}
关于系统错误的响应
当系统发生错误时,错误信息会包含在error字段中。默认情况下,错误信息为INTERNAL_ERROR,如下所示。如果能够获取到data字段的部分数据,那么部分数据将会返回。由于所有错误都被归为INTERNAL_ERROR,因此GraphQL服务器需要修改error字段以便进行相应处理。
{
"errors": [
{
"message": "INTERNAL_ERROR for df936d34-ec18-732a-1f3e-a2564cc06d29",
"locations": [
{
"line": 37,
"column": 7
}
],
"path": [
"bookById",
"book",
"author"
],
"extensions": {
"classification": "INTERNAL_ERROR"
}
}
],
"data": {
"bookById": {
"book": {
"id": "book-1",
"pageCount": 416,
"author": null
}
}
}
}
错误处理部分
在Spring中构建GraphQL服务器的配置如下图所示。控制GraphQL请求和响应的是拦截器。服务器的主要处理位于控制器、服务和存储库层,当在这些层中发生异常时,将使用异常处理器来处理错误。结果通过拦截器返回给客户端。根据不同的错误,处理的阶段也会有所不同。
業務出错时的错误处理部分 shí de
业务错误无法完全通过拦截器或异常处理来处理,因为它们无法将错误信息放入data部分。拦截器的处理方式不适合本次错误响应策略,因为它可以将错误信息放入而不依赖于模式定义。此外,稍后会详细解释,GraphQL的ExceptionHandler只返回error部分,无法将错误信息放入data部分。因此,我们将在Controller、Service或Repository层中使用try-catch语句等进行错误处理。
系统错误时的错误处理部分
在控制器(Controller)、服务(Service)和仓库(Repository)层内的处理基本上只针对GraphQL的数据部分,因此不适合处理错误部分。因此,系统错误的处理,包括针对在控制器、服务和仓库层发生的异常,要使用异常处理程序(ExceptionHandler)来处理。其他异常情况则使用拦截器(Interceptor)来处理。
在Spring Boot的实现中处理业务错误
業務錯誤的定義
我們將實現與GraphQL響應數據部分相同的代碼。首先,我們將定義一個POJO來對應GraphQL的模式定義類。GraphQLError類的對象將對應到GraphQL中的接口和聯合。
public class GraphQLError {
private String message;
}
public class GraphQLInputValidationError extends GraphQLError {
private String validationType;
public GraphQLInputValidationError(String message, String validationType) {
super(message);
this.validationType = validationType;
}
}
public class GraphQLNotFoundError extends GraphQLError {
private final String resourceId;
public GraphQLNotFoundError(String message, String resourceId) {
super(message);
this.resourceId = resourceId;
}
}
如果在POJO中定义了业务错误,则进行响应的Payload定义。
@Data
@Builder
public class SelectBookPayload {
private Book book;
private List<GraphQLError> errors;
}
对于错误的Union类型处理
由于GraphQL的Union类型与Java中的类没有对应关系,如果没有进行任何设置,将会出现以下错误。
java.util.concurrent.CompletionException: graphql.AssertException: You have asked for named object type 'GraphQLError' but it's not an object type but rather a 'graphql.schema.GraphQLUnionType'
因此,需要在Config文件中设置GraphQL的Union类型对应的类。下面的代码是创建名为GraphQLConfig.java的Config文件,并进行了Union类型和Java类的对应关系的示例代码。
@Configuration
public class GraphQLConfig {
@Bean
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return (builder) -> {
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.addMapping(CustomNotFoundGraphQLError.class, "NotFoundError");
classNameTypeResolver.addMapping(CustomInputValidationGraphQLError.class, "InputValidationError");
builder.defaultTypeResolver(classNameTypeResolver);
};
}
}
实现代码的概要如下。
-
- JavaのCustomNotFoundGraphQLErrorクラスをGraphQLスキーマのNotFoundErrorと紐づける
- JavaのCustomInputValidationGraphQLErrorクラスをGraphQLスキーマのInputValidationErrorに紐づける
这个`sourceBuilderCustomizer`方法的代码是用来扩展模式的功能。它接收一个`SchemaResourceBuilder`类的对象`builder`,并对`builder`进行扩展。
捕捉业务错误
在Controller、Service和Repository层处理业务错误。由于目前没有关于业务错误的最佳实践,所以这里只提供一个实现示例。使用try-catch语句将错误信息存入数据部分的error类型中。通过使用try-catch语句,可以捕获多个异常,使客户端能够获取多个错误。
public SelectBookRepositoryOutDto bookById(SelectBookRepositoryInDto selectBookRepositoryInDto) {
Book foundBook = null;
List<CustomGraphQLError> errors = new ArrayList<>();
try {
foundBook = books.stream()
.filter(book -> book.getId().equals(selectBookRepositoryInDto.getId()))
.findFirst()
.orElse(null);
} catch(Exception e) {
errors.add(new CustomNotFoundGraphQLError("book is not found", 404));
}
return SelectBookRepositoryOutDto.builder()
.book(foundBook)
.errors(errors)
.build();
}
Spring Boot的实现在系统错误中。
为GraphQL的错误部分创建对应的Java对象。
在Spring Boot中,错误部分的数据可以通过GraphQLError对象进行实现。GraphQLError对象可以通过以下方式定义和生成。newError方法对应于builder()方法。因此,只需要在方法链中设置需要配置的字段,然后生成对象。
GraphQLError graphQLError = GraphQLError.newError()
.errorType(ErrorType.INTERNAL_ERROR)
.message(e.getMessage())
.path(List.of("path1")) // オプション
.locations(List.of(new SourceLocation(1,10))) // オプション
.extensions(Map.of("classification", "errorType")) // オプション
.build();
在下表中展示了与每个JSON条目相关的方法的说明。
errorType
ErrorClassification
message
String
path
List<Object>
location
または locations
SourceLocation
または List<SourceLocation>
extensions
Map<String, Object>
接下来,我们将详细讨论类型的细节。
错误分类
errorType方法的参数是ErrorClassification接口,但是不能直接使用。在使用时需要使用枚举类型ErrorType。GraphQL的ErrorType有两种存在。
graphql.ErrorType
エラー時のデフォルトのerror部のextensionsエントリのclassificationに入っている
InvalidSyntax, ValidationError, DataFetchingException, NullValueInNonNullableField, OperationNotSupported, ExecutionAborted
org.springframework.graphql.execution.ErrorType
HTTPレスポンスのステータスコードに対応
BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_ERROR
来源地点
SourceLocation类是用于设置发生错误的查询的详细位置的类。该类的字段变量有line和column。作为记录日志的第三个字段变量,还有sourceName。根据情况选择使用哪个构造函数。
@PublicApi
public class SourceLocation implements Serializable {
public static final SourceLocation EMPTY = new SourceLocation(-1, -1);
private final int line;
private final int column;
private final String sourceName;
public SourceLocation(int line, int column) {
this(line, column, null);
}
public SourceLocation(int line, int column, String sourceName) {
this.line = line;
this.column = column;
this.sourceName = sourceName;
}
...
}
使用 GraphQLExceptionHandler 进行实现。
在使用REST API时,可以使用ExceptionHandler来处理错误,同样地,在GraphQL中也可以使用GraphQLExceptionHandler来轻松处理错误。
@ControllerAdvice
public class GraphQLExceptionHandler {
@GraphQlExceptionHandler
public GraphQLError exceptionHandle(Exception e) {
return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message(e.getMessage()).build();
}
}
有另一种方法来实现异常解析并进行处理,这就是实现例外解析器的方法。虽然具体内容略有不同,但可以按以下步骤实施和使用。首先,我们需要覆盖`resolveToSingleError`方法并进行定义。
CustomExceptionResolver.java
@Component
public class CustomExceptionResolver extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError(@NonNull Throwable e, @NonNull DataFetchingEnvironment env) {
if (e instanceof RuntimeJsonMappingException) {
return GraphQLError.newError().message(e.getMessage()).errorType(ErrorType.FORBIDDEN).build();
}
return GraphQLError.newError().message(e.getMessage()).errorType(ErrorType.INTERNAL_ERROR).build();
}
}
使用拦截器进行实施
在Controller、Service和Repository层发生的异常可以通过GraphQLExceptionHandler进行错误处理,但在此之前发生的异常无法进行错误处理。例如,
在这种情况下,无法进行错误处理。这种情况下会返回默认错误。如果想要处理这种错误,需要实现拦截器。
拦截器的实现步骤如下:
@Component
public class RequestErrorInterceptor implements WebGraphQlInterceptor {
@Override
public @NonNull Mono<WebGraphQlResponse> intercept(@NonNull WebGraphQlRequest request, Chain chain) {
return chain.next(request).map(response -> {
if (response.isValid())
return response;
List<GraphQLError> errors = response.getErrors().stream()
.map(error -> GraphQLError.newError()
.errorType(ErrorType.BAD_REQUEST)
.message(error.getMessage())
.extensions(error.getExtensions())
.locations(error.getLocations())
.build()).toList();
return response.transform(builder -> builder.errors(errors).build());
});
}
}
这段代码执行了以下操作。
-
- 如果响应中没有错误,就直接返回。
- 如果存在错误,则将错误类型更改为BAD_REQUEST并返回。
需要注意的是,在拦截器中,不仅可以处理错误,还可以大幅度改变响应的格式。
总结
我介绍了如何处理使用Spring Boot实现的GraphQL服务器的错误处理方式。在处理时,我根据错误类型和错误发生的位置进行了分类。总结如下:
-
- 如果是业务错误,就在模式定义和数据部分附加错误内容。
-
- 如果是系统错误,则根据两种模式,在错误部分附加错误内容。
可以通过GraphQLExceptionHandler来处理在控制器层可以捕捉到的错误。
对于其他情况,可以通过实现拦截器来进行错误处理。
请提供参考资料。
最后,本文列出了写作时所参考的文献。