diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java index 895f8f2a51..3e46e0e110 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java @@ -29,7 +29,6 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerAdapter; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.properties.ManagementServerProperties; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -40,7 +39,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; -import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; @@ -91,15 +89,6 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, return mapping; } - @Bean - @ConditionalOnMissingBean - public EndpointHandlerAdapter endpointHandlerAdapter( - final HttpMessageConverters messageConverters) { - EndpointHandlerAdapter adapter = new EndpointHandlerAdapter(); - adapter.setMessageConverters(messageConverters.getConverters()); - return adapter; - } - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java index cb37692695..5d255ba854 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.boot.actuate.autoconfigure; -import java.util.Map; - import javax.servlet.Filter; import org.springframework.beans.factory.BeanFactory; @@ -26,9 +24,7 @@ import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.actuate.endpoint.AbstractEndpoint; -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerAdapter; +import org.springframework.boot.actuate.endpoint.ManagementErrorEndpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.properties.ManagementServerProperties; import org.springframework.boot.actuate.web.ErrorController; @@ -42,11 +38,11 @@ import org.springframework.boot.context.embedded.ErrorPage; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.web.context.request.RequestAttributes; -import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; /** * Configuration triggered from {@link EndpointWebMvcAutoConfiguration} when a new @@ -57,6 +53,9 @@ import org.springframework.web.servlet.HandlerMapping; @Configuration public class EndpointWebMvcChildContextConfiguration { + @Value("${error.path:/error}") + private String errorPath = "/error"; + @Configuration protected static class ServerCustomization implements EmbeddedServletContainerCustomizer { @@ -100,13 +99,19 @@ public class EndpointWebMvcChildContextConfiguration { } @Bean - public HandlerMapping handlerMapping() { - return new EndpointHandlerMapping(); + public HandlerAdapter handlerAdapter() { + // TODO: maybe this needs more configuration for non-basic response use cases + RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); + adapter.setMessageConverters(new RestTemplate().getMessageConverters()); + return adapter; } @Bean - public HandlerAdapter handlerAdapter() { - return new EndpointHandlerAdapter(); + public HandlerMapping handlerMapping() { + EndpointHandlerMapping mapping = new EndpointHandlerMapping(); + // In a child context we definitely want to see the parent endpoints + mapping.setDetectHandlerMethodsInAncestorContexts(true); + return mapping; } /* @@ -116,15 +121,8 @@ public class EndpointWebMvcChildContextConfiguration { * endpoints. */ @Bean - public Endpoint> errorEndpoint(final ErrorController controller) { - return new AbstractEndpoint>("/error", false, true) { - @Override - protected Map doInvoke() { - RequestAttributes attributes = RequestContextHolder - .currentRequestAttributes(); - return controller.extract(attributes, false); - } - }; + public ManagementErrorEndpoint errorEndpoint(final ErrorController controller) { + return new ManagementErrorEndpoint(this.errorPath, controller); } @Configuration diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AbstractEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AbstractEndpoint.java index dff785b980..221bd36c71 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AbstractEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AbstractEndpoint.java @@ -19,28 +19,15 @@ package org.springframework.boot.actuate.endpoint; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; - /** * Abstract base for {@link Endpoint} implementations. *

- * {@link Endpoint}s that support other {@link HttpMethod}s than {@link HttpMethod#GET} - * should override {@link #methods()} and provide a list of supported methods. * * @author Phillip Webb * @author Christian Dupuis */ public abstract class AbstractEndpoint implements Endpoint { - private static final MediaType[] NO_MEDIA_TYPES = new MediaType[0]; - - protected static final HttpMethod[] NO_HTTP_METHOD = new HttpMethod[0]; - - protected static final HttpMethod[] GET_HTTP_METHOD = new HttpMethod[] { HttpMethod.GET }; - - protected static final HttpMethod[] POST_HTTP_METHOD = new HttpMethod[] { HttpMethod.POST }; - @NotNull @Pattern(regexp = "/[^/]*", message = "Path must start with /") private String path; @@ -85,23 +72,4 @@ public abstract class AbstractEndpoint implements Endpoint { this.sensitive = sensitive; } - @Override - public MediaType[] produces() { - return NO_MEDIA_TYPES; - } - - @Override - public HttpMethod[] methods() { - return GET_HTTP_METHOD; - } - - @Override - public final T invoke() { - if (this.enabled) { - return doInvoke(); - } - throw new EndpointDisabledException(); - } - - protected abstract T doInvoke(); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java index a6445182ee..a10237be0b 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java @@ -21,6 +21,7 @@ import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.AutoConfigurationReportEndpoint.Report; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.autoconfigure.AutoConfigurationReport; import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcome; import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcomes; @@ -31,6 +32,8 @@ import org.springframework.util.ClassUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; import com.fasterxml.jackson.annotation.JsonPropertyOrder; @@ -42,6 +45,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; * @author Dave Syer */ @ConfigurationProperties(name = "endpoints.autoconfig", ignoreUnknownFields = false) +@FrameworkEndpoint public class AutoConfigurationReportEndpoint extends AbstractEndpoint { @Autowired @@ -52,7 +56,9 @@ public class AutoConfigurationReportEndpoint extends AbstractEndpoint { } @Override - protected Report doInvoke() { + @RequestMapping + @ResponseBody + public Report invoke() { return new Report(this.autoConfigurationReport); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/BeansEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/BeansEndpoint.java index 6ee2000f30..b07f3c9408 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/BeansEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/BeansEndpoint.java @@ -17,12 +17,15 @@ package org.springframework.boot.actuate.endpoint; import org.springframework.beans.BeansException; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.support.LiveBeansView; import org.springframework.core.env.Environment; import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; /** * Exposes JSON view of Spring beans. If the {@link Environment} contains a key setting @@ -33,6 +36,7 @@ import org.springframework.http.MediaType; * @author Dave Syer */ @ConfigurationProperties(name = "endpoints.beans", ignoreUnknownFields = false) +@FrameworkEndpoint public class BeansEndpoint extends AbstractEndpoint implements ApplicationContextAware { @@ -51,12 +55,9 @@ public class BeansEndpoint extends AbstractEndpoint implements } @Override - public MediaType[] produces() { - return new MediaType[] { MediaType.APPLICATION_JSON }; - } - - @Override - protected String doInvoke() { + @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public String invoke() { return this.liveBeansView.getSnapshotAsJson(); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java index d2f4c1b8c8..fca2a4eea4 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java @@ -19,10 +19,13 @@ package org.springframework.boot.actuate.endpoint; import java.util.Map; import org.springframework.beans.BeansException; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; import com.fasterxml.jackson.databind.ObjectMapper; @@ -37,6 +40,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; * @author Christian Dupuis */ @ConfigurationProperties(name = "endpoints.configprops", ignoreUnknownFields = false) +@FrameworkEndpoint public class ConfigurationPropertiesReportEndpoint extends AbstractEndpoint> implements ApplicationContextAware { @@ -64,7 +68,9 @@ public class ConfigurationPropertiesReportEndpoint extends @Override @SuppressWarnings("unchecked") - protected Map doInvoke() { + @RequestMapping + @ResponseBody + public Map invoke() { Map beans = this.context .getBeansWithAnnotation(ConfigurationProperties.class); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/DumpEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/DumpEndpoint.java index 15fbc55e27..33d7f72a83 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/DumpEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/DumpEndpoint.java @@ -21,7 +21,10 @@ import java.lang.management.ThreadInfo; import java.util.Arrays; import java.util.List; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; /** * {@link Endpoint} to expose thread info. @@ -29,6 +32,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author Dave Syer */ @ConfigurationProperties(name = "endpoints.dump", ignoreUnknownFields = false) +@FrameworkEndpoint public class DumpEndpoint extends AbstractEndpoint> { /** @@ -39,7 +43,9 @@ public class DumpEndpoint extends AbstractEndpoint> { } @Override - protected List doInvoke() { + @RequestMapping + @ResponseBody + public List invoke() { return Arrays.asList(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true)); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Endpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Endpoint.java index 7fe94e4ecd..6a54123892 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Endpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Endpoint.java @@ -16,9 +16,6 @@ package org.springframework.boot.actuate.endpoint; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; - /** * An endpoint that can be used to expose useful information to operations. Usually * exposed via Spring MVC but could also be exposed using some other technique. @@ -41,16 +38,6 @@ public interface Endpoint { */ boolean isSensitive(); - /** - * Returns the {@link MediaType}s that this endpoint produces or {@code null}. - */ - MediaType[] produces(); - - /** - * Returns the {@link HttpMethod}s that this endpoint supports. - */ - HttpMethod[] methods(); - /** * Called to invoke the endpoint. * @return the results of the invocation diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpoint.java index 1e0e8d600b..a176d150d6 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpoint.java @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint; import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.ConfigurableEnvironment; @@ -26,6 +27,11 @@ import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; /** * {@link Endpoint} to expose {@link ConfigurableEnvironment environment} information. @@ -34,6 +40,7 @@ import org.springframework.core.env.StandardEnvironment; * @author Phillip Webb */ @ConfigurationProperties(name = "endpoints.env", ignoreUnknownFields = false) +@FrameworkEndpoint public class EnvironmentEndpoint extends AbstractEndpoint> implements EnvironmentAware { @@ -47,7 +54,9 @@ public class EnvironmentEndpoint extends AbstractEndpoint> i } @Override - protected Map doInvoke() { + @RequestMapping + @ResponseBody + public Map invoke() { Map result = new LinkedHashMap(); result.put("profiles", this.environment.getActiveProfiles()); for (PropertySource source : getPropertySources()) { @@ -63,6 +72,16 @@ public class EnvironmentEndpoint extends AbstractEndpoint> i return result; } + @RequestMapping("/{name:.*}") + @ResponseBody + public Object value(@PathVariable String name) { + String result = this.environment.getProperty(name); + if (result == null) { + throw new NoSuchPropertyException("No such property: " + name); + } + return sanitize(name, result); + } + private Iterable> getPropertySources() { if (this.environment != null && this.environment instanceof ConfigurableEnvironment) { @@ -84,4 +103,13 @@ public class EnvironmentEndpoint extends AbstractEndpoint> i this.environment = environment; } + @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No such property") + public static class NoSuchPropertyException extends RuntimeException { + + public NoSuchPropertyException(String string) { + super(string); + } + + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java index 9863bc6243..4ee546a542 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java @@ -16,9 +16,12 @@ package org.springframework.boot.actuate.endpoint; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; /** * {@link Endpoint} to expose application health. @@ -26,6 +29,7 @@ import org.springframework.util.Assert; * @author Dave Syer */ @ConfigurationProperties(name = "endpoints.health", ignoreUnknownFields = false) +@FrameworkEndpoint public class HealthEndpoint extends AbstractEndpoint { private HealthIndicator indicator; @@ -41,8 +45,14 @@ public class HealthEndpoint extends AbstractEndpoint { this.indicator = indicator; } + HealthEndpoint() { + super("/health", false, true); + } + @Override - protected T doInvoke() { + @RequestMapping + @ResponseBody + public T invoke() { return this.indicator.health(); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InfoEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InfoEndpoint.java index 06ae714086..27fc113ece 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InfoEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InfoEndpoint.java @@ -20,8 +20,11 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; /** * {@link Endpoint} to expose arbitrary application information. @@ -29,6 +32,7 @@ import org.springframework.util.Assert; * @author Dave Syer */ @ConfigurationProperties(name = "endpoints.info", ignoreUnknownFields = false) +@FrameworkEndpoint public class InfoEndpoint extends AbstractEndpoint> { private Map info; @@ -45,7 +49,9 @@ public class InfoEndpoint extends AbstractEndpoint> { } @Override - protected Map doInvoke() { + @RequestMapping + @ResponseBody + public Map invoke() { Map info = new LinkedHashMap(this.info); info.putAll(getAdditionalInfo()); return info; diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/JolokiaEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/JolokiaEndpoint.java index fbdc991c27..9d39408799 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/JolokiaEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/JolokiaEndpoint.java @@ -17,7 +17,6 @@ package org.springframework.boot.actuate.endpoint; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.http.HttpMethod; /** * {@link Endpoint} implementation to register the Jolokia infrastructure with the Boot @@ -33,13 +32,8 @@ public class JolokiaEndpoint extends AbstractEndpoint { } @Override - protected String doInvoke() { + public String invoke() { return null; } - @Override - public HttpMethod[] methods() { - return NO_HTTP_METHOD; - } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ManagementErrorEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ManagementErrorEndpoint.java new file mode 100644 index 0000000000..c7e5745524 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ManagementErrorEndpoint.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2013 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; + +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; +import org.springframework.boot.actuate.web.ErrorController; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +/** + * Special endpoint for handling "/error" path when the management servlet is in a child + * context. The regular {@link ErrorController} should be available there but because of + * the way the handler mappings are set up it will not be detected. + * + * @author Dave Syer + */ +@FrameworkEndpoint +@ConfigurationProperties(name = "error") +public class ManagementErrorEndpoint extends AbstractEndpoint> { + + private final ErrorController controller; + + public ManagementErrorEndpoint(String path, ErrorController controller) { + super(path, false, true); + this.controller = controller; + } + + @Override + @RequestMapping + @ResponseBody + public Map invoke() { + RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); + return this.controller.extract(attributes, false); + } +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/MetricsEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/MetricsEndpoint.java index 47f8234655..3e0b30331e 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/MetricsEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/MetricsEndpoint.java @@ -19,9 +19,15 @@ package org.springframework.boot.actuate.endpoint; import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.actuate.metrics.Metric; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpStatus; import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; /** * {@link Endpoint} to expose {@link PublicMetrics}. @@ -29,6 +35,7 @@ import org.springframework.util.Assert; * @author Dave Syer */ @ConfigurationProperties(name = "endpoints.metrics", ignoreUnknownFields = false) +@FrameworkEndpoint public class MetricsEndpoint extends AbstractEndpoint> { private PublicMetrics metrics; @@ -45,7 +52,9 @@ public class MetricsEndpoint extends AbstractEndpoint> { } @Override - protected Map doInvoke() { + @RequestMapping + @ResponseBody + public Map invoke() { Map result = new LinkedHashMap(); for (Metric metric : this.metrics.metrics()) { result.put(metric.getName(), metric.getValue()); @@ -53,4 +62,22 @@ public class MetricsEndpoint extends AbstractEndpoint> { return result; } + @RequestMapping("/{name:.*}") + @ResponseBody + public Object value(@PathVariable String name) { + Object value = invoke().get(name); + if (value == null) { + throw new NoSuchMetricException("No such metric: " + name); + } + return value; + } + + @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No such metric") + public static class NoSuchMetricException extends RuntimeException { + + public NoSuchMetricException(String string) { + super(string); + } + + } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ShutdownEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ShutdownEndpoint.java index fc7be0a833..9585a799ef 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ShutdownEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ShutdownEndpoint.java @@ -20,11 +20,14 @@ import java.util.Collections; import java.util.Map; import org.springframework.beans.BeansException; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.http.HttpMethod; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; /** * {@link Endpoint} to shutdown the {@link ApplicationContext}. @@ -33,6 +36,7 @@ import org.springframework.http.HttpMethod; * @author Christian Dupuis */ @ConfigurationProperties(name = "endpoints.shutdown", ignoreUnknownFields = false) +@FrameworkEndpoint public class ShutdownEndpoint extends AbstractEndpoint> implements ApplicationContextAware { @@ -46,7 +50,9 @@ public class ShutdownEndpoint extends AbstractEndpoint> impl } @Override - protected Map doInvoke() { + @RequestMapping(method = RequestMethod.POST) + @ResponseBody + public Map invoke() { if (this.context == null) { return Collections. singletonMap("message", @@ -77,9 +83,4 @@ public class ShutdownEndpoint extends AbstractEndpoint> impl } } - @Override - public HttpMethod[] methods() { - return POST_HTTP_METHOD; - } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/TraceEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/TraceEndpoint.java index d21f4acd0b..a003170af8 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/TraceEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/TraceEndpoint.java @@ -18,10 +18,13 @@ package org.springframework.boot.actuate.endpoint; import java.util.List; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.actuate.trace.Trace; import org.springframework.boot.actuate.trace.TraceRepository; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; /** * {@link Endpoint} to expose {@link Trace} information. @@ -29,6 +32,7 @@ import org.springframework.util.Assert; * @author Dave Syer */ @ConfigurationProperties(name = "endpoints.trace", ignoreUnknownFields = false) +@FrameworkEndpoint public class TraceEndpoint extends AbstractEndpoint> { private TraceRepository repository; @@ -45,7 +49,9 @@ public class TraceEndpoint extends AbstractEndpoint> { } @Override - protected List doInvoke() { + @RequestMapping + @ResponseBody + public List invoke() { return this.repository.findAll(); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerAdapter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerAdapter.java deleted file mode 100644 index debd78158b..0000000000 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerAdapter.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright 2012-2013 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 java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.EndpointDisabledException; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.accept.ContentNegotiationManager; -import org.springframework.web.context.request.ServletWebRequest; -import org.springframework.web.servlet.HandlerAdapter; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; -import org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor; -import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; - -import com.fasterxml.jackson.databind.SerializationFeature; - -/** - * MVC {@link HandlerAdapter} for {@link Endpoint}s. Similar in may respects to - * {@link AbstractMessageConverterMethodProcessor} but not tied to annotated methods. - * - * @author Phillip Webb - * - * @see EndpointHandlerMapping - */ -public final class EndpointHandlerAdapter implements HandlerAdapter { - - private final Log logger = LogFactory.getLog(getClass()); - - private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); - - private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); - - private List> messageConverters; - - private List allSupportedMediaTypes; - - public EndpointHandlerAdapter() { - WebMvcConfigurationSupportConventions conventions = new WebMvcConfigurationSupportConventions(); - setMessageConverters(conventions.getDefaultHttpMessageConverters()); - } - - @Override - public boolean supports(Object handler) { - return handler instanceof Endpoint; - } - - @Override - public long getLastModified(HttpServletRequest request, Object handler) { - return -1; - } - - @Override - public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, - Object handler) throws Exception { - handle(request, response, (Endpoint) handler); - return null; - } - - @SuppressWarnings("unchecked") - private void handle(HttpServletRequest request, HttpServletResponse response, - Endpoint endpoint) throws Exception { - - Object result = null; - try { - result = endpoint.invoke(); - } - catch (EndpointDisabledException e) { - // Disabled endpoints should get mapped to a HTTP 404 - throw new NoSuchRequestHandlingMethodException(request); - } - - Class resultClass = result.getClass(); - - List mediaTypes = getMediaTypes(request, endpoint, resultClass); - MediaType selectedMediaType = selectMediaType(mediaTypes); - - ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response); - try { - if (selectedMediaType != null) { - selectedMediaType = selectedMediaType.removeQualityValue(); - for (HttpMessageConverter messageConverter : this.messageConverters) { - if (messageConverter.canWrite(resultClass, selectedMediaType)) { - ((HttpMessageConverter) messageConverter).write(result, - selectedMediaType, outputMessage); - if (this.logger.isDebugEnabled()) { - this.logger.debug("Written [" + result + "] as \"" - + selectedMediaType + "\" using [" + messageConverter - + "]"); - } - return; - } - } - } - throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); - } - finally { - outputMessage.close(); - } - } - - private List getMediaTypes(HttpServletRequest request, - Endpoint endpoint, Class resultClass) - throws HttpMediaTypeNotAcceptableException { - List requested = getAcceptableMediaTypes(request); - List producible = getProducibleMediaTypes(endpoint, resultClass); - - Set compatible = new LinkedHashSet(); - for (MediaType r : requested) { - for (MediaType p : producible) { - if (r.isCompatibleWith(p)) { - compatible.add(getMostSpecificMediaType(r, p)); - } - } - } - if (compatible.isEmpty()) { - throw new HttpMediaTypeNotAcceptableException(producible); - } - List mediaTypes = new ArrayList(compatible); - MediaType.sortBySpecificityAndQuality(mediaTypes); - return mediaTypes; - } - - private List getAcceptableMediaTypes(HttpServletRequest request) - throws HttpMediaTypeNotAcceptableException { - List mediaTypes = this.contentNegotiationManager - .resolveMediaTypes(new ServletWebRequest(request)); - return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) - : mediaTypes; - } - - private List getProducibleMediaTypes(Endpoint endpoint, - Class returnValueClass) { - MediaType[] mediaTypes = endpoint.produces(); - if (mediaTypes != null && mediaTypes.length != 0) { - return Arrays.asList(mediaTypes); - } - - if (this.allSupportedMediaTypes.isEmpty()) { - return Collections.singletonList(MediaType.ALL); - } - - List result = new ArrayList(); - for (HttpMessageConverter converter : this.messageConverters) { - if (converter.canWrite(returnValueClass, null)) { - result.addAll(converter.getSupportedMediaTypes()); - } - } - return result; - } - - private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) { - produceType = produceType.copyQualityValue(acceptType); - return MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceType) <= 0 ? acceptType - : produceType; - } - - private MediaType selectMediaType(List mediaTypes) { - MediaType selectedMediaType = null; - for (MediaType mediaType : mediaTypes) { - if (mediaType.isConcrete()) { - selectedMediaType = mediaType; - break; - } - else if (mediaType.equals(MediaType.ALL) - || mediaType.equals(MEDIA_TYPE_APPLICATION)) { - selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; - break; - } - } - return selectedMediaType; - } - - public void setContentNegotiationManager( - ContentNegotiationManager contentNegotiationManager) { - this.contentNegotiationManager = contentNegotiationManager; - } - - public void setMessageConverters(List> messageConverters) { - this.messageConverters = messageConverters; - Set allSupportedMediaTypes = new LinkedHashSet(); - for (HttpMessageConverter messageConverter : messageConverters) { - allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); - } - this.allSupportedMediaTypes = new ArrayList(allSupportedMediaTypes); - MediaType.sortBySpecificity(this.allSupportedMediaTypes); - } - - /** - * Default conventions, taken from {@link WebMvcConfigurationSupport} with a few minor - * tweaks. - */ - private static class WebMvcConfigurationSupportConventions extends - WebMvcConfigurationSupport { - public List> getDefaultHttpMessageConverters() { - List> converters = new ArrayList>(); - addDefaultHttpMessageConverters(converters); - for (HttpMessageConverter converter : converters) { - if (converter instanceof MappingJackson2HttpMessageConverter) { - MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converter; - jacksonConverter.getObjectMapper().disable( - SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - } - } - return converters; - } - } -} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java index 9e65bfa966..5263e36a3c 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java @@ -16,33 +16,46 @@ package org.springframework.boot.actuate.endpoint.mvc; +import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; - -import javax.servlet.http.HttpServletRequest; +import java.util.Set; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.http.HttpMethod; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping; +import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; /** * {@link HandlerMapping} to map {@link Endpoint}s to URLs via {@link Endpoint#getPath()}. + * Only endpoints that are annotated as @FrameworkEndpoint will be mapped, + * and within that class only those methods with @RequestMapping will be + * exposed. The semantics of @RequestMapping should be identical to a normal + * @Controller, but the endpoints should not be annotated as + * @Controller (otherwise they will be mapped by the normal MVC mechanisms). + * + *

+ * One of the aims of the mapping is to support endpoints that work as HTTP endpoints but + * can still provide useful service interfaces when there is no HTTP server (and no Spring + * MVC on the classpath). Note that any endpoints having method signaturess will break in + * a non-servlet environment. + *

* * @author Phillip Webb * @author Christian Dupuis - * @see EndpointHandlerAdapter + * @author Dave Syer + * */ -public class EndpointHandlerMapping extends AbstractUrlHandlerMapping implements +public class EndpointHandlerMapping extends RequestMappingHandlerMapping implements InitializingBean, ApplicationContextAware { private List> endpoints; @@ -56,28 +69,16 @@ public class EndpointHandlerMapping extends AbstractUrlHandlerMapping implements * detected from the {@link ApplicationContext}. */ public EndpointHandlerMapping() { - setOrder(HIGHEST_PRECEDENCE); - } - - /** - * Create a new {@link EndpointHandlerMapping} with the specified endpoints. - * @param endpoints the endpoints - */ - public EndpointHandlerMapping(Collection> endpoints) { - Assert.notNull(endpoints, "Endpoints must not be null"); - this.endpoints = new ArrayList>(endpoints); + // By default the static resource handler mapping is LOWEST_PRECEDENCE - 1 + setOrder(LOWEST_PRECEDENCE - 2); } @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { + super.afterPropertiesSet(); if (this.endpoints == null) { this.endpoints = findEndpointBeans(); } - if (!this.disabled) { - for (Endpoint endpoint : this.endpoints) { - registerHandler(this.prefix + endpoint.getPath(), endpoint); - } - } } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -86,20 +87,60 @@ public class EndpointHandlerMapping extends AbstractUrlHandlerMapping implements getApplicationContext(), Endpoint.class).values()); } + /** + * Detects @FrameworkEndpoint annotations in handler beans. + * + * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler(java.lang.Class) + */ @Override - protected Object lookupHandler(String urlPath, HttpServletRequest request) - throws Exception { - Object handler = super.lookupHandler(urlPath, request); - if (handler != null) { - Object endpoint = (handler instanceof HandlerExecutionChain ? ((HandlerExecutionChain) handler) - .getHandler() : handler); - HttpMethod method = HttpMethod.valueOf(request.getMethod()); - if (endpoint instanceof Endpoint - && supportsMethod(((Endpoint) endpoint).methods(), method)) { - return endpoint; + protected boolean isHandler(Class beanType) { + if (this.disabled) { + return false; + } + return AnnotationUtils.findAnnotation(beanType, FrameworkEndpoint.class) != null; + } + + @Override + protected void registerHandlerMethod(Object handler, Method method, + RequestMappingInfo mapping) { + + if (mapping == null) { + return; + } + + Set defaultPatterns = mapping.getPatternsCondition().getPatterns(); + String[] patterns = new String[defaultPatterns.isEmpty() ? 1 : defaultPatterns + .size()]; + + String path = ""; + Object bean = handler; + if (bean instanceof String) { + bean = getApplicationContext().getBean((String) handler); + } + if (bean instanceof Endpoint) { + Endpoint endpoint = (Endpoint) bean; + path = endpoint.getPath(); + } + + int i = 0; + String prefix = StringUtils.hasText(this.prefix) ? this.prefix + path : path; + if (defaultPatterns.isEmpty()) { + patterns[0] = prefix; + } + else { + for (String pattern : defaultPatterns) { + patterns[i] = prefix + pattern; + i++; } } - return null; + PatternsRequestCondition patternsInfo = new PatternsRequestCondition(patterns); + + RequestMappingInfo modified = new RequestMappingInfo(patternsInfo, + mapping.getMethodsCondition(), mapping.getParamsCondition(), + mapping.getHeadersCondition(), mapping.getConsumesCondition(), + mapping.getProducesCondition(), mapping.getCustomCondition()); + + super.registerHandlerMethod(handler, method, modified); } /** @@ -131,16 +172,4 @@ public class EndpointHandlerMapping extends AbstractUrlHandlerMapping implements public List> getEndpoints() { return Collections.unmodifiableList(this.endpoints); } - - private boolean supportsMethod(HttpMethod[] supportedMethods, - HttpMethod requestedMethod) { - Assert.notNull(supportedMethods, "SupportMethods must not be null"); - Assert.notNull(supportedMethods, "RequestedMethod must not be null"); - for (HttpMethod supportedMethod : supportedMethods) { - if (supportedMethod.equals(requestedMethod)) { - return true; - } - } - return false; - } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/FrameworkEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/FrameworkEndpoint.java new file mode 100644 index 0000000000..f1a37e7277 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/FrameworkEndpoint.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2013 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Component; + +/** + * @author Dave Syer + */ +@Component +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface FrameworkEndpoint { + +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/BasicErrorController.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/BasicErrorController.java index 151a20ce2d..5067aaaacc 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/BasicErrorController.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/BasicErrorController.java @@ -63,7 +63,7 @@ public class BasicErrorController implements ErrorController { @RequestMapping(value = "${error.path:/error}", produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request) { - Map map = extract(new ServletRequestAttributes(request), false); + Map map = error(request); return new ModelAndView(ERROR_KEY, map); } 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 76b70affe1..521a4f587d 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 @@ -25,7 +25,7 @@ import org.junit.After; import org.junit.Test; import org.springframework.boot.TestUtils; import org.springframework.boot.actuate.endpoint.AbstractEndpoint; -import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.actuate.endpoint.mvc.FrameworkEndpoint; import org.springframework.boot.actuate.properties.ManagementServerProperties; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; @@ -200,13 +200,8 @@ public class EndpointWebMvcAutoConfigurationTests { } @Bean - public Endpoint testEndpoint() { - return new AbstractEndpoint("/endpoint", false, true) { - @Override - public String doInvoke() { - return "endpointoutput"; - } - }; + public TestEndpoint testEndpoint() { + return new TestEndpoint(); } } @@ -245,4 +240,20 @@ public class EndpointWebMvcAutoConfigurationTests { } + @FrameworkEndpoint + public static class TestEndpoint extends AbstractEndpoint { + + public TestEndpoint() { + super("/endpoint", false, true); + } + + @Override + @RequestMapping + @ResponseBody + public String invoke() { + return "endpointoutput"; + } + + } + } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AbstractEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AbstractEndpointTests.java index 46abfb8ba6..f9f6fb982f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AbstractEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AbstractEndpointTests.java @@ -25,7 +25,6 @@ import org.springframework.boot.TestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; -import org.springframework.http.MediaType; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -49,16 +48,13 @@ public abstract class AbstractEndpointTests> { private final String property; - private MediaType[] produces; - public AbstractEndpointTests(Class configClass, Class type, String path, - boolean sensitive, String property, MediaType... produces) { + boolean sensitive, String property) { this.configClass = configClass; this.type = type; this.path = path; this.sensitive = sensitive; this.property = property; - this.produces = produces; } @Before @@ -75,11 +71,6 @@ public abstract class AbstractEndpointTests> { } } - @Test - public void producesMediaType() { - assertThat(getEndpointBean().produces(), equalTo(this.produces)); - } - @Test public void getPath() throws Exception { assertThat(getEndpointBean().getPath(), equalTo(this.path)); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointTests.java index 04804d171d..ac35cbd3cf 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/BeansEndpointTests.java @@ -17,11 +17,9 @@ package org.springframework.boot.actuate.endpoint; import org.junit.Test; -import org.springframework.boot.actuate.endpoint.BeansEndpoint; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.MediaType; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; @@ -34,8 +32,7 @@ import static org.junit.Assert.assertThat; public class BeansEndpointTests extends AbstractEndpointTests { public BeansEndpointTests() { - super(Config.class, BeansEndpoint.class, "/beans", true, "endpoints.beans", - MediaType.APPLICATION_JSON); + super(Config.class, BeansEndpoint.class, "/beans", true, "endpoints.beans"); } @Test diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerAdapterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerAdapterTests.java deleted file mode 100644 index 5877f63a7d..0000000000 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerAdapterTests.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2012-2013 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 java.util.Collections; -import java.util.Map; - -import org.junit.Test; -import org.springframework.boot.actuate.endpoint.AbstractEndpoint; -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link EndpointHandlerAdapter}. - * - * @author Phillip Webb - */ -public class EndpointHandlerAdapterTests { - - private EndpointHandlerAdapter adapter = new EndpointHandlerAdapter(); - private MockHttpServletRequest request = new MockHttpServletRequest(); - private MockHttpServletResponse response = new MockHttpServletResponse(); - - @Test - public void onlySupportsEndpoints() throws Exception { - assertTrue(this.adapter.supports(mock(Endpoint.class))); - assertFalse(this.adapter.supports(mock(Object.class))); - } - - @Test - public void rendersJson() throws Exception { - this.adapter.handle(this.request, this.response, - new AbstractEndpoint>("/foo") { - @Override - protected Map doInvoke() { - return Collections.singletonMap("hello", "world"); - } - }); - assertEquals("{\"hello\":\"world\"}", this.response.getContentAsString()); - } - - @Test - public void rendersString() throws Exception { - this.request.addHeader("Accept", "text/plain"); - this.adapter.handle(this.request, this.response, new AbstractEndpoint( - "/foo") { - @Override - protected String doInvoke() { - return "hello world"; - } - }); - assertEquals("hello world", this.response.getContentAsString()); - } -} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMappingTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMappingTests.java index 05311da42f..f18afa2228 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMappingTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMappingTests.java @@ -16,12 +16,18 @@ package org.springframework.boot.actuate.endpoint.mvc; -import java.util.Arrays; +import java.lang.reflect.Method; +import org.junit.Before; import org.junit.Test; import org.springframework.boot.actuate.endpoint.AbstractEndpoint; -import org.springframework.http.HttpMethod; +import org.springframework.context.support.StaticApplicationContext; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; @@ -33,21 +39,38 @@ import static org.junit.Assert.assertThat; * Tests for {@link EndpointHandlerMapping}. * * @author Phillip Webb + * @author Dave Syer */ public class EndpointHandlerMappingTests { + private StaticApplicationContext context = new StaticApplicationContext(); + private EndpointHandlerMapping mapping = new EndpointHandlerMapping(); + private Method method; + + @Before + public void init() throws Exception { + this.context.getDefaultListableBeanFactory().registerSingleton("mapping", + this.mapping); + this.mapping.setApplicationContext(this.context); + this.method = ReflectionUtils.findMethod(TestEndpoint.class, "invoke"); + } + @Test public void withoutPrefix() throws Exception { TestEndpoint endpointA = new TestEndpoint("/a"); TestEndpoint endpointB = new TestEndpoint("/b"); - EndpointHandlerMapping mapping = new EndpointHandlerMapping(Arrays.asList( - endpointA, endpointB)); - mapping.afterPropertiesSet(); - assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/a")) - .getHandler(), equalTo((Object) endpointA)); - assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/b")) - .getHandler(), equalTo((Object) endpointB)); - assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/c")), + this.context.getDefaultListableBeanFactory().registerSingleton( + endpointA.getPath(), endpointA); + this.context.getDefaultListableBeanFactory().registerSingleton( + endpointB.getPath(), endpointB); + this.mapping.afterPropertiesSet(); + assertThat(this.mapping.getHandler(new MockHttpServletRequest("GET", "/a")) + .getHandler(), + equalTo((Object) new HandlerMethod(endpointA, this.method))); + assertThat(this.mapping.getHandler(new MockHttpServletRequest("GET", "/b")) + .getHandler(), + equalTo((Object) new HandlerMethod(endpointB, this.method))); + assertThat(this.mapping.getHandler(new MockHttpServletRequest("GET", "/c")), nullValue()); } @@ -55,49 +78,63 @@ public class EndpointHandlerMappingTests { public void withPrefix() throws Exception { TestEndpoint endpointA = new TestEndpoint("/a"); TestEndpoint endpointB = new TestEndpoint("/b"); - EndpointHandlerMapping mapping = new EndpointHandlerMapping(Arrays.asList( - endpointA, endpointB)); - mapping.setPrefix("/a"); - mapping.afterPropertiesSet(); - assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/a/a")) - .getHandler(), equalTo((Object) endpointA)); - assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/a/b")) - .getHandler(), equalTo((Object) endpointB)); - assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/a")), + this.context.getDefaultListableBeanFactory().registerSingleton( + endpointA.getPath(), endpointA); + this.context.getDefaultListableBeanFactory().registerSingleton( + endpointB.getPath(), endpointB); + this.mapping.setPrefix("/a"); + this.mapping.afterPropertiesSet(); + assertThat(this.mapping.getHandler(new MockHttpServletRequest("GET", "/a/a")) + .getHandler(), + equalTo((Object) new HandlerMethod(endpointA, this.method))); + assertThat(this.mapping.getHandler(new MockHttpServletRequest("GET", "/a/b")) + .getHandler(), + equalTo((Object) new HandlerMethod(endpointB, this.method))); + assertThat(this.mapping.getHandler(new MockHttpServletRequest("GET", "/a")), nullValue()); } - @Test + @Test(expected = HttpRequestMethodNotSupportedException.class) public void onlyGetHttpMethodForNonActionEndpoints() throws Exception { TestEndpoint endpoint = new TestEndpoint("/a"); - EndpointHandlerMapping mapping = new EndpointHandlerMapping( - Arrays.asList(endpoint)); - mapping.afterPropertiesSet(); - assertNotNull(mapping.getHandler(new MockHttpServletRequest("GET", "/a"))); - assertNull(mapping.getHandler(new MockHttpServletRequest("POST", "/a"))); + this.context.getDefaultListableBeanFactory().registerSingleton( + endpoint.getPath(), endpoint); + this.mapping.afterPropertiesSet(); + assertNotNull(this.mapping.getHandler(new MockHttpServletRequest("GET", "/a"))); + assertNull(this.mapping.getHandler(new MockHttpServletRequest("POST", "/a"))); } @Test + public void postHttpMethodForActionEndpoints() throws Exception { + TestEndpoint endpoint = new TestActionEndpoint("/a"); + this.context.getDefaultListableBeanFactory().registerSingleton( + endpoint.getPath(), endpoint); + this.mapping.afterPropertiesSet(); + assertNotNull(this.mapping.getHandler(new MockHttpServletRequest("POST", "/a"))); + } + + @Test(expected = HttpRequestMethodNotSupportedException.class) public void onlyPostHttpMethodForActionEndpoints() throws Exception { TestEndpoint endpoint = new TestActionEndpoint("/a"); - EndpointHandlerMapping mapping = new EndpointHandlerMapping( - Arrays.asList(endpoint)); - mapping.afterPropertiesSet(); - assertNull(mapping.getHandler(new MockHttpServletRequest("GET", "/a"))); - assertNotNull(mapping.getHandler(new MockHttpServletRequest("POST", "/a"))); + this.context.getDefaultListableBeanFactory().registerSingleton( + endpoint.getPath(), endpoint); + this.mapping.afterPropertiesSet(); + assertNotNull(this.mapping.getHandler(new MockHttpServletRequest("POST", "/a"))); + assertNull(this.mapping.getHandler(new MockHttpServletRequest("GET", "/a"))); } @Test public void disabled() throws Exception { - TestEndpoint endpointA = new TestEndpoint("/a"); - EndpointHandlerMapping mapping = new EndpointHandlerMapping( - Arrays.asList(endpointA)); - mapping.setDisabled(true); - mapping.afterPropertiesSet(); - assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/a")), + TestEndpoint endpoint = new TestEndpoint("/a"); + this.context.getDefaultListableBeanFactory().registerSingleton( + endpoint.getPath(), endpoint); + this.mapping.setDisabled(true); + this.mapping.afterPropertiesSet(); + assertThat(this.mapping.getHandler(new MockHttpServletRequest("GET", "/a")), nullValue()); } + @FrameworkEndpoint private static class TestEndpoint extends AbstractEndpoint { public TestEndpoint(String path) { @@ -105,12 +142,14 @@ public class EndpointHandlerMappingTests { } @Override - public Object doInvoke() { + @RequestMapping(method = RequestMethod.GET) + public Object invoke() { return null; } } + @FrameworkEndpoint private static class TestActionEndpoint extends TestEndpoint { public TestActionEndpoint(String path) { @@ -118,8 +157,9 @@ public class EndpointHandlerMappingTests { } @Override - public HttpMethod[] methods() { - return POST_HTTP_METHOD; + @RequestMapping(method = RequestMethod.POST) + public Object invoke() { + return null; } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerIntegrationTests.java index 7dbfefdf89..d23b10a670 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerIntegrationTests.java @@ -26,8 +26,10 @@ 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.ManagementSecurityAutoConfiguration; import org.springframework.boot.actuate.web.BasicErrorControllerIntegrationTests.TestConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -81,7 +83,8 @@ public class BasicErrorControllerIntegrationTests { } @Configuration - @EnableAutoConfiguration + @EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class, + ManagementSecurityAutoConfiguration.class }) public static class TestConfiguration { // For manual testing diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerSpecialIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerSpecialIntegrationTests.java index 7cabdf463d..efbc69ff7f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerSpecialIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerSpecialIntegrationTests.java @@ -19,7 +19,9 @@ package org.springframework.boot.actuate.web; import org.junit.After; import org.junit.Test; import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.ManagementSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; @@ -77,13 +79,15 @@ public class BasicErrorControllerSpecialIntegrationTests { } @Configuration - @EnableAutoConfiguration + @EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class, + ManagementSecurityAutoConfiguration.class }) protected static class ParentConfiguration { } @Configuration - @EnableAutoConfiguration + @EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class, + ManagementSecurityAutoConfiguration.class }) @EnableWebMvc protected static class WebMvcIncludedConfiguration { // For manual testing @@ -94,7 +98,19 @@ public class BasicErrorControllerSpecialIntegrationTests { } @Configuration - @EnableAutoConfiguration + @EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class, + ManagementSecurityAutoConfiguration.class }) + protected static class VanillaConfiguration { + // For manual testing + public static void main(String[] args) { + SpringApplication.run(VanillaConfiguration.class, args); + } + + } + + @Configuration + @EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class, + ManagementSecurityAutoConfiguration.class }) protected static class ChildConfiguration { // For manual testing