优秀的编程知识分享平台

网站首页 > 技术文章 正文

Spring 中的 GraphQL 错误处理(graphql springmvc)

nanyue 2024-09-01 20:40:13 技术文章 8 ℃

让我们讨论 Spring for GraphQL 中的错误处理。我们还将查看能够处理自定义和内置异常的 ErrorHandler 实现。

问题
最近写了一些GraphQL 的端点,在谈到错误处理机制时有点卡住了。@ExceptionHandler通常,在编写 REST 端点时,您要么为控制器选择一个特定的端点,要么@ControllerAdvice为多个控制器全局处理异常。显然,GraphQL并非如此。有一种完全不同的方法来处理错误。

首先,我应该提到的最重要的事情是我正在使用:

implementation("org.springframework.boot:spring-boot-starter-graphql")

并不是:

implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:14.0.0")

这是两个完全不同的东西,在不同网站的开发和研究过程中应该牢记这一点。

那么问题是什么?每当您运行GraphQL查询/突变并且您的服务/外观抛出异常时——假设是一个NotFoundException ——默认情况下,您将获得以下结果输出:

{
"errors": [
{
"message": "INTERNAL_ERROR for 2ce1d7be-86f2-da5d-bdba-aac45f4a534f",
"locations": [
{
"line": 1,
"column": 13
}
],
"path": [
"deleteCourseById"
],
"extensions": {
"classification": "INTERNAL_ERROR"
}
}
],
"data": {
"deleteCourseById": null
}
}


嗯,这根本不直观!我们错过了异常消息,对吧?这需要修复。我希望能够提供异常消息,并且在某些情况下,能够覆盖某些异常的异常消息并显示它。

我最大的错误是直接用谷歌搜索,而不是先浏览文档 。这让我踏上了我从未见过的试错之旅,所有这一切都是因为大多数研究生态系统都充满了com.graphql-java-kickstart:graphql-spring-boot-starter图书馆或图书馆的 QA 和教程,而关于Springio.leangen.graphql的信息却很少对于 GraphQL。GraphQLError 通过实现或通过实现自定义GraphQLErrorHandler 或通过启用某种属性等,有很多关于错误处理的有效答案,但它们都不能在Spring for GraphQL中工作,因为它是一个完全不同的库。

顿悟
在尝试了一切之后,让我们看看文档 中关于异常解决的内容:

DataFetcherExceptionResolver是一个异步合约。DataFetcherExceptionResolverAdapter对于大多数实现,扩展和覆盖其同步解决异常的方法resolveToSingleError之一就足够了。resolveToMultipleErrors

哇,怎么这么简单?学过的知识。始终首先检查文档!

为了演示Spring for GraphQL的错误处理,让我们配置一个关于课程和讲师的迷你项目。为此,我使用了Kotlin,但该解决方案也适用于 Java。为简洁起见,此处不会显示许多类,但您可以继续查看GitHub 上的完整源代码。以下是正在使用的 DTO:

data class CourseRequest(
@get:NotBlank(message = "must not be blank") val name: String,
@get:NotBlank(message = "must not be blank") val category: String,
val instructor: InstructorRequest
)

data class CourseResponse(
val id: Int?,
val name: String,
val category: String,
val createdAt: String,
val updatedAt: String,
val instructor: InstructorResponse
)

data class InstructorRequest(
@get:NotBlank(message = "must not be blank") val name: String,
)

data class InstructorResponse(
val id: Int?,
val name: String?,
)

这是他们在以下方面的代表schema.graphqls:
type CourseResponse {
id: ID
name: String
category: String
instructor: InstructorResponse
}

input CourseRequest{
name: String
category: String
instructor: InstructorRequest
}

type InstructorResponse {
id: ID
name: String
}

input InstructorRequest {
name: String
}
现在我们有了控制器:

@Controller
class CourseGraphQLController(val courseFacade: CourseFacade) {

@QueryMapping
fun getCourseById(@Argument id: Int): CourseResponse = courseFacade.findById(id)

@QueryMapping
fun getAllCourses(): List<CourseResponse> = courseFacade.findAll()

@SchemaMapping(typeName = "CourseResponse", field = "instructor")
fun getInstructor(course: CourseResponse): InstructorResponse = course.instructor

@MutationMapping
fun deleteCourseById(@Argument id: Int) = courseFacade.deleteById(id)

@MutationMapping
fun createCourse(@Valid @Argument request: CourseRequest): CourseResponse = courseFacade.save(request)
}

只是为了提一下,Spring for GraphQL只是以更自以为是的方式提供对GraphQL Java的支持——一种基于注释的方法。因此,我们不使用GraphQLQueryResolver/ ,而是GraphQLMutationResolver使用@QueryMappingand来解析方法参数。还有(/的父级)允许方法充当模式映射中的字段。 @MutationMapping@Argument@SchemaMapping@QueryMapping@MutationMappingDataFetcher

好的,这是查询/突变的模式映射:
type Query {
getAllCourses: [CourseResponse]!
getCourseById(id: Int): CourseResponse
}

type Mutation {
deleteCourseById(id: Int): Boolean
createCourse(request: CourseRequest): CourseResponse
}

为了获得有关错误的一些背景信息,这是我NotFoundException从服务中抛出的泛型:

