2

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)
  • I've tried to replicate this issue, but unsuccessfully. I've used a mutation query and a Kotlin data class with nullable fields and default values. Could you create a minimal project that reproduces this issue, ideally starting from start.spring.io and only adding the required classes? Then please create a new issue here: https://github.com/spring-projects/spring-graphql/issues – Brian Clozel Jan 11 '23 at 13:50

0 Answers0