/*
* Copyright 2012-2017 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.lang.reflect.Modifier;
import javax.servlet.Servlet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.mvc.ManagementServletContext;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.filter.ApplicationContextHeaderFilter;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.context.annotation.Import;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
/**
* {@link EnableAutoConfiguration Auto-configuration} to enable Spring MVC to handle
* {@link Endpoint} requests. If the {@link ManagementServerProperties} specifies a
* different port to {@link ServerProperties} a new child context is created, otherwise it
* is assumed that endpoint requests will be mapped and handled via an already registered
* {@link DispatcherServlet}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Christian Dupuis
* @author Andy Wilkinson
* @author Johannes Edmeier
* @author EddĂș MelĂ©ndez
* @author Venil Noronha
* @author Madhura Bhave
*/
@Configuration
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ManagementServerProperties.class)
@AutoConfigureAfter({ PropertyPlaceholderAutoConfiguration.class,
ServletWebServerFactoryAutoConfiguration.class, WebMvcAutoConfiguration.class,
RepositoryRestMvcAutoConfiguration.class, HypermediaAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class })
public class EndpointWebMvcAutoConfiguration
implements ApplicationContextAware, SmartInitializingSingleton {
private static final Log logger = LogFactory
.getLog(EndpointWebMvcAutoConfiguration.class);
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Bean
public ManagementContextResolver managementContextResolver() {
return new ManagementContextResolver(this.applicationContext);
}
@Bean
public ManagementServletContext managementServletContext(
final ManagementServerProperties properties) {
return new ManagementServletContext() {
@Override
public String getContextPath() {
return properties.getContextPath();
}
};
}
@Override
public void afterSingletonsInstantiated() {
ManagementServerPort managementPort = ManagementServerPort.DIFFERENT;
Environment environment = this.applicationContext.getEnvironment();
if (this.applicationContext instanceof WebApplicationContext) {
managementPort = ManagementServerPort.get(environment);
}
if (managementPort == ManagementServerPort.DIFFERENT) {
if (this.applicationContext instanceof ServletWebServerApplicationContext
&& ((ServletWebServerApplicationContext) this.applicationContext)
.getWebServer() != null) {
createChildManagementContext();
}
else {
logger.warn("Could not start management web server on "
+ "different port (management endpoints are still available "
+ "through JMX)");
}
}
if (managementPort == ManagementServerPort.SAME) {
if (environment.getProperty("management.ssl.enabled") != null) {
throw new IllegalStateException(
"Management-specific SSL cannot be configured as the management "
+ "server is not listening on a separate port");
}
if (environment instanceof ConfigurableEnvironment) {
addLocalManagementPortPropertyAlias(
(ConfigurableEnvironment) environment);
}
}
}
private void createChildManagementContext() {
AnnotationConfigServletWebServerApplicationContext childContext = new AnnotationConfigServletWebServerApplicationContext();
childContext.setParent(this.applicationContext);
childContext.setNamespace("management");
childContext.setId(this.applicationContext.getId() + ":management");
childContext.setClassLoader(this.applicationContext.getClassLoader());
childContext.register(EndpointWebMvcChildContextConfiguration.class,
PropertyPlaceholderAutoConfiguration.class,
ServletWebServerFactoryAutoConfiguration.class,
DispatcherServletAutoConfiguration.class);
registerServletWebServerFactory(childContext);
CloseManagementContextListener.addIfPossible(this.applicationContext,
childContext);
childContext.refresh();
managementContextResolver().setApplicationContext(childContext);
}
private void registerServletWebServerFactory(
AnnotationConfigServletWebServerApplicationContext childContext) {
try {
ConfigurableListableBeanFactory beanFactory = childContext.getBeanFactory();
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
registry.registerBeanDefinition("ServletWebServerFactory",
new RootBeanDefinition(determineServletWebServerFactoryClass()));
}
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore and assume auto-configuration
}
}
private Class<?> determineServletWebServerFactoryClass()
throws NoSuchBeanDefinitionException {
Class<?> factoryClass = this.applicationContext
.getBean(ServletWebServerFactory.class).getClass();
if (cannotBeInstantiated(factoryClass)) {
throw new FatalBeanException("ServletWebServerFactory implementation "
+ factoryClass.getName() + " cannot be instantiated. "
+ "To allow a separate management port to be used, a top-level class "
+ "or static inner class should be used instead");
}
return factoryClass;
}
private boolean cannotBeInstantiated(Class<?> clazz) {
return clazz.isLocalClass()
|| (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers()))
|| clazz.isAnonymousClass();
}
/**
* Add an alias for 'local.management.port' that actually resolves using
* 'local.server.port'.
* @param environment the environment
*/
private void addLocalManagementPortPropertyAlias(
final ConfigurableEnvironment environment) {
environment.getPropertySources()
.addLast(new PropertySource<Object>("Management Server") {
@Override
public Object getProperty(String name) {
if ("local.management.port".equals(name)) {
return environment.getProperty("local.server.port");
}
return null;
}
});
}
// Put Servlets and Filters in their own nested class so they don't force early
// instantiation of ManagementServerProperties.
@Configuration
@ConditionalOnProperty(prefix = "management", name = "add-application-context-header", havingValue = "true")
protected static class ApplicationContextFilterConfiguration {
@Bean
public ApplicationContextHeaderFilter applicationContextIdFilter(
ApplicationContext context) {
return new ApplicationContextHeaderFilter(context);
}
}
@Configuration
@Conditional(OnManagementMvcCondition.class)
@Import(ManagementContextConfigurationsImportSelector.class)
protected static class EndpointWebMvcConfiguration {
}
/**
* {@link ApplicationListener} to propagate the {@link ContextClosedEvent} and
* {@link ApplicationFailedEvent} from a parent to a child.
*/
private static class CloseManagementContextListener
implements ApplicationListener<ApplicationEvent> {
private final ApplicationContext parentContext;
private final ConfigurableApplicationContext childContext;
CloseManagementContextListener(ApplicationContext parentContext,
ConfigurableApplicationContext childContext) {
this.parentContext = parentContext;
this.childContext = childContext;
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextClosedEvent) {
onContextClosedEvent((ContextClosedEvent) event);
}
if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent((ApplicationFailedEvent) event);
}
};
private void onContextClosedEvent(ContextClosedEvent event) {
propagateCloseIfNecessary(event.getApplicationContext());
}
private void onApplicationFailedEvent(ApplicationFailedEvent event) {
propagateCloseIfNecessary(event.getApplicationContext());
}
private void propagateCloseIfNecessary(ApplicationContext applicationContext) {
if (applicationContext == this.parentContext) {
this.childContext.close();
}
}
public static void addIfPossible(ApplicationContext parentContext,
ConfigurableApplicationContext childContext) {
if (parentContext instanceof ConfigurableApplicationContext) {
add((ConfigurableApplicationContext) parentContext, childContext);
}
}
private static void add(ConfigurableApplicationContext parentContext,
ConfigurableApplicationContext childContext) {
parentContext.addApplicationListener(
new CloseManagementContextListener(parentContext, childContext));
}
}
private static class OnManagementMvcCondition extends SpringBootCondition
implements ConfigurationCondition {
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage
.forCondition("Management Server MVC");
if (!(context.getResourceLoader() instanceof WebApplicationContext)) {
return ConditionOutcome
.noMatch(message.because("non WebApplicationContext"));
}
ManagementServerPort port = ManagementServerPort
.get(context.getEnvironment());
if (port == ManagementServerPort.SAME) {
return ConditionOutcome.match(message.because("port is same"));
}
return ConditionOutcome.noMatch(message.because("port is not same"));
}
}
protected enum ManagementServerPort {
DISABLE, SAME, DIFFERENT;
public static ManagementServerPort get(Environment environment) {
Integer serverPort = getPortProperty(environment, "server.");
Integer managementPort = getPortProperty(environment, "management.");
if (managementPort != null && managementPort < 0) {
return DISABLE;
}
return ((managementPort == null)
|| (serverPort == null && managementPort.equals(8080))
|| (managementPort != 0 && managementPort.equals(serverPort)) ? SAME
: DIFFERENT);
}
private static Integer getPortProperty(Environment environment, String prefix) {
return environment.getProperty(prefix + "port", Integer.class);
}
}
}