Refactor the Actuator MVC configuration to allow more customization

There is a new spring.factories entry for
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcConfiguration
which loads extra beans into the MVC config for the Actuator.
If the management context is a child context all the beans go in the
child (except the Spring Security filter still). A big bonus is that
you can add WebConfigurerAdapters to configure static resources etc.
A new component called ManagementContextResolver can be used to
locate the ApplicationContext for the MVC endpoints.

Fixes gh-3345
This commit is contained in:
Dave Syer 2015-06-30 09:01:35 +01:00
parent 4187b5d8fb
commit 1e464da248
20 changed files with 929 additions and 281 deletions

View File

@ -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<EndpointHandlerMappingCustomizer> mappingCustomizers;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Bean
@ConditionalOnMissingBean
public EndpointHandlerMapping endpointHandlerMapping() {
Set<? extends MvcEndpoint> 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<Class<?>> configurations = new ArrayList<Class<?>>();
configurations.addAll(Arrays.<Class<?>> 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;
}
}
}
}

View File

@ -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<HandlerMapping> 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<HandlerMapping> extractMappings() {
List<HandlerMapping> list = new ArrayList<HandlerMapping>();
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<HandlerAdapter> adapters;
private List<HandlerAdapter> extractAdapters() {
List<HandlerAdapter> list = new ArrayList<HandlerAdapter>();
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<HandlerExceptionResolver> resolvers;
private List<HandlerExceptionResolver> extractResolvers() {
List<HandlerExceptionResolver> list = new ArrayList<HandlerExceptionResolver>();
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<EndpointHandlerMappingCustomizer> mappingCustomizers;
@Bean
public HandlerMapping handlerMapping(MvcEndpoints endpoints,
ListableBeanFactory beanFactory) {
Set<MvcEndpoint> set = new HashSet<MvcEndpoint>(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,

View File

@ -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<EndpointHandlerMappingCustomizer> mappingCustomizers;
@Bean
@ConditionalOnMissingBean
public EndpointHandlerMapping endpointHandlerMapping() {
Set<? extends MvcEndpoint> 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);
}
}

View File

@ -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 <code>/META-INF/spring.factories</code> 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<String> factories = new ArrayList<String>(new LinkedHashSet<String>(
SpringFactoriesLoader.loadFactoryNames(EndpointWebMvcConfiguration.class,
this.beanClassLoader)));
return factories.toArray(new String[0]);
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
beanClassLoader = classLoader;
}
}

View File

@ -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<HttpSecurity>.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.<MvcEndpoint> emptySet());
}
if (delegate == null) {
List<RequestMatcher> pathMatchers = new ArrayList<RequestMatcher>();
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) {

View File

@ -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 {
}

View File

@ -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> T getBeanCarefully(ConditionContext context, Class<T> 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;
}
}

View File

@ -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;

View File

@ -52,8 +52,8 @@ public class MvcEndpoints implements ApplicationContextAware, InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
Collection<MvcEndpoint> existing = this.applicationContext.getBeansOfType(
MvcEndpoint.class).values();
Collection<MvcEndpoint> existing = BeanFactoryUtils.beansOfTypeIncludingAncestors(
this.applicationContext, MvcEndpoint.class).values();
this.endpoints.addAll(existing);
this.customTypes = findEndpointClasses(existing);
@SuppressWarnings("rawtypes")

View File

@ -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

View File

@ -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<EmbeddedServletContainerInitializedEvent> {
ApplicationListener<EmbeddedServletContainerInitializedEvent> {
private ApplicationContext rootContext;

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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<Resource> getFaviconLocations() {
List<Resource> locations = new ArrayList<Resource>(
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;
}
}

View File

@ -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<Resource> getLocations() {
List<Resource> locations = new ArrayList<Resource>(
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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1 @@
<html><body>Hello!</body></html>

View File

@ -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]
----

View File

@ -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.

View File

@ -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