From 58db5a3889a9335b36c4902690615104731c6be1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 6 Aug 2015 15:56:06 +0100 Subject: [PATCH] Combine /links and /hal into a single /actuator endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit provides a single endpoint, /actuator, that serves HTML (the HAL browser) or JSON depending on the request’s accept header that enables discovery of all of the actuator’s other endpoints. When the management context path is configured, the /actuator endpoint moves to the configured path, e.g. if the management context path is set to /management, the actuator endpoint will be available from /management. Closes gh-3696 --- .../hypermedia/EndpointDocumentation.java | 2 +- .../HypermediaEndpointDocumentation.java | 2 +- ...ermediaManagementContextConfiguration.java | 352 ++++++++---------- .../actuate/autoconfigure/LinksEnhancer.java | 2 +- ...Endpoint.java => ActuatorMvcEndpoint.java} | 85 +++-- .../endpoint/mvc/HypermediaDisabled.java | 4 +- .../endpoint/mvc/LinksMvcEndpoint.java | 95 ----- ...BrowserPathHypermediaIntegrationTests.java | 22 +- ...tomHomepageHypermediaIntegrationTests.java | 101 ----- .../EndpointWebMvcAutoConfigurationTests.java | 4 +- ...ontextPathHypermediaIntegrationTests.java} | 34 +- ...ContextPathHypermediaIntegrationTests.java | 32 +- .../ServerPortHypermediaIntegrationTests.java | 20 +- .../VanillaHypermediaIntegrationTests.java | 25 +- .../appendix-application-properties.adoc | 6 +- .../asciidoc/production-ready-features.adoc | 23 +- 16 files changed, 301 insertions(+), 508 deletions(-) rename spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/{HalBrowserMvcEndpoint.java => ActuatorMvcEndpoint.java} (74%) delete mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java delete mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java rename spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/{ContextPathHypermediaIntegrationTests.java => ManagementContextPathHypermediaIntegrationTests.java} (79%) diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java index 7d319b894d5..79b6c925cb8 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java @@ -61,7 +61,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration @TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true", - "endpoints.health.sensitive=true", "endpoints.links.enabled=false" }) + "endpoints.health.sensitive=true", "endpoints.actuator.enabled=false" }) @DirtiesContext public class EndpointDocumentation { diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java index 6188868134b..b343e421d1e 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java @@ -82,7 +82,7 @@ public class HypermediaEndpointDocumentation { @Test public void home() throws Exception { - this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON)) + this.mockMvc.perform(get("/actuator").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("admin")); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java index c95db6d57b5..e5e75ab0045 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java @@ -25,32 +25,23 @@ import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.mvc.ActuatorDocsEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.HalBrowserMvcEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.HalBrowserMvcEndpoint.HalBrowserLocation; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HypermediaDisabled; -import org.springframework.boot.actuate.endpoint.mvc.LinksMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.context.annotation.Conditional; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.ResourceLoader; -import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; @@ -75,7 +66,6 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -import static org.springframework.hateoas.mvc.BasicLinkBuilder.linkToCurrentMapping; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; /** @@ -83,6 +73,7 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; * * @author Dave Syer * @author Phillip Webb + * @author Andy Wilkinson * @since 1.3.0 */ @ManagementContextConfiguration @@ -93,16 +84,15 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; @EnableConfigurationProperties(ResourceProperties.class) public class EndpointWebMvcHypermediaManagementContextConfiguration { - @ConditionalOnProperty(value = "endpoints.hal.enabled", matchIfMissing = true) - @Conditional(HalBrowserCondition.class) + @ConditionalOnProperty(prefix = "endpoints.actuator", name = "enabled", matchIfMissing = true) @Bean - public HalBrowserMvcEndpoint halBrowserMvcEndpoint( - ManagementServerProperties management, ResourceProperties resources) { - return new HalBrowserMvcEndpoint(management); + public ActuatorMvcEndpoint actuatorMvcEndpoint(ManagementServerProperties management, + ResourceProperties resources) { + return new ActuatorMvcEndpoint(management); } @Bean - @ConditionalOnProperty(value = "endpoints.docs.enabled", matchIfMissing = true) + @ConditionalOnProperty(prefix = "endpoints.docs", name = "enabled", matchIfMissing = true) @ConditionalOnResource(resources = "classpath:/META-INF/resources/spring-boot-actuator/docs/index.html") public ActuatorDocsEndpoint actuatorDocsEndpoint(ManagementServerProperties management) { return new ActuatorDocsEndpoint(management); @@ -111,7 +101,7 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration { @Bean @ConditionalOnBean(ActuatorDocsEndpoint.class) @ConditionalOnMissingBean(CurieProvider.class) - @ConditionalOnProperty(value = "endpoints.docs.curies.enabled", matchIfMissing = false) + @ConditionalOnProperty(prefix = "endpoints.docs.curies", name = "enabled", matchIfMissing = false) public DefaultCurieProvider curieProvider(ServerProperties server, ManagementServerProperties management, ActuatorDocsEndpoint endpoint) { String path = management.getContextPath() + endpoint.getPath() @@ -124,217 +114,167 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration { } /** - * {@link SpringBootCondition} to detect the HAL browser. + * Controller advice that adds links to the actuator endpoint's path. */ - protected static class HalBrowserCondition extends SpringBootCondition { + @ControllerAdvice + public static class ActuatorEndpointLinksAdvice implements ResponseBodyAdvice { + + @Autowired + private MvcEndpoints endpoints; + + @Autowired(required = false) + private ActuatorMvcEndpoint actuatorEndpoint; + + @Autowired + private ManagementServerProperties management; + + private LinksEnhancer linksEnhancer; + + @PostConstruct + public void init() { + this.linksEnhancer = new LinksEnhancer(this.management.getContextPath(), + this.endpoints); + } @Override - public ConditionOutcome getMatchOutcome(ConditionContext context, - AnnotatedTypeMetadata metadata) { - ResourceLoader loader = context.getResourceLoader(); - loader = (loader == null ? new DefaultResourceLoader() : loader); - HalBrowserLocation found = HalBrowserMvcEndpoint - .getHalBrowserLocation(loader); - return new ConditionOutcome(found != null, "HAL Browser " - + (found == null ? "not found" : "at " + found)); + public boolean supports(MethodParameter returnType, + Class> converterType) { + returnType.increaseNestingLevel(); + Type nestedType = returnType.getNestedGenericParameterType(); + returnType.decreaseNestingLevel(); + return ResourceSupport.class.isAssignableFrom(returnType.getParameterType()) + || TypeUtils.isAssignable(ResourceSupport.class, nestedType); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + if (request instanceof ServletServerHttpRequest) { + beforeBodyWrite(body, (ServletServerHttpRequest) request); + } + return body; + } + + private void beforeBodyWrite(Object body, ServletServerHttpRequest request) { + Object pattern = request.getServletRequest().getAttribute( + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + if (pattern != null && body instanceof ResourceSupport) { + beforeBodyWrite(pattern.toString(), (ResourceSupport) body); + } + } + + private void beforeBodyWrite(String path, ResourceSupport body) { + if (isActuatorEndpointPath(path)) { + this.linksEnhancer + .addEndpointLinks(body, this.actuatorEndpoint.getPath()); + } + } + + private boolean isActuatorEndpointPath(String path) { + return this.actuatorEndpoint != null + && (this.management.getContextPath() + this.actuatorEndpoint + .getPath()).equals(path); } } /** - * Configuration for Endpoint links. + * Controller advice that adds links to the existing Actuator endpoints. By default + * all the top-level resources are enhanced with a "self" link. Those resources that + * could not be enhanced (e.g. "/env/{name}") because their values are "primitive" are + * ignored. Those that have values of type Collection (e.g. /trace) are transformed in + * to maps, and the original collection value is added with a key equal to the + * endpoint name. */ - @ConditionalOnProperty(value = "endpoints.links.enabled", matchIfMissing = true) - public static class LinksConfiguration { + @ControllerAdvice(assignableTypes = MvcEndpoint.class) + public static class MvcEndpointAdvice implements ResponseBodyAdvice { - @Bean - public LinksMvcEndpoint linksMvcEndpoint(ResourceProperties resources) { - return new LinksMvcEndpoint(); + @Autowired + private ManagementServerProperties management; + + @Autowired + private HttpMessageConverters converters; + + private Map> converterCache = new ConcurrentHashMap>(); + + @Autowired + private ObjectMapper mapper; + + @Override + public boolean supports(MethodParameter returnType, + Class> converterType) { + Class controllerType = returnType.getDeclaringClass(); + return !ActuatorMvcEndpoint.class.isAssignableFrom(controllerType); } - /** - * Controller advice that adds links to the home page and/or the management - * context path. The home page is enhanced if it is composed already of a - * {@link ResourceSupport} (e.g. when using Spring Data REST). - */ - @ControllerAdvice - public static class HomePageLinksAdvice implements ResponseBodyAdvice { - - @Autowired - private MvcEndpoints endpoints; - - @Autowired(required = false) - private LinksMvcEndpoint linksEndpoint; - - @Autowired - private ManagementServerProperties management; - - private LinksEnhancer linksEnhancer; - - @PostConstruct - public void init() { - this.linksEnhancer = new LinksEnhancer(this.management.getContextPath(), - this.endpoints); + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + if (request instanceof ServletServerHttpRequest) { + return beforeBodyWrite(body, returnType, selectedContentType, + selectedConverterType, (ServletServerHttpRequest) request, + response); } - - @Override - public boolean supports(MethodParameter returnType, - Class> converterType) { - Class controllerType = returnType.getDeclaringClass(); - if (!LinksMvcEndpoint.class.isAssignableFrom(controllerType) - && MvcEndpoint.class.isAssignableFrom(controllerType)) { - return false; - } - returnType.increaseNestingLevel(); - Type nestedType = returnType.getNestedGenericParameterType(); - returnType.decreaseNestingLevel(); - return ResourceSupport.class.isAssignableFrom(returnType - .getParameterType()) - || TypeUtils.isAssignable(ResourceSupport.class, nestedType); - } - - @Override - public Object beforeBodyWrite(Object body, MethodParameter returnType, - MediaType selectedContentType, - Class> selectedConverterType, - ServerHttpRequest request, ServerHttpResponse response) { - if (request instanceof ServletServerHttpRequest) { - beforeBodyWrite(body, (ServletServerHttpRequest) request); - } - return body; - } - - private void beforeBodyWrite(Object body, ServletServerHttpRequest request) { - Object pattern = request.getServletRequest().getAttribute( - HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); - if (pattern != null && body instanceof ResourceSupport) { - beforeBodyWrite(pattern.toString(), (ResourceSupport) body); - } - } - - private void beforeBodyWrite(String path, ResourceSupport body) { - if (isLinksPath(path)) { - this.linksEnhancer.addEndpointLinks(body, - this.linksEndpoint.getPath()); - } - else if (isHomePage(path)) { - body.add(linkToCurrentMapping() - .slash(this.management.getContextPath()) - .slash(this.linksEndpoint.getPath()).withRel("actuator")); - } - } - - private boolean isLinksPath(String path) { - return this.linksEndpoint != null - && (this.management.getContextPath() + this.linksEndpoint - .getPath()).equals(path); - } - - private boolean isHomePage(String path) { - return "".equals(path) || "/".equals(path); - } - + return body; } - /** - * Controller advice that adds links to the existing Actuator endpoints. By - * default all the top-level resources are enhanced with a "self" link. Those - * resources that could not be enhanced (e.g. "/env/{name}") because their values - * are "primitive" are ignored. Those that have values of type Collection (e.g. - * /trace) are transformed in to maps, and the original collection value is added - * with a key equal to the endpoint name. - */ - @ControllerAdvice(assignableTypes = MvcEndpoint.class) - public static class MvcEndpointAdvice implements ResponseBodyAdvice { - - @Autowired - private ManagementServerProperties management; - - @Autowired - private HttpMessageConverters converters; - - private Map> converterCache = new ConcurrentHashMap>(); - - @Autowired - private ObjectMapper mapper; - - @Override - public boolean supports(MethodParameter returnType, - Class> converterType) { - Class controllerType = returnType.getDeclaringClass(); - return !LinksMvcEndpoint.class.isAssignableFrom(controllerType) - && !HalBrowserMvcEndpoint.class.isAssignableFrom(controllerType); - } - - @Override - public Object beforeBodyWrite(Object body, MethodParameter returnType, - MediaType selectedContentType, - Class> selectedConverterType, - ServerHttpRequest request, ServerHttpResponse response) { - if (request instanceof ServletServerHttpRequest) { - return beforeBodyWrite(body, returnType, selectedContentType, - selectedConverterType, (ServletServerHttpRequest) request, - response); - } + private Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServletServerHttpRequest request, ServerHttpResponse response) { + if (body == null || body instanceof Resource) { + // Assume it already was handled or it already has its links return body; } - - private Object beforeBodyWrite(Object body, MethodParameter returnType, - MediaType selectedContentType, - Class> selectedConverterType, - ServletServerHttpRequest request, ServerHttpResponse response) { - if (body == null || body instanceof Resource) { - // Assume it already was handled or it already has its links - return body; - } - HttpMessageConverter converter = findConverter( - selectedConverterType, selectedContentType); - if (converter == null || isHypermediaDisabled(returnType)) { - // Not a resource that can be enhanced with a link - return body; - } - String path = getPath(request); - try { - converter.write(new EndpointResource(body, path), - selectedContentType, response); - } - catch (IOException ex) { - throw new HttpMessageNotWritableException("Cannot write response", ex); - } - return null; + HttpMessageConverter converter = findConverter(selectedConverterType, + selectedContentType); + if (converter == null || isHypermediaDisabled(returnType)) { + // Not a resource that can be enhanced with a link + return body; } + String path = getPath(request); + try { + converter.write(new EndpointResource(body, path), selectedContentType, + response); + } + catch (IOException ex) { + throw new HttpMessageNotWritableException("Cannot write response", ex); + } + return null; + } - @SuppressWarnings("unchecked") - private HttpMessageConverter findConverter( - Class> selectedConverterType, - MediaType mediaType) { - if (this.converterCache.containsKey(mediaType)) { - return (HttpMessageConverter) this.converterCache - .get(mediaType); + @SuppressWarnings("unchecked") + private HttpMessageConverter findConverter( + Class> selectedConverterType, + MediaType mediaType) { + if (this.converterCache.containsKey(mediaType)) { + return (HttpMessageConverter) this.converterCache.get(mediaType); + } + for (HttpMessageConverter converter : this.converters) { + if (selectedConverterType.isAssignableFrom(converter.getClass()) + && converter.canWrite(EndpointResource.class, mediaType)) { + this.converterCache.put(mediaType, converter); + return (HttpMessageConverter) converter; } - for (HttpMessageConverter converter : this.converters) { - if (selectedConverterType.isAssignableFrom(converter.getClass()) - && converter.canWrite(EndpointResource.class, mediaType)) { - this.converterCache.put(mediaType, converter); - return (HttpMessageConverter) converter; - } - } - return null; } + return null; + } - private boolean isHypermediaDisabled(MethodParameter returnType) { - return AnnotationUtils.findAnnotation(returnType.getMethod(), - HypermediaDisabled.class) != null - || AnnotationUtils.findAnnotation(returnType.getMethod() - .getDeclaringClass(), HypermediaDisabled.class) != null; - } - - private String getPath(ServletServerHttpRequest request) { - String path = (String) request.getServletRequest().getAttribute( - HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); - return (path == null ? "" : path); - } + private boolean isHypermediaDisabled(MethodParameter returnType) { + return AnnotationUtils.findAnnotation(returnType.getMethod(), + HypermediaDisabled.class) != null + || AnnotationUtils.findAnnotation(returnType.getMethod() + .getDeclaringClass(), HypermediaDisabled.class) != null; + } + private String getPath(ServletServerHttpRequest request) { + String path = (String) request.getServletRequest().getAttribute( + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + return (path == null ? "" : path); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/LinksEnhancer.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/LinksEnhancer.java index c37c701bb64..6207f9ece9f 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/LinksEnhancer.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/LinksEnhancer.java @@ -27,7 +27,7 @@ import org.springframework.util.StringUtils; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; /** - * Adds endpoints links to {@link ResourceSupport}. + * Adds endpoint links to {@link ResourceSupport}. * * @author Dave Syer */ diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMvcEndpoint.java similarity index 74% rename from spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpoint.java rename to spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMvcEndpoint.java index 97fd03ee8af..1013b8aac12 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMvcEndpoint.java @@ -23,16 +23,20 @@ import javax.servlet.http.HttpServletRequest; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.resource.ResourceTransformer; @@ -40,15 +44,16 @@ import org.springframework.web.servlet.resource.ResourceTransformerChain; import org.springframework.web.servlet.resource.TransformedResource; /** - * {@link MvcEndpoint} to support the HAL browser. + * {@link MvcEndpoint} for the actuator. Uses content negotiation to provide access to the + * HAL browser (when on the classpath), and to HAL-formatted JSON. * * @author Dave Syer - * @author Phillip Webb - * @since 1.3.0 + * @author Phil Webb + * @author Andy Wilkinson */ -@ConfigurationProperties("endpoints.hal") -public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements - MvcEndpoint, ResourceLoaderAware { +@ConfigurationProperties("endpoints.actuator") +public class ActuatorMvcEndpoint extends WebMvcConfigurerAdapter implements MvcEndpoint, + ResourceLoaderAware { private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); @@ -63,8 +68,8 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements * Endpoint URL path. */ @NotNull - @Pattern(regexp = "/[^/]*", message = "Path must start with /") - private String path = "/hal"; + @Pattern(regexp = "^$|/[^/]*", message = "Path must be empty or start with /") + private String path; /** * Enable security on the endpoint. @@ -78,13 +83,16 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements private final ManagementServerProperties management; - @Autowired(required = false) - private LinksMvcEndpoint linksMvcEndpoint; - private HalBrowserLocation location; - public HalBrowserMvcEndpoint(ManagementServerProperties management) { + public ActuatorMvcEndpoint(ManagementServerProperties management) { this.management = management; + if (StringUtils.hasText(management.getContextPath())) { + this.path = ""; + } + else { + this.path = "/actuator"; + } } @Override @@ -94,22 +102,34 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public String browse(HttpServletRequest request) { - String contextPath = this.management.getContextPath() + this.path + "/"; + if (this.location == null) { + throw new HalBrowserUnavailableException(); + } + String contextPath = this.management.getContextPath() + + (this.path.endsWith("/") ? this.path : this.path + "/"); if (request.getRequestURI().endsWith("/")) { return "forward:" + contextPath + this.location.getHtmlFile(); } return "redirect:" + contextPath; } + @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public ResourceSupport links() { + return new ResourceSupport(); + } + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // Make sure the root path is not cached so the browser comes back for the JSON // and add a transformer to set the initial link - String start = this.management.getContextPath() + this.path; - registry.addResourceHandler(start + "/", start + "/**") - .addResourceLocations(this.location.getResourceLocation()) - .setCachePeriod(0).resourceChain(true) - .addTransformer(new InitialUrlTransformer()); + if (this.location != null) { + String start = this.management.getContextPath() + this.path; + registry.addResourceHandler(start + "/", start + "/**") + .addResourceLocations(this.location.getResourceLocation()) + .setCachePeriod(0).resourceChain(true) + .addTransformer(new InitialUrlTransformer()); + } } public void setPath(String path) { @@ -121,23 +141,23 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements return this.path; } - public void setSensitive(boolean sensitive) { - this.sensitive = sensitive; - } - @Override public boolean isSensitive() { return this.sensitive; } - public void setEnabled(boolean enabled) { - this.enabled = enabled; + public void setSensitive(boolean sensitive) { + this.sensitive = sensitive; } public boolean isEnabled() { return this.enabled; } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + @Override public Class> getEndpointType() { return null; @@ -167,21 +187,17 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements ResourceTransformerChain transformerChain) throws IOException { resource = transformerChain.transform(request, resource); if (resource.getFilename().equalsIgnoreCase( - HalBrowserMvcEndpoint.this.location.getHtmlFile())) { + ActuatorMvcEndpoint.this.location.getHtmlFile())) { return replaceInitialLink(resource); } return resource; } private Resource replaceInitialLink(Resource resource) throws IOException { - LinksMvcEndpoint linksEndpoint = HalBrowserMvcEndpoint.this.linksMvcEndpoint; - if (linksEndpoint == null) { - return resource; - } byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream()); String content = new String(bytes, DEFAULT_CHARSET); - String initialLink = HalBrowserMvcEndpoint.this.management.getContextPath() - + linksEndpoint.getPath(); + String initialLink = ActuatorMvcEndpoint.this.management.getContextPath() + + getPath(); content = content.replace("entryPoint: '/'", "entryPoint: '" + initialLink + "'"); return new TransformedResource(resource, content.getBytes(DEFAULT_CHARSET)); @@ -215,4 +231,9 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements } + @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) + private static class HalBrowserUnavailableException extends RuntimeException { + + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java index 032a531b23f..9be27aad91b 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java @@ -25,8 +25,8 @@ import java.lang.annotation.Target; import org.springframework.web.bind.annotation.RequestMapping; /** - * Annotation to that {@link MvcEndpoint} class or {@link RequestMapping} method should't - * generate a hypermedia response. + * Annotation to indicate that an {@link MvcEndpoint} class or {@link RequestMapping} + * method should't generate a hypermedia response. * * @author Dave Syer * @since 1.3.0 diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java deleted file mode 100644 index 8e2f4aa20d0..00000000000 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2013-2015 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.actuate.endpoint.mvc; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; - -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.hateoas.ResourceSupport; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -/** - * {@link MvcEndpoint} to add hypermedia links. - * - * @author Dave Syer - * @since 1.3.0 - */ -@ConfigurationProperties("endpoints.links") -public class LinksMvcEndpoint implements MvcEndpoint { - - /** - * Endpoint URL path. - */ - @NotNull - @Pattern(regexp = "/[^/]*", message = "Path must start with /") - private String path = "/links"; - - /** - * Enable security on the endpoint. - */ - private boolean sensitive = false; - - /** - * Enable the endpoint. - */ - private boolean enabled = true; - - public LinksMvcEndpoint() { - } - - @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) - @ResponseBody - public ResourceSupport links() { - return new ResourceSupport(); - } - - public void setPath(String path) { - this.path = path; - } - - @Override - public String getPath() { - return this.path; - } - - @Override - public boolean isSensitive() { - return this.sensitive; - } - - public void setSensitive(boolean sensitive) { - this.sensitive = sensitive; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @Override - public Class> getEndpointType() { - return null; - } - -} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java index 818ab120719..2f7ef02ac2c 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java @@ -20,8 +20,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.autoconfigure.BrowserPathHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.context.annotation.Configuration; @@ -40,10 +40,16 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +/** + * Integration tests for {@link ActuatorMvcEndpoint}'s HAL Browser support + * + * @author Dave Syer + * @author Andy Wilkinson + */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration -@TestPropertySource(properties = "endpoints.hal.path=/hal") +@TestPropertySource(properties = "endpoints.actuator.path=/actuator") @DirtiesContext public class BrowserPathHypermediaIntegrationTests { @@ -63,26 +69,22 @@ public class BrowserPathHypermediaIntegrationTests { @Test public void browser() throws Exception { MvcResult response = this.mockMvc - .perform(get("/hal/").accept(MediaType.TEXT_HTML)) + .perform(get("/actuator/").accept(MediaType.TEXT_HTML)) .andExpect(status().isOk()).andReturn(); - assertEquals("/hal/browser.html", response.getResponse().getForwardedUrl()); + assertEquals("/actuator/browser.html", response.getResponse().getForwardedUrl()); } @Test public void redirect() throws Exception { - this.mockMvc.perform(get("/hal").accept(MediaType.TEXT_HTML)) + this.mockMvc.perform(get("/actuator").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()) - .andExpect(header().string("location", "/hal/")); + .andExpect(header().string("location", "/actuator/")); } @MinimalActuatorHypermediaApplication @Configuration public static class SpringBootHypermediaApplication { - public static void main(String[] args) { - SpringApplication.run(SpringBootHypermediaApplication.class, args); - } - } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java deleted file mode 100644 index fbe0da4615c..00000000000 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2012-2015 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.actuate.autoconfigure; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.actuate.autoconfigure.CustomHomepageHypermediaIntegrationTests.SpringBootHypermediaApplication; -import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.hateoas.ResourceSupport; -import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.WebApplicationContext; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) -@WebAppConfiguration -@DirtiesContext -public class CustomHomepageHypermediaIntegrationTests { - - @Autowired - private WebApplicationContext context; - - @Autowired - private MvcEndpoints mvcEndpoints; - - private MockMvc mockMvc; - - @Before - public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); - } - - @Test - public void links() throws Exception { - this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); - } - - @Test - public void endpointsAllListed() throws Exception { - for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { - String path = endpoint.getPath(); - if ("/links".equals(path)) { - continue; - } - path = path.startsWith("/") ? path.substring(1) : path; - this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._links.%s.href", path).exists()); - } - } - - @MinimalActuatorHypermediaApplication - @RestController - public static class SpringBootHypermediaApplication { - - @RequestMapping("") - public ResourceSupport home() { - ResourceSupport resource = new ResourceSupport(); - resource.add(linkTo(SpringBootHypermediaApplication.class).slash("/") - .withSelfRel()); - return resource; - } - - public static void main(String[] args) { - SpringApplication.run(SpringBootHypermediaApplication.class, args); - } - - } - -} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index 76b591643e4..c6318442830 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -292,9 +292,9 @@ public class EndpointWebMvcAutoConfigurationTests { this.applicationContext.register(RootConfig.class, BaseConfiguration.class, ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); this.applicationContext.refresh(); - // /health, /metrics, /env (/shutdown is disabled by default) + // /health, /metrics, /env, /actuator (/shutdown is disabled by default) assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class).size(), - is(equalTo(5))); + is(equalTo(4))); } @Test diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ContextPathHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementContextPathHypermediaIntegrationTests.java similarity index 79% rename from spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ContextPathHypermediaIntegrationTests.java rename to spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementContextPathHypermediaIntegrationTests.java index e6cfaa0311d..f3a3e3a75fa 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ContextPathHypermediaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementContextPathHypermediaIntegrationTests.java @@ -20,10 +20,10 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.autoconfigure.ContextPathHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.actuate.autoconfigure.ManagementContextPathHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; -import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.ResourceSupport; import org.springframework.http.MediaType; @@ -39,15 +39,23 @@ import org.springframework.web.context.WebApplicationContext; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +/** + * Integration tests for {@link ActuatorMvcEndpoint} when a custom management context path + * has been configured. + * + * @author Dave Syer + * @author Andy Wilkinson + */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration @TestPropertySource(properties = "management.contextPath:/admin") @DirtiesContext -public class ContextPathHypermediaIntegrationTests { +public class ManagementContextPathHypermediaIntegrationTests { @Autowired private WebApplicationContext context; @@ -63,15 +71,16 @@ public class ContextPathHypermediaIntegrationTests { } @Test - public void home() throws Exception { - this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + public void actuatorHomeJson() throws Exception { + this.mockMvc.perform(get("/admin").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); } @Test - public void links() throws Exception { - this.mockMvc.perform(get("/admin/links").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); + public void actuatorHomeHtml() throws Exception { + this.mockMvc.perform(get("/admin/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/admin/browser.html")); } @Test @@ -89,13 +98,13 @@ public class ContextPathHypermediaIntegrationTests { public void endpointsAllListed() throws Exception { for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { String path = endpoint.getPath(); - if ("/links".equals(path)) { + if ("/actuator".equals(path)) { continue; } path = path.startsWith("/") ? path.substring(1) : path; path = path.length() > 0 ? path : "self"; this.mockMvc - .perform(get("/admin/links").accept(MediaType.APPLICATION_JSON)) + .perform(get("/admin").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect( jsonPath("$._links.%s.href", path).value( @@ -115,11 +124,6 @@ public class ContextPathHypermediaIntegrationTests { return resource; } - public static void main(String[] args) { - new SpringApplicationBuilder(SpringBootHypermediaApplication.class) - .properties("management.contextPath:/admin").run(args); - } - } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java index 7a8383d6b75..1c17e25b05d 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java @@ -22,7 +22,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.ServerContextPathHypermediaIntegrationTests.SpringBootHypermediaApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.test.IntegrationTest; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.TestRestTemplate; @@ -43,6 +43,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +/** + * Integration tests for {@link ActuatorMvcEndpoint} when a custom server context path has + * been configured. + * + * @author Dave Syer + * @author Andy Wilkinson + */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration @@ -54,7 +61,7 @@ public class ServerContextPathHypermediaIntegrationTests { private int port; @Test - public void links() throws Exception { + public void linksAddedToHomePage() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); ResponseEntity entity = new TestRestTemplate().exchange( @@ -66,16 +73,28 @@ public class ServerContextPathHypermediaIntegrationTests { } @Test - public void browser() throws Exception { + public void actuatorBrowser() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); ResponseEntity entity = new TestRestTemplate().exchange( - "http://localhost:" + this.port + "/spring/hal/", HttpMethod.GET, + "http://localhost:" + this.port + "/spring/actuator/", HttpMethod.GET, new HttpEntity(null, headers), String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains(" entity = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/spring/actuator", HttpMethod.GET, + new HttpEntity(null, headers), String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertTrue("Wrong body: " + entity.getBody(), + entity.getBody().contains("\"_links\":")); + } + @MinimalActuatorHypermediaApplication @RestController public static class SpringBootHypermediaApplication { @@ -88,11 +107,6 @@ public class ServerContextPathHypermediaIntegrationTests { return resource; } - public static void main(String[] args) { - new SpringApplicationBuilder(SpringBootHypermediaApplication.class) - .properties("server.contextPath=/spring").run(args); - } - } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerPortHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerPortHypermediaIntegrationTests.java index 0743f84d044..8a04cd1c0fc 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerPortHypermediaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerPortHypermediaIntegrationTests.java @@ -22,7 +22,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.ServerPortHypermediaIntegrationTests.SpringBootHypermediaApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.test.IntegrationTest; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.TestRestTemplate; @@ -43,6 +43,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +/** + * Integration tests for {@link ActuatorMvcEndpoint} when a custom server port has been + * configured. + * + * @author Dave Syer + * @author Andy Wilkinson + */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration @@ -58,11 +65,13 @@ public class ServerPortHypermediaIntegrationTests { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); ResponseEntity entity = new TestRestTemplate().exchange( - "http://localhost:" + this.port + "/links", HttpMethod.GET, + "http://localhost:" + this.port + "/actuator", HttpMethod.GET, new HttpEntity(null, headers), String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains("\"_links\":")); + assertTrue("Wrong body: " + entity.getBody(), + entity.getBody().contains(":" + this.port)); } @Test @@ -70,7 +79,7 @@ public class ServerPortHypermediaIntegrationTests { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); ResponseEntity entity = new TestRestTemplate().exchange( - "http://localhost:" + this.port + "/hal/", HttpMethod.GET, + "http://localhost:" + this.port + "/actuator/", HttpMethod.GET, new HttpEntity(null, headers), String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains(" 0 ? path : "/"; this.mockMvc .perform(get(path).accept(MediaType.APPLICATION_JSON)) @@ -126,10 +129,6 @@ public class VanillaHypermediaIntegrationTests { @Configuration public static class SpringBootHypermediaApplication { - public static void main(String[] args) { - SpringApplication.run(SpringBootHypermediaApplication.class, args); - } - } } diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 1783d80da1f..4bdc4abcd70 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -699,6 +699,9 @@ content into your application; rather pick only the properties that you need. endpoints.trace.enabled=true # HYPERMEDIA ENDPOINTS + endpoints.actuator.enabled=true + endpoints.actuator.path=/actuator + endpoints.actuator.sensitive=false endpoints.docs.curies.enabled=false endpoints.docs.enabled=true endpoints.docs.path=/docs @@ -709,9 +712,6 @@ content into your application; rather pick only the properties that you need. endpoints.hal.enabled=true endpoints.hal.path= # Redirects root HTML traffic to the HAL browser endpoints.hal.sensitive=false - endpoints.links.enabled=true - endpoints.links.path=/links - endpoints.links.sensitive=false endpoints.liquibase.enabled=true endpoints.liquibase.id=liquibase endpoints.liquibase.sensitive=false 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 22964fd5f7d..9ee714e32f1 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -67,6 +67,10 @@ The following endpoints are available: |=== | ID | Description | Sensitive +|`actuator` +|Provides a hypermedia-based "`discovery page`" for the other endpoints. Requires Spring +HATEOAS to be on the classpath. + |`autoconfig` |Displays an auto-configuration report showing all auto-configuration candidates and the reason why they '`were`' or '`were not`' applied. @@ -169,18 +173,19 @@ If http://projects.spring.io/spring-hateoas[Spring HATEOAS] is on the classpath through the `spring-boot-starter-hateoas` or if you are using http://projects.spring.io/spring-data-rest[Spring Data REST]) then the HTTP endpoints from the Actuator are enhanced with hypermedia links, and a "`discovery page`" is added -with links to all the endpoints. The "`discovery page`" is actually an endpoint itself, -so it can be disabled along with the rest of the hypermedia by setting -`endpoints.links.enabled=false`. If it is not explicitly disabled the links -endpoint renders a JSON object with a link for each other endpoint on `/links`. If Spring -Data REST is used, the root endpoint is enhanced with an extra `actuator` links that -points to the "`discovery page`". +with links to all the endpoints. The "`discovery page`" is available on `/actuator` by +default. It is implemented as an endpoint, allowing properties to be used to configure +its path (`endpoints.actuator.path`) and whether or not it is enabled +(`endpoints.actuator.enabled`). + +When a custom management context path is configured, the "`discovery page`" will +automatically move from `/actuator` 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`. If the https://github.com/mikekelly/hal-browser[HAL Browser] is on the classpath via its webjar (`org.webjars:hal-browser`), or via the `spring-data-rest-hal-browser` then -the default home page for HTML clients will be the HAL Browser. This is also exposed via -an endpoint ("`hal`") so it can be disabled and have its path explicitly configured like -the other endpoints. +an HTML "`discovery page`", in the form of the HAL Browser, is also provided.