spring-boot icon indicating copy to clipboard operation
spring-boot copied to clipboard

Auto-configure Spring Data's reactive method argument resolvers in a reactive web application

Open cbornet opened this issue 5 years ago • 12 comments

Spring Data now has argument resolvers for Pageable and Sort for Webflux. It would be great to have these autoconfigured if spring-data-commons is detected on the classpath like what is done for MVC. From the discussion with @mp911de (https://github.com/spring-projects/spring-data-commons/pull/264#issuecomment-604939814), could you give advice on where it's best to provide the configuration components (esp. the creation of the resolver beans and their customizers) ? I can work on this once settled if you want.

cbornet avatar Mar 27 '20 12:03 cbornet

I suspect that there's some subtlety to this that I am missing. On the face of it, it would seem pretty straightforward for us to add configuration that is similar to the following:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ReactiveSortHandlerMethodArgumentResolver.class)
@ConditionalOnWebApplication(type = Type.REACTIVE)
public class SpringDataWebFluxAutoConfiguration {

    @Bean
    WebFluxConfigurer springDataArgumentResolversConfigurer() {
        return new WebFluxConfigurer() {

            @Override
            public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
                configurer.addCustomResolver(new ReactiveSortHandlerMethodArgumentResolver(),
                        new ReactivePageableHandlerMethodArgumentResolver());
            }

        };
    }

}

wilkinsona avatar Mar 27 '20 13:03 wilkinsona

I was cautious as @gregturn is working on improved configuration support for WebFlux from a Spring HATEOAS perspective. Given the proximity of this issue towards Spring Data REST I wanted to make sure to keep everyone involved in the loop and to not come up with an implementation that would contradict with ideas that are evolving around other config API in Spring HATEOAS/Spring Data REST.

mp911de avatar Mar 27 '20 13:03 mp911de

This is what Spring HATEOAS does (register a WebFluxConfigurer).

https://github.com/spring-projects/spring-hateoas/blob/master/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java#L79-L94

I'm not sure if this is a collision with what Spring Data Commons is doing, or not.

gregturn avatar Apr 01 '20 17:04 gregturn

Thanks, both. I can't see any danger of a clash based on that. It looks to me like we can safely add some auto-configuration like that in my earlier comment.

wilkinsona avatar Apr 01 '20 18:04 wilkinsona

Spring MVC has customizers for these resolvers. Maybe they should also be added for Webflux ?

cbornet avatar Apr 01 '20 20:04 cbornet

@cbornet Sorry, I don't understand what you're suggesting. What Spring MVC customisers are you referring to?

wilkinsona avatar Apr 02 '20 07:04 wilkinsona

I'm referring to the customizers in SpringDataWebConfiguration : PageableHandlerMethodArgumentResolverCustomizer and SortHandlerMethodArgumentResolverCustomizer

cbornet avatar Apr 02 '20 09:04 cbornet

Thanks. FWIW, those are part of Spring Data rather than Spring MVC.

The customizers make sense for Spring Data's web support on a Servlet-based stack as it provides a way to customise the argument resolvers that are automatically defined by SpringDataWebConfiguration.

I'm changing my mind about the issue now. It's not clear to me why Spring Data Commons cannot provide a WebFlux equivalent of @EnableSpringDataWebSupport. It looks to me like a lot, if not all, of what it does would also be applicable to a WebFlux-based app that's rolling its own @RestControllers and making use of reactive Spring Data repositories.

If Spring Data Commons did this, we could then customise the reactive argument resolvers that it defines via a similar mechanism to that used today for the Servlet/MVC side of things.

wilkinsona avatar Apr 02 '20 14:04 wilkinsona

@mp911de do Andy's comments above make sense? Do you want us to raise an issue?

philwebb avatar Apr 16 '20 18:04 philwebb

It makes sense for Spring Data Commons to provide a similar annotation to @EnableSpringDataWebSupport to register Spring Data's WebFlux HandlerMethodArgumentResolvers. We already have a corresponding ticket so we could repurpose this one to pick up the annotation or close it for the time being until Spring Data provides the desired functionality.

mp911de avatar Apr 19 '20 17:04 mp911de

