If a user only provides RouterFunction endpoints it's a shame to load up the application context with loads of annotation-processing (RequestMappingHandlerMapping etc.). But everything happens in the same place right now.

See https://github.com/spring-projects/spring-boot/issues/22314.

Comment From: dsyer

This (Spring-Boot style) works for me, but I can't say if it is more or less complicated than it needs to be. It's a drop-in replacement for WebFluxAutoConfiguration, so I suppose the most relevant part for spring-framework is the nested EnableFunctionalConfiguration:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebFluxConfigurer.class)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@AutoConfigureAfter({ ReactiveWebServerFactoryAutoConfiguration.class, CodecsAutoConfiguration.class,
        ValidationAutoConfiguration.class })
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class RouterFunctionAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    @ConditionalOnProperty(prefix = "spring.webflux.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }

    @Configuration(proxyBeanMethods = false)
    public static class WelcomePageConfiguration {

        @Bean
        public RouterFunctionMapping welcomePageRouterFunctionMapping(ApplicationContext applicationContext,
                WebFluxProperties webFluxProperties, ResourceProperties resourceProperties) {
            WelcomePageRouterFunctionFactory factory = new WelcomePageRouterFunctionFactory(
                    new TemplateAvailabilityProviders(applicationContext), applicationContext,
                    resourceProperties.getStaticLocations(), webFluxProperties.getStaticPathPattern());
            RouterFunction<ServerResponse> routerFunction = factory.createRouterFunction();
            if (routerFunction != null) {
                RouterFunctionMapping routerFunctionMapping = new RouterFunctionMapping(routerFunction);
                routerFunctionMapping.setOrder(1);
                return routerFunctionMapping;
            }
            return null;
        }

    }

    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties({ ResourceProperties.class, WebFluxProperties.class })
    public static class EnableFunctionalConfiguration implements ApplicationContextAware {

        private WebFluxAutoConfiguration.EnableWebFluxConfiguration delegate;

        @Override
        public void setApplicationContext(@Nullable ApplicationContext applicationContext) {
            delegate.setApplicationContext(applicationContext);
        }

        public EnableFunctionalConfiguration(WebFluxProperties webFluxProperties,
                ObjectProvider<WebFluxRegistrations> webFluxRegistrations) {
            delegate = new WebFluxAutoConfiguration.EnableWebFluxConfiguration(webFluxProperties, webFluxRegistrations);
        }

        @Bean
        public DispatcherHandler webHandler() {
            return delegate.webHandler();
        }

        @Bean
        @Order(0)
        public WebExceptionHandler responseStatusExceptionHandler() {
            return delegate.responseStatusExceptionHandler();
        }

        @Bean
        public RequestedContentTypeResolver webFluxContentTypeResolver() {
            return delegate.webFluxContentTypeResolver();
        }

        @Bean
        public RouterFunctionMapping routerFunctionMapping(ServerCodecConfigurer serverCodecConfigurer) {
            return delegate.routerFunctionMapping(serverCodecConfigurer);
        }

        @Bean
        public ResourceUrlProvider resourceUrlProvider() {
            return delegate.resourceUrlProvider();
        }

        @Bean
        public ServerCodecConfigurer serverCodecConfigurer() {
            return delegate.serverCodecConfigurer();
        }

        @Bean
        public LocaleContextResolver localeContextResolver() {
            return delegate.localeContextResolver();
        }

        @Bean
        public ReactiveAdapterRegistry webFluxAdapterRegistry() {
            return delegate.webFluxAdapterRegistry();
        }

        @Bean
        public HandlerFunctionAdapter handlerFunctionAdapter() {
            return delegate.handlerFunctionAdapter();
        }

        @Bean
        public SimpleHandlerAdapter simpleHandlerAdapter() {
            return delegate.simpleHandlerAdapter();
        }

        @Bean
        public ServerResponseResultHandler serverResponseResultHandler(ServerCodecConfigurer serverCodecConfigurer) {
            return delegate.serverResponseResultHandler(serverCodecConfigurer);
        }

        @Bean
        public FormattingConversionService webFluxConversionService() {
            return delegate.webFluxConversionService();
        }

        @Bean
        public Validator webFluxValidator() {
            return delegate.webFluxValidator();
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnEnabledResourceChain
    static class ResourceChainCustomizerConfiguration {

        @Bean
        ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() {
            return new ResourceChainResourceHandlerRegistrationCustomizer();
        }

    }

}

and the MVC version:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
        ValidationAutoConfiguration.class })
@EnableConfigurationProperties({ResourceProperties.class, WebMvcProperties.class})
public class RouterFunctionAutoConfiguration {

