From 0b2e62eaf355fbd7e061f536cbdff786880584ed Mon Sep 17 00:00:00 2001 From: seonwoo_jung Date: Thu, 18 Jun 2026 12:29:55 +0900 Subject: [PATCH] Register reflection hints for Relay pagination types graphql-java's PropertyFetchingImpl resolves Connection edges, nodes, cursors and page info via reflection. The Relay implementation classes that ConnectionFieldTypeVisitor constructs (DefaultConnection, DefaultConnectionCursor, DefaultEdge, DefaultPageInfo) are no longer present in the GraalVM reachability metadata for recent graphql-java releases, so in a native image those getter calls fail and edges are serialized with null nodes and page info. Add a RuntimeHintsRegistrar that registers INVOKE_PUBLIC_METHODS for the four Relay types so they remain reflectively accessible in a native image, and wire it through META-INF/spring/aot.factories. Closes gh-1479 Signed-off-by: seonwoo_jung --- .../RelayPaginationRuntimeHints.java | 51 ++++++++++++++ .../resources/META-INF/spring/aot.factories | 3 +- .../RelayPaginationRuntimeHintsTests.java | 66 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/pagination/RelayPaginationRuntimeHints.java create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/data/pagination/RelayPaginationRuntimeHintsTests.java diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/RelayPaginationRuntimeHints.java b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/RelayPaginationRuntimeHints.java new file mode 100644 index 000000000..3cd2aa377 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/RelayPaginationRuntimeHints.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.data.pagination; + +import graphql.relay.DefaultConnection; +import graphql.relay.DefaultConnectionCursor; +import graphql.relay.DefaultEdge; +import graphql.relay.DefaultPageInfo; +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; + +/** + * {@link RuntimeHintsRegistrar} that registers reflection hints for the + * {@code graphql-java} Relay implementation classes returned by + * {@link ConnectionFieldTypeVisitor}. {@code graphql-java}'s + * {@code PropertyFetchingImpl} reads {@code cursor}, {@code node}, + * {@code edges}, {@code pageInfo}, {@code hasNextPage}, etc. reflectively, + * so without these hints relay edges and page info are serialized as {@code null} + * in a native image. + * + * @author Seonwoo Jung + */ +class RelayPaginationRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + hints.reflection() + .registerType(DefaultConnection.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(DefaultConnectionCursor.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(DefaultEdge.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(DefaultPageInfo.class, MemberCategory.INVOKE_PUBLIC_METHODS); + } + +} diff --git a/spring-graphql/src/main/resources/META-INF/spring/aot.factories b/spring-graphql/src/main/resources/META-INF/spring/aot.factories index 95d83aed7..9b026752a 100644 --- a/spring-graphql/src/main/resources/META-INF/spring/aot.factories +++ b/spring-graphql/src/main/resources/META-INF/spring/aot.factories @@ -1 +1,2 @@ -org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=org.springframework.graphql.data.method.annotation.support.SchemaMappingBeanFactoryInitializationAotProcessor \ No newline at end of file +org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=org.springframework.graphql.data.method.annotation.support.SchemaMappingBeanFactoryInitializationAotProcessor +org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.graphql.data.pagination.RelayPaginationRuntimeHints diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/pagination/RelayPaginationRuntimeHintsTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/pagination/RelayPaginationRuntimeHintsTests.java new file mode 100644 index 000000000..b983647e0 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/pagination/RelayPaginationRuntimeHintsTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.data.pagination; + +import graphql.relay.DefaultConnection; +import graphql.relay.DefaultConnectionCursor; +import graphql.relay.DefaultEdge; +import graphql.relay.DefaultPageInfo; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.beans.factory.aot.AotServices; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RelayPaginationRuntimeHints}. + * + * @author Seonwoo Jung + */ +class RelayPaginationRuntimeHintsTests { + + private final RuntimeHints hints = new RuntimeHints(); + + @Test + void registrarIsRegisteredInAotFactories() { + assertThat(AotServices.factories(getClass().getClassLoader()).load(RuntimeHintsRegistrar.class)) + .anyMatch(RelayPaginationRuntimeHints.class::isInstance); + } + + @Test + void registersReflectionHintsForRelayTypes() { + new RelayPaginationRuntimeHints().registerHints(this.hints, getClass().getClassLoader()); + + assertThat(RuntimeHintsPredicates.reflection() + .onType(DefaultConnection.class) + .withMemberCategory(MemberCategory.INVOKE_PUBLIC_METHODS)).accepts(this.hints); + assertThat(RuntimeHintsPredicates.reflection() + .onType(DefaultConnectionCursor.class) + .withMemberCategory(MemberCategory.INVOKE_PUBLIC_METHODS)).accepts(this.hints); + assertThat(RuntimeHintsPredicates.reflection() + .onType(DefaultEdge.class) + .withMemberCategory(MemberCategory.INVOKE_PUBLIC_METHODS)).accepts(this.hints); + assertThat(RuntimeHintsPredicates.reflection() + .onType(DefaultPageInfo.class) + .withMemberCategory(MemberCategory.INVOKE_PUBLIC_METHODS)).accepts(this.hints); + } + +}