diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementContextAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementContextAutoConfiguration.java index 3bdf56e4506..79c7d728843 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementContextAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementContextAutoConfiguration.java @@ -82,7 +82,6 @@ public class ManagementContextAutoConfiguration { @Override public void afterSingletonsInstantiated() { verifySslConfiguration(); - verifyContextPathConfiguration(); if (this.environment instanceof ConfigurableEnvironment) { addLocalManagementPortPropertyAlias( (ConfigurableEnvironment) this.environment); @@ -97,15 +96,6 @@ public class ManagementContextAutoConfiguration { + "server is not listening on a separate port"); } - private void verifyContextPathConfiguration() { - String contextPath = this.environment.getProperty("management.context-path"); - if ("".equals(contextPath) || "/".equals(contextPath)) { - throw new IllegalStateException("A management context path of '" - + contextPath + "' requires the management server to be " - + "listening on a separate port"); - } - } - /** * Add an alias for 'local.management.port' that actually resolves using * 'local.server.port'. diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/WebEndpointInfrastructureManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/WebEndpointInfrastructureManagementContextConfiguration.java index 37fff4eb876..34acfe2b1dc 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/WebEndpointInfrastructureManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/infrastructure/WebEndpointInfrastructureManagementContextConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.WebEndpointOperation; import org.springframework.boot.endpoint.web.jersey.JerseyEndpointResourceFactory; import org.springframework.boot.endpoint.web.mvc.WebEndpointServletHandlerMapping; @@ -66,7 +67,8 @@ class WebEndpointInfrastructureManagementContextConfiguration { ManagementServerProperties managementServerProperties) { return (resourceConfig) -> resourceConfig.registerResources(new HashSet<>( new JerseyEndpointResourceFactory().createEndpointResources( - managementServerProperties.getContextPath(), + new EndpointMapping( + managementServerProperties.getContextPath()), provider.getEndpoints()))); } @@ -93,8 +95,8 @@ class WebEndpointInfrastructureManagementContextConfiguration { CorsEndpointProperties corsProperties, ManagementServerProperties managementServerProperties) { WebEndpointServletHandlerMapping handlerMapping = new WebEndpointServletHandlerMapping( - managementServerProperties.getContextPath(), provider.getEndpoints(), - getCorsConfiguration(corsProperties)); + new EndpointMapping(managementServerProperties.getContextPath()), + provider.getEndpoints(), getCorsConfiguration(corsProperties)); for (WebEndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) { customizer.customize(handlerMapping); } @@ -137,7 +139,8 @@ class WebEndpointInfrastructureManagementContextConfiguration { EndpointProvider provider, ManagementServerProperties managementServerProperties) { return new WebEndpointReactiveHandlerMapping( - managementServerProperties.getContextPath(), provider.getEndpoints()); + new EndpointMapping(managementServerProperties.getContextPath()), + provider.getEndpoints()); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java index 964a64cd9f8..c947723a8c8 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java @@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.WebEndpointOperation; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; @@ -69,8 +70,9 @@ public class CloudFoundryActuatorAutoConfiguration { EndpointProvider provider, Environment environment, RestTemplateBuilder builder) { return new CloudFoundryWebEndpointServletHandlerMapping( - "/cloudfoundryapplication", provider.getEndpoints(), - getCorsConfiguration(), getSecurityInterceptor(builder, environment)); + new EndpointMapping("/cloudfoundryapplication"), + provider.getEndpoints(), getCorsConfiguration(), + getSecurityInterceptor(builder, environment)); } private CloudFoundrySecurityInterceptor getSecurityInterceptor( diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryWebEndpointServletHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryWebEndpointServletHandlerMapping.java index 645ce12ebea..c49edc94621 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryWebEndpointServletHandlerMapping.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryWebEndpointServletHandlerMapping.java @@ -35,6 +35,7 @@ import org.springframework.boot.endpoint.EndpointInfo; import org.springframework.boot.endpoint.OperationInvoker; import org.springframework.boot.endpoint.ParameterMappingException; import org.springframework.boot.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.Link; import org.springframework.boot.endpoint.web.WebEndpointOperation; import org.springframework.boot.endpoint.web.WebEndpointResponse; @@ -72,11 +73,11 @@ class CloudFoundryWebEndpointServletHandlerMapping private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver(); - CloudFoundryWebEndpointServletHandlerMapping(String endpointPath, + CloudFoundryWebEndpointServletHandlerMapping(EndpointMapping endpointMapping, Collection> webEndpoints, CorsConfiguration corsConfiguration, CloudFoundrySecurityInterceptor securityInterceptor) { - super(endpointPath, webEndpoints, corsConfiguration); + super(endpointMapping, webEndpoints, corsConfiguration); this.securityInterceptor = securityInterceptor; } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java index 1ee0ee22cfb..90c6fb268a7 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java @@ -79,7 +79,7 @@ public class CloudFoundryActuatorAutoConfigurationTests { @Test public void cloudFoundryPlatformActive() throws Exception { CloudFoundryWebEndpointServletHandlerMapping handlerMapping = getHandlerMapping(); - assertThat(handlerMapping.getEndpointPath()) + assertThat(handlerMapping.getEndpointMapping().getPath()) .isEqualTo("/cloudfoundryapplication"); CorsConfiguration corsConfiguration = (CorsConfiguration) ReflectionTestUtils .getField(handlerMapping, "corsConfiguration"); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryMvcWebEndpointIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryMvcWebEndpointIntegrationTests.java index e3deb30b9d5..59e42ac6bd6 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryMvcWebEndpointIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryMvcWebEndpointIntegrationTests.java @@ -31,6 +31,7 @@ import org.springframework.boot.endpoint.OperationParameterMapper; import org.springframework.boot.endpoint.ReadOperation; import org.springframework.boot.endpoint.Selector; import org.springframework.boot.endpoint.WriteOperation; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; @@ -196,7 +197,8 @@ public class CloudFoundryMvcWebEndpointIntegrationTests { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com")); corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST")); - return new CloudFoundryWebEndpointServletHandlerMapping("/cfApplication", + return new CloudFoundryWebEndpointServletHandlerMapping( + new EndpointMapping("/cfApplication"), webEndpointDiscoverer.discoverEndpoints(), corsConfiguration, interceptor); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/RequestMappingEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/RequestMappingEndpointTests.java index 1f90253c950..f38479eaeff 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/RequestMappingEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/RequestMappingEndpointTests.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.springframework.boot.endpoint.EndpointInfo; import org.springframework.boot.endpoint.OperationType; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.OperationRequestPredicate; import org.springframework.boot.endpoint.web.WebEndpointHttpMethod; import org.springframework.boot.endpoint.web.WebEndpointOperation; @@ -138,7 +139,8 @@ public class RequestMappingEndpointTests { WebEndpointOperation operation = new WebEndpointOperation(OperationType.READ, (arguments) -> "Invoked", true, requestPredicate, "test"); WebEndpointServletHandlerMapping mapping = new WebEndpointServletHandlerMapping( - "application", Collections.singleton(new EndpointInfo<>("test", true, + new EndpointMapping("application"), + Collections.singleton(new EndpointInfo<>("test", true, Collections.singleton(operation)))); mapping.setApplicationContext(new StaticApplicationContext()); mapping.afterPropertiesSet(); diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 4cb903bcf0e..26294c486b5 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -223,14 +223,15 @@ property. For example, the following will disable _all_ endpoints except for `in [[production-ready-endpoint-hypermedia]] -=== Hypermedia for actuator MVC endpoints +=== Hypermedia for actuator web endpoints A "`discovery page`" is added with links to all the endpoints. The "`discovery page`" is available on `/application` by default. When a custom management context path is configured, the "`discovery page`" will automatically move from `/application` to the root of the management context. For example, if the management context path is `/management` then the discovery page will be available -from `/management`. +from `/management`. When the management context path is set to `/` the discovery page +is disabled to prevent the possiblility of a clash with other mappings. diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointMapping.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointMapping.java new file mode 100644 index 00000000000..3915cb2e7cb --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointMapping.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.boot.endpoint.web; + +import org.springframework.util.StringUtils; + +/** + * A value object for the base mapping for endpoints. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public class EndpointMapping { + + private final String path; + + /** + * Creates a new {@code EndpointMapping} using the given {@code path}. + * + * @param path the path + */ + public EndpointMapping(String path) { + this.path = normalizePath(path); + } + + private static String normalizePath(String path) { + if (!StringUtils.hasText(path)) { + return path; + } + String normalizedPath = path; + if (!normalizedPath.startsWith("/")) { + normalizedPath = "/" + normalizedPath; + } + if (normalizedPath.endsWith("/")) { + normalizedPath = normalizedPath.substring(0, normalizedPath.length() - 1); + } + return normalizedPath; + } + + /** + * Returns the path to which endpoints should be mapped. + * @return the path + */ + public String getPath() { + return this.path; + } + + public String createSubPath(String path) { + return this.path + normalizePath(path); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java index 6de32a2d3af..9cd95b328ab 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java @@ -27,6 +27,7 @@ import java.util.function.Function; import javax.ws.rs.HttpMethod; import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @@ -41,12 +42,14 @@ import org.springframework.boot.endpoint.EndpointInfo; import org.springframework.boot.endpoint.OperationInvoker; import org.springframework.boot.endpoint.ParameterMappingException; import org.springframework.boot.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.Link; import org.springframework.boot.endpoint.web.OperationRequestPredicate; import org.springframework.boot.endpoint.web.WebEndpointOperation; import org.springframework.boot.endpoint.web.WebEndpointResponse; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; /** * A factory for creating Jersey {@link Resource Resources} for web endpoint operations. @@ -61,25 +64,29 @@ public class JerseyEndpointResourceFactory { /** * Creates {@link Resource Resources} for the operations of the given * {@code webEndpoints}. - * @param endpointPath the path beneath which all endpoints should be mapped + * @param endpointMapping the base mapping for all endpoints * @param webEndpoints the web endpoints * @return the resources for the operations */ - public Collection createEndpointResources(String endpointPath, + public Collection createEndpointResources(EndpointMapping endpointMapping, Collection> webEndpoints) { List resources = new ArrayList<>(); webEndpoints.stream() .flatMap((endpointInfo) -> endpointInfo.getOperations().stream()) - .map((operation) -> createResource(endpointPath, operation)) + .map((operation) -> createResource(endpointMapping, operation)) .forEach(resources::add); - resources.add(createEndpointLinksResource(endpointPath, webEndpoints)); + if (StringUtils.hasText(endpointMapping.getPath())) { + resources.add( + createEndpointLinksResource(endpointMapping.getPath(), webEndpoints)); + } return resources; } - private Resource createResource(String endpointPath, WebEndpointOperation operation) { + private Resource createResource(EndpointMapping endpointMapping, + WebEndpointOperation operation) { OperationRequestPredicate requestPredicate = operation.getRequestPredicate(); Builder resourceBuilder = Resource.builder() - .path(endpointPath + "/" + requestPredicate.getPath()); + .path(endpointMapping.createSubPath(requestPredicate.getPath())); resourceBuilder.addMethod(requestPredicate.getHttpMethod().name()) .consumes(toStringArray(requestPredicate.getConsumes())) .produces(toStringArray(requestPredicate.getProduces())) @@ -95,7 +102,7 @@ public class JerseyEndpointResourceFactory { private Resource createEndpointLinksResource(String endpointPath, Collection> webEndpoints) { Builder resourceBuilder = Resource.builder().path(endpointPath); - resourceBuilder.addMethod("GET").handledBy( + resourceBuilder.addMethod("GET").produces(MediaType.APPLICATION_JSON).handledBy( new EndpointLinksInflector(webEndpoints, this.endpointLinksResolver)); return resourceBuilder.build(); } diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/AbstractWebEndpointServletHandlerMapping.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/AbstractWebEndpointServletHandlerMapping.java index b04bf74f83d..d8ccfd9f1b1 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/AbstractWebEndpointServletHandlerMapping.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/AbstractWebEndpointServletHandlerMapping.java @@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.endpoint.EndpointInfo; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.OperationRequestPredicate; import org.springframework.boot.endpoint.web.WebEndpointOperation; import org.springframework.util.StringUtils; @@ -50,7 +51,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi public abstract class AbstractWebEndpointServletHandlerMapping extends RequestMappingInfoHandlerMapping implements InitializingBean { - private final String endpointPath; + private final EndpointMapping endpointMapping; private final Collection> webEndpoints; @@ -59,25 +60,25 @@ public abstract class AbstractWebEndpointServletHandlerMapping /** * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the * operations of the given {@code webEndpoints}. - * @param endpointPath the path beneath which all endpoints should be mapped + * @param endpointMapping the base mapping for all endpoints * @param collection the web endpoints operations */ - public AbstractWebEndpointServletHandlerMapping(String endpointPath, + public AbstractWebEndpointServletHandlerMapping(EndpointMapping endpointMapping, Collection> collection) { - this(endpointPath, collection, null); + this(endpointMapping, collection, null); } /** * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the * operations of the given {@code webEndpoints}. - * @param endpointPath the path beneath which all endpoints should be mapped + * @param endpointMapping the base mapping for all endpoints * @param webEndpoints the web endpoints * @param corsConfiguration the CORS configuration for the endpoints */ - public AbstractWebEndpointServletHandlerMapping(String endpointPath, + public AbstractWebEndpointServletHandlerMapping(EndpointMapping endpointMapping, Collection> webEndpoints, CorsConfiguration corsConfiguration) { - this.endpointPath = (endpointPath.startsWith("/") ? "" : "/") + endpointPath; + this.endpointMapping = endpointMapping; this.webEndpoints = webEndpoints; this.corsConfiguration = corsConfiguration; setOrder(-100); @@ -87,8 +88,8 @@ public abstract class AbstractWebEndpointServletHandlerMapping return this.webEndpoints; } - public String getEndpointPath() { - return this.endpointPath; + public EndpointMapping getEndpointMapping() { + return this.endpointMapping; } @Override @@ -96,6 +97,12 @@ public abstract class AbstractWebEndpointServletHandlerMapping this.webEndpoints.stream() .flatMap((webEndpoint) -> webEndpoint.getOperations().stream()) .forEach(this::registerMappingForOperation); + if (StringUtils.hasText(this.endpointMapping.getPath())) { + registerLinksRequestMapping(); + } + } + + private void registerLinksRequestMapping() { PatternsRequestCondition patterns = patternsRequestConditionForPattern(""); RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( RequestMethod.GET); @@ -130,8 +137,7 @@ public abstract class AbstractWebEndpointServletHandlerMapping } private PatternsRequestCondition patternsRequestConditionForPattern(String path) { - String[] patterns = new String[] { - this.endpointPath + (StringUtils.hasText(path) ? "/" + path : "") }; + String[] patterns = new String[] { this.endpointMapping.createSubPath(path) }; return new PatternsRequestCondition(patterns, null, null, false, false); } diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java index 0797da651ad..abdac223061 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java @@ -29,6 +29,7 @@ import org.springframework.boot.endpoint.EndpointInfo; import org.springframework.boot.endpoint.OperationInvoker; import org.springframework.boot.endpoint.ParameterMappingException; import org.springframework.boot.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.Link; import org.springframework.boot.endpoint.web.WebEndpointOperation; import org.springframework.boot.endpoint.web.WebEndpointResponse; @@ -63,25 +64,25 @@ public class WebEndpointServletHandlerMapping /** * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the * operations of the given {@code webEndpoints}. - * @param endpointPath the path beneath which all endpoints should be mapped + * @param endpointMapping the base mapping for all endpoints * @param collection the web endpoints operations */ - public WebEndpointServletHandlerMapping(String endpointPath, + public WebEndpointServletHandlerMapping(EndpointMapping endpointMapping, Collection> collection) { - this(endpointPath, collection, null); + this(endpointMapping, collection, null); } /** * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the * operations of the given {@code webEndpoints}. - * @param endpointPath the path beneath which all endpoints should be mapped + * @param endpointMapping the base mapping for all endpoints * @param webEndpoints the web endpoints * @param corsConfiguration the CORS configuration for the endpoints */ - public WebEndpointServletHandlerMapping(String endpointPath, + public WebEndpointServletHandlerMapping(EndpointMapping endpointMapping, Collection> webEndpoints, CorsConfiguration corsConfiguration) { - super(endpointPath, webEndpoints, corsConfiguration); + super(endpointMapping, webEndpoints, corsConfiguration); setOrder(-100); } diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/WebEndpointReactiveHandlerMapping.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/WebEndpointReactiveHandlerMapping.java index 5223e2f86b2..05410eccfe7 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/WebEndpointReactiveHandlerMapping.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/WebEndpointReactiveHandlerMapping.java @@ -33,6 +33,7 @@ import org.springframework.boot.endpoint.OperationInvoker; import org.springframework.boot.endpoint.OperationType; import org.springframework.boot.endpoint.ParameterMappingException; import org.springframework.boot.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.Link; import org.springframework.boot.endpoint.web.OperationRequestPredicate; import org.springframework.boot.endpoint.web.WebEndpointOperation; @@ -42,6 +43,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @@ -80,7 +82,7 @@ public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandler private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver(); - private final String endpointPath; + private final EndpointMapping endpointMapping; private final Collection> webEndpoints; @@ -89,25 +91,25 @@ public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandler /** * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the * operations of the given {@code webEndpoints}. - * @param endpointPath the path beneath which all endpoints should be mapped + * @param endpointMapping the base mapping for all endpoints * @param collection the web endpoints */ - public WebEndpointReactiveHandlerMapping(String endpointPath, + public WebEndpointReactiveHandlerMapping(EndpointMapping endpointMapping, Collection> collection) { - this(endpointPath, collection, null); + this(endpointMapping, collection, null); } /** * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the * operations of the given {@code webEndpoints}. - * @param endpointPath the path beneath which all endpoints should be mapped + * @param endpointMapping the path beneath which all endpoints should be mapped * @param webEndpoints the web endpoints * @param corsConfiguration the CORS configuration for the endpoints */ - public WebEndpointReactiveHandlerMapping(String endpointPath, + public WebEndpointReactiveHandlerMapping(EndpointMapping endpointMapping, Collection> webEndpoints, CorsConfiguration corsConfiguration) { - this.endpointPath = (endpointPath.startsWith("/") ? "" : "/") + endpointPath; + this.endpointMapping = endpointMapping; this.webEndpoints = webEndpoints; this.corsConfiguration = corsConfiguration; setOrder(-100); @@ -118,8 +120,15 @@ public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandler this.webEndpoints.stream() .flatMap((webEndpoint) -> webEndpoint.getOperations().stream()) .forEach(this::registerMappingForOperation); + if (StringUtils.hasText(this.endpointMapping.getPath())) { + registerLinksMapping(); + } + } + + private void registerLinksMapping() { registerMapping(new RequestMappingInfo( - new PatternsRequestCondition(pathPatternParser.parse(this.endpointPath)), + new PatternsRequestCondition( + pathPatternParser.parse(this.endpointMapping.getPath())), new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null, null, null), this, this.links); } @@ -148,7 +157,7 @@ public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandler WebEndpointOperation operationInfo) { OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate(); PatternsRequestCondition patterns = new PatternsRequestCondition(pathPatternParser - .parse(this.endpointPath + "/" + requestPredicate.getPath())); + .parse(this.endpointMapping.createSubPath(requestPredicate.getPath()))); RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( RequestMethod.valueOf(requestPredicate.getHttpMethod().name())); ConsumesRequestCondition consumes = new ConsumesRequestCondition( diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java index 38ba9efa62f..dbef9127021 100644 --- a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java @@ -26,6 +26,7 @@ import java.util.function.Consumer; import org.junit.Test; import reactor.core.publisher.Mono; +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; import org.springframework.boot.endpoint.CachingConfiguration; import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper; import org.springframework.boot.endpoint.DeleteOperation; @@ -40,6 +41,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.MapPropertySource; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; @@ -71,6 +73,14 @@ public abstract class AbstractWebEndpointIntegrationTests client.get().uri("/test").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody().jsonPath("All") + .isEqualTo(true)); + } + @Test public void readOperationWithSelector() { load(TestEndpointConfiguration.class, @@ -101,6 +111,13 @@ public abstract class AbstractWebEndpointIntegrationTests client.get().uri("").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isNotFound()); + } + @Test public void readOperationWithSingleQueryParameters() { load(QueryEndpointConfiguration.class, @@ -268,12 +285,20 @@ public abstract class AbstractWebEndpointIntegrationTests configuration, BiConsumer consumer) { + load(configuration, "/endpoints", consumer); + } + + private void load(Class configuration, String endpointPath, + BiConsumer consumer) { T context = createApplicationContext(configuration, this.exporterConfiguration); + context.getEnvironment().getPropertySources().addLast(new MapPropertySource( + "test", Collections.singletonMap("endpointPath", endpointPath))); + context.refresh(); try { consumer.accept(context, WebTestClient.bindToServer() .baseUrl( - "http://localhost:" + getPort(context) + "/endpoints") + "http://localhost:" + getPort(context) + endpointPath) .build()); } finally { @@ -282,7 +307,14 @@ public abstract class AbstractWebEndpointIntegrationTests configuration, Consumer clientConsumer) { - load(configuration, (context, client) -> clientConsumer.accept(client)); + load(configuration, "/endpoints", + (context, client) -> clientConsumer.accept(client)); + } + + protected void load(Class configuration, String endpointPath, + Consumer clientConsumer) { + load(configuration, endpointPath, + (context, client) -> clientConsumer.accept(client)); } @Configuration @@ -304,6 +336,11 @@ public abstract class AbstractWebEndpointIntegrationTests... config) { - return new AnnotationConfigServletWebServerApplicationContext(config); + AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); + context.register(config); + return context; } @Override @@ -74,11 +78,12 @@ public class JerseyWebEndpointIntegrationTests extends } @Bean - public ResourceConfig resourceConfig( + public ResourceConfig resourceConfig(Environment environment, WebAnnotationEndpointDiscoverer endpointDiscoverer) { ResourceConfig resourceConfig = new ResourceConfig(); Collection resources = new JerseyEndpointResourceFactory() - .createEndpointResources("endpoints", + .createEndpointResources( + new EndpointMapping(environment.getProperty("endpointPath")), endpointDiscoverer.discoverEndpoints()); resourceConfig.registerResources(new HashSet<>(resources)); resourceConfig.register(JacksonFeature.class); diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/mvc/MvcWebEndpointIntegrationTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/mvc/MvcWebEndpointIntegrationTests.java index 1f9c77f6c0e..f58c7f4fe00 100644 --- a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/mvc/MvcWebEndpointIntegrationTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/mvc/MvcWebEndpointIntegrationTests.java @@ -21,11 +21,13 @@ import java.util.Arrays; import org.junit.Test; import org.springframework.boot.endpoint.web.AbstractWebEndpointIntegrationTests; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; import org.springframework.http.MediaType; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.DispatcherServlet; @@ -59,7 +61,9 @@ public class MvcWebEndpointIntegrationTests extends @Override protected AnnotationConfigServletWebServerApplicationContext createApplicationContext( Class... config) { - return new AnnotationConfigServletWebServerApplicationContext(config); + AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); + context.register(config); + return context; } @Override @@ -83,11 +87,13 @@ public class MvcWebEndpointIntegrationTests extends @Bean public WebEndpointServletHandlerMapping webEndpointHandlerMapping( + Environment environment, WebAnnotationEndpointDiscoverer webEndpointDiscoverer) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com")); corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST")); - return new WebEndpointServletHandlerMapping("/endpoints", + return new WebEndpointServletHandlerMapping( + new EndpointMapping(environment.getProperty("endpointPath")), webEndpointDiscoverer.discoverEndpoints(), corsConfiguration); } diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/reactive/ReactiveWebEndpointIntegrationTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/reactive/ReactiveWebEndpointIntegrationTests.java index 2335671daea..7371d89c243 100644 --- a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/reactive/ReactiveWebEndpointIntegrationTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/reactive/ReactiveWebEndpointIntegrationTests.java @@ -21,6 +21,7 @@ import java.util.Arrays; import org.junit.Test; import org.springframework.boot.endpoint.web.AbstractWebEndpointIntegrationTests; +import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext; @@ -29,6 +30,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.cors.CorsConfiguration; @@ -63,7 +65,9 @@ public class ReactiveWebEndpointIntegrationTests @Override protected ReactiveWebServerApplicationContext createApplicationContext( Class... config) { - return new ReactiveWebServerApplicationContext(config); + ReactiveWebServerApplicationContext context = new ReactiveWebServerApplicationContext(); + context.register(config); + return context; } @Override @@ -89,11 +93,13 @@ public class ReactiveWebEndpointIntegrationTests @Bean public WebEndpointReactiveHandlerMapping webEndpointHandlerMapping( + Environment environment, WebAnnotationEndpointDiscoverer endpointDiscoverer) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com")); corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST")); - return new WebEndpointReactiveHandlerMapping("endpoints", + return new WebEndpointReactiveHandlerMapping( + new EndpointMapping(environment.getProperty("endpointPath")), endpointDiscoverer.discoverEndpoints(), corsConfiguration); }