I'm trying to use Spring Native with Spring GraphQL.
Problem
When specifying @Argument
with data class, an error occurs.
(Note that even if UserListInput
is a regular class (non-data class), the result is same)
data class UserListInput(
val pageNumber: Int,
val pageSize: Int,
val sortBy: UserSortBy = UserSortBy.ID,
val sortDirection: SortDirection = SortDirection.ASCENDING,
val keyword: String?
)
@Controller
class UserController(...) {
suspend fun users(dfe: DataFetchingEnvironment, @Argument input: UserListInput): UserListResponse {
// .....
}
}
Then I query with these query
query Query {
users(input: { pageSize: 50, pageNumber: 1 }) {
totalCount
nodes {
id
lastName
firstName
email
role
}
}
}
}
This will cause error
java.lang.IllegalStateException: No primary or single unique constructor found for class application.controller.graphql.entity.UserListInput
at org.springframework.beans.BeanUtils.getResolvableConstructor(BeanUtils.java:266)
at org.springframework.graphql.data.GraphQlArgumentBinder.bindMap(GraphQlArgumentBinder.java:232)
at org.springframework.graphql.data.GraphQlArgumentBinder.bindRawValue(GraphQlArgumentBinder.java:181)
at org.springframework.graphql.data.GraphQlArgumentBinder.bind(GraphQlArgumentBinder.java:132)
at org.springframework.graphql.data.method.annotation.support.ArgumentMethodArgumentResolver.resolveArgument(ArgumentMethodArgumentResolver.java:73)
at org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:83)
at org.springframework.graphql.data.method.annotation.support.DataFetcherHandlerMethod.getMethodArgumentValues(DataFetcherHandlerMethod.java:173)
at org.springframework.graphql.data.method.annotation.support.DataFetcherHandlerMethod.invoke(DataFetcherHandlerMethod.java:118)
at org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer$SchemaMappingDataFetcher.get(AnnotatedControllerConfigurer.java:529)
at org.springframework.graphql.execution.ContextDataFetcherDecorator.lambda$get$0(ContextDataFetcherDecorator.java:74)
at io.micrometer.context.ContextSnapshot.lambda$wrap$1(ContextSnapshot.java:92)
at org.springframework.graphql.execution.ContextDataFetcherDecorator.get(ContextDataFetcherDecorator.java:74)
at graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:282)
at graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:211)
at graphql.execution.AsyncExecutionStrategy.execute(AsyncExecutionStrategy.java:59)
at graphql.execution.Execution.executeOperation(Execution.java:159)
at graphql.execution.Execution.execute(Execution.java:105)
at graphql.GraphQL.execute(GraphQL.java:645)
at graphql.GraphQL.lambda$parseValidateAndExecute$11(GraphQL.java:564)
at graphql.GraphQL.parseValidateAndExecute(GraphQL.java:559) [2 skipped]
at graphql.GraphQL.executeAsync(GraphQL.java:527)
at org.springframework.graphql.execution.DefaultExecutionGraphQlService.lambda$execute$2(DefaultExecutionGraphQlService.java:82)
at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:47)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:165)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:158)
at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)
at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299)
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337)
at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2071)
at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145)
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260)
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:413)
at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:426)
at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:621)
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:113)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:269)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at org.graalvm.nativeimage.builder/com.oracle.svm.core.thread.PlatformThreads.threadStartRoutine(PlatformThreads.java:775) [1 skipped]
at org.graalvm.nativeimage.builder/com.oracle.svm.core.posix.thread.PosixPlatformThreads.pthreadStartRoutine(PosixPlatformThreads.java:203)
How can I resolve on this?
Versions:
- Spring Boot: 3.0.1
- Spring GraphQL: 1.1.1
Investigation
I tried to find a constructor based on org.springframework.beans.BeanUtils.getResolvableConstructor, that is the error cause.
val clazz = UserListInput::class.java
var ctor = BeanUtils.findPrimaryConstructor(clazz)
if (ctor != null) {
logger.info("Found primary constructor: $ctor")
} else {
var ctors = clazz.constructors
if (ctors.size == 1) {
logger.info("Found single constructor: ${ctors[0]}")
} else if (ctors.isEmpty()) {
ctors = clazz.declaredConstructors
if (ctors.size == 1) {
logger.info("Found single declared constructor: : ${ctors[0]}")
} else {
logger.info("Declared constructor size: ${ctors.size}")
}
} else {
logger.info("Constructor size: ${ctors.size}")
ctors.forEach {
logger.info(" Ctor = $it")
it.parameters.forEachIndexed { index, parameter ->
logger.info(" [$index] = ${parameter.name}: ${parameter.type}")
}
}
try {
ctor = clazz.getDeclaredConstructor()
logger.info("Found single declared constructor (tried): $ctor")
} catch (e: NoSuchMethodException) {
logger.warn("No constructors found")
}
}
}
To run with this code, I found that there are 2 constructors when running with native binary, even if only 1 constructor when running with JVM.
When running with JVM: It's okay (Matched with class definition)
Found primary constructor: public application.controller.graphql.entity.UserListInput(int,int,domain.common.entity.UserSortBy,domain.entity.SortDirection,java.lang.String)
When running with native: Suspicious (Found one another constructor)
Constructor size: 2
Ctor = public application.controller.graphql.entity.UserListInput(int,int,domain.common.entity.UserSortBy,domain.entity.SortDirection,java.lang.String)
[0] = pageNumber: int
[1] = pageSize: int
[2] = sortBy: class domain.common.entity.UserSortBy
[3] = sortDirection: class domain.entity.SortDirection
[4] = keyword: class java.lang.String
Ctor = public application.controller.graphql.entity.UserListInput(int,int,domain.common.entity.UserSortBy,domain.entity.SortDirection,java.lang.String,int,kotlin.jvm.internal.DefaultConstructorMarker)
[0] = arg0: int
[1] = arg1: int
[2] = arg2: class domain.common.entity.UserSortBy
[3] = arg3: class domain.entity.SortDirection
[4] = arg4: class java.lang.String
[5] = arg5: int
[6] = arg6: class kotlin.jvm.internal.DefaultConstructorMarker
No constructors found
I'm not sure what is the second constructors with additional 2 arguments (arg5 and arg6).
-> Maybe this?
When finding constructor that won't use with @Argument
it works.
(UserListResponse
is a returning type of users
method)
data class UserListResponse(
val totalCount: Int,
val totalPages: Int,
val currentPage: Int,
val nodes: List<User>,
)
Found single constructor: public application.controller.graphql.entity.UserListResponse(int,int,int,java.util.List)