Has this problem been resolved? The version of springboot I am using is 3.0.5. The project maven dependency is like this:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>r2dbc-postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>
    </dependencies>

But, ReactivePageableHandlerMethodArgumentResolver is not automatically configured into the context. Reported the following error:

java.lang.IllegalStateException: No primary or single unique constructor found for interface org.springframework.data.domain.Pageable
	at org.springframework.beans.BeanUtils.getResolvableConstructor(BeanUtils.java:267) ~[spring-beans-6.0.6.jar:6.0.6]
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ HTTP GET "/search" [ExceptionHandlingWebHandler]
Original Stack Trace:
		at org.springframework.beans.BeanUtils.getResolvableConstructor(BeanUtils.java:267) ~[spring-beans-6.0.6.jar:6.0.6]
		at org.springframework.web.reactive.result.method.annotation.ModelAttributeMethodArgumentResolver.createAttribute(ModelAttributeMethodArgumentResolver.java:218) ~[spring-webflux-6.0.6.jar:6.0.6]
		at org.springframework.web.reactive.result.method.annotation.ModelAttributeMethodArgumentResolver.prepareAttributeMono(ModelAttributeMethodArgumentResolver.java:179) ~[spring-webflux-6.0.6.jar:6.0.6]
		at org.springframework.web.reactive.result.method.annotation.ModelAttributeMethodArgumentResolver.resolveArgument(ModelAttributeMethodArgumentResolver.java:113) ~[spring-webflux-6.0.6.jar:6.0.6]
		at org.springframework.web.reactive.result.method.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:119) ~[spring-webflux-6.0.6.jar:6.0.6]
		at org.springframework.web.reactive.result.method.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:205) ~[spring-webflux-6.0.6.jar:6.0.6]
		at org.springframework.web.reactive.result.method.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:136) ~[spring-webflux-6.0.6.jar:6.0.6]
		at org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.lambda$handle$1(RequestMappingHandlerAdapter.java:201) ~[spring-webflux-6.0.6.jar:6.0.6]
		at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:240) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onComplete(MonoIgnoreThen.java:203) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.onComplete(MonoFlatMap.java:189) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.Operators.complete(Operators.java:137) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoZip.subscribe(MonoZip.java:121) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4485) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:263) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:165) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:74) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.innerNext(FluxConcatMapNoPrefetch.java:258) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:863) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:180) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2545) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.request(MonoPeekTerminal.java:139) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:171) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.request(Operators.java:2305) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.request(FluxConcatMapNoPrefetch.java:338) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoNext$NextSubscriber.request(MonoNext.java:108) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2341) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2215) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoNext$NextSubscriber.onSubscribe(MonoNext.java:70) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.onSubscribe(FluxConcatMapNoPrefetch.java:164) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.Mono.subscribe(Mono.java:4485) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:263) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:55) ~[reactor-core-3.5.3.jar:3.5.3]
		at reactor.netty.http.server.HttpServer$HttpServerHandle.onStateChange(HttpServer.java:1006) ~[reactor-netty-http-1.1.4.jar:1.1.4]
		at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:710) ~[reactor-netty-core-1.1.4.jar:1.1.4]
		at reactor.netty.transport.ServerTransport$ChildObserver.onStateChange(ServerTransport.java:477) ~[reactor-netty-core-1.1.4.jar:1.1.4]
		at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:633) ~[reactor-netty-http-1.1.4.jar:1.1.4]
		at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:113) ~[reactor-netty-core-1.1.4.jar:1.1.4]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:228) ~[reactor-netty-http-1.1.4.jar:1.1.4]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346) ~[netty-codec-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318) ~[netty-codec-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) ~[netty-transport-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.89.Final.jar:4.1.89.Final]
		at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.89.Final.jar:4.1.89.Final]
		at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

holmofy avatar May 05 '23 07:05 holmofy

@holmofy No. The issue's still open and is labelled as blocked. It's blocked because the related Spring Data issue (https://github.com/spring-projects/spring-data-commons/issues/1846) that's linked to above has not be resolved.

wilkinsona avatar May 05 '23 08:05 wilkinsona