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 df1151c542..f11e908282 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 @@ -17,8 +17,9 @@ package org.springframework.boot.actuate.autoconfigure; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import java.util.Set; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -33,29 +34,13 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.SmartInitializingSingleton; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties.Security; -import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.condition.ConditionalOnManagementMvcContext; import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; -import org.springframework.boot.actuate.endpoint.HealthEndpoint; -import org.springframework.boot.actuate.endpoint.MetricsEndpoint; -import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; -import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer; -import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.LogFileMvcEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; -import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; -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.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; @@ -64,19 +49,17 @@ import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; import org.springframework.boot.context.embedded.EmbeddedServletContainerException; import org.springframework.boot.context.embedded.EmbeddedWebApplicationContext; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.context.event.ContextClosedEvent; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertySource; -import org.springframework.util.CollectionUtils; import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.servlet.DispatcherServlet; @@ -99,75 +82,27 @@ import org.springframework.web.servlet.DispatcherServlet; @AutoConfigureAfter({ PropertyPlaceholderAutoConfiguration.class, EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class }) -@EnableConfigurationProperties({ HealthMvcEndpointProperties.class, - EndpointCorsProperties.class }) -public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, - SmartInitializingSingleton { +public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, SmartInitializingSingleton { private static Log logger = LogFactory.getLog(EndpointWebMvcAutoConfiguration.class); private ApplicationContext applicationContext; - @Autowired - private HealthMvcEndpointProperties healthMvcEndpointProperties; - - @Autowired - private ManagementServerProperties managementServerProperties; - - @Autowired - private EndpointCorsProperties corsProperties; - - @Autowired(required = false) - private List mappingCustomizers; - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } - @Bean - @ConditionalOnMissingBean - public EndpointHandlerMapping endpointHandlerMapping() { - Set endpoints = mvcEndpoints().getEndpoints(); - CorsConfiguration corsConfiguration = getCorsConfiguration(this.corsProperties); - EndpointHandlerMapping mapping = new EndpointHandlerMapping(endpoints, - corsConfiguration); - boolean disabled = ManagementServerPort.get(this.applicationContext) != ManagementServerPort.SAME; - mapping.setDisabled(disabled); - if (!disabled) { - mapping.setPrefix(this.managementServerProperties.getContextPath()); - } - if (this.mappingCustomizers != null) { - for (EndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) { - customizer.customize(mapping); - } - } - return mapping; + @ConditionalOnManagementMvcContext + @Configuration + @Import(EndpointWebMvcImportSelector.class) + protected static class EndpointWebMvcConfiguration { } - private CorsConfiguration getCorsConfiguration(EndpointCorsProperties properties) { - if (CollectionUtils.isEmpty(properties.getAllowedOrigins())) { - return null; - } - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(properties.getAllowedOrigins()); - if (!CollectionUtils.isEmpty(properties.getAllowedHeaders())) { - configuration.setAllowedHeaders(properties.getAllowedHeaders()); - } - if (!CollectionUtils.isEmpty(properties.getAllowedMethods())) { - configuration.setAllowedMethods(properties.getAllowedMethods()); - } - if (!CollectionUtils.isEmpty(properties.getExposedHeaders())) { - configuration.setExposedHeaders(properties.getExposedHeaders()); - } - if (properties.getMaxAge() != null) { - configuration.setMaxAge(properties.getMaxAge()); - } - if (properties.getAllowCredentials() != null) { - configuration.setAllowCredentials(properties.getAllowCredentials()); - } - return configuration; + @Bean + public ManagementContextResolver managementContextResolver() { + return new ManagementContextResolver(this.applicationContext); } @Override @@ -187,53 +122,6 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, } } - @Bean - @ConditionalOnMissingBean - public MvcEndpoints mvcEndpoints() { - return new MvcEndpoints(); - } - - @Bean - @ConditionalOnBean(EnvironmentEndpoint.class) - @ConditionalOnEnabledEndpoint("env") - public EnvironmentMvcEndpoint environmentMvcEndpoint(EnvironmentEndpoint delegate) { - return new EnvironmentMvcEndpoint(delegate); - } - - @Bean - @ConditionalOnBean(HealthEndpoint.class) - @ConditionalOnEnabledEndpoint("health") - public HealthMvcEndpoint healthMvcEndpoint(HealthEndpoint delegate) { - Security security = this.managementServerProperties.getSecurity(); - boolean secure = (security != null && security.isEnabled()); - HealthMvcEndpoint healthMvcEndpoint = new HealthMvcEndpoint(delegate, secure); - if (this.healthMvcEndpointProperties.getMapping() != null) { - healthMvcEndpoint.addStatusMapping(this.healthMvcEndpointProperties - .getMapping()); - } - return healthMvcEndpoint; - } - - @Bean - @ConditionalOnBean(MetricsEndpoint.class) - @ConditionalOnEnabledEndpoint("metrics") - public MetricsMvcEndpoint metricsMvcEndpoint(MetricsEndpoint delegate) { - return new MetricsMvcEndpoint(delegate); - } - - @Bean - @ConditionalOnEnabledEndpoint("logfile") - public LogFileMvcEndpoint logfileMvcEndpoint() { - return new LogFileMvcEndpoint(); - } - - @Bean - @ConditionalOnBean(ShutdownEndpoint.class) - @ConditionalOnEnabledEndpoint(value = "shutdown", enabledByDefault = false) - public ShutdownMvcEndpoint shutdownMvcEndpoint(ShutdownEndpoint delegate) { - return new ShutdownMvcEndpoint(delegate); - } - private void createChildManagementContext() { final AnnotationConfigEmbeddedWebApplicationContext childContext = new AnnotationConfigEmbeddedWebApplicationContext(); @@ -241,13 +129,16 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, childContext.setNamespace("management"); childContext.setId(this.applicationContext.getId() + ":management"); + List> configurations = new ArrayList>(); + configurations.addAll(Arrays.> asList( + EndpointWebMvcChildContextConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + EmbeddedServletContainerAutoConfiguration.class, + DispatcherServletAutoConfiguration.class)); // Register the ManagementServerChildContextConfiguration first followed // by various specific AutoConfiguration classes. NOTE: The child context // is intentionally not completely auto-configured. - childContext.register(EndpointWebMvcChildContextConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, - EmbeddedServletContainerAutoConfiguration.class, - DispatcherServletAutoConfiguration.class); + childContext.register(configurations.toArray(new Class[0])); // Ensure close on the parent also closes the child if (this.applicationContext instanceof ConfigurableApplicationContext) { @@ -261,6 +152,7 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, } }); } + managementContextResolver().setApplicationContext(childContext); try { childContext.refresh(); } @@ -375,4 +267,31 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, } + public static class ManagementContextResolver { + + private ApplicationContext applicationContext; + + public ManagementContextResolver(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public ApplicationContext getApplicationContext() { + return this.applicationContext; + } + + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public MvcEndpoints getMvcEndpoints() { + try { + return applicationContext.getBean(MvcEndpoints.class); + } + catch (Exception e) { + return null; + } + } + + } + } 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 fe45e2b34d..b22bb2f2f6 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,12 +16,13 @@ package org.springframework.boot.actuate.autoconfigure; +import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -33,7 +34,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.ManagementSecurityAutoConfiguration.ManagementWebSecurityConfigurerAdapter; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; -import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer; import org.springframework.boot.actuate.endpoint.mvc.ManagementErrorEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; @@ -41,8 +41,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.SearchStrategy; +import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.ErrorAttributes; -import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainer; @@ -50,13 +50,18 @@ import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomi import org.springframework.boot.context.embedded.ErrorPage; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * Configuration triggered from {@link EndpointWebMvcAutoConfiguration} when a new @@ -75,9 +80,17 @@ public class EndpointWebMvcChildContextConfiguration { @Value("${error.path:/error}") private String errorPath = "/error"; + @Autowired + private ManagementServerProperties managementServerProperties; + + @Configuration + @Import(EndpointWebMvcImportSelector.class) + protected static class EndpointWebMvcConfiguration { + } + @Configuration protected static class ServerCustomization implements - EmbeddedServletContainerCustomizer, Ordered { + EmbeddedServletContainerCustomizer, Ordered { @Value("${error.path:/error}") private String errorPath = "/error"; @@ -113,13 +126,12 @@ public class EndpointWebMvcChildContextConfiguration { // and add the management-specific bits container.setPort(this.managementServerProperties.getPort()); container.setAddress(this.managementServerProperties.getAddress()); - container.setContextPath(this.managementServerProperties.getContextPath()); container.addErrorPages(new ErrorPage(this.errorPath)); } } - @Bean + @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServlet dispatcherServlet() { DispatcherServlet dispatcherServlet = new DispatcherServlet(); @@ -132,11 +144,132 @@ public class EndpointWebMvcChildContextConfiguration { return dispatcherServlet; } - @Bean - public HandlerAdapter handlerAdapter(HttpMessageConverters converters) { - RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); - adapter.setMessageConverters(converters.getConverters()); - return adapter; + @Configuration(DispatcherServlet.HANDLER_MAPPING_BEAN_NAME) + @EnableWebMvc + public static class CompositeHandlerMapping implements HandlerMapping { + + @Autowired + private ListableBeanFactory beanFactory; + + private List mappings; + + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) + throws Exception { + if (this.mappings == null) { + this.mappings = extractMappings(); + } + for (HandlerMapping mapping : this.mappings) { + HandlerExecutionChain handler = mapping.getHandler(request); + if (handler != null) { + return handler; + } + } + return null; + } + + private List extractMappings() { + List list = new ArrayList(); + list.addAll(this.beanFactory.getBeansOfType(HandlerMapping.class).values()); + list.remove(this); + AnnotationAwareOrderComparator.sort(list); + return list; + } + + } + + @Configuration(DispatcherServlet.HANDLER_ADAPTER_BEAN_NAME) + public static class CompositeHandlerAdapter implements HandlerAdapter { + + @Autowired + private ListableBeanFactory beanFactory; + + private List adapters; + + private List extractAdapters() { + List list = new ArrayList(); + list.addAll(this.beanFactory.getBeansOfType(HandlerAdapter.class).values()); + list.remove(this); + AnnotationAwareOrderComparator.sort(list); + return list; + } + + @Override + public boolean supports(Object handler) { + if (this.adapters == null) { + this.adapters = extractAdapters(); + } + for (HandlerAdapter mapping : this.adapters) { + if (mapping.supports(handler)) { + return true; + } + } + return false; + } + + @Override + public ModelAndView handle(HttpServletRequest request, + HttpServletResponse response, Object handler) throws Exception { + if (this.adapters == null) { + this.adapters = extractAdapters(); + } + for (HandlerAdapter mapping : this.adapters) { + if (mapping.supports(handler)) { + return mapping.handle(request, response, handler); + } + } + return null; + } + + @Override + public long getLastModified(HttpServletRequest request, Object handler) { + if (this.adapters == null) { + this.adapters = extractAdapters(); + } + for (HandlerAdapter mapping : this.adapters) { + if (mapping.supports(handler)) { + return mapping.getLastModified(request, handler); + } + } + return 0; + } + + } + + @Configuration(DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME) + public static class CompositeHandlerExceptionResolver implements + HandlerExceptionResolver { + + @Autowired + private ListableBeanFactory beanFactory; + + private List resolvers; + + private List extractResolvers() { + List list = new ArrayList(); + list.addAll(this.beanFactory.getBeansOfType(HandlerExceptionResolver.class) + .values()); + list.remove(this); + AnnotationAwareOrderComparator.sort(list); + return list; + } + + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, Object handler, Exception ex) { + if (this.resolvers == null) { + this.resolvers = extractResolvers(); + } + for (HandlerExceptionResolver mapping : this.resolvers) { + ModelAndView mav = mapping.resolveException(request, response, handler, + ex); + if (mav != null) { + return mav; + } + } + return null; + } + } /* @@ -158,24 +291,12 @@ public class EndpointWebMvcChildContextConfiguration { @ConditionalOnMissingClass("org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter") protected static class EndpointHandlerMappingConfiguration { - @Autowired(required = false) - private List mappingCustomizers; - - @Bean - public HandlerMapping handlerMapping(MvcEndpoints endpoints, - ListableBeanFactory beanFactory) { - Set set = new HashSet(endpoints.getEndpoints()); - set.addAll(beanFactory.getBeansOfType(MvcEndpoint.class).values()); - EndpointHandlerMapping mapping = new EndpointHandlerMapping(set); + @Autowired + public void handlerMapping(MvcEndpoints endpoints, + ListableBeanFactory beanFactory, EndpointHandlerMapping mapping) { // In a child context we definitely want to see the parent endpoints mapping.setDetectHandlerMethodsInAncestorContexts(true); postProcessMapping(beanFactory, mapping); - if (this.mappingCustomizers != null) { - for (EndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) { - customizer.customize(mapping); - } - } - return mapping; } /** @@ -196,7 +317,7 @@ public class EndpointWebMvcChildContextConfiguration { @Configuration @ConditionalOnClass(WebSecurityConfigurerAdapter.class) protected static class SecureEndpointHandlerMappingConfiguration extends - EndpointHandlerMappingConfiguration { + EndpointHandlerMappingConfiguration { @Override protected void postProcessMapping(ListableBeanFactory beanFactory, diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcConfiguration.java new file mode 100644 index 0000000000..d33d292d48 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcConfiguration.java @@ -0,0 +1,162 @@ +/* + * Copyright 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 java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties.Security; +import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; +import org.springframework.boot.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.actuate.endpoint.MetricsEndpoint; +import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; +import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer; +import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.LogFileMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.CollectionUtils; +import org.springframework.web.cors.CorsConfiguration; + +/** + * @author Dave Syer + * + */ +@Configuration +@EnableConfigurationProperties({ HealthMvcEndpointProperties.class, + EndpointCorsProperties.class }) +public class EndpointWebMvcConfiguration { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private HealthMvcEndpointProperties healthMvcEndpointProperties; + + @Autowired + private ManagementServerProperties managementServerProperties; + + @Autowired + private EndpointCorsProperties corsProperties; + + @Autowired(required = false) + private List mappingCustomizers; + + @Bean + @ConditionalOnMissingBean + public EndpointHandlerMapping endpointHandlerMapping() { + Set endpoints = mvcEndpoints().getEndpoints(); + CorsConfiguration corsConfiguration = getCorsConfiguration(this.corsProperties); + EndpointHandlerMapping mapping = new EndpointHandlerMapping(endpoints, + corsConfiguration); + boolean disabled = this.managementServerProperties.getPort()!=null && this.managementServerProperties.getPort()==-1; + mapping.setDisabled(disabled); + if (!disabled) { + mapping.setPrefix(this.managementServerProperties.getContextPath()); + } + if (this.mappingCustomizers != null) { + for (EndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) { + customizer.customize(mapping); + } + } + return mapping; + } + + private CorsConfiguration getCorsConfiguration(EndpointCorsProperties properties) { + if (CollectionUtils.isEmpty(properties.getAllowedOrigins())) { + return null; + } + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(properties.getAllowedOrigins()); + if (!CollectionUtils.isEmpty(properties.getAllowedHeaders())) { + configuration.setAllowedHeaders(properties.getAllowedHeaders()); + } + if (!CollectionUtils.isEmpty(properties.getAllowedMethods())) { + configuration.setAllowedMethods(properties.getAllowedMethods()); + } + if (!CollectionUtils.isEmpty(properties.getExposedHeaders())) { + configuration.setExposedHeaders(properties.getExposedHeaders()); + } + if (properties.getMaxAge() != null) { + configuration.setMaxAge(properties.getMaxAge()); + } + if (properties.getAllowCredentials() != null) { + configuration.setAllowCredentials(properties.getAllowCredentials()); + } + return configuration; + } + + @Bean + @ConditionalOnMissingBean + public MvcEndpoints mvcEndpoints() { + return new MvcEndpoints(); + } + + @Bean + @ConditionalOnBean(EnvironmentEndpoint.class) + @ConditionalOnEnabledEndpoint("env") + public EnvironmentMvcEndpoint environmentMvcEndpoint(EnvironmentEndpoint delegate) { + return new EnvironmentMvcEndpoint(delegate); + } + + @Bean + @ConditionalOnBean(HealthEndpoint.class) + @ConditionalOnEnabledEndpoint("health") + public HealthMvcEndpoint healthMvcEndpoint(HealthEndpoint delegate) { + Security security = this.managementServerProperties.getSecurity(); + boolean secure = (security != null && security.isEnabled()); + HealthMvcEndpoint healthMvcEndpoint = new HealthMvcEndpoint(delegate, secure); + if (this.healthMvcEndpointProperties.getMapping() != null) { + healthMvcEndpoint.addStatusMapping(this.healthMvcEndpointProperties + .getMapping()); + } + return healthMvcEndpoint; + } + + @Bean + @ConditionalOnBean(MetricsEndpoint.class) + @ConditionalOnEnabledEndpoint("metrics") + public MetricsMvcEndpoint metricsMvcEndpoint(MetricsEndpoint delegate) { + return new MetricsMvcEndpoint(delegate); + } + + @Bean + @ConditionalOnEnabledEndpoint("logfile") + public LogFileMvcEndpoint logfileMvcEndpoint() { + return new LogFileMvcEndpoint(); + } + + @Bean + @ConditionalOnBean(ShutdownEndpoint.class) + @ConditionalOnEnabledEndpoint(value = "shutdown", enabledByDefault = false) + public ShutdownMvcEndpoint shutdownMvcEndpoint(ShutdownEndpoint delegate) { + return new ShutdownMvcEndpoint(delegate); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcImportSelector.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcImportSelector.java new file mode 100644 index 0000000000..2019455705 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcImportSelector.java @@ -0,0 +1,55 @@ +/* + * 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 java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.context.annotation.DeferredImportSelector; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Selects configuration classes for the Actuator MVC endpoints. Customize the MVC + * endpoints by adding an entries to /META-INF/spring.factories under the + * {@link EndpointWebMvcConfiguration} key. + * + * @author Dave Syer + * + */ +public class EndpointWebMvcImportSelector implements DeferredImportSelector, + BeanClassLoaderAware { + + private ClassLoader beanClassLoader; + + @Override + public String[] selectImports(AnnotationMetadata metadata) { + // Find all possible auto configuration classes, filtering duplicates + List factories = new ArrayList(new LinkedHashSet( + SpringFactoriesLoader.loadFactoryNames(EndpointWebMvcConfiguration.class, + this.beanClassLoader))); + return factories.toArray(new String[0]); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + beanClassLoader = classLoader; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java index a190a16d84..7982b12bc0 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java @@ -18,12 +18,15 @@ package org.springframework.boot.actuate.autoconfigure; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration.ManagementContextResolver; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; @@ -45,6 +48,7 @@ import org.springframework.boot.autoconfigure.security.SpringBootWebSecurityConf import org.springframework.boot.autoconfigure.web.ErrorController; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; @@ -61,6 +65,10 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.StringUtils; /** @@ -209,7 +217,10 @@ public class ManagementSecurityAutoConfiguration { @Autowired private ManagementServerProperties management; - @Autowired + @Autowired(required = false) + private ManagementContextResolver contextResolver; + + @Autowired(required = false) private ServerProperties server; @Autowired(required = false) @@ -223,19 +234,17 @@ public class ManagementSecurityAutoConfiguration { @Override protected void configure(HttpSecurity http) throws Exception { // secure endpoints - String[] paths = getEndpointPaths(this.endpointHandlerMapping); - if (paths.length > 0 && this.management.getSecurity().isEnabled()) { + RequestMatcher matcher = getRequestMatcher(); + if (matcher != null) { // Always protect them if present if (this.security.isRequireSsl()) { http.requiresChannel().anyRequest().requiresSecure(); } AuthenticationEntryPoint entryPoint = entryPoint(); http.exceptionHandling().authenticationEntryPoint(entryPoint); - paths = this.server.getPathsArray(paths); - http.requestMatchers().antMatchers(paths); - String[] endpointPaths = this.server.getPathsArray(getEndpointPaths( - this.endpointHandlerMapping, false)); - configureAuthorizeRequests(endpointPaths, http.authorizeRequests()); + http.requestMatcher(matcher); + configureAuthorizeRequests(new EndpointPathRequestMatcher(false), + http.authorizeRequests()); http.httpBasic().authenticationEntryPoint(entryPoint); // No cookies for management endpoints by default http.csrf().disable(); @@ -246,6 +255,19 @@ public class ManagementSecurityAutoConfiguration { } } + private RequestMatcher getRequestMatcher() { + if (!this.management.getSecurity().isEnabled()) { + return null; + } + String path = management.getContextPath(); + if (StringUtils.hasText(path)) { + AntPathRequestMatcher matcher = new AntPathRequestMatcher( + server.getPath(path) + "/**"); + return matcher; + } + return new EndpointPathRequestMatcher(); + } + private AuthenticationEntryPoint entryPoint() { BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint(); entryPoint.setRealmName(this.security.getBasic().getRealm()); @@ -253,12 +275,55 @@ public class ManagementSecurityAutoConfiguration { } private void configureAuthorizeRequests( - String[] endpointPaths, + RequestMatcher permitAllMatcher, ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry requests) { - requests.antMatchers(endpointPaths).permitAll(); + requests.requestMatchers(permitAllMatcher).permitAll(); requests.anyRequest().hasRole(this.management.getSecurity().getRole()); } + private final class EndpointPathRequestMatcher implements RequestMatcher { + + private boolean sensitive; + + private RequestMatcher delegate; + + public EndpointPathRequestMatcher(boolean sensitive) { + this.sensitive = sensitive; + } + + public EndpointPathRequestMatcher() { + this(true); + } + + @Override + public boolean matches(HttpServletRequest request) { + if (endpointHandlerMapping == null && contextResolver != null) { + ApplicationContext context = contextResolver.getApplicationContext(); + if (context != null + && context.getBeanNamesForType(EndpointHandlerMapping.class).length > 0) { + endpointHandlerMapping = context + .getBean(EndpointHandlerMapping.class); + } + } + if (endpointHandlerMapping == null) { + endpointHandlerMapping = new EndpointHandlerMapping( + Collections. emptySet()); + } + if (delegate == null) { + List pathMatchers = new ArrayList(); + String[] paths = !sensitive ? getEndpointPaths( + endpointHandlerMapping, false) + : getEndpointPaths(endpointHandlerMapping); + for (String path : paths) { + pathMatchers.add(new AntPathRequestMatcher(server.getPath(path))); + } + delegate = pathMatchers.isEmpty() ? AnyRequestMatcher.INSTANCE + : new OrRequestMatcher(pathMatchers); + } + return delegate.matches(request); + } + } + } private static String[] getEndpointPaths(EndpointHandlerMapping endpointHandlerMapping) { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/ConditionalOnManagementMvcContext.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/ConditionalOnManagementMvcContext.java new file mode 100644 index 0000000000..e5a132165d --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/ConditionalOnManagementMvcContext.java @@ -0,0 +1,38 @@ +/* + * 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.condition; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional} that matches according to the way the management MVC endpoints are + * deployed. + * + * @author Dave Syer + * @since 1.3.0 + */ +@Conditional(OnManagementMvcCondition.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface ConditionalOnManagementMvcContext { + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/OnManagementMvcCondition.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/OnManagementMvcCondition.java new file mode 100644 index 0000000000..7617d98e2f --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/condition/OnManagementMvcCondition.java @@ -0,0 +1,120 @@ +/* + * 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.condition; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.web.context.WebApplicationContext; + +/** + * {@link Condition} that checks whether or not the management MVC endpoints are in the + * main context. + * + * @author Dave Syer + * @since 1.3.0 + */ +class OnManagementMvcCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + RelaxedPropertyResolver management = new RelaxedPropertyResolver( + context.getEnvironment(), "management."); + RelaxedPropertyResolver server = new RelaxedPropertyResolver( + context.getEnvironment(), "server."); + Integer managementPort = management.getProperty("port", Integer.class); + if (managementPort == null) { + ManagementServerProperties managementServerProperties = getBeanCarefully( + context, ManagementServerProperties.class); + if (managementServerProperties != null) { + managementPort = managementServerProperties.getPort(); + } + } + if (managementPort != null && managementPort < 0) { + return new ConditionOutcome(false, "The mangagement port is disabled"); + } + if (!(context.getResourceLoader() instanceof WebApplicationContext)) { + // Current context is not a webapp + return new ConditionOutcome(false, "The context is not a webapp"); + } + Integer serverPort = server.getProperty("port", Integer.class); + if (serverPort == null) { + ServerProperties serverProperties = getBeanCarefully(context, + ServerProperties.class); + if (serverProperties != null) { + serverPort = serverProperties.getPort(); + } + } + if ((managementPort == null) + || (serverPort == null && managementPort.equals(8080)) + || (managementPort != 0 && managementPort.equals(serverPort))) { + return new ConditionOutcome(true, + "The main context is the management context"); + } + return new ConditionOutcome(false, + "The main context is not the management context"); + } + + private T getBeanCarefully(ConditionContext context, Class type) { + String[] names = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + context.getBeanFactory(), type, false, false); + if (names.length == 1) { + BeanDefinition original = findBeanDefinition(context.getBeanFactory(), names[0]); + if (original instanceof RootBeanDefinition) { + DefaultListableBeanFactory temp = new DefaultListableBeanFactory(); + temp.setParentBeanFactory(context.getBeanFactory()); + temp.registerBeanDefinition("bean", + ((RootBeanDefinition) original).cloneBeanDefinition()); + return temp.getBean(type); + } + return BeanFactoryUtils.beanOfType(context.getBeanFactory(), type, false, + false); + } + ; + return null; + } + + private BeanDefinition findBeanDefinition(ConfigurableListableBeanFactory beanFactory, String name) { + BeanDefinition original = null; + while (beanFactory!=null && original==null){ + if (beanFactory.containsLocalBean(name)) { + original = beanFactory.getBeanDefinition(name); + } else { + BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory(); + if (parentBeanFactory instanceof ConfigurableListableBeanFactory) { + beanFactory = (ConfigurableListableBeanFactory) parentBeanFactory; + } else { + beanFactory = null; + } + } + } + return original; + } + +} 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 1d1e84197b..5309467e08 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 @@ -26,6 +26,7 @@ import java.util.Set; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.Ordered; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.cors.CorsConfiguration; diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java index abbd74df11..2e801f273c 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java @@ -52,8 +52,8 @@ public class MvcEndpoints implements ApplicationContextAware, InitializingBean { @Override public void afterPropertiesSet() throws Exception { - Collection existing = this.applicationContext.getBeansOfType( - MvcEndpoint.class).values(); + Collection existing = BeanFactoryUtils.beansOfTypeIncludingAncestors( + this.applicationContext, MvcEndpoint.class).values(); this.endpoints.addAll(existing); this.customTypes = findEndpointClasses(existing); @SuppressWarnings("rawtypes") diff --git a/spring-boot-actuator/src/main/resources/META-INF/spring.factories b/spring-boot-actuator/src/main/resources/META-INF/spring.factories index fea96720c6..70773f6d40 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/spring.factories +++ b/spring-boot-actuator/src/main/resources/META-INF/spring.factories @@ -17,3 +17,6 @@ org.springframework.boot.actuate.autoconfigure.MetricExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.PublicMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.TraceRepositoryAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.TraceWebFilterAutoConfiguration + +org.springframework.boot.actuate.autoconfigure.EndpointWebMvcConfiguration=\ +org.springframework.boot.actuate.autoconfigure.EndpointWebMvcConfiguration \ No newline at end of file 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 7d32ffac58..00b3becc7d 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 @@ -16,6 +16,18 @@ package org.springframework.boot.actuate.autoconfigure; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + import java.io.FileNotFoundException; import java.net.SocketException; import java.net.URI; @@ -30,6 +42,7 @@ import javax.servlet.http.HttpServletResponse; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration.ManagementContextResolver; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer; @@ -71,18 +84,6 @@ import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - /** * Tests for {@link EndpointWebMvcAutoConfiguration}. * @@ -146,9 +147,10 @@ public class EndpointWebMvcAutoConfigurationTests { assertContent("/endpoint", ports.get().server, null); assertContent("/controller", ports.get().management, null); assertContent("/endpoint", ports.get().management, "endpointoutput"); + ApplicationContext managementContext = this.applicationContext.getBean( + ManagementContextResolver.class).getApplicationContext(); List interceptors = (List) ReflectionTestUtils.getField( - this.applicationContext.getBean(EndpointHandlerMapping.class), - "interceptors"); + managementContext.getBean(EndpointHandlerMapping.class), "interceptors"); assertEquals(1, interceptors.size()); this.applicationContext.close(); assertAllClosed(); @@ -243,7 +245,7 @@ public class EndpointWebMvcAutoConfigurationTests { this.applicationContext.register(RootConfig.class, BaseConfiguration.class, ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); new ServerPortInfoApplicationContextInitializer() - .initialize(this.applicationContext); + .initialize(this.applicationContext); this.applicationContext.refresh(); Integer localServerPort = this.applicationContext.getEnvironment().getProperty( "local.server.port", Integer.class); @@ -259,7 +261,7 @@ public class EndpointWebMvcAutoConfigurationTests { @Test public void portPropertiesOnDifferentPort() throws Exception { new ServerPortInfoApplicationContextInitializer() - .initialize(this.applicationContext); + .initialize(this.applicationContext); this.applicationContext.register(RootConfig.class, DifferentPortConfig.class, BaseConfiguration.class, EndpointWebMvcAutoConfiguration.class, ErrorMvcAutoConfiguration.class); @@ -420,12 +422,12 @@ public class EndpointWebMvcAutoConfigurationTests { @Configuration @Import({ PropertyPlaceholderAutoConfiguration.class, - EmbeddedServletContainerAutoConfiguration.class, - EndpointAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class, - ServerPropertiesAutoConfiguration.class, WebMvcAutoConfiguration.class }) + EmbeddedServletContainerAutoConfiguration.class, + EndpointAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, WebMvcAutoConfiguration.class }) protected static class BaseConfiguration { } @@ -583,7 +585,7 @@ public class EndpointWebMvcAutoConfigurationTests { } private static class GrabManagementPort implements - ApplicationListener { + ApplicationListener { private ApplicationContext rootContext; diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java index 5c778e3505..e72825b029 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java @@ -16,6 +16,12 @@ package org.springframework.boot.actuate.endpoint.mvc; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,12 +43,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalToIgnoringCase; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - /** * Tests for {@link EnvironmentMvcEndpoint} * @@ -103,11 +103,6 @@ public class EnvironmentMvcEndpointTests { return new EnvironmentEndpoint(); } - @Bean - public EnvironmentMvcEndpoint mvcEndpoint() { - return new EnvironmentMvcEndpoint(endpoint()); - } - } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java index 32fb1cacb4..1002cdc244 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java @@ -16,6 +16,12 @@ package org.springframework.boot.actuate.endpoint.mvc; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -41,12 +47,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - /** * Tests for {@link MetricsMvcEndpoint} * @@ -150,11 +150,6 @@ public class MetricsMvcEndpointTests { }); } - @Bean - public MetricsMvcEndpoint mvcEndpoint() { - return new MetricsMvcEndpoint(endpoint()); - } - } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java index 3c67da9f50..f5abaa5ed1 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java @@ -16,17 +16,51 @@ package org.springframework.boot.autoconfigure.web; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; /** * Properties used to configure resource handling. * * @author Phillip Webb * @author Brian Clozel + * @author Dave Syer * @since 1.1.0 */ @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) -public class ResourceProperties { +public class ResourceProperties implements ResourceLoaderAware { + + private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" }; + + private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { + "classpath:/META-INF/resources/", "classpath:/resources/", + "classpath:/static/", "classpath:/public/" }; + + private static final String[] RESOURCE_LOCATIONS; + static { + RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length + + SERVLET_RESOURCE_LOCATIONS.length]; + System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0, + SERVLET_RESOURCE_LOCATIONS.length); + System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, + SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length); + } + + private static final String[] STATIC_INDEX_HTML_RESOURCES; + static { + STATIC_INDEX_HTML_RESOURCES = new String[RESOURCE_LOCATIONS.length]; + for (int i = 0; i < STATIC_INDEX_HTML_RESOURCES.length; i++) { + STATIC_INDEX_HTML_RESOURCES[i] = RESOURCE_LOCATIONS[i] + "index.html"; + } + } /** * Cache period for the resources served by the resource handler, in seconds. @@ -40,6 +74,19 @@ public class ResourceProperties { private final Chain chain = new Chain(); + /** + * Locations of static resources. Defaults to classpath:[/META-INF/resources/, /resources/, /static/, /public/] + * plus context:/ (the root of the servlet context). + */ + private String[] staticLocations = RESOURCE_LOCATIONS; + + private ResourceLoader resourceLoader; + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + public Integer getCachePeriod() { return this.cachePeriod; } @@ -211,4 +258,52 @@ public class ResourceProperties { } + public String[] getStaticLocations() { + return this.staticLocations; + } + + public void setStaticLocations(String[] staticLocations) { + this.staticLocations = staticLocations; + } + + private String[] getStaticWelcomePageLocations() { + String[] result = new String[staticLocations.length]; + for (int i = 0; i < result.length; i++) { + String location = staticLocations[i]; + if (!location.endsWith("/")) { + location = location + "/"; + } + result[i] = location + "index.html"; + } + return result; + } + + public List getFaviconLocations() { + List locations = new ArrayList( + CLASSPATH_RESOURCE_LOCATIONS.length + 1); + if (resourceLoader != null) { + for (String location : CLASSPATH_RESOURCE_LOCATIONS) { + locations.add(this.resourceLoader.getResource(location)); + } + } + locations.add(new ClassPathResource("/")); + return Collections.unmodifiableList(locations); + } + + public Resource getWelcomePage() { + for (String location : getStaticWelcomePageLocations()) { + Resource resource = this.resourceLoader.getResource(location); + if (resource.exists()) { + try { + resource.getURL(); + return resource; + } + catch (IOException ex) { + // Ignore + } + } + } + return null; + } + } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java index 8cbae048ec..b6b7284bee 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.boot.autoconfigure.web; -import java.io.IOException; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -41,7 +39,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.web.OrderedHiddenHttpMethodFilter; -import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -49,7 +46,6 @@ import org.springframework.context.annotation.Primary; import org.springframework.core.Ordered; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.format.Formatter; @@ -104,30 +100,6 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver; @AutoConfigureAfter(DispatcherServletAutoConfiguration.class) public class WebMvcAutoConfiguration { - private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" }; - - private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { - "classpath:/META-INF/resources/", "classpath:/resources/", - "classpath:/static/", "classpath:/public/" }; - - private static final String[] RESOURCE_LOCATIONS; - static { - RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length - + SERVLET_RESOURCE_LOCATIONS.length]; - System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0, - SERVLET_RESOURCE_LOCATIONS.length); - System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, - SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length); - } - - private static final String[] STATIC_INDEX_HTML_RESOURCES; - static { - STATIC_INDEX_HTML_RESOURCES = new String[RESOURCE_LOCATIONS.length]; - for (int i = 0; i < STATIC_INDEX_HTML_RESOURCES.length; i++) { - STATIC_INDEX_HTML_RESOURCES[i] = RESOURCE_LOCATIONS[i] + "index.html"; - } - } - public static String DEFAULT_PREFIX = ""; public static String DEFAULT_SUFFIX = ""; @@ -267,7 +239,7 @@ public class WebMvcAutoConfiguration { } if (!registry.hasMappingForPattern("/**")) { registerResourceChain(registry.addResourceHandler("/**") - .addResourceLocations(RESOURCE_LOCATIONS) + .addResourceLocations(resourceProperties.getStaticLocations()) .setCachePeriod(cachePeriod)); } } @@ -310,31 +282,19 @@ public class WebMvcAutoConfiguration { @Override public void addViewControllers(ViewControllerRegistry registry) { - addStaticIndexHtmlViewControllers(registry); - } - - private void addStaticIndexHtmlViewControllers(ViewControllerRegistry registry) { - for (String resource : STATIC_INDEX_HTML_RESOURCES) { - if (this.resourceLoader.getResource(resource).exists()) { - try { - logger.info("Adding welcome page: " - + this.resourceLoader.getResource(resource).getURL()); - } - catch (IOException ex) { - // Ignore - } - // Use forward: prefix so that no view resolution is done - registry.addViewController("/").setViewName("forward:index.html"); - return; - } + Resource page = resourceProperties.getWelcomePage(); + if (page != null) { + logger.info("Adding welcome page: " + page); + registry.addViewController("/").setViewName("forward:index.html"); } } @Configuration @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true) - public static class FaviconConfiguration implements ResourceLoaderAware { + public static class FaviconConfiguration { - private ResourceLoader resourceLoader; + @Autowired + private ResourceProperties resourceProperties = new ResourceProperties(); @Bean public SimpleUrlHandlerMapping faviconHandlerMapping() { @@ -345,28 +305,13 @@ public class WebMvcAutoConfiguration { return mapping; } - @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - @Bean public ResourceHttpRequestHandler faviconRequestHandler() { ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); - requestHandler.setLocations(getLocations()); + requestHandler.setLocations(resourceProperties.getFaviconLocations()); return requestHandler; } - private List getLocations() { - List locations = new ArrayList( - CLASSPATH_RESOURCE_LOCATIONS.length + 1); - for (String location : CLASSPATH_RESOURCE_LOCATIONS) { - locations.add(this.resourceLoader.getResource(location)); - } - locations.add(new ClassPathResource("/")); - return Collections.unmodifiableList(locations); - } - } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WelcomePageMockMvcTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WelcomePageMockMvcTests.java new file mode 100644 index 0000000000..e38f0bb1f6 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WelcomePageMockMvcTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2014 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.autoconfigure.web; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.ConfigurableWebApplicationContext; + +/** + * Tests for welcome page using {@link MockMvc} and {@link SpringJUnit4ClassRunner}. + * + * @author Dave Syer + */ +public class WelcomePageMockMvcTests { + + private ConfigurableWebApplicationContext wac; + + private MockMvc mockMvc; + + @After + public void close() { + if (wac != null) { + wac.close(); + } + } + + @Test + public void homePageNotFound() throws Exception { + wac = (ConfigurableWebApplicationContext) new SpringApplicationBuilder( + TestConfiguration.class).run(); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockMvc.perform(get("/")).andExpect(status().isNotFound()).andReturn(); + } + + @Test + public void homePageCustomLocation() throws Exception { + wac = (ConfigurableWebApplicationContext) new SpringApplicationBuilder( + TestConfiguration.class).properties( + "spring.resources.staticLocations:classpath:/custom/").run(); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockMvc.perform(get("/")).andExpect(status().isOk()).andReturn(); + } + + @Test + public void homePageCustomLocationNoTrailingSlash() throws Exception { + wac = (ConfigurableWebApplicationContext) new SpringApplicationBuilder( + TestConfiguration.class).properties("spring.resources.staticLocations:classpath:/custom").run(); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockMvc.perform(get("/")).andExpect(status().isOk()).andReturn(); + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Import({ EmbeddedServletContainerAutoConfiguration.EmbeddedTomcat.class, + EmbeddedServletContainerAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + protected static @interface MinimalWebConfiguration { + } + + @Configuration + @MinimalWebConfiguration + public static class TestConfiguration { + + // For manual testing + public static void main(String[] args) { + SpringApplication.run(TestConfiguration.class, args); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/test/resources/custom/index.html b/spring-boot-autoconfigure/src/test/resources/custom/index.html new file mode 100644 index 0000000000..5124d86069 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/resources/custom/index.html @@ -0,0 +1 @@ +Hello! \ No newline at end of file diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index fdd35211fe..98a88f6904 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -150,6 +150,23 @@ For example, the following will disable _all_ endpoints except for `info`: endpoints.info.enabled=true ---- +[[production-ready-customizing-endpoints-programmatically]] +=== Adding Custom Endpoints + +If you add a `@Bean` of type `Endpoint` then it will automatically be +exposed over JMX and HTTP (if there is an server available). An HTTP +endpoints can be customized further by creating a bean of type +`MvcEndpoint`. Your `MvcEndpoint` is not a `@Controller` but it can use +`@RequestMapping` (and `@Managed*`) to expose resources. + +TIP: If you are doing this as a library feature consider adding a +configuration class to `/META-INF/spring.factories` under the key +`org.springframework.boot.actuate.autoconfigure.EndpointWebMvcConfiguration`. +If you do that then the endpoint will move to a child context with all +the other MVC endpoints if your users ask for a separate management port +or address. A configuration declared this way can be a `WebConfigurerAdapter` +if it wants to add static resources (for instance) to the management +endpoints. [[production-ready-health]] @@ -1284,7 +1301,8 @@ described below. [[production-ready-process-monitoring-configuration]] === Extend configuration -In `META-INF/spring.factories` file you have to activate the listener(s): +In `META-INF/spring.factories` file you can activate the listener(s) that +writes a PID file. Example: [indent=0] ---- diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 9e3dc266b6..05cbd99fdf 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1175,6 +1175,13 @@ Spring decides not to handle it. Most of the time this will not happen (unless y the default MVC configuration) because Spring will always be able to handle requests through the `DispatcherServlet`. +You can customize the static resource locations using +`spring.resources.staticLocations` (replacing the default values with +a list of directory locations). If you do this the default welcome +page detection will switch to your custom locations, so if there is an +`index.html` in any of your locations on startup, it will be the home +page of the application. + In addition to the '`standard`' static resource locations above, a special case is made for http://www.webjars.org/[Webjars content]. Any resources with a path in `+/webjars/**+` will be served from jar files if they are packaged in the Webjars format. diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties index e155eac6e5..ccadbece71 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties @@ -1,5 +1,5 @@ # logging.file: /tmp/logs/app.log -# logging.level.org.springframework.security: INFO +# logging.level.org.springframework.security: DEBUG management.address: 127.0.0.1 #management.port: 8181 endpoints.shutdown.enabled: true