Skip to content

Provide equivalent of @EnableWebFlux and @EnableWebMvc for the RouterFunction approach #25497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
dsyer opened this issue Jul 31, 2020 · 2 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: declined A suggestion or change that we don't feel we should currently apply

Comments

@dsyer
Copy link
Member

dsyer commented Jul 31, 2020

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 spring-projects/spring-boot#22314.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jul 31, 2020
@dsyer
Copy link
Member Author

dsyer commented Jul 31, 2020

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();
		}

	}

}

@rstoyanchev rstoyanchev added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Nov 10, 2021
@sdeleuze
Copy link
Contributor

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 Refine RouterFunctionMapping ordering consistency #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).

@sdeleuze sdeleuze closed this as not planned Won't fix, can't repro, duplicate, stale Nov 27, 2023
@sdeleuze sdeleuze self-assigned this Nov 27, 2023
@sdeleuze sdeleuze added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Nov 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

4 participants