全面涵盖!彻底攻略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
エラーメッセージlocationslineエラーが起きたクエリの行数locationscolumnエラーが起きたクエリの列数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请求和响应的是拦截器。服务器的主要处理位于控制器、服务和存储库层,当在这些层中发生异常时,将使用异常处理器来处理错误。结果通过拦截器返回给客户端。根据不同的错误,处理的阶段也会有所不同。

chain.png

業務出错时的错误处理部分 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条目相关的方法的说明。

メソッド名(JSON エントリ名)引数の型errorTypeErrorClassificationmessageStringpathList<Object>location または locationsSourceLocation または List<SourceLocation>extensionsMap<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进行错误处理,但在此之前发生的异常无法进行错误处理。例如,

image.png

在这种情况下,无法进行错误处理。这种情况下会返回默认错误。如果想要处理这种错误,需要实现拦截器。

拦截器的实现步骤如下:

@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());
        });
    }
}

这段代码执行了以下操作。

    1. 如果响应中没有错误,就直接返回。

 

    如果存在错误,则将错误类型更改为BAD_REQUEST并返回。

需要注意的是,在拦截器中,不仅可以处理错误,还可以大幅度改变响应的格式。

总结

我介绍了如何处理使用Spring Boot实现的GraphQL服务器的错误处理方式。在处理时,我根据错误类型和错误发生的位置进行了分类。总结如下:

    1. 如果是业务错误,就在模式定义和数据部分附加错误内容。

 

    1. 如果是系统错误,则根据两种模式,在错误部分附加错误内容。

可以通过GraphQLExceptionHandler来处理在控制器层可以捕捉到的错误。
对于其他情况,可以通过实现拦截器来进行错误处理。

请提供参考资料。

最后,本文列出了写作时所参考的文献。

 

广告
将在 10 秒后关闭
bannerAds