    private static final String[] SERVLET_LOCATIONS = { "/" };

    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }

    @Bean
    @ConditionalOnMissingBean(FormContentFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
    public OrderedFormContentFilter formContentFilter() {
        return new OrderedFormContentFilter();
    }

    static String[] getResourceLocations(String[] staticLocations) {
        String[] locations = new String[staticLocations.length + SERVLET_LOCATIONS.length];
        System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
        System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length, SERVLET_LOCATIONS.length);
        return locations;
    }

    @Configuration(proxyBeanMethods = false)
    public static class EnableFunctionalConfiguration implements ResourceLoaderAware {

        private WebMvcAutoConfiguration.EnableWebMvcConfiguration delegate;
        @Nullable
        private List<Object> interceptors;

        public EnableFunctionalConfiguration(ResourceProperties resourceProperties,
                ObjectProvider<WebMvcProperties> mvcPropertiesProvider, ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider, ListableBeanFactory beanFactory) {
            delegate = new WebMvcAutoConfiguration.EnableWebMvcConfiguration(resourceProperties, mvcPropertiesProvider, mvcRegistrationsProvider, beanFactory);
        }

        @Override
        public void setResourceLoader(ResourceLoader resourceLoader) {
            delegate.setResourceLoader(resourceLoader);
        }

        @Bean
        public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
                FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
            return delegate.welcomePageHandlerMapping(applicationContext, mvcConversionService, mvcResourceUrlProvider);
        }

        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnProperty(prefix = "spring.mvc", name = "locale", matchIfMissing = true)
        public LocaleResolver localeResolver() {
            return delegate.localeResolver();
        }

        @Bean
        public FormattingConversionService mvcConversionService() {
            return delegate.mvcConversionService();
        }

        @Bean
        public Validator mvcValidator() {
            return delegate.mvcValidator();
        }

        @Bean
        public ContentNegotiationManager mvcContentNegotiationManager() {
            return delegate.mvcContentNegotiationManager();
        }

        @Bean
        public UrlPathHelper mvcUrlPathHelper() {
            return delegate.mvcUrlPathHelper();
        }

        @Bean
        public PathMatcher mvcPathMatcher() {
            return delegate.mvcPathMatcher();
        }

        @Bean
        public RouterFunctionMapping routerFunctionMapping(
                @Qualifier("mvcConversionService") FormattingConversionService conversionService,
                @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
            return delegate.routerFunctionMapping(conversionService, resourceUrlProvider);
        }

        @Bean
        @Nullable
        public HandlerMapping resourceHandlerMapping(
                @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
                @Qualifier("mvcConversionService") FormattingConversionService conversionService,
                @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
            return delegate.resourceHandlerMapping(contentNegotiationManager, conversionService, resourceUrlProvider);
        }

        @Bean
        public ResourceUrlProvider mvcResourceUrlProvider() {
            return delegate.mvcResourceUrlProvider();
        }

        @Bean
        public HandlerFunctionAdapter handlerFunctionAdapter() {
            return delegate.handlerFunctionAdapter();
        }

        @Bean
        public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
            return delegate.httpRequestHandlerAdapter();
        }

        @Bean
        public HandlerExceptionResolver handlerExceptionResolver(
                @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
            return delegate.handlerExceptionResolver(contentNegotiationManager);
        }

        @Bean
        public ViewResolver mvcViewResolver(
                @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
            return delegate.mvcViewResolver(contentNegotiationManager);
        }

        @Bean
        @Lazy
        public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
            return delegate.mvcHandlerMappingIntrospector();
        }

        @Bean
        public ThemeResolver themeResolver() {
            return delegate.themeResolver();
        }

        @Bean
        public FlashMapManager flashMapManager() {
            return delegate.flashMapManager();
        }

        @Bean
        public RequestToViewNameTranslator viewNameTranslator() {
            return delegate.viewNameTranslator();
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnEnabledResourceChain
    static class ResourceChainCustomizerConfiguration {

        @Bean
        ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() {
            return new ResourceChainResourceHandlerRegistrationCustomizer();
        }

    }

}

Comment From: sdeleuze

After a team related discussion, we think this is an area where there could be interesting options to explore in the future, but I am going to decline this specific enhancement request for several reasons: - As of Spring Framework 6.1, functional mappings are ordered first, avoiding to pay the price of annotation-based mapping resolution for functional web endpoints, more details on https://github.com/spring-projects/spring-framework/issues/30278. - Just one 👍🏼 in multiple years seems to indicate low traction for the proposed feature - What is asked here would probably have to be part of a wider first class functional story that we don't have yet, so whatever we do here would feel incomplete and could be inconsistent with a wider potential functional story (for exemple that wouldn't be a good fit with approaches like https://github.com/spring-projects-experimental/spring-fu).