class NotFoundException(clazz: KClass<*>, property: String, propertyValue: String) :
RuntimeException("${clazz.java.simpleName} with $property equal to [$propertyValue] could not be found!")

因此,通过运行以下 GraphQL 查询:
query { getCourseById(id: -999) {
id
name
instructor {
id
}
}}

我期待得到类似“找不到 ID 等于 [-999] 的课程!” 但事实并非如此,正如我们在开始时所看到的那样。

解决方案
好了,说够了;是时候解决这个问题了。根据文档,这是所需的子类:

@Component
class GraphQLExceptionHandler : DataFetcherExceptionResolverAdapter() {
companion object {
private val log: Logger = LoggerFactory.getLogger(this::class.java)
}

override fun resolveToSingleError(e: Throwable, env: DataFetchingEnvironment): GraphQLError? {
return when (e) {
is NotFoundException -> toGraphQLError(e)
else -> super.resolveToSingleError(e, env)
}
}
private fun toGraphQLError(e: Throwable): GraphQLError? {
log.warn("Exception while handling request: ${e.message}", e)
return GraphqlErrorBuilder.newError().message(e.message).errorType(ErrorType.DataFetchingException).build()
}
}
因此,我们扩展DataFetcherExceptionResolverAdapter并覆盖了该resolveToSingleError方法以正确处理我们的异常。基本上,它是NotFoundExceptionto的翻译GraphQLError。现在,如果我们再次运行查询:

{
"errors": [
{
"message": "Course with id equal to [-999] could not be found!",
"locations": [],
"extensions": {
"classification": "DataFetchingException"
}
}
],
"data": {
"getCourseById": null
}
}

漂亮,不是吗?

可是等等; 还有更多。这是一个自定义异常。一些内置异常(如,当无效ConstraintViolationException时抛出)呢?@Valid如您所见,我CourseRequest的名字带有注释@NotBlank:

data class CourseRequest(
@get:NotBlank(message = "must not be blank") val name: String,
@get:NotBlank(message = "must not be blank") val category: String,
val instructor: InstructorRequest
)

当我尝试创建一个Course空名称时会发生什么,像这样?

mutation { createCourse(
request: {
name: "",
category: "DEVELOPMENT",
instructor: {
name: "Thomas William"
}
}) {
id
name
}}

哦,上帝,不……再一次,那个INTERNAL_ERROR信息……

但不用担心——在我们GraphQLExceptionHandler到位后,只需添加一个要处理的新异常即可。另外,为了安全起见,我也会在Exception那里添加,随着时间的推移,可以添加新的专业,但默认情况下,对于未处理的异常,总是会显示异常消息。所以这是我们的新实现:

@Component

class GraphQLExceptionHandler : DataFetcherExceptionResolverAdapter() {
companion object {
private val log: Logger = LoggerFactory.getLogger(this::class.java)
}

override fun resolveToSingleError(e: Throwable, env: DataFetchingEnvironment): GraphQLError? {
return when (e) {
is NotFoundException -> toGraphQLError(e)
is ConstraintViolationException -> handleConstraintViolationException(e)
is Exception -> toGraphQLError(e)
else -> super.resolveToSingleError(e, env)
}
}

private fun toGraphQLError(e: Throwable): GraphQLError? {
log.warn("Exception while handling request: ${e.message}", e)
return GraphqlErrorBuilder.newError().message(e.message).errorType(ErrorType.DataFetchingException).build()
}

private fun handleConstraintViolationException(e: ConstraintViolationException): GraphQLError? {
val errorMessages = mutableSetOf<String>()
e.constraintViolations.forEach { errorMessages.add("Field '${it.propertyPath}' ${it.message}, but value was [${it.invalidValue}]") }
val message = errorMessages.joinToString("\n")
log.warn("Exception while handling request: $message", e)
return GraphqlErrorBuilder.newError().message(message).errorType(ErrorType.DataFetchingException).build()
}
}

如您所见,NotFoundException/Exception将被简单地转换为GraphQLError(是的,目前,逻辑相同,NotFoundException可能会被删除,但我更愿意将它们分开以备将来可能的更改)。ConstraintViolationException通过构造一个合理的消息来单独处理。

现在,如果我们再次运行我们的mutation,瞧!

{
"errors": [
{
"message": "Field 'createCourse.request.name' must not be blank, but value was []",
"locations": [],
"extensions": {
"classification": "DataFetchingException"
}
}
],
"data": {
"createCourse": null
}

结论
在本文中,我们讨论了Spring for GraphQL中的错误处理,并查看了ErrorHandler能够处理自定义异常和内置异常的实现。我们学到了重要的一课:始终先检查文档!

就是这样。希望你喜欢它。如果你错过了,这里是完整的项目。

PS 对于仍在尝试实现GraphQLError和扩展RuntimeException并获得“意外覆盖:以下声明具有相同的 JVM 签名 (getMessage()Ljava/lang/String;) ”的 Kotlin 用户,这是一个不相关的提示。肮脏的解决方法是让它在 Java 中实现,并在 100% Kotlin 项目中拥有一个 Java 类。优雅的解决方法是根据打开的 GitHub问题扩展GraphqlErrorException专门为 Kotlin 用户创建的新创建。

Tags:

最近发表
标签列表