diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java index d7f7c597894..6d1bab9ae3c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web; +import java.util.Set; +import java.util.stream.Collectors; + import org.glassfish.jersey.server.ResourceConfig; import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter; @@ -31,6 +34,7 @@ import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPathP import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; import org.springframework.web.servlet.DispatcherServlet; /** @@ -70,14 +74,27 @@ public class ServletEndpointManagementContextConfiguration { ServletEndpointsSupplier servletEndpointsSupplier) { DispatcherServletPathProvider servletPathProvider = this.context .getBean(DispatcherServletPathProvider.class); - String servletPath = servletPathProvider.getServletPath(); - if (servletPath.equals("/")) { - servletPath = ""; - } - return new ServletEndpointRegistrar(servletPath + properties.getBasePath(), + Set cleanedPaths = getServletPaths(properties, servletPathProvider); + return new ServletEndpointRegistrar(cleanedPaths, servletEndpointsSupplier.getEndpoints()); } + private Set getServletPaths(WebEndpointProperties properties, + DispatcherServletPathProvider servletPathProvider) { + Set servletPaths = servletPathProvider.getServletPaths(); + return servletPaths.stream().map((p) -> { + String path = cleanServletPath(p); + return path + properties.getBasePath(); + }).collect(Collectors.toSet()); + } + + private String cleanServletPath(String servletPath) { + if (StringUtils.hasText(servletPath) && servletPath.endsWith("/")) { + return servletPath.substring(0, servletPath.length() - 1); + } + return servletPath; + } + } @Configuration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java index f79289ca6d1..dbcc6da2018 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java @@ -137,10 +137,9 @@ public final class EndpointRequest { private RequestMatcher createDelegate(WebApplicationContext context) { try { - String servletPath = getServletPath(context); - RequestMatcherFactory requestMatcherFactory = (StringUtils - .hasText(servletPath) ? new RequestMatcherFactory(servletPath) - : RequestMatcherFactory.withEmptyServletPath()); + Set servletPaths = getServletPaths(context); + RequestMatcherFactory requestMatcherFactory = new RequestMatcherFactory( + servletPaths); return createDelegate(context, requestMatcherFactory); } catch (NoSuchBeanDefinitionException ex) { @@ -148,13 +147,13 @@ public final class EndpointRequest { } } - private String getServletPath(WebApplicationContext context) { + private Set getServletPaths(WebApplicationContext context) { try { return context.getBean(DispatcherServletPathProvider.class) - .getServletPath(); + .getServletPaths(); } catch (NoSuchBeanDefinitionException ex) { - return ""; + return Collections.singleton(""); } } @@ -226,7 +225,7 @@ public final class EndpointRequest { requestMatcherFactory, paths); if (this.includeLinks && StringUtils.hasText(pathMappedEndpoints.getBasePath())) { - delegateMatchers.add( + delegateMatchers.addAll( requestMatcherFactory.antPath(pathMappedEndpoints.getBasePath())); } return new OrRequestMatcher(delegateMatchers); @@ -259,7 +258,8 @@ public final class EndpointRequest { private List getDelegateMatchers( RequestMatcherFactory requestMatcherFactory, Set paths) { return paths.stream() - .map((path) -> requestMatcherFactory.antPath(path, "/**")) + .flatMap( + (path) -> requestMatcherFactory.antPath(path, "/**").stream()) .collect(Collectors.toList()); } @@ -276,7 +276,9 @@ public final class EndpointRequest { WebEndpointProperties properties = context .getBean(WebEndpointProperties.class); if (StringUtils.hasText(properties.getBasePath())) { - return requestMatcherFactory.antPath(properties.getBasePath()); + List matchers = requestMatcherFactory + .antPath(properties.getBasePath()); + return new OrRequestMatcher(matchers); } return EMPTY_MATCHER; } @@ -288,25 +290,27 @@ public final class EndpointRequest { */ private static class RequestMatcherFactory { - private final String servletPath; - - private static final RequestMatcherFactory EMPTY_SERVLET_PATH = new RequestMatcherFactory( - ""); - - RequestMatcherFactory(String servletPath) { - this.servletPath = servletPath; - } + private final Set servletPaths = new LinkedHashSet<>(); - RequestMatcher antPath(String... parts) { - String pattern = (this.servletPath.equals("/") ? "" : this.servletPath); - for (String part : parts) { - pattern += part; - } - return new AntPathRequestMatcher(pattern); + RequestMatcherFactory(Set servletPaths) { + this.servletPaths.addAll(servletPaths); } - static RequestMatcherFactory withEmptyServletPath() { - return EMPTY_SERVLET_PATH; + List antPath(String... parts) { + List matchers = new ArrayList<>(); + this.servletPaths.stream().map((p) -> { + if (StringUtils.hasText(p)) { + return p; + } + return ""; + }).distinct().forEach((path) -> { + String pattern = (path.equals("/") ? "" : path); + for (String part : parts) { + pattern += part; + } + matchers.add(new AntPathRequestMatcher(pattern)); + }); + return matchers; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java index 92e9940ab1f..52479598d7b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.web.servlet; +import java.util.Collections; + import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType; @@ -95,7 +97,7 @@ class WebMvcEndpointChildContextConfiguration { @Bean public DispatcherServletPathProvider childDispatcherServletPathProvider() { - return () -> ""; + return () -> Collections.singleton(""); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java index 2c3c037458a..70cd7db5065 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java @@ -17,6 +17,8 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web; import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Test; @@ -48,18 +50,22 @@ public class ServletEndpointManagementContextConfigurationTests { .withUserConfiguration(TestConfig.class); @Test + @SuppressWarnings("unchecked") public void contextShouldContainServletEndpointRegistrar() { FilteredClassLoader classLoader = new FilteredClassLoader(ResourceConfig.class); this.contextRunner.withClassLoader(classLoader).run((context) -> { assertThat(context).hasSingleBean(ServletEndpointRegistrar.class); ServletEndpointRegistrar bean = context .getBean(ServletEndpointRegistrar.class); - String basePath = (String) ReflectionTestUtils.getField(bean, "basePath"); - assertThat(basePath).isEqualTo("/test/actuator"); + Set basePaths = (Set) ReflectionTestUtils.getField(bean, + "basePaths"); + assertThat(basePaths).containsExactlyInAnyOrder("/test/actuator", "/actuator", + "/foo/actuator"); }); } @Test + @SuppressWarnings("unchecked") public void servletPathShouldNotAffectJerseyConfiguration() { FilteredClassLoader classLoader = new FilteredClassLoader( DispatcherServlet.class); @@ -67,8 +73,9 @@ public class ServletEndpointManagementContextConfigurationTests { assertThat(context).hasSingleBean(ServletEndpointRegistrar.class); ServletEndpointRegistrar bean = context .getBean(ServletEndpointRegistrar.class); - String basePath = (String) ReflectionTestUtils.getField(bean, "basePath"); - assertThat(basePath).isEqualTo("/actuator"); + Set basePaths = (Set) ReflectionTestUtils.getField(bean, + "basePaths"); + assertThat(basePaths).containsExactly("/actuator"); }); } @@ -91,7 +98,13 @@ public class ServletEndpointManagementContextConfigurationTests { @Bean public DispatcherServletPathProvider servletPathProvider() { - return () -> "/test"; + return () -> { + Set paths = new LinkedHashSet<>(); + paths.add("/"); + paths.add("/test"); + paths.add("/foo/"); + return paths; + }; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java index ac8e6f103bd..e16c01e53fd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java @@ -17,6 +17,8 @@ package org.springframework.boot.actuate.autoconfigure.security.servlet; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -76,23 +78,23 @@ public class EndpointRequestTests { @Test public void toAnyEndpointWhenServletPathNotEmptyShouldMatch() { RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); - assertMatcher(matcher, "/actuator", "/spring").matches("/spring", - "/actuator/foo"); - assertMatcher(matcher, "/actuator", "/spring").matches("/spring", - "/actuator/bar"); - assertMatcher(matcher, "/actuator", "/spring").matches("/spring", "/actuator"); - assertMatcher(matcher, "/actuator", "/spring").doesNotMatch("/spring", - "/actuator/baz"); - assertMatcher(matcher, "/actuator", "/spring").doesNotMatch("", "/actuator/foo"); + assertMatcher(matcher, "/actuator", "/spring", "/admin").matches("/actuator/foo", + "/spring", "/admin"); + assertMatcher(matcher, "/actuator", "/spring", "/admin").matches("/actuator/bar", + "/spring", "/admin"); + assertMatcher(matcher, "/actuator", "/spring").matches("/actuator", "/spring"); + assertMatcher(matcher, "/actuator", "/spring").doesNotMatch("/actuator/baz", + "/spring"); + assertMatcher(matcher, "/actuator", "/spring").doesNotMatch("/actuator/foo", ""); } @Test public void toAnyEndpointWhenDispatcherServletPathProviderNotAvailableUsesEmptyPath() { RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); - assertMatcher(matcher, "/actuator", null).matches("/actuator/foo"); - assertMatcher(matcher, "/actuator", null).matches("/actuator/bar"); - assertMatcher(matcher, "/actuator", null).matches("/actuator"); - assertMatcher(matcher, "/actuator", null).doesNotMatch("/actuator/baz"); + assertMatcher(matcher, "/actuator", (String) null).matches("/actuator/foo"); + assertMatcher(matcher, "/actuator", (String) null).matches("/actuator/bar"); + assertMatcher(matcher, "/actuator", (String) null).matches("/actuator"); + assertMatcher(matcher, "/actuator", (String) null).doesNotMatch("/actuator/baz"); } @Test @@ -219,8 +221,8 @@ public class EndpointRequestTests { } private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePath, - String servletPath) { - return assertMatcher(matcher, mockPathMappedEndpoints(basePath), servletPath); + String... servletPaths) { + return assertMatcher(matcher, mockPathMappedEndpoints(basePath), servletPaths); } private PathMappedEndpoints mockPathMappedEndpoints(String basePath) { @@ -243,7 +245,7 @@ public class EndpointRequestTests { } private RequestMatcherAssert assertMatcher(RequestMatcher matcher, - PathMappedEndpoints pathMappedEndpoints, String servletPath) { + PathMappedEndpoints pathMappedEndpoints, String... servletPaths) { StaticWebApplicationContext context = new StaticWebApplicationContext(); context.registerBean(WebEndpointProperties.class); if (pathMappedEndpoints != null) { @@ -254,8 +256,9 @@ public class EndpointRequestTests { properties.setBasePath(pathMappedEndpoints.getBasePath()); } } - if (servletPath != null) { - DispatcherServletPathProvider pathProvider = () -> servletPath; + if (servletPaths != null) { + DispatcherServletPathProvider pathProvider = () -> new LinkedHashSet<>( + Arrays.asList(servletPaths)); context.registerBean(DispatcherServletPathProvider.class, () -> pathProvider); } return assertThat(new RequestMatcherAssert(context, matcher)); @@ -276,8 +279,8 @@ public class EndpointRequestTests { matches(mockRequest(servletPath)); } - public void matches(String servletPath, String pathInfo) { - matches(mockRequest(servletPath, pathInfo)); + public void matches(String pathInfo, String... servletPaths) { + Arrays.stream(servletPaths).forEach((p) -> matches(mockRequest(p, pathInfo))); } private void matches(HttpServletRequest request) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationTests.java index 4d67053661b..61e5c599343 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationTests.java @@ -68,8 +68,8 @@ public class WebMvcEndpointChildContextConfigurationTests { this.contextRunner .withUserConfiguration(WebMvcEndpointChildContextConfiguration.class) .run((context) -> assertThat(context - .getBean(DispatcherServletPathProvider.class).getServletPath()) - .isEmpty()); + .getBean(DispatcherServletPathProvider.class).getServletPaths()) + .containsExactly("")); } static class ExistingConfig { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrar.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrar.java index 9388ed10e86..8024fae074f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrar.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrar.java @@ -17,6 +17,8 @@ package org.springframework.boot.actuate.endpoint.web; import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -27,26 +29,38 @@ import org.apache.commons.logging.LogFactory; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * {@link ServletContextInitializer} to register {@link ExposableServletEndpoint servlet * endpoints}. * * @author Phillip Webb + * @author Madhura Bhave * @since 2.0.0 */ public class ServletEndpointRegistrar implements ServletContextInitializer { private static final Log logger = LogFactory.getLog(ServletEndpointRegistrar.class); - private final String basePath; + private final Set basePaths = new LinkedHashSet<>(); private final Collection servletEndpoints; public ServletEndpointRegistrar(String basePath, Collection servletEndpoints) { Assert.notNull(servletEndpoints, "ServletEndpoints must not be null"); - this.basePath = (basePath != null ? basePath : ""); + this.basePaths.add((basePath != null ? basePath : "")); + this.servletEndpoints = servletEndpoints; + } + + public ServletEndpointRegistrar(Set basePaths, + Collection servletEndpoints) { + Assert.notNull(servletEndpoints, "ServletEndpoints must not be null"); + this.basePaths.addAll(basePaths); + if (CollectionUtils.isEmpty(this.basePaths)) { + this.basePaths.add(""); + } this.servletEndpoints = servletEndpoints; } @@ -59,14 +73,20 @@ public class ServletEndpointRegistrar implements ServletContextInitializer { private void register(ServletContext servletContext, ExposableServletEndpoint endpoint) { String name = endpoint.getId() + "-actuator-endpoint"; - String path = this.basePath + "/" + endpoint.getRootPath(); - String urlMapping = (path.endsWith("/") ? path + "*" : path + "/*"); EndpointServlet endpointServlet = endpoint.getEndpointServlet(); Dynamic registration = servletContext.addServlet(name, endpointServlet.getServlet()); - registration.addMapping(urlMapping); + registration.addMapping(getUrlMappings(endpoint.getRootPath(), name)); registration.setInitParameters(endpointServlet.getInitParameters()); - logger.info("Registered '" + path + "' to " + name); + } + + private String[] getUrlMappings(String endpointPath, String name) { + return this.basePaths.stream() + .map((bp) -> (bp != null ? bp + "/" + endpointPath : "/" + endpointPath)) + .distinct().map((p) -> { + logger.info("Registered '" + p + "' to " + name); + return (p.endsWith("/") ? p + "*" : p + "/*"); + }).toArray(String[]::new); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrarTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrarTests.java index 542f435bc25..9221133b6c7 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrarTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrarTests.java @@ -18,6 +18,8 @@ package org.springframework.boot.actuate.endpoint.web; import java.io.IOException; import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; import javax.servlet.GenericServlet; import javax.servlet.Servlet; @@ -47,6 +49,7 @@ import static org.mockito.Mockito.verify; * Tests for {@link ServletEndpointRegistrar}. * * @author Phillip Webb + * @author Madhura Bhave */ public class ServletEndpointRegistrarTests { @@ -73,14 +76,14 @@ public class ServletEndpointRegistrarTests { public void createWhenServletEndpointsIsNullShouldThrowException() { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("ServletEndpoints must not be null"); - new ServletEndpointRegistrar(null, null); + new ServletEndpointRegistrar((String) null, null); } @Test public void onStartupShouldRegisterServlets() throws Exception { ExposableServletEndpoint endpoint = mockEndpoint( new EndpointServlet(TestServlet.class)); - ServletEndpointRegistrar registrar = new ServletEndpointRegistrar(null, + ServletEndpointRegistrar registrar = new ServletEndpointRegistrar((String) null, Collections.singleton(endpoint)); registrar.onStartup(this.servletContext); verify(this.servletContext).addServlet(eq("test-actuator-endpoint"), @@ -102,6 +105,64 @@ public class ServletEndpointRegistrarTests { verify(this.dynamic).addMapping("/actuator/test/*"); } + @Test + public void onStartupWhenHasMultipleBasePathsShouldIncludeAllBasePaths() + throws Exception { + ExposableServletEndpoint endpoint = mockEndpoint( + new EndpointServlet(TestServlet.class)); + Set basePaths = new LinkedHashSet<>(); + basePaths.add("/actuator"); + basePaths.add("/admin"); + basePaths.add("/application"); + ServletEndpointRegistrar registrar = new ServletEndpointRegistrar(basePaths, + Collections.singleton(endpoint)); + registrar.onStartup(this.servletContext); + verify(this.servletContext).addServlet(eq("test-actuator-endpoint"), + this.servlet.capture()); + assertThat(this.servlet.getValue()).isInstanceOf(TestServlet.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(this.dynamic).addMapping(captor.capture()); + assertThat(captor.getAllValues()).containsExactlyInAnyOrder("/application/test/*", + "/admin/test/*", "/actuator/test/*"); + } + + @Test + public void onStartupWhenHasEmptyBasePathsShouldIncludeRoot() throws Exception { + ExposableServletEndpoint endpoint = mockEndpoint( + new EndpointServlet(TestServlet.class)); + Set basePaths = Collections.emptySet(); + ServletEndpointRegistrar registrar = new ServletEndpointRegistrar(basePaths, + Collections.singleton(endpoint)); + registrar.onStartup(this.servletContext); + verify(this.dynamic).addMapping("/test/*"); + } + + @Test + public void onStartupWhenHasBasePathsHasNullValueShouldIncludeRoot() + throws Exception { + ExposableServletEndpoint endpoint = mockEndpoint( + new EndpointServlet(TestServlet.class)); + Set basePaths = new LinkedHashSet<>(); + basePaths.add(null); + ServletEndpointRegistrar registrar = new ServletEndpointRegistrar(basePaths, + Collections.singleton(endpoint)); + registrar.onStartup(this.servletContext); + verify(this.dynamic).addMapping("/test/*"); + } + + @Test + public void onStartupWhenDuplicateValuesShouldIncludeDistinct() throws Exception { + ExposableServletEndpoint endpoint = mockEndpoint( + new EndpointServlet(TestServlet.class)); + Set basePaths = new LinkedHashSet<>(); + basePaths.add(""); + basePaths.add(null); + ServletEndpointRegistrar registrar = new ServletEndpointRegistrar(basePaths, + Collections.singleton(endpoint)); + registrar.onStartup(this.servletContext); + verify(this.dynamic).addMapping("/test/*"); + } + @Test public void onStartupWhenHasInitParametersShouldRegisterInitParameters() throws Exception { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java index dfc475b4bb5..055cdc32cf2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.web.servlet; import java.util.Arrays; +import java.util.Collections; import java.util.List; import javax.servlet.MultipartConfigElement; @@ -151,8 +152,9 @@ public class DispatcherServletAutoConfiguration { @ConditionalOnMissingBean(DispatcherServletPathProvider.class) @ConditionalOnSingleCandidate(DispatcherServlet.class) public DispatcherServletPathProvider dispatcherServletPathProvider() { - return () -> DispatcherServletRegistrationConfiguration.this.webMvcProperties - .getServlet().getPath(); + return () -> Collections.singleton( + DispatcherServletRegistrationConfiguration.this.webMvcProperties + .getServlet().getPath()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletPathProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletPathProvider.java index 8adcee4c1dc..d0cb308d6b6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletPathProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletPathProvider.java @@ -16,11 +16,13 @@ package org.springframework.boot.autoconfigure.web.servlet; +import java.util.Set; + import org.springframework.web.servlet.DispatcherServlet; /** - * Interface that provides the path of the {@link DispatcherServlet} in an application - * context. + * Interface that provides the paths that the {@link DispatcherServlet} in an application + * context is mapped to. * * @author Madhura Bhave * @since 2.0.2 @@ -28,6 +30,6 @@ import org.springframework.web.servlet.DispatcherServlet; @FunctionalInterface public interface DispatcherServletPathProvider { - String getServletPath(); + Set getServletPaths(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java index af000e0ce0f..d2da32c0494 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java @@ -112,7 +112,7 @@ public class DispatcherServletAutoConfigurationTests { .containsExactly("/spring/*"); assertThat(registration.getMultipartConfig()).isNull(); assertThat(context.getBean(DispatcherServletPathProvider.class) - .getServletPath()).isEqualTo("/spring"); + .getServletPaths()).containsExactly("/spring"); }); } @@ -129,8 +129,8 @@ public class DispatcherServletAutoConfigurationTests { this.contextRunner.withUserConfiguration(CustomDispatcherServletSameName.class) .withPropertyValues("spring.mvc.servlet.path:/spring") .run((context) -> assertThat(context - .getBean(DispatcherServletPathProvider.class).getServletPath()) - .isEqualTo("/spring")); + .getBean(DispatcherServletPathProvider.class).getServletPaths()) + .containsExactly("/spring")); } @Test