diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java index faafdfd8326..db87e0dafd6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java @@ -35,7 +35,7 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import org.springframework.web.servlet.handler.DefaultPreFlightRequestHandler; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; @@ -72,14 +72,14 @@ public abstract class MvcNamespaceUtils { private static final String CORS_CONFIGURATION_BEAN_NAME = "mvcCorsConfigurations"; - private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; + private static final String PRE_FLIGHT_REQUEST_HANDLER = "mvcPreFlightRequestHandler"; public static void registerDefaultComponents(ParserContext context, @Nullable Object source) { registerBeanNameUrlHandlerMapping(context, source); registerHttpRequestHandlerAdapter(context, source); registerSimpleControllerHandlerAdapter(context, source); - registerHandlerMappingIntrospector(context, source); + registerPreFlightRequestHandler(context, source); registerLocaleResolver(context, source); registerViewNameTranslator(context, source); registerFlashMapManager(context, source); @@ -276,18 +276,17 @@ public abstract class MvcNamespaceUtils { } /** - * Registers an {@link HandlerMappingIntrospector} under a well-known name + * Registers an {@link DefaultPreFlightRequestHandler} under a well-known name * unless already registered. */ - @SuppressWarnings("removal") - private static void registerHandlerMappingIntrospector(ParserContext context, @Nullable Object source) { - if (!context.getRegistry().containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { - RootBeanDefinition beanDef = new RootBeanDefinition(HandlerMappingIntrospector.class); + private static void registerPreFlightRequestHandler(ParserContext context, @Nullable Object source) { + if (!context.getRegistry().containsBeanDefinition(PRE_FLIGHT_REQUEST_HANDLER)) { + RootBeanDefinition beanDef = new RootBeanDefinition(DefaultPreFlightRequestHandler.class); beanDef.setSource(source); beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); beanDef.setLazyInit(true); - context.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, beanDef); - context.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)); + context.getRegistry().registerBeanDefinition(PRE_FLIGHT_REQUEST_HANDLER, beanDef); + context.registerComponent(new BeanComponentDefinition(beanDef, PRE_FLIGHT_REQUEST_HANDLER)); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 242e416cded..f210ae47b9d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -61,6 +61,7 @@ import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.ServletContextAware; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.PreFlightRequestHandler; import org.springframework.web.method.support.CompositeUriComponentsContributor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -76,8 +77,8 @@ import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor; +import org.springframework.web.servlet.handler.DefaultPreFlightRequestHandler; import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; @@ -1161,11 +1162,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv protected void addCorsMappings(CorsRegistry registry) { } - @SuppressWarnings("removal") @Bean @Lazy - public HandlerMappingIntrospector mvcHandlerMappingIntrospector() { - return new HandlerMappingIntrospector(); + public PreFlightRequestHandler mvcPreFlightRequestHandler() { + return new DefaultPreFlightRequestHandler(); } @Bean diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandler.java new file mode 100644 index 00000000000..c07caa07a73 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandler.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.handler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.PreFlightRequestHandler; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.util.ServletRequestPathUtils; + +/** + * Default implementation of {@link PreFlightRequestHandler} that emulates the + * DispatcherServlet's algorithm for finding a matching {@link HandlerMapping} + * for a pre-flight request, and then invokes the matched handler as a + * {@link PreFlightRequestHandler}. + * + * @author Rossen Stoyanchev + * @since 7.1 + */ +public class DefaultPreFlightRequestHandler + implements PreFlightRequestHandler, ApplicationContextAware, InitializingBean { + + private @Nullable ApplicationContext applicationContext; + + private @Nullable List handlerMappings; + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * Return the configured or detected {@code HandlerMapping}s. + */ + public List getHandlerMappings() { + return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); + } + + @Override + public void afterPropertiesSet() { + if (this.handlerMappings == null) { + Assert.notNull(this.applicationContext, "No ApplicationContext"); + this.handlerMappings = initHandlerMappings(this.applicationContext); + } + } + + private static List initHandlerMappings(ApplicationContext context) { + + Map beans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); + + if (!beans.isEmpty()) { + List mappings = new ArrayList<>(beans.values()); + AnnotationAwareOrderComparator.sort(mappings); + return Collections.unmodifiableList(mappings); + } + + return Collections.unmodifiableList(initFallback(context)); + } + + private static List initFallback(ApplicationContext applicationContext) { + Properties properties; + try { + Resource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class); + properties = PropertiesLoaderUtils.loadProperties(resource); + } + catch (IOException ex) { + throw new IllegalStateException("Could not load DispatcherServlet.properties: " + ex.getMessage()); + } + + String value = properties.getProperty(HandlerMapping.class.getName()); + String[] names = StringUtils.commaDelimitedListToStringArray(value); + List result = new ArrayList<>(names.length); + for (String name : names) { + try { + Class clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader()); + Object mapping = applicationContext.getAutowireCapableBeanFactory().createBean(clazz); + result.add((HandlerMapping) mapping); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]"); + } + } + return result; + } + + + /** + * Find the matching {@link HandlerMapping} for the request, and invoke the + * handler it returns as a {@link PreFlightRequestHandler}. + * @throws NoHandlerFoundException if no handler matches the request + */ + @Override + public void handlePreFlight(HttpServletRequest request, HttpServletResponse response) throws Exception { + Assert.state(this.handlerMappings != null, "Not yet initialized via afterPropertiesSet."); + Assert.state(CorsUtils.isPreFlightRequest(request), "Not a pre-flight request."); + RequestPath previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + try { + ServletRequestPathUtils.parseAndCache(request); + for (HandlerMapping mapping : this.handlerMappings) { + HandlerExecutionChain chain = mapping.getHandler(request); + if (chain != null) { + Object handler = chain.getHandler(); + if (handler instanceof PreFlightRequestHandler preFlightHandler) { + preFlightHandler.handlePreFlight(request, response); + return; + } + throw new IllegalStateException("Expected PreFlightRequestHandler: " + handler.getClass()); + } + } + throw new NoHandlerFoundException( + request.getMethod(), request.getRequestURI(), new ServletServerHttpRequest(request).getHeaders()); + } + finally { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index 3de64e17c6b..1f9de1e9b4b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -95,7 +95,7 @@ import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import org.springframework.web.servlet.handler.DefaultPreFlightRequestHandler; import org.springframework.web.servlet.handler.MappedInterceptor; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor; @@ -261,12 +261,12 @@ class MvcNamespaceTests { assertThat(uriComponentsContributor).isNotNull(); - String name = "mvcHandlerMappingIntrospector"; - HandlerMappingIntrospector introspector = this.appContext.getBean(name, HandlerMappingIntrospector.class); - assertThat(introspector).isNotNull(); - assertThat(introspector.getHandlerMappings()).hasSize(2); - assertThat(introspector.getHandlerMappings()).element(0).isSameAs(mapping); - assertThat(introspector.getHandlerMappings().get(1).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); + String name = "mvcPreFlightRequestHandler"; + DefaultPreFlightRequestHandler requestHandler = this.appContext.getBean(name, DefaultPreFlightRequestHandler.class); + assertThat(requestHandler).isNotNull(); + assertThat(requestHandler.getHandlerMappings()).hasSize(2); + assertThat(requestHandler.getHandlerMappings()).element(0).isSameAs(mapping); + assertThat(requestHandler.getHandlerMappings().get(1).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); } @Test // gh-25290 diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandlerTests.java new file mode 100644 index 00000000000..305d5270b9d --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandlerTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.handler; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.testfixture.servlet.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link DefaultPreFlightRequestHandler}. + * + * @author Rossen Stoyanchev + * @since 7.1 + */ +class DefaultPreFlightRequestHandlerTests { + + @Test + void detectHandlerMappings() { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.registerSingleton("A", SimpleUrlHandlerMapping.class); + context.registerSingleton("B", SimpleUrlHandlerMapping.class); + context.registerSingleton("C", SimpleUrlHandlerMapping.class); + context.refresh(); + + List expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C")); + List actual = initHandler(context).getHandlerMappings(); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void detectHandlerMappingsOrdered() { + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("B", SimpleUrlHandlerMapping.class, () -> { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setOrder(2); + return mapping; + }); + context.registerBean("C", SimpleUrlHandlerMapping.class, () -> { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setOrder(3); + return mapping; + }); + context.registerBean("A", SimpleUrlHandlerMapping.class, () -> { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setOrder(1); + return mapping; + }); + context.refresh(); + + List expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C")); + List actual = initHandler(context).getHandlerMappings(); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void handlePreFlight() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path"); + request.addHeader("Origin", "http://localhost:9000"); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + initHandler(context).handlePreFlight(request, response); + + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("http://localhost:9000"); + assertThat(response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).containsExactly("POST"); + } + + @Test + void handlePreFlightWithNoHandlerFoundException() { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/unknownPath"); + request.addHeader("Origin", "http://localhost:9000"); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + assertThatThrownBy(() -> initHandler(context).handlePreFlight(request, response)) + .isInstanceOf(NoHandlerFoundException.class); + + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isNull(); + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isNull(); + } + + private static DefaultPreFlightRequestHandler initHandler(WebApplicationContext context) { + DefaultPreFlightRequestHandler handler = new DefaultPreFlightRequestHandler(); + handler.setApplicationContext(context); + handler.afterPropertiesSet(); + return handler; + } + + + @Configuration + static class TestConfig { + + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + + @Bean + public RequestMappingHandlerMapping handlerMapping() { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; + } + + @Bean + public TestController testController() { + return new TestController(); + } + + @Bean + public RouterFunction routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } + } + + + @CrossOrigin("http://localhost:9000") + @Controller + private static class TestController { + + @PostMapping("/path") + void handle() { + } + } + +}