mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-07-15 01:07:30 +08:00
Combine /links and /hal into a single /actuator endpoint
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
This commit is contained in:
parent
ce512a18f3
commit
58db5a3889
@ -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 {
|
||||
|
||||
|
@ -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"));
|
||||
}
|
||||
|
||||
|
@ -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<Object> {
|
||||
|
||||
@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<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<Object> {
|
||||
|
||||
@Bean
|
||||
public LinksMvcEndpoint linksMvcEndpoint(ResourceProperties resources) {
|
||||
return new LinksMvcEndpoint();
|
||||
@Autowired
|
||||
private ManagementServerProperties management;
|
||||
|
||||
@Autowired
|
||||
private HttpMessageConverters converters;
|
||||
|
||||
private Map<MediaType, HttpMessageConverter<?>> converterCache = new ConcurrentHashMap<MediaType, HttpMessageConverter<?>>();
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType,
|
||||
Class<? extends HttpMessageConverter<?>> 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<Object> {
|
||||
|
||||
@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<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<Object> {
|
||||
|
||||
@Autowired
|
||||
private ManagementServerProperties management;
|
||||
|
||||
@Autowired
|
||||
private HttpMessageConverters converters;
|
||||
|
||||
private Map<MediaType, HttpMessageConverter<?>> converterCache = new ConcurrentHashMap<MediaType, HttpMessageConverter<?>>();
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType,
|
||||
Class<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<Object> 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<Object> 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<Object> findConverter(
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
MediaType mediaType) {
|
||||
if (this.converterCache.containsKey(mediaType)) {
|
||||
return (HttpMessageConverter<Object>) this.converterCache
|
||||
.get(mediaType);
|
||||
@SuppressWarnings("unchecked")
|
||||
private HttpMessageConverter<Object> findConverter(
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
MediaType mediaType) {
|
||||
if (this.converterCache.containsKey(mediaType)) {
|
||||
return (HttpMessageConverter<Object>) 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<Object>) converter;
|
||||
}
|
||||
for (HttpMessageConverter<?> converter : this.converters) {
|
||||
if (selectedConverterType.isAssignableFrom(converter.getClass())
|
||||
&& converter.canWrite(EndpointResource.class, mediaType)) {
|
||||
this.converterCache.put(mediaType, converter);
|
||||
return (HttpMessageConverter<Object>) 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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<? extends Endpoint<?>> 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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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<? extends Endpoint<?>> getEndpointType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<String> 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<String> entity = new TestRestTemplate().exchange(
|
||||
"http://localhost:" + this.port + "/spring/hal/", HttpMethod.GET,
|
||||
"http://localhost:" + this.port + "/spring/actuator/", HttpMethod.GET,
|
||||
new HttpEntity<Void>(null, headers), String.class);
|
||||
assertEquals(HttpStatus.OK, entity.getStatusCode());
|
||||
assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains("<title"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actuatorLinks() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
|
||||
ResponseEntity<String> entity = new TestRestTemplate().exchange(
|
||||
"http://localhost:" + this.port + "/spring/actuator", HttpMethod.GET,
|
||||
new HttpEntity<Void>(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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<String> entity = new TestRestTemplate().exchange(
|
||||
"http://localhost:" + this.port + "/links", HttpMethod.GET,
|
||||
"http://localhost:" + this.port + "/actuator", HttpMethod.GET,
|
||||
new HttpEntity<Void>(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<String> entity = new TestRestTemplate().exchange(
|
||||
"http://localhost:" + this.port + "/hal/", HttpMethod.GET,
|
||||
"http://localhost:" + this.port + "/actuator/", HttpMethod.GET,
|
||||
new HttpEntity<Void>(null, headers), String.class);
|
||||
assertEquals(HttpStatus.OK, entity.getStatusCode());
|
||||
assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains("<title"));
|
||||
@ -88,11 +97,6 @@ public class ServerPortHypermediaIntegrationTests {
|
||||
return resource;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
new SpringApplicationBuilder(SpringBootHypermediaApplication.class)
|
||||
.properties("management.port:9000").run(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.VanillaHypermediaIntegrationTests.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.test.SpringApplicationConfiguration;
|
||||
@ -41,6 +41,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
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}
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class)
|
||||
@WebAppConfiguration
|
||||
@ -62,7 +68,7 @@ public class VanillaHypermediaIntegrationTests {
|
||||
|
||||
@Test
|
||||
public void links() throws Exception {
|
||||
this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON))
|
||||
this.mockMvc.perform(get("/actuator").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$._links").exists())
|
||||
.andExpect(header().doesNotExist("cache-control"));
|
||||
}
|
||||
@ -70,9 +76,9 @@ public class VanillaHypermediaIntegrationTests {
|
||||
@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
|
||||
@ -95,11 +101,11 @@ public class VanillaHypermediaIntegrationTests {
|
||||
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;
|
||||
this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON))
|
||||
this.mockMvc.perform(get("/actuator").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.%s.href", path).exists());
|
||||
}
|
||||
@ -109,9 +115,6 @@ public class VanillaHypermediaIntegrationTests {
|
||||
public void endpointsEachHaveSelf() throws Exception {
|
||||
for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) {
|
||||
String path = endpoint.getPath();
|
||||
if ("/hal".equals(path)) {
|
||||
continue;
|
||||
}
|
||||
path = path.length() > 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user