Replace HandlerMappingIntrospector with DefaultPreFlightRequestHandler

See gh-36481
This commit is contained in:
rstoyanchev
2026-07-29 18:03:08 +03:00
parent 67d54dccd2
commit f53674d582
5 changed files with 356 additions and 21 deletions
@@ -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));
}
}
@@ -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
@@ -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<HandlerMapping> handlerMappings;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* Return the configured or detected {@code HandlerMapping}s.
*/
public List<HandlerMapping> 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<HandlerMapping> initHandlerMappings(ApplicationContext context) {
Map<String, HandlerMapping> beans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!beans.isEmpty()) {
List<HandlerMapping> mappings = new ArrayList<>(beans.values());
AnnotationAwareOrderComparator.sort(mappings);
return Collections.unmodifiableList(mappings);
}
return Collections.unmodifiableList(initFallback(context));
}
private static List<HandlerMapping> 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<HandlerMapping> 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);
}
}
}
@@ -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
@@ -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<HandlerMapping> 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<HandlerMapping> 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() {
}
}
}