In absence of HAL browser, serve browsers JSON from actuator entry point

Following the changes made to combines the /links and /hal endpoints
into a single /actuator endpoint, a web browser accessing /actuator
would receive a 406 response if HAL browser was not on the classpath.

This commit updates the /actuator main entry point so that it will
serve JSON to a web browser when HAL browser is not on the classpath.

The actuator's embedded documentation has also been updated to reflect
the recent changes.

Closes gh-3696
This commit is contained in:
Andy Wilkinson 2015-08-10 15:57:53 +01:00
parent 3763eda64e
commit dad0574fd5
10 changed files with 196 additions and 162 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 432 KiB

View File

@ -60,18 +60,20 @@ WARNING: Beware of Actuator endpoint paths clashing with application endpoints.
The easiest way to avoid that is to use a `management.context-path`, e.g. "/admin".
TIP: You can disable the hypermedia support in Actuator endpoints by setting
`endpoints.links.enabled=false`.
`endpoints.actuator.enabled=false`.
=== Default home page
If the `management.context-path` is empty, or if the home page provided by the application
happens to be a response body of type `ResourceSupport`, then it will be enhanced with
links to the actuator endpoints. The latter would happen for instance if you use Spring
Data REST to expose `Repository` endpoints.
=== Main entry point
When the hypermedia support is enabled, the Actuator provides a main entry point that
provides links to all of its endpoints. If `management.context-path` is empty, this entry
point is available at `/actuator`. If a management context path has been configured then
the entry point is available at the root of that context. For example, if
`management.context-path` has been set to `/admin` then the main entry point will be
available at `/admin`.
Example vanilla "/" endpoint if the `management.context-path` is empty (the "/admin" page
would be the same with different links if `management.context-path=/admin`):
TIP: The endpoint path can always, as with all MVC endpoints, be overridden using
`endpoints.actuator.path=/yourpath` (note the leading slash).
include::{generated}/admin/http-response.adoc[]
@ -117,19 +119,13 @@ or in Gradle:
NOTE: If you are using Spring Data REST, then a dependency on the
`spring-data-rest-hal-browser` will have an equivalent effect.
If you do that then a new endpoint will appear at `/` or `/hal` (relative to the
`management.context-path`) serving up a static HTML page with some JavaScript that lets you
browse the available resources. The default endpoint path depends on whether or not there
is already a static home page ("`index.html`") - if there is not and the
`management.context-path` is empty, then the HAL browser shows up on the home page.
If you do that then the main entry point will server a static HTML page to browser clients
with some JavaScript that lets you browse the available resources.
Example:
image::hal-browser.png[HAL Browser]
TIP: The endpoint path can always, as with all MVC endpoints, be overridden using
`endpoints.hal.path=/yourpath` (note the leading slash).
== Actuator Documentation Browser

View File

@ -25,7 +25,8 @@ 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.ActuatorMvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorHalBrowserEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorHalJsonEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.HypermediaDisabled;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
@ -40,8 +41,10 @@ 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.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.ResourceLoader;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
@ -86,9 +89,13 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration {
@ConditionalOnProperty(prefix = "endpoints.actuator", name = "enabled", matchIfMissing = true)
@Bean
public ActuatorMvcEndpoint actuatorMvcEndpoint(ManagementServerProperties management,
ResourceProperties resources) {
return new ActuatorMvcEndpoint(management);
public ActuatorHalJsonEndpoint actuatorMvcEndpoint(
ManagementServerProperties management, ResourceProperties resources,
ResourceLoader resourceLoader) {
if (ActuatorHalBrowserEndpoint.getHalBrowserLocation(resourceLoader) != null) {
return new ActuatorHalBrowserEndpoint(management);
}
return new ActuatorHalJsonEndpoint(management);
}
@Bean
@ -113,6 +120,12 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration {
return new DefaultCurieProvider("boot", new UriTemplate(path));
}
@ConditionalOnProperty(prefix = "endpoints.actuator", name = "enabled", matchIfMissing = true)
@Configuration
static class ActuatorMvcEndpointConfiguration {
}
/**
* Controller advice that adds links to the actuator endpoint's path.
*/
@ -123,7 +136,7 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration {
private MvcEndpoints endpoints;
@Autowired(required = false)
private ActuatorMvcEndpoint actuatorEndpoint;
private ActuatorHalJsonEndpoint actuatorEndpoint;
@Autowired
private ManagementServerProperties management;
@ -206,7 +219,7 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration {
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
Class<?> controllerType = returnType.getDeclaringClass();
return !ActuatorMvcEndpoint.class.isAssignableFrom(controllerType);
return !ActuatorHalJsonEndpoint.class.isAssignableFrom(controllerType);
}
@Override

View File

@ -20,39 +20,20 @@ import java.io.IOException;
import java.nio.charset.Charset;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
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;
import org.springframework.web.servlet.resource.ResourceTransformerChain;
import org.springframework.web.servlet.resource.TransformedResource;
/**
* {@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 Phil Webb
* @author Andy Wilkinson
*/
@ConfigurationProperties("endpoints.actuator")
public class ActuatorMvcEndpoint extends WebMvcConfigurerAdapter implements MvcEndpoint,
public class ActuatorHalBrowserEndpoint extends ActuatorHalJsonEndpoint implements
ResourceLoaderAware {
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@ -64,35 +45,20 @@ public class ActuatorMvcEndpoint extends WebMvcConfigurerAdapter implements MvcE
"classpath:/META-INF/resources/webjars/hal-browser/b7669f1-1/",
"browser.html") };
/**
* Endpoint URL path.
*/
@NotNull
@Pattern(regexp = "^$|/[^/]*", message = "Path must be empty or start with /")
private String path;
/**
* Enable security on the endpoint.
*/
private boolean sensitive = false;
/**
* Enable the endpoint.
*/
private boolean enabled = true;
private final ManagementServerProperties management;
private HalBrowserLocation location;
public ActuatorMvcEndpoint(ManagementServerProperties management) {
this.management = management;
if (StringUtils.hasText(management.getContextPath())) {
this.path = "";
}
else {
this.path = "/actuator";
public ActuatorHalBrowserEndpoint(ManagementServerProperties management) {
super(management);
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public String browse(HttpServletRequest request) {
String contextPath = getManagement().getContextPath()
+ (getPath().endsWith("/") ? getPath() : getPath() + "/");
if (request.getRequestURI().endsWith("/")) {
return "forward:" + contextPath + this.location.getHtmlFile();
}
return "redirect:" + contextPath;
}
@Override
@ -100,31 +66,12 @@ public class ActuatorMvcEndpoint extends WebMvcConfigurerAdapter implements MvcE
this.location = getHalBrowserLocation(resourceLoader);
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public String browse(HttpServletRequest request) {
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
if (this.location != null) {
String start = this.management.getContextPath() + this.path;
String start = getManagement().getContextPath() + getPath();
registry.addResourceHandler(start + "/", start + "/**")
.addResourceLocations(this.location.getResourceLocation())
.setCachePeriod(0).resourceChain(true)
@ -132,37 +79,6 @@ public class ActuatorMvcEndpoint extends WebMvcConfigurerAdapter implements MvcE
}
}
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<? extends Endpoint<?>> getEndpointType() {
return null;
}
public static HalBrowserLocation getHalBrowserLocation(ResourceLoader resourceLoader) {
for (HalBrowserLocation candidate : HAL_BROWSER_RESOURCE_LOCATIONS) {
try {
@ -177,34 +93,6 @@ public class ActuatorMvcEndpoint extends WebMvcConfigurerAdapter implements MvcE
return null;
}
/**
* {@link ResourceTransformer} to change the initial link location.
*/
private class InitialUrlTransformer implements ResourceTransformer {
@Override
public Resource transform(HttpServletRequest request, Resource resource,
ResourceTransformerChain transformerChain) throws IOException {
resource = transformerChain.transform(request, resource);
if (resource.getFilename().equalsIgnoreCase(
ActuatorMvcEndpoint.this.location.getHtmlFile())) {
return replaceInitialLink(resource);
}
return resource;
}
private Resource replaceInitialLink(Resource resource) throws IOException {
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
String content = new String(bytes, DEFAULT_CHARSET);
String initialLink = ActuatorMvcEndpoint.this.management.getContextPath()
+ getPath();
content = content.replace("entryPoint: '/'", "entryPoint: '" + initialLink
+ "'");
return new TransformedResource(resource, content.getBytes(DEFAULT_CHARSET));
}
}
public static class HalBrowserLocation {
private final String resourceLocation;
@ -231,8 +119,30 @@ public class ActuatorMvcEndpoint extends WebMvcConfigurerAdapter implements MvcE
}
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
private static class HalBrowserUnavailableException extends RuntimeException {
/**
* {@link ResourceTransformer} to change the initial link location.
*/
private class InitialUrlTransformer implements ResourceTransformer {
@Override
public Resource transform(HttpServletRequest request, Resource resource,
ResourceTransformerChain transformerChain) throws IOException {
resource = transformerChain.transform(request, resource);
if (resource.getFilename().equalsIgnoreCase(
ActuatorHalBrowserEndpoint.this.location.getHtmlFile())) {
return replaceInitialLink(resource);
}
return resource;
}
private Resource replaceInitialLink(Resource resource) throws IOException {
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
String content = new String(bytes, DEFAULT_CHARSET);
String initialLink = getManagement().getContextPath() + getPath();
content = content.replace("entryPoint: '/'", "entryPoint: '" + initialLink
+ "'");
return new TransformedResource(resource, content.getBytes(DEFAULT_CHARSET));
}
}

View File

@ -0,0 +1,114 @@
/*
* 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.endpoint.mvc;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties;
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.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* {@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 Phil Webb
* @author Andy Wilkinson
*/
@ConfigurationProperties("endpoints.actuator")
public class ActuatorHalJsonEndpoint extends WebMvcConfigurerAdapter implements
MvcEndpoint {
/**
* Endpoint URL path.
*/
@NotNull
@Pattern(regexp = "^$|/[^/]*", message = "Path must be empty or start with /")
private String path;
/**
* Enable security on the endpoint.
*/
private boolean sensitive = false;
/**
* Enable the endpoint.
*/
private boolean enabled = true;
private final ManagementServerProperties management;
public ActuatorHalJsonEndpoint(ManagementServerProperties management) {
this.management = management;
if (StringUtils.hasText(management.getContextPath())) {
this.path = "";
}
else {
this.path = "/actuator";
}
}
@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<? extends Endpoint<?>> getEndpointType() {
return null;
}
protected final ManagementServerProperties getManagement() {
return this.management;
}
}

View File

@ -21,7 +21,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.BrowserPathHypermediaIntegrationTests.SpringBootHypermediaApplication;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorHalBrowserEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Configuration;
@ -41,7 +41,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for {@link ActuatorMvcEndpoint}'s HAL Browser support
* Integration tests for {@link ActuatorHalBrowserEndpoint}'s support for producing
* text/html
*
* @author Dave Syer
* @author Andy Wilkinson

View File

@ -21,7 +21,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.ManagementContextPathHypermediaIntegrationTests.SpringBootHypermediaApplication;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorHalBrowserEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.test.SpringApplicationConfiguration;
@ -44,8 +44,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for {@link ActuatorMvcEndpoint} when a custom management context path
* has been configured.
* Integration tests for {@link ActuatorHalBrowserEndpoint} when a custom management
* context path has been configured.
*
* @author Dave Syer
* @author Andy Wilkinson

View File

@ -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.actuate.endpoint.mvc.ActuatorMvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorHalBrowserEndpoint;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
@ -44,8 +44,8 @@ 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.
* Integration tests for {@link ActuatorHalBrowserEndpoint} when a custom server context
* path has been configured.
*
* @author Dave Syer
* @author Andy Wilkinson

View File

@ -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.actuate.endpoint.mvc.ActuatorMvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorHalBrowserEndpoint;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
@ -44,8 +44,8 @@ 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.
* Integration tests for {@link ActuatorHalBrowserEndpoint} when a custom server port has
* been configured.
*
* @author Dave Syer
* @author Andy Wilkinson

View File

@ -21,7 +21,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.VanillaHypermediaIntegrationTests.SpringBootHypermediaApplication;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.ActuatorHalBrowserEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.test.SpringApplicationConfiguration;
@ -42,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for {@link ActuatorMvcEndpoint}
* Integration tests for {@link ActuatorHalBrowserEndpoint}
*
* @author Dave Syer
* @author Andy Wilkinson