diff --git a/pom.xml b/pom.xml index 54949350..93e15e7c 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,25 @@ spring-boot-starter-web true + + org.springframework.boot + spring-boot-starter-aop + true + + + org.springframework.security + spring-security-crypto + + + org.springframework.security + spring-security-rsa + true + + + org.springframework.integration + spring-integration-jmx + true + org.projectlombok lombok diff --git a/src/main/java/org/springframework/cloud/autoconfigure/LifecycleMvcEndpointAutoConfiguration.java b/src/main/java/org/springframework/cloud/autoconfigure/LifecycleMvcEndpointAutoConfiguration.java new file mode 100644 index 00000000..8032236f --- /dev/null +++ b/src/main/java/org/springframework/cloud/autoconfigure/LifecycleMvcEndpointAutoConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-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.cloud.autoconfigure; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +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.web.WebMvcAutoConfiguration; +import org.springframework.cloud.bootstrap.config.RefreshEndpoint; +import org.springframework.cloud.context.environment.EnvironmentManager; +import org.springframework.cloud.context.environment.EnvironmentManagerMvcEndpoint; +import org.springframework.cloud.context.restart.RestartEndpoint; +import org.springframework.cloud.context.restart.RestartMvcEndpoint; +import org.springframework.cloud.endpoint.GenericPostableMvcEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Autoconfiguration for some MVC endpoints governing the application context lifecycle. + * Provides restart, pause, resume, refresh (environment) and environment update + * endpoints. + * + * @author Dave Syer + * + */ +@Configuration +@ConditionalOnClass(EnvironmentEndpoint.class) +@ConditionalOnProperty(value = "endpoints.env.enabled", matchIfMissing = true) +@ConditionalOnWebApplication +@ConditionalOnBean(RestartEndpoint.class) +@AutoConfigureAfter({ WebMvcAutoConfiguration.class, EndpointAutoConfiguration.class, + RefreshAutoConfiguration.class }) +public class LifecycleMvcEndpointAutoConfiguration { + + @Autowired + private RestartEndpoint restartEndpoint; + + @Bean + @ConditionalOnBean(EnvironmentEndpoint.class) + public EnvironmentManagerMvcEndpoint environmentManagerEndpoint( + EnvironmentEndpoint delegate, EnvironmentManager environment) { + return new EnvironmentManagerMvcEndpoint(delegate, environment); + } + + @Bean + @ConditionalOnBean(RefreshEndpoint.class) + public MvcEndpoint refreshMvcEndpoint(RefreshEndpoint endpoint) { + return new GenericPostableMvcEndpoint(endpoint); + } + + @Bean + public RestartMvcEndpoint restartMvcEndpoint() { + return new RestartMvcEndpoint(restartEndpoint); + } + + @Bean + public MvcEndpoint pauseMvcEndpoint(RestartMvcEndpoint restartEndpoint) { + return restartEndpoint.getPauseEndpoint(); + } + + @Bean + public MvcEndpoint resumeMvcEndpoint(RestartMvcEndpoint restartEndpoint) { + return restartEndpoint.getResumeEndpoint(); + } + +} diff --git a/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java b/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java new file mode 100644 index 00000000..c0222be5 --- /dev/null +++ b/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java @@ -0,0 +1,216 @@ +/* + * Copyright 2013-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.cloud.autoconfigure; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.actuate.endpoint.InfoEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +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.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar; +import org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration; +import org.springframework.cloud.bootstrap.config.RefreshEndpoint; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.context.environment.EnvironmentManager; +import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; +import org.springframework.cloud.context.restart.RestartEndpoint; +import org.springframework.cloud.context.scope.refresh.RefreshScope; +import org.springframework.cloud.logging.LoggingRebinder; +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.core.env.ConfigurableEnvironment; +import org.springframework.integration.monitor.IntegrationMBeanExporter; + +/** + * Autoconfiguration for the refresh scope and associated features to do with changes in + * the Environment (e.g. rebinding logger levels). + * + * @author Dave Syer + * + */ +@Configuration +@ConditionalOnClass(RefreshScope.class) +@AutoConfigureAfter(WebMvcAutoConfiguration.class) +public class RefreshAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public static RefreshScope refreshScope() { + return new RefreshScope(); + } + + @Bean + @ConditionalOnMissingBean + public static LoggingRebinder loggingRebinder() { + return new LoggingRebinder(); + } + + @Bean + @ConditionalOnMissingBean + public EnvironmentManager environmentManager(ConfigurableEnvironment environment) { + return new EnvironmentManager(environment); + } + + @Configuration + @ConditionalOnClass(InfoEndpoint.class) + @ConditionalOnBean(EndpointAutoConfiguration.class) + protected static class InfoEndpointRebinderConfiguration implements + ApplicationListener { + + @Autowired + private EndpointAutoConfiguration endpoints; + + @Autowired + private ConfigurableEnvironment environment; + + private Map map = new LinkedHashMap(); + + @Override + public void onApplicationEvent(EnvironmentChangeEvent event) { + for (String key : event.getKeys()) { + if (key.startsWith("info.")) { + this.map.put(key.substring("info.".length()), + this.environment.getProperty(key)); + } + } + } + + @Bean + public InfoEndpoint infoEndpoint() throws Exception { + return new InfoEndpoint(this.endpoints.infoEndpoint().invoke()) { + @Override + public Map invoke() { + Map info = new LinkedHashMap( + super.invoke()); + info.putAll(InfoEndpointRebinderConfiguration.this.map); + return info; + } + }; + } + + } + + @Configuration + @ConditionalOnBean(ConfigurationPropertiesBindingPostProcessor.class) + protected static class ConfigurationPropertiesRebinderConfiguration implements + BeanFactoryAware { + + private BeanFactory context; + + @Override + public void setBeanFactory(BeanFactory applicationContext) throws BeansException { + this.context = applicationContext; + } + + @Bean + @ConditionalOnMissingBean + public ConfigurationPropertiesRebinder configurationPropertiesRebinder() { + // Since this is a BeanPostProcessor we have to be super careful not to cause + // a cascade of bean instantiation. Knowing the *name* of the beans we need is + // super optimal, but a little brittle (unfortunately we have no choice). + ConfigurationPropertiesBindingPostProcessor binder = this.context + .getBean( + ConfigurationPropertiesBindingPostProcessorRegistrar.BINDER_BEAN_NAME, + ConfigurationPropertiesBindingPostProcessor.class); + ConfigurationBeanFactoryMetaData metaData = this.context.getBean( + ConfigurationPropertiesBindingPostProcessorRegistrar.BINDER_BEAN_NAME + + ".store", ConfigurationBeanFactoryMetaData.class); + ConfigurationPropertiesRebinder rebinder = new ConfigurationPropertiesRebinder( + binder); + rebinder.setBeanMetaDataStore(metaData); + return rebinder; + } + } + + @ConditionalOnClass(Endpoint.class) + protected static class RefreshEndpointsConfiguration { + + @ConditionalOnClass(IntegrationMBeanExporter.class) + protected static class RestartEndpointWithIntegration { + + @Autowired(required = false) + private IntegrationMBeanExporter exporter; + + @Bean + @ConditionalOnMissingBean + public RestartEndpoint restartEndpoint() { + RestartEndpoint endpoint = new RestartEndpoint(); + if (this.exporter != null) { + endpoint.setIntegrationMBeanExporter(this.exporter); + } + return endpoint; + } + + } + + @ConditionalOnMissingClass(name = "org.springframework.integration.monitor.IntegrationMBeanExporter") + protected static class RestartEndpointWithoutIntegration { + + @Bean + @ConditionalOnMissingBean + public RestartEndpoint restartEndpoint() { + return new RestartEndpoint(); + } + } + + @Bean + @ConfigurationProperties("endpoints.pause") + public Endpoint pauseEndpoint(RestartEndpoint restartEndpoint) { + return restartEndpoint.getPauseEndpoint(); + } + + @Bean + @ConfigurationProperties("endpoints.resume") + public Endpoint resumeEndpoint(RestartEndpoint restartEndpoint) { + return restartEndpoint.getResumeEndpoint(); + } + + @Configuration + @ConditionalOnProperty(value = "endpoints.refresh.enabled", matchIfMissing = true) + @ConditionalOnBean(PropertySourceBootstrapConfiguration.class) + protected static class RefreshEndpointConfiguration { + + @Bean + @ConditionalOnMissingBean + public RefreshEndpoint refreshEndpoint( + ConfigurableApplicationContext context, RefreshScope scope) { + RefreshEndpoint endpoint = new RefreshEndpoint(context, scope); + return endpoint; + } + + } + + } +} diff --git a/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java b/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java new file mode 100644 index 00000000..39e0d553 --- /dev/null +++ b/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java @@ -0,0 +1,217 @@ +/* + * Copyright 2013-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.cloud.bootstrap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.builder.ParentContextApplicationContextInitializer; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * A listener that prepares a SpringApplication (e.g. populating its Environment) by + * delegating to {@link ApplicationContextInitializer} beans in a separate bootstrap + * context. The bootstrap context is a SpringApplication created from sources defined in + * spring.factories as {@link BootstrapConfiguration}, and initialized with external + * config taken from "bootstrap.properties" (or yml), instead of the normal + * "application.properties". + * + * @author Dave Syer + * + */ +public class BootstrapApplicationListener implements + ApplicationListener, Ordered { + + public static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = "bootstrap"; + + public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 5; + + private int order = DEFAULT_ORDER; + + @Override + public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { + ConfigurableEnvironment environment = event.getEnvironment(); + if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class, + true)) { + return; + } + // don't listen to events in a bootstrap context + if (environment.getPropertySources().contains("bootstrapInProgress")) { + return; + } + ConfigurableApplicationContext context = bootstrapServiceContext(environment, + event.getSpringApplication()); + apply(context, event.getSpringApplication(), environment); + } + + private ConfigurableApplicationContext bootstrapServiceContext( + ConfigurableEnvironment environment, final SpringApplication application) { + StandardEnvironment bootstrapEnvironment = new StandardEnvironment(); + MutablePropertySources bootstrapProperties = bootstrapEnvironment + .getPropertySources(); + for (PropertySource source : bootstrapProperties) { + bootstrapProperties.remove(source.getName()); + } + String configName = environment + .resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}"); + String configLocation = environment + .resolvePlaceholders("${spring.cloud.bootstrap.location:}"); + Map bootstrapMap = new HashMap<>(); + bootstrapMap.put("spring.config.name", configName); + if (StringUtils.hasText(configLocation)) { + bootstrapMap.put("spring.config.location", configLocation); + } + bootstrapProperties.addFirst(new MapPropertySource( + BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap)); + bootstrapProperties.addFirst(new MapPropertySource("bootstrapInProgress", + Collections. emptyMap())); + for (PropertySource source : environment.getPropertySources()) { + bootstrapProperties.addLast(source); + } + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + // Use names and ensure unique to protect against duplicates + List names = SpringFactoriesLoader.loadFactoryNames( + BootstrapConfiguration.class, classLoader); + // TODO: is it possible or sensible to share a ResourceLoader? + SpringApplicationBuilder builder = new SpringApplicationBuilder() + .profiles(environment.getActiveProfiles()).showBanner(false) + .environment(bootstrapEnvironment) + .properties("spring.application.name:" + configName).web(false); + List> sources = new ArrayList<>(); + for (String name : names) { + Class cls = ClassUtils.resolveClassName(name, null); + try { + cls.getDeclaredAnnotations(); + } + catch (Exception e) { + continue; + } + sources.add(cls); + } + builder.sources(sources.toArray(new Class[sources.size()])); + final ConfigurableApplicationContext context = builder.run(); + // Make the bootstrap context a parent of the app context + addAncestorInitializer(application, context); + bootstrapProperties.remove("bootstrapInProgress"); + return context; + } + + private void addAncestorInitializer(SpringApplication application, + ConfigurableApplicationContext context) { + boolean installed = false; + for (ApplicationContextInitializer initializer : application.getInitializers()) { + if (initializer instanceof AncestorInitializer) { + installed = true; + // New parent + ((AncestorInitializer) initializer).setParent(context); + } + } + if (!installed) { + application.addInitializers(new AncestorInitializer(context)); + } + + } + + private void apply(ConfigurableApplicationContext context, + SpringApplication application, ConfigurableEnvironment environment) { + @SuppressWarnings("rawtypes") + List initializers = getOrderedBeansOfType(context, + ApplicationContextInitializer.class); + application.addInitializers(initializers + .toArray(new ApplicationContextInitializer[initializers.size()])); + } + + private List getOrderedBeansOfType(ListableBeanFactory context, Class type) { + List result = new ArrayList(); + for (String name : context.getBeanNamesForType(type)) { + result.add(context.getBean(name, type)); + } + AnnotationAwareOrderComparator.sort(result); + return result; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + private static class AncestorInitializer implements + ApplicationContextInitializer, Ordered { + + private ConfigurableApplicationContext parent; + + public AncestorInitializer(ConfigurableApplicationContext parent) { + this.parent = parent; + } + + public void setParent(ConfigurableApplicationContext parent) { + this.parent = parent; + } + + @Override + public int getOrder() { + // Need to run not too late (so not unordered), so that, for instance, the + // ContextIdApplicationContextInitializer runs later and picks up the merged + // Environment. Also not too early so that other initializers can pick up the + // parent (especially the Environment). + return Ordered.HIGHEST_PRECEDENCE + 10; + } + + @Override + public void initialize(ConfigurableApplicationContext context) { + preemptMerge( + context.getEnvironment().getPropertySources(), + new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, Collections + . emptyMap())); + while (context.getParent() != null && context.getParent() != context) { + context = (ConfigurableApplicationContext) context.getParent(); + } + new ParentContextApplicationContextInitializer(parent).initialize(context); + } + + private void preemptMerge(MutablePropertySources propertySources, + PropertySource propertySource) { + if (propertySource != null + && !propertySources.contains(propertySource.getName())) { + propertySources.addFirst(propertySource); + } + } + } +} diff --git a/src/main/java/org/springframework/cloud/bootstrap/BootstrapConfiguration.java b/src/main/java/org/springframework/cloud/bootstrap/BootstrapConfiguration.java new file mode 100644 index 00000000..d8df65d1 --- /dev/null +++ b/src/main/java/org/springframework/cloud/bootstrap/BootstrapConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-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.cloud.bootstrap; + +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; + +/** + * A marker interface used as a key in META-INF/spring.factories. Entries in + * the factories file are used to create the bootstrap application context. + * + * @author Dave Syer + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface BootstrapConfiguration { + + /** + * Exclude specific auto-configuration classes such that they will never be applied. + */ + Class[] exclude() default {}; + +} diff --git a/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java b/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java new file mode 100644 index 00000000..938b6b06 --- /dev/null +++ b/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013-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.cloud.bootstrap.config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.bind.PropertySourcesPropertyValues; +import org.springframework.boot.bind.RelaxedDataBinder; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.bootstrap.BootstrapApplicationListener; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.logging.LoggingRebinder; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; + +/** + * @author Dave Syer + * + */ +@Configuration +@EnableConfigurationProperties(PropertySourceBootstrapProperties.class) +public class PropertySourceBootstrapConfiguration implements + ApplicationContextInitializer { + + private static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME; + + private static Log logger = LogFactory + .getLog(PropertySourceBootstrapConfiguration.class); + + @Autowired(required = false) + private List propertySourceLocators = new ArrayList<>(); + + @Autowired + private PropertySourceBootstrapProperties properties; + + public void setPropertySourceLocators( + Collection propertySourceLocators) { + this.propertySourceLocators = new ArrayList<>(propertySourceLocators); + } + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + CompositePropertySource composite = new CompositePropertySource( + BOOTSTRAP_PROPERTY_SOURCE_NAME); + AnnotationAwareOrderComparator.sort(this.propertySourceLocators); + boolean empty = true; + for (PropertySourceLocator locator : this.propertySourceLocators) { + PropertySource source = null; + source = locator.locate(applicationContext.getEnvironment()); + if (source == null) { + continue; + } + logger.info("Located property source: " + source); + composite.addPropertySource(source); + empty = false; + } + if (!empty) { + MutablePropertySources propertySources = applicationContext.getEnvironment() + .getPropertySources(); + if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) { + propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME); + } + insertPropertySources(propertySources, composite); + setLogLevels(applicationContext.getEnvironment()); + } + } + + private void setLogLevels(ConfigurableEnvironment environment) { + LoggingRebinder rebinder = new LoggingRebinder(); + rebinder.setEnvironment(environment); + // We can't fire the event in the ApplicationContext here (too early), but we can + // create our own listener and poke it (it doesn't need the key changes) + rebinder.onApplicationEvent(new EnvironmentChangeEvent(Collections + . emptySet())); + } + + private void insertPropertySources(MutablePropertySources propertySources, + CompositePropertySource composite) { + MutablePropertySources incoming = new MutablePropertySources(); + incoming.addFirst(composite); + PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties(); + new RelaxedDataBinder(remoteProperties, "spring.cloud.config") + .bind(new PropertySourcesPropertyValues(incoming)); + if (!remoteProperties.isAllowOverride() + || remoteProperties.isOverrideSystemProperties()) { + propertySources.addFirst(composite); + return; + } + if (propertySources + .contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) { + propertySources.addAfter( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + composite); + } + else { + propertySources.addLast(composite); + } + } + +} diff --git a/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapProperties.java b/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapProperties.java new file mode 100644 index 00000000..cfe2692b --- /dev/null +++ b/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapProperties.java @@ -0,0 +1,37 @@ +package org.springframework.cloud.bootstrap.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("spring.cloud.config") +public class PropertySourceBootstrapProperties { + + /** + * Flag to indicate that the external properties should override system properties. + * Default true. + */ + private boolean overrideSystemProperties = true; + + /** + * Flag to indicate that {@link #isSystemPropertiesOverride() + * systemPropertiesOverride} can be used. Set to false to prevent users from changing + * the default accidentally. Default true. + */ + private boolean allowOverride = true; + + public boolean isOverrideSystemProperties() { + return overrideSystemProperties; + } + + public void setOverrideSystemProperties(boolean overrideSystemProperties) { + this.overrideSystemProperties = overrideSystemProperties; + } + + public boolean isAllowOverride() { + return allowOverride; + } + + public void setAllowOverride(boolean allowOverride) { + this.allowOverride = allowOverride; + } + +} diff --git a/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceLocator.java b/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceLocator.java new file mode 100644 index 00000000..96fce80d --- /dev/null +++ b/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceLocator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-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.cloud.bootstrap.config; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; + +/** + * Strategy for locating (possibly remote) property sources for the Environment. + * Implementations should not fail unless they intend to prevent the application from + * starting. + * + * @author Dave Syer + * + */ +public interface PropertySourceLocator { + + /** + * @param environment the current Environment + * @return a PropertySource or null if there is none + * + * @throws IllegalStateException if there is a fail fast condition + */ + PropertySource locate(Environment environment); + +} diff --git a/src/main/java/org/springframework/cloud/bootstrap/config/RefreshEndpoint.java b/src/main/java/org/springframework/cloud/bootstrap/config/RefreshEndpoint.java new file mode 100644 index 00000000..8a137b4f --- /dev/null +++ b/src/main/java/org/springframework/cloud/bootstrap/config/RefreshEndpoint.java @@ -0,0 +1,165 @@ +/* + * Copyright 2013-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.cloud.bootstrap.config; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.context.scope.refresh.RefreshScope; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.web.context.support.StandardServletEnvironment; + +/** + * @author Dave Syer + * + */ +@ConfigurationProperties(prefix = "endpoints.refresh", ignoreUnknownFields = false) +@ManagedResource +public class RefreshEndpoint extends AbstractEndpoint> { + + private Set standardSources = new HashSet(Arrays.asList( + StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME, + StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME, + StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME)); + + private ConfigurableApplicationContext context; + + private RefreshScope scope; + + public RefreshEndpoint(ConfigurableApplicationContext context, RefreshScope scope) { + super("refresh"); + this.context = context; + this.scope = scope; + } + + @ManagedOperation + public synchronized String[] refresh() { + Map before = extract(context.getEnvironment().getPropertySources()); + addConfigFilesToEnvironment(); + Set keys = changes(before, + extract(context.getEnvironment().getPropertySources())).keySet(); + scope.refreshAll(); + if (keys.isEmpty()) { + return new String[0]; + } + context.publishEvent(new EnvironmentChangeEvent(keys)); + return keys.toArray(new String[keys.size()]); + } + + private void addConfigFilesToEnvironment() { + ConfigurableApplicationContext capture = new SpringApplicationBuilder(Empty.class).showBanner( + false).web(false).environment(context.getEnvironment()).run(); + MutablePropertySources target = context.getEnvironment().getPropertySources(); + for (PropertySource source : capture.getEnvironment().getPropertySources()) { + String name = source.getName(); + if (!standardSources.contains(name)) { + if (target.contains(name)) { + target.replace(name, source); + } else { + if (target.contains("defaultProperties")) { + target.addBefore("defaultProperties", source); + } else { + target.addLast(source); + } + } + } + } + } + + @Override + public Collection invoke() { + return Arrays.asList(refresh()); + } + + private Map changes(Map before, + Map after) { + Map result = new HashMap(); + for (String key : before.keySet()) { + if (!after.containsKey(key)) { + result.put(key, null); + } else if (!equal(before.get(key), after.get(key))) { + result.put(key, after.get(key)); + } + } + for (String key : after.keySet()) { + if (!before.containsKey(key)) { + result.put(key, after.get(key)); + } + } + return result; + } + + private boolean equal(Object one, Object two) { + if (one == null && two == null) { + return true; + } + if (one == null || two == null) { + return false; + } + return one.equals(two); + } + + private Map extract(MutablePropertySources propertySources) { + Map result = new HashMap(); + for (PropertySource parent : propertySources) { + if (!standardSources.contains(parent.getName())) { + extract(parent, result); + } + } + return result; + } + + private void extract(PropertySource parent, Map result) { + if (parent instanceof CompositePropertySource) { + try { + for (PropertySource source : ((CompositePropertySource) parent).getPropertySources()) { + extract(source, result); + } + } catch (Exception e) { + return; + } + } else if (parent instanceof EnumerablePropertySource) { + for (String key : ((EnumerablePropertySource) parent).getPropertyNames()) { + result.put(key, parent.getProperty(key)); + } + } + } + + @Configuration + protected static class Empty { + + } + +} diff --git a/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java b/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java new file mode 100644 index 00000000..9bfa1feb --- /dev/null +++ b/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java @@ -0,0 +1,155 @@ +/* + * Copyright 2013-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.cloud.bootstrap.encrypt; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties.KeyStore; +import org.springframework.cloud.context.encrypt.EncryptorFactory; +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.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; +import org.springframework.security.rsa.crypto.RsaSecretEncryptor; +import org.springframework.util.StringUtils; + +/** + * @author Dave Syer + * + */ +@Configuration +@ConditionalOnClass({ TextEncryptor.class, RsaSecretEncryptor.class }) +@EnableConfigurationProperties(KeyProperties.class) +public class EncryptionBootstrapConfiguration { + + @Autowired(required = false) + private TextEncryptor encryptor; + + @Autowired + private KeyProperties key; + + @Configuration + @Conditional(KeyCondition.class) + @ConditionalOnClass(RsaSecretEncryptor.class) + protected static class RsaEncryptionConfiguration { + + @Autowired + private KeyProperties key; + + @Bean + @ConditionalOnMissingBean(TextEncryptor.class) + public TextEncryptor textEncryptor() { + KeyStore keyStore = key.getKeyStore(); + if (keyStore.getLocation() != null && keyStore.getLocation().exists()) { + return new RsaSecretEncryptor( + new KeyStoreKeyFactory(keyStore.getLocation(), keyStore + .getPassword().toCharArray()).getKeyPair( + keyStore.getAlias(), keyStore.getSecret().toCharArray())); + } + return new EncryptorFactory().create(key.getKey()); + } + + } + + @Configuration + @Conditional(KeyCondition.class) + @ConditionalOnMissingClass(name = "org.springframework.security.rsa.crypto.RsaSecretEncryptor") + protected static class VanillaEncryptionConfiguration { + + @Autowired + private KeyProperties key; + + @Bean + @ConditionalOnMissingBean(TextEncryptor.class) + public TextEncryptor textEncryptor() { + return new EncryptorFactory().create(key.getKey()); + } + + } + + @Bean + public EnvironmentDecryptApplicationInitializer environmentDecryptApplicationListener() { + if (encryptor == null) { + encryptor = new FailsafeTextEncryptor(); + } + EnvironmentDecryptApplicationInitializer listener = new EnvironmentDecryptApplicationInitializer( + encryptor); + listener.setFailOnError(key.isFailOnError()); + return listener; + } + + public static class KeyCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + Environment environment = context.getEnvironment(); + if (hasProperty(environment, "encrypt.keyStore.location")) { + if (hasProperty(environment, "encrypt.keyStore.password")) { + return ConditionOutcome.match("Keystore found in Environment"); + } + return ConditionOutcome + .noMatch("Keystore found but no password in Environment"); + } + else if (hasProperty(environment, "encrypt.key")) { + return ConditionOutcome.match("Key found in Environment"); + } + return ConditionOutcome.noMatch("Keystore nor key found in Environment"); + } + + private boolean hasProperty(Environment environment, String key) { + String value = environment.getProperty(key); + if (value == null) { + return false; + } + return StringUtils.hasText(environment.resolvePlaceholders(value)); + } + + } + + /** + * TextEncryptor that just fails, so that users don't get a false sense of security + * adding ciphers to config files and not getting them decrypted. + * + * @author Dave Syer + * + */ + protected static class FailsafeTextEncryptor implements TextEncryptor { + + @Override + public String encrypt(String text) { + throw new UnsupportedOperationException( + "No encryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); + } + + @Override + public String decrypt(String encryptedText) { + throw new UnsupportedOperationException( + "No decryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); + } + + } + +} diff --git a/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java b/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java new file mode 100644 index 00000000..163e71d8 --- /dev/null +++ b/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013-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.cloud.bootstrap.encrypt; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.security.crypto.encrypt.TextEncryptor; + +/** + * @author Dave Syer + * + */ +public class EnvironmentDecryptApplicationInitializer implements + ApplicationContextInitializer, Ordered { + + private static Log logger = LogFactory + .getLog(EnvironmentDecryptApplicationInitializer.class); + + private int order = Ordered.HIGHEST_PRECEDENCE + 15; + + private TextEncryptor encryptor; + + private boolean failOnError = true; + + public EnvironmentDecryptApplicationInitializer(TextEncryptor encryptor) { + this.encryptor = encryptor; + } + + /** + * Strategy to determine how to handle exceptions during decryption. + * + * @param failOnError the flag value (default true) + */ + public void setFailOnError(boolean failOnError) { + this.failOnError = failOnError; + } + + @Override + public int getOrder() { + return order; + } + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + Map overrides = new LinkedHashMap(); + for (PropertySource source : environment.getPropertySources()) { + decrypt(source, overrides); + } + if (!overrides.isEmpty()) { + environment.getPropertySources().addFirst( + new MapPropertySource("decrypted", overrides)); + } + } + + private void decrypt(PropertySource source, Map overrides) { + + if (source instanceof EnumerablePropertySource) { + + EnumerablePropertySource enumerable = (EnumerablePropertySource) source; + for (String key : enumerable.getPropertyNames()) { + String value = source.getProperty(key).toString(); + if (value.startsWith("{cipher}")) { + value = value.substring("{cipher}".length()); + try { + value = encryptor.decrypt(value); + if (logger.isDebugEnabled()) { + logger.debug("Decrypted: key=" + key); + } + } + catch (Exception e) { + String message = "Cannot decrypt: key=" + key; + if (failOnError) { + throw new IllegalStateException(message, e); + } + if (logger.isDebugEnabled()) { + logger.warn(message, e); + } + else { + logger.warn(message); + } + // Set value to empty to avoid making a password out of the + // cipher text + value = ""; + } + overrides.put(key, value); + } + } + + } + else if (source instanceof CompositePropertySource) { + + for (PropertySource nested : ((CompositePropertySource) source) + .getPropertySources()) { + decrypt(nested, overrides); + } + + } + + } + +} diff --git a/src/main/java/org/springframework/cloud/bootstrap/encrypt/KeyProperties.java b/src/main/java/org/springframework/cloud/bootstrap/encrypt/KeyProperties.java new file mode 100644 index 00000000..d3d3b11e --- /dev/null +++ b/src/main/java/org/springframework/cloud/bootstrap/encrypt/KeyProperties.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-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.cloud.bootstrap.encrypt; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; + +@ConfigurationProperties("encrypt") +public class KeyProperties { + + private String key; + + private boolean failOnError = true; + + private KeyProperties.KeyStore keyStore = new KeyStore(); + + public boolean isFailOnError() { + return failOnError; + } + + public void setFailOnError(boolean failOnError) { + this.failOnError = failOnError; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public KeyStore getKeyStore() { + return keyStore; + } + + public void setKeyStore(KeyProperties.KeyStore keyStore) { + this.keyStore = keyStore; + } + + public static class KeyStore { + + private Resource location; + private String password; + private String alias; + private String secret; + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public Resource getLocation() { + return location; + } + + public void setLocation(Resource location) { + this.location = location; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getSecret() { + return secret==null ? password : secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/cloud/context/config/BeanLifecycleDecorator.java b/src/main/java/org/springframework/cloud/context/config/BeanLifecycleDecorator.java new file mode 100644 index 00000000..1488d095 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/config/BeanLifecycleDecorator.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2011 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.cloud.context.config; + + +/** + * A helper interface providing optional decoration of bean instances and their + * destruction callbacks. Users can supply custom implementations of this + * strategy if they want tighter control over method invocation on the bean or + * its destruction callback. + * + * @param + * the type of auxiliary context object that can be passed between + * methods. Implementations can choose what type of data to supply as + * it is passed around unchanged by the caller. + * + * @author Dave Syer + * + */ +public interface BeanLifecycleDecorator { + + /** + * Optionally decorate and provide a new instance of a compatible bean for + * the caller to use instead of the input. + * + * @param bean + * the bean to optionally decorate + * @param context + * the context as created by + * {@link #decorateDestructionCallback(Runnable)} + * @return the replacement bean for the caller to use + */ + Object decorateBean(Object bean, Context context); + + /** + * Optionally decorate the destruction callback provided, and also return + * some context that can be used later by the + * {@link #decorateBean(Object, Context)} method. + * + * @param callback + * the destruction callback that will be used by the container + * @return a context wrapper + */ + Context decorateDestructionCallback(Runnable callback); + + static class Context { + + private final T auxiliary; + private final Runnable callback; + + public Context(Runnable callback, T auxiliary) { + this.callback = callback; + this.auxiliary = auxiliary; + } + + public Runnable getCallback() { + return callback; + } + + public T getAuxiliary() { + return auxiliary; + } + + } + +} diff --git a/src/main/java/org/springframework/cloud/context/config/StandardBeanLifecycleDecorator.java b/src/main/java/org/springframework/cloud/context/config/StandardBeanLifecycleDecorator.java new file mode 100644 index 00000000..32736f61 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/config/StandardBeanLifecycleDecorator.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2011 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.cloud.context.config; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; + +/** + * A {@link BeanLifecycleDecorator} that tries to protect against concurrent access to a bean during its own destruction. + * A read-write lock is used, and method access is protected using the read lock, while the destruction callback is + * protected more strictly with the write lock. In this way concurrent access is possible to the bean as long as it is + * not being destroyed, in which case only one thread has access. If the bean has no destruction callback the lock and + * associated proxies are never created. + * + * @author Dave Syer + * + */ +public class StandardBeanLifecycleDecorator implements BeanLifecycleDecorator { + + private final boolean proxyTargetClass; + + public StandardBeanLifecycleDecorator(boolean proxyTargetClass) { + this.proxyTargetClass = proxyTargetClass; + } + + public Object decorateBean(Object bean, Context context) { + if (context != null) { + bean = getDisposalLockProxy(bean, context.getAuxiliary().readLock()); + } + return bean; + } + + public Context decorateDestructionCallback(final Runnable callback) { + if (callback == null) { + return null; + } + final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + return new Context(new Runnable() { + public void run() { + Lock lock = readWriteLock.writeLock(); + lock.lock(); + try { + callback.run(); + } finally { + lock.unlock(); + } + } + }, readWriteLock); + } + + /** + * Apply a lock (preferably a read lock allowing multiple concurrent access) to the bean. Callers should replace the + * bean input with the output. + * + * @param bean the bean to lock + * @param lock the lock to apply + * @return a proxy that locks while its methods are executed + */ + private Object getDisposalLockProxy(Object bean, final Lock lock) { + ProxyFactory factory = new ProxyFactory(bean); + factory.setProxyTargetClass(proxyTargetClass); + factory.addAdvice(new MethodInterceptor() { + public Object invoke(MethodInvocation invocation) throws Throwable { + lock.lock(); + try { + return invocation.proceed(); + } finally { + lock.unlock(); + } + } + }); + return factory.getProxy(); + } + +} diff --git a/src/main/java/org/springframework/cloud/context/config/annotation/RefreshScope.java b/src/main/java/org/springframework/cloud/context/config/annotation/RefreshScope.java new file mode 100644 index 00000000..d46daa24 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/config/annotation/RefreshScope.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-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.cloud.context.config.annotation; + +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.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; + +/** + * Convenience annotation to put a @Bean definition in + * {@link org.springframework.cloud.context.scope.refresh.RefreshScope refresh scope}. + * Beans annotated this way can be refreshed at runtime and any components that are using + * them will get a new instance on the next method call, fully initialized and injected + * with all dependencies. + * + * @author Dave Syer + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Scope("refresh") +@Documented +public @interface RefreshScope { + /** + * @see Scope#proxyMode() + */ + ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; + +} diff --git a/src/main/java/org/springframework/cloud/context/encrypt/EncryptorFactory.java b/src/main/java/org/springframework/cloud/context/encrypt/EncryptorFactory.java new file mode 100644 index 00000000..7e306ea9 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/encrypt/EncryptorFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-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.cloud.context.encrypt; + +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.security.rsa.crypto.RsaSecretEncryptor; + +/** + * @author Dave Syer + * + */ +public class EncryptorFactory { + + // TODO: expose as config property + private static final String SALT = "deadbeef"; + + public TextEncryptor create(String data) { + + TextEncryptor encryptor; + if (data.contains("RSA PRIVATE KEY")) { + + try { + encryptor = new RsaSecretEncryptor(data); + } + catch (IllegalArgumentException e) { + throw new KeyFormatException(); + } + + } + else if (data.startsWith("ssh-rsa") || data.contains("RSA PUBLIC KEY")) { + throw new KeyFormatException(); + } + else { + encryptor = Encryptors.text(data, SALT); + } + + return encryptor; + } + +} + + diff --git a/src/main/java/org/springframework/cloud/context/encrypt/KeyFormatException.java b/src/main/java/org/springframework/cloud/context/encrypt/KeyFormatException.java new file mode 100644 index 00000000..08e6fa47 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/encrypt/KeyFormatException.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013-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.cloud.context.encrypt; + +@SuppressWarnings("serial") +public class KeyFormatException extends RuntimeException { +} \ No newline at end of file diff --git a/src/main/java/org/springframework/cloud/context/environment/EnvironmentChangeEvent.java b/src/main/java/org/springframework/cloud/context/environment/EnvironmentChangeEvent.java new file mode 100644 index 00000000..d0fb5e50 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/environment/EnvironmentChangeEvent.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-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.cloud.context.environment; + +import java.util.Set; + +import org.springframework.context.ApplicationEvent; +import org.springframework.core.env.Environment; + +/** + * Event published to signal a change in the {@link Environment}. + * + * @author Dave Syer + * + */ +@SuppressWarnings("serial") +public class EnvironmentChangeEvent extends ApplicationEvent { + + private Set keys; + + public EnvironmentChangeEvent(Set keys) { + super(keys); + this.keys = keys; + } + + /** + * @return the keys + */ + public Set getKeys() { + return keys; + } + +} diff --git a/src/main/java/org/springframework/cloud/context/environment/EnvironmentManager.java b/src/main/java/org/springframework/cloud/context/environment/EnvironmentManager.java new file mode 100644 index 00000000..91e625cf --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/environment/EnvironmentManager.java @@ -0,0 +1,109 @@ +/* + * Copyright 2013-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.cloud.context.environment; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.stereotype.Component; + +/** + * Entry point for making local (but volatile) changes to the {@link Environment} of a + * running application. Allows properties to be added and values changed, simply by adding + * them to a high priority property source in the existing Environment. + * + * @author Dave Syer + * + */ +@Component +@ManagedResource +public class EnvironmentManager implements ApplicationEventPublisherAware { + + private static final String MANAGER_PROPERTY_SOURCE = "manager"; + private Map map = new LinkedHashMap(); + + private ConfigurableEnvironment environment; + private ApplicationEventPublisher publisher; + + public EnvironmentManager(ConfigurableEnvironment environment) { + this.environment = environment; + MutablePropertySources sources = environment.getPropertySources(); + if (sources.contains(MANAGER_PROPERTY_SOURCE)) { + @SuppressWarnings("unchecked") + Map map = (Map) sources.get( + MANAGER_PROPERTY_SOURCE).getSource(); + this.map = map; + } + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @ManagedOperation + public Map reset() { + Map result = new LinkedHashMap(map); + if (!map.isEmpty()) { + Set keys = map.keySet(); + map.clear(); + publish(new EnvironmentChangeEvent(keys)); + } + return result; + } + + @ManagedOperation + public void setProperty(String name, String value) { + + if (!environment.getPropertySources().contains(MANAGER_PROPERTY_SOURCE)) { + synchronized (map) { + if (!environment.getPropertySources().contains(MANAGER_PROPERTY_SOURCE)) { + MapPropertySource source = new MapPropertySource( + MANAGER_PROPERTY_SOURCE, map); + environment.getPropertySources().addFirst(source); + } + } + } + + if (!value.equals(environment.getProperty(name))) { + map.put(name, value); + publish(new EnvironmentChangeEvent(Collections.singleton(name))); + } + + } + + @ManagedOperation + public Object getProperty(String name) { + return environment.getProperty(name); + } + + private void publish(EnvironmentChangeEvent environmentChangeEvent) { + if (publisher != null) { + publisher.publishEvent(environmentChangeEvent); + } + } + +} diff --git a/src/main/java/org/springframework/cloud/context/environment/EnvironmentManagerMvcEndpoint.java b/src/main/java/org/springframework/cloud/context/environment/EnvironmentManagerMvcEndpoint.java new file mode 100644 index 00000000..652b08bd --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/environment/EnvironmentManagerMvcEndpoint.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-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.cloud.context.environment; + +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * MVC endpoint for the {@link EnvironmentManager} providing a POST to /env as a simple + * way to change the Environment. + * + * @author Dave Syer + * + */ +public class EnvironmentManagerMvcEndpoint implements MvcEndpoint { + + private EnvironmentManager environment; + private EnvironmentEndpoint delegate; + + public EnvironmentManagerMvcEndpoint(EnvironmentEndpoint delegate, + EnvironmentManager enviroment) { + this.delegate = delegate; + environment = enviroment; + } + + @RequestMapping(value = "", method = RequestMethod.POST) + @ResponseBody + public Object value(@RequestParam Map params) { + for (String name : params.keySet()) { + environment.setProperty(name, params.get(name)); + } + return params; + } + + @RequestMapping(value = "reset", method = RequestMethod.POST) + @ResponseBody + public Map reset() { + return environment.reset(); + } + + public void setEnvironmentManager(EnvironmentManager environment) { + this.environment = environment; + } + + @Override + public String getPath() { + return "/" + this.delegate.getId(); + } + + @Override + public boolean isSensitive() { + return this.delegate.isSensitive(); + } + + @Override + @SuppressWarnings("rawtypes") + public Class getEndpointType() { + return this.delegate.getClass(); + } +} diff --git a/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java b/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java new file mode 100644 index 00000000..c0799857 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java @@ -0,0 +1,133 @@ +/* + * Copyright 2013-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.cloud.context.properties; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.stereotype.Component; + +/** + * Listens for {@link EnvironmentChangeEvent} and rebinds beans that were bound to the + * {@link Environment} using {@link ConfigurationProperties + * @ConfigurationProperties}. When these beans are re-bound and + * re-initialized the changes are available immediately to any component that is using the + * @ConfigurationProperties bean. + * + * @see RefreshScope for a deeper and optionally more focused refresh of bean components + * + * @author Dave Syer + * + */ +@Component +@ManagedResource +public class ConfigurationPropertiesRebinder implements BeanPostProcessor, + ApplicationListener, ApplicationContextAware { + + private ConfigurationBeanFactoryMetaData metaData; + + private ConfigurationPropertiesBindingPostProcessor binder; + + public ConfigurationPropertiesRebinder( + ConfigurationPropertiesBindingPostProcessor binder) { + this.binder = binder; + } + + private Map beans = new HashMap(); + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } + + /** + * @param beans the bean meta data to set + */ + public void setBeanMetaDataStore(ConfigurationBeanFactoryMetaData beans) { + this.metaData = beans; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + ConfigurationProperties annotation = AnnotationUtils.findAnnotation( + bean.getClass(), ConfigurationProperties.class); + if (annotation != null) { + beans.put(beanName, bean); + } + else if (metaData != null) { + annotation = this.metaData.findFactoryAnnotation(beanName, + ConfigurationProperties.class); + if (annotation != null) { + beans.put(beanName, bean); + } + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + @ManagedOperation + public void rebind() { + for (String name : beans.keySet()) { + rebind(name); + } + } + + @ManagedOperation + public void rebind(String name) { + binder.postProcessBeforeInitialization(beans.get(name), name); + if (applicationContext != null) { + applicationContext.getAutowireCapableBeanFactory().initializeBean( + beans.get(name), name); + } + } + + @ManagedAttribute + public Set getBeanNames() { + return new HashSet(beans.keySet()); + } + + @Override + public void onApplicationEvent(EnvironmentChangeEvent event) { + rebind(); + } + +} diff --git a/src/main/java/org/springframework/cloud/context/restart/RestartEndpoint.java b/src/main/java/org/springframework/cloud/context/restart/RestartEndpoint.java new file mode 100644 index 00000000..4e903f8e --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/restart/RestartEndpoint.java @@ -0,0 +1,206 @@ +/* + * Copyright 2013-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.cloud.context.restart; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.integration.monitor.IntegrationMBeanExporter; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.util.ClassUtils; + +/** + * An endpoint that restarts the application context. Install as a bean and also register + * a {@link RestartListener} with the {@link SpringApplication} that starts the context. + * Those two components communicate via an {@link ApplicationEvent} and set up the state + * needed to restart the context. + * + * @author Dave Syer + * + */ +@ConfigurationProperties("endpoints.restart") +@ManagedResource +public class RestartEndpoint extends AbstractEndpoint implements + ApplicationListener { + + private static Log logger = LogFactory.getLog(RestartEndpoint.class); + + public RestartEndpoint() { + super("restart", true, false); + } + + private ConfigurableApplicationContext context; + + private SpringApplication application; + + private String[] args; + + private ApplicationPreparedEvent event; + + private IntegrationShutdown integrationShutdown; + + private long timeout; + + @ManagedAttribute + public long getTimeout() { + return this.timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public void setIntegrationMBeanExporter(Object exporter) { + if (exporter != null) { + this.integrationShutdown = new IntegrationShutdown(exporter); + } + } + + @Override + public void onApplicationEvent(ApplicationPreparedEvent input) { + this.event = input; + if (this.context == null) { + this.context = this.event.getApplicationContext(); + this.args = this.event.getArgs(); + this.application = this.event.getSpringApplication(); + } + } + + @Override + public Boolean invoke() { + try { + restart(); + logger.info("Restarted"); + return true; + } + catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.info("Could not restart", e); + } + else { + logger.info("Could not restart: " + e.getMessage()); + } + return false; + } + } + + public Endpoint getPauseEndpoint() { + return new PauseEndpoint(); + } + + public Endpoint getResumeEndpoint() { + return new ResumeEndpoint(); + } + + private class PauseEndpoint extends AbstractEndpoint { + + public PauseEndpoint() { + super("pause", true, true); + } + + @Override + public Boolean invoke() { + if (isRunning()) { + pause(); + return true; + } + return false; + } + } + + private class ResumeEndpoint extends AbstractEndpoint { + + public ResumeEndpoint() { + super("resume", true, true); + } + + @Override + public Boolean invoke() { + if (!isRunning()) { + resume(); + return true; + } + return false; + } + } + + @ManagedOperation + public synchronized ConfigurableApplicationContext restart() { + if (this.context != null) { + if (this.integrationShutdown != null) { + this.integrationShutdown.stop(this.timeout); + } + this.application.setEnvironment(this.context.getEnvironment()); + this.context.close(); + // If running in a webapp then the context classloader is probably going to + // die so we need to revert to a safe place before starting again + overrideClassLoaderForRestart(); + this.context = this.application.run(this.args); + } + return this.context; + } + + @ManagedAttribute + public boolean isRunning() { + if (this.context != null) { + return this.context.isRunning(); + } + return false; + } + + @ManagedOperation + public synchronized void pause() { + if (this.context != null) { + this.context.stop(); + } + } + + @ManagedOperation + public synchronized void resume() { + if (this.context != null) { + this.context.start(); + } + } + + private void overrideClassLoaderForRestart() { + ClassUtils.overrideThreadContextClassLoader(this.application.getClass() + .getClassLoader()); + } + + private class IntegrationShutdown { + + private IntegrationMBeanExporter exporter; + + public IntegrationShutdown(Object exporter) { + this.exporter = (IntegrationMBeanExporter) exporter; + } + + public void stop(long timeout) { + this.exporter.stopActiveComponents(timeout); + } + } + +} diff --git a/src/main/java/org/springframework/cloud/context/restart/RestartListener.java b/src/main/java/org/springframework/cloud/context/restart/RestartListener.java new file mode 100644 index 00000000..4dcad0ad --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/restart/RestartListener.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013-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.cloud.context.restart; + +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.SmartApplicationListener; + +/** + * A listener that stores enough information about an application as it starts, to be able + * to restart it later if needed. + * + * @author Dave Syer + * + */ +public class RestartListener implements SmartApplicationListener { + + private ConfigurableApplicationContext context; + + private ApplicationPreparedEvent event; + + @Override + public int getOrder() { + return 0; + } + + @Override + public boolean supportsEventType(Class eventType) { + return ApplicationPreparedEvent.class.isAssignableFrom(eventType) + || ContextRefreshedEvent.class.isAssignableFrom(eventType) + || ContextClosedEvent.class.isAssignableFrom(eventType); + + } + + @Override + public boolean supportsSourceType(Class sourceType) { + return true; + } + + @Override + public void onApplicationEvent(ApplicationEvent input) { + if (input instanceof ApplicationPreparedEvent) { + event = (ApplicationPreparedEvent) input; + if (context == null) { + context = event.getApplicationContext(); + } + } + else if (input instanceof ContextRefreshedEvent) { + if (context != null && input.getSource().equals(context) && event != null) { + context.publishEvent(event); + } + } + else { + if (context != null && input.getSource().equals(context)) { + context = null; + } + } + } + +} diff --git a/src/main/java/org/springframework/cloud/context/restart/RestartMvcEndpoint.java b/src/main/java/org/springframework/cloud/context/restart/RestartMvcEndpoint.java new file mode 100644 index 00000000..ab8b32e9 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/restart/RestartMvcEndpoint.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-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.cloud.context.restart; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.cloud.endpoint.GenericPostableMvcEndpoint; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * MVC endpoint to allow an application to be restarted on a POST (to /restart by + * default). + * + * @author Dave Syer + * + */ +public class RestartMvcEndpoint extends EndpointMvcAdapter { + + public RestartMvcEndpoint(RestartEndpoint delegate) { + super(delegate); + } + + @RequestMapping(method = RequestMethod.POST) + @ResponseBody + @Override + public Object invoke() { + if (!getDelegate().isEnabled()) { + return new ResponseEntity>(Collections.singletonMap( + "message", "This endpoint is disabled"), HttpStatus.NOT_FOUND); + } + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + RestartMvcEndpoint.super.invoke(); + } + }); + thread.setDaemon(false); + thread.start(); + return Collections.singletonMap("message", "Restarting"); + } + + public MvcEndpoint getPauseEndpoint() { + return new GenericPostableMvcEndpoint( + ((RestartEndpoint) getDelegate()).getPauseEndpoint()); + } + + public MvcEndpoint getResumeEndpoint() { + return new GenericPostableMvcEndpoint( + ((RestartEndpoint) getDelegate()).getResumeEndpoint()); + } + +} diff --git a/src/main/java/org/springframework/cloud/context/scope/GenericScope.java b/src/main/java/org/springframework/cloud/context/scope/GenericScope.java new file mode 100644 index 00000000..ac940e4a --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/scope/GenericScope.java @@ -0,0 +1,367 @@ +/* + * Copyright 2002-2009 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.cloud.context.scope; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.cloud.context.config.BeanLifecycleDecorator; +import org.springframework.cloud.context.config.BeanLifecycleDecorator.Context; +import org.springframework.cloud.context.config.StandardBeanLifecycleDecorator; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.StringUtils; + +/** + *

+ * A generic Scope implementation. + *

+ * + * @author Dave Syer + * + * @since 3.1 + * + */ +public class GenericScope implements Scope, BeanFactoryPostProcessor, DisposableBean { + + private static final Log logger = LogFactory.getLog(GenericScope.class); + + public static final String SCOPED_TARGET_PREFIX = "scopedTarget."; + + private BeanLifecycleWrapperCache cache = new BeanLifecycleWrapperCache( + new StandardScopeCache()); + + private String name = "generic"; + + private boolean proxyTargetClass = true; + + private ConfigurableListableBeanFactory beanFactory; + + private StandardEvaluationContext evaluationContext; + + private String id; + + private BeanLifecycleDecorator lifecycle; + + /** + * Manual override for the serialization id that will be used to identify the bean + * factory. The default is a unique key based on the bean names in the bean factory. + * + * @param id the id to set + */ + public void setId(String id) { + this.id = id; + } + + /** + * The name of this scope. Default "refresh". + * + * @param name the name value to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * Flag to indicate that proxies should be created for the concrete type, not just the + * interfaces, of the scoped beans. + * + * @param proxyTargetClass the flag value to set + */ + public void setProxyTargetClass(boolean proxyTargetClass) { + this.proxyTargetClass = proxyTargetClass; + } + + /** + * The cache implementation to use for bean instances in this scope. + * + * @param cache the cache to use + */ + public void setScopeCache(ScopeCache cache) { + this.cache = new BeanLifecycleWrapperCache(cache); + } + + /** + * Helper to manage the creation and destruction of beans. + * + * @param lifecycle the bean lifecycle to set + */ + public void setBeanLifecycleManager(BeanLifecycleDecorator lifecycle) { + this.lifecycle = lifecycle; + } + + @Override + public void destroy() { + List errors = new ArrayList(); + Collection wrappers = this.cache.clear(); + for (BeanLifecycleWrapper wrapper : wrappers) { + try { + wrapper.destroy(); + } + catch (RuntimeException e) { + errors.add(e); + } + } + if (!errors.isEmpty()) { + throw wrapIfNecessary(errors.get(0)); + } + } + + protected void destroy(String name) { + BeanLifecycleWrapper wrapper = this.cache.remove(name); + if (wrapper != null) { + wrapper.destroy(); + } + } + + @Override + public Object get(String name, ObjectFactory objectFactory) { + if (this.lifecycle == null) { + this.lifecycle = new StandardBeanLifecycleDecorator(this.proxyTargetClass); + } + BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, + objectFactory, this.lifecycle)); + return value.getBean(); + } + + @Override + public String getConversationId() { + return this.name; + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + BeanLifecycleWrapper value = this.cache.get(name); + if (value == null) { + return; + } + value.setDestroyCallback(callback); + } + + @Override + public Object remove(String name) { + BeanLifecycleWrapper value = this.cache.remove(name); + if (value == null) { + return null; + } + // Someone might have added another object with the same key, but we + // keep the method contract by removing the + // value we found anyway + return value.getBean(); + } + + @Override + public Object resolveContextualObject(String key) { + Expression expression = parseExpression(key); + return expression.getValue(this.evaluationContext, this.beanFactory); + } + + private Expression parseExpression(String input) { + if (StringUtils.hasText(input)) { + ExpressionParser parser = new SpelExpressionParser(); + try { + return parser.parseExpression(input); + } + catch (ParseException e) { + throw new IllegalArgumentException("Cannot parse expression: " + input, e); + } + + } + else { + return null; + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + beanFactory.registerScope(this.name, this); + setSerializationId(beanFactory); + } + + /** + * If the bean factory is a DefaultListableBeanFactory then it can serialize scoped + * beans and deserialize them in another context (even in another JVM), as long as the + * ids of the bean factories match. This method sets up the serialization id to be + * either the id provided to the scope instance, or if that is null, a hash of all the + * bean names. + * + * @param beanFactory the bean factory to configure + */ + private void setSerializationId(ConfigurableListableBeanFactory beanFactory) { + + if (beanFactory instanceof DefaultListableBeanFactory) { + + String id = this.id; + if (id == null) { + String names = Arrays.asList(beanFactory.getBeanDefinitionNames()) + .toString(); + logger.debug("Generating bean factory id from names: " + names); + id = UUID.nameUUIDFromBytes(names.getBytes()).toString(); + } + + logger.info("BeanFactory id=" + id); + ((DefaultListableBeanFactory) beanFactory).setSerializationId(id); + + } + else { + logger.warn("BeanFactory was not a DefaultListableBeanFactory, so RefreshScope beans " + + "cannot be serialized reliably and passed to a remote JVM."); + } + + } + + static RuntimeException wrapIfNecessary(Throwable throwable) { + if (throwable instanceof RuntimeException) { + return (RuntimeException) throwable; + } + if (throwable instanceof Error) { + throw (Error) throwable; + } + return new IllegalStateException(throwable); + } + + private static class BeanLifecycleWrapperCache { + + private final ScopeCache cache; + + public BeanLifecycleWrapperCache(ScopeCache cache) { + this.cache = cache; + } + + public BeanLifecycleWrapper remove(String name) { + return (BeanLifecycleWrapper) this.cache.remove(name); + } + + public Collection clear() { + Collection values = this.cache.clear(); + Collection wrappers = new LinkedHashSet(); + for (Object object : values) { + wrappers.add((BeanLifecycleWrapper) object); + } + return wrappers; + } + + public BeanLifecycleWrapper get(String name) { + return (BeanLifecycleWrapper) this.cache.get(name); + } + + public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) { + return (BeanLifecycleWrapper) this.cache.put(name, value); + } + + } + + /** + * Wrapper for a bean instance and any destruction callback (DisposableBean etc.) that + * is registered for it. Also decorates the bean to optionally guard it from + * concurrent access (for instance). + * + * @author Dave Syer + * + */ + private static class BeanLifecycleWrapper { + + private Object bean; + + private Context context; + + private final String name; + + @SuppressWarnings("rawtypes") + private final BeanLifecycleDecorator lifecycle; + + private final ObjectFactory objectFactory; + + @SuppressWarnings("rawtypes") + public BeanLifecycleWrapper(String name, ObjectFactory objectFactory, + BeanLifecycleDecorator lifecycle) { + this.name = name; + this.objectFactory = objectFactory; + this.lifecycle = lifecycle; + } + + public void setDestroyCallback(Runnable callback) { + this.context = this.lifecycle.decorateDestructionCallback(callback); + } + + @SuppressWarnings("unchecked") + public Object getBean() { + if (this.bean == null) { + this.bean = this.lifecycle.decorateBean(this.objectFactory.getObject(), + this.context); + } + return this.bean; + } + + public void destroy() { + if (this.context == null) { + return; + } + Runnable callback = this.context.getCallback(); + if (callback != null) { + callback.run(); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + BeanLifecycleWrapper other = (BeanLifecycleWrapper) obj; + if (this.name == null) { + if (other.name != null) { + return false; + } + } + else if (!this.name.equals(other.name)) { + return false; + } + return true; + } + + } + +} diff --git a/src/main/java/org/springframework/cloud/context/scope/ScopeCache.java b/src/main/java/org/springframework/cloud/context/scope/ScopeCache.java new file mode 100644 index 00000000..1be557c0 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/scope/ScopeCache.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2011 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.cloud.context.scope; + +import java.util.Collection; + +/** + * A special purpose cache interface specifically for the {@link GenericScope} to use to manage cached bean instances. + * Implementations generally fall into two categories: those that store values "globally" (i.e. one instance per key), + * and those that store potentially multiple instances per key based on context (e.g. via a thread local). All + * implementations should be thread safe. + * + * @author Dave Syer + * + */ +public interface ScopeCache { + + /** + * Remove the object with this name from the cache. + * + * @param name the object name + * @return the object removed or null if there was none + */ + Object remove(String name); + + /** + * Clear the cache and return all objects in an unmodifiable collection. + * + * @return all objects stored in the cache + */ + Collection clear(); + + /** + * Get the named object from the cache. + * + * @param name the name of the object + * @return the object with that name or null if there is none + */ + Object get(String name); + + /** + * Put a value in the cache if the key is not already used. If one is already present with the name provided, it is + * not replaced, but is returned to the caller. + * + * @param name the key + * @param value the new candidate value + * @return the value that is in the cache at the end of the operation + */ + Object put(String name, Object value); + +} diff --git a/src/main/java/org/springframework/cloud/context/scope/StandardScopeCache.java b/src/main/java/org/springframework/cloud/context/scope/StandardScopeCache.java new file mode 100644 index 00000000..e3591a0d --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/scope/StandardScopeCache.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2011 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.cloud.context.scope; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A simple cache implementation backed by a concurrent map. + * + * @author Dave Syer + * + */ +public class StandardScopeCache implements ScopeCache { + + private final ConcurrentMap cache = new ConcurrentHashMap(); + + public Object remove(String name) { + return cache.remove(name); + } + + public Collection clear() { + Collection values = new ArrayList(cache.values()); + cache.clear(); + return values; + } + + public Object get(String name) { + return cache.get(name); + } + + public Object put(String name, Object value) { + Object result = cache.putIfAbsent(name, value); + if (result!=null) { + return result; + } + return value; + } + +} diff --git a/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java b/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java new file mode 100644 index 00000000..953e5103 --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2009 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.cloud.context.scope.refresh; + +import java.io.Serializable; + +import org.springframework.beans.BeansException; +import org.springframework.cloud.context.scope.GenericScope; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; + +/** + *

+ * A Scope implementation that allows for beans to be refreshed dynamically at runtime (see {@link #refresh(String)} and + * {@link #refreshAll()}). If a bean is refreshed then the next time the bean is accessed (i.e. a method is executed) a + * new instance is created. All lifecycle methods are applied to the bean instances, so any destruction callbacks that + * were registered in the bean factory are called when it is refreshed, and then the initialization callbacks are + * invoked as normal when the new instance is created. A new bean instance is created from the original bean definition, + * so any externalized content (property placeholders or expressions in string literals) is re-evaluated when it is + * created. + *

+ * + *

+ * Note that all beans in this scope are only initialized when first accessed, so the scope forces lazy + * initialization semantics. The implementation involves creating a proxy for every bean in the scope, so there is a + * flag {@link #setProxyTargetClass(boolean) proxyTargetClass} which controls the proxy creation, defaulting to JDK + * dynamic proxies and therefore only exposing the interfaces implemented by a bean. If callers need access to other + * methods then the flag needs to be set (and CGLib present on the classpath). Because this scope automatically proxies + * all its beans, there is no need to add <aop:auto-proxy/> to any bean definitions. + *

+ * + *

+ * The scoped proxy approach adopted here has a side benefit that bean instances are automatically {@link Serializable}, + * and can be sent across the wire as long as the receiver has an identical application context on the other side. To + * ensure that the two contexts agree that they are identical they have to have the same serialization id. One will be + * generated automatically by default from the bean names, so two contexts with the same bean names are by default able + * to exchange beans by name. If you need to override the default id then provide an explicit {@link #setId(String) id} + * when the Scope is declared. + *

+ * + * @author Dave Syer + * + * @since 3.1 + * + */ +@ManagedResource +public class RefreshScope extends GenericScope implements ApplicationContextAware { + + private ApplicationContext context; + + /** + * Create a scope instance and give it the default name: "refresh". + */ + public RefreshScope() { + super(); + super.setName("refresh"); + } + + @ManagedOperation(description = "Dispose of the current instance of bean name provided and force a refresh on next method execution.") + public void refresh(String name) { + if (!name.startsWith(SCOPED_TARGET_PREFIX)) { + // User wants to refresh the bean with this name but that isn't the one in the cache... + name = SCOPED_TARGET_PREFIX + name; + } + // Ensure lifecycle is finished if bean was disposable + super.destroy(name); + context.publishEvent(new RefreshScopeRefreshedEvent(name)); + } + + @ManagedOperation(description = "Dispose of the current instance of all beans in this scope and force a refresh on next method execution.") + public void refreshAll() { + super.destroy(); + context.publishEvent(new RefreshScopeRefreshedEvent()); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.context = context; + } +} diff --git a/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScopeRefreshedEvent.java b/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScopeRefreshedEvent.java new file mode 100644 index 00000000..0216108d --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScopeRefreshedEvent.java @@ -0,0 +1,25 @@ +package org.springframework.cloud.context.scope.refresh; + +import org.springframework.context.ApplicationEvent; + +/** + * @author Spencer Gibb + */ +@SuppressWarnings("serial") +public class RefreshScopeRefreshedEvent extends ApplicationEvent { + public static final String DEFAULT_NAME = "__refreshAll__"; + private String name; + + public RefreshScopeRefreshedEvent() { + this(DEFAULT_NAME); + } + + public RefreshScopeRefreshedEvent(String name) { + super(name); + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/org/springframework/cloud/context/scope/thread/ThreadLocalScopeCache.java b/src/main/java/org/springframework/cloud/context/scope/thread/ThreadLocalScopeCache.java new file mode 100644 index 00000000..c140d63d --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/scope/thread/ThreadLocalScopeCache.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2011 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.cloud.context.scope.thread; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.cloud.context.scope.ScopeCache; + +/** + * @author Dave Syer + * + */ +public class ThreadLocalScopeCache implements ScopeCache { + + private ThreadLocal> data = new ThreadLocal>() { + protected ConcurrentMap initialValue() { + return new ConcurrentHashMap(); + } + }; + + public Object remove(String name) { + return data.get().remove(name); + } + + public Collection clear() { + ConcurrentMap map = data.get(); + Collection values = new ArrayList(map.values()); + map.clear(); + return values; + } + + public Object get(String name) { + return data.get().get(name); + } + + public Object put(String name, Object value) { + Object result = data.get().putIfAbsent(name, value); + if (result!=null) { + return result; + } + return value; + } + +} diff --git a/src/main/java/org/springframework/cloud/context/scope/thread/ThreadScope.java b/src/main/java/org/springframework/cloud/context/scope/thread/ThreadScope.java new file mode 100644 index 00000000..8637cd2b --- /dev/null +++ b/src/main/java/org/springframework/cloud/context/scope/thread/ThreadScope.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2009 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.cloud.context.scope.thread; + +import org.springframework.cloud.context.scope.GenericScope; + +/** + * + * @author Dave Syer + * + * @since 3.1 + * + */ +public class ThreadScope extends GenericScope { + + /** + * Create a scope instance and give it the default name: "thread". + */ + public ThreadScope() { + super(); + super.setName("thread"); + super.setScopeCache(new ThreadLocalScopeCache()); + } + +} diff --git a/src/main/java/org/springframework/cloud/endpoint/GenericPostableMvcEndpoint.java b/src/main/java/org/springframework/cloud/endpoint/GenericPostableMvcEndpoint.java new file mode 100644 index 00000000..b9547e03 --- /dev/null +++ b/src/main/java/org/springframework/cloud/endpoint/GenericPostableMvcEndpoint.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-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.cloud.endpoint; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * A convenient base class for MVC endpoints that accept a POST (instead of the default + * GET). + * + * @author Dave Syer + * + */ +public class GenericPostableMvcEndpoint extends EndpointMvcAdapter { + + public GenericPostableMvcEndpoint(Endpoint delegate) { + super(delegate); + } + + @RequestMapping(method = RequestMethod.POST) + @ResponseBody + @Override + public Object invoke() { + if (!getDelegate().isEnabled()) { + return new ResponseEntity>(Collections.singletonMap( + "message", "This endpoint is disabled"), HttpStatus.NOT_FOUND); + } + return super.invoke(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java b/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java new file mode 100644 index 00000000..b67cdb40 --- /dev/null +++ b/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013-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.cloud.logging; + +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; + +/** + * Listener that looks for {@link EnvironmentChangeEvent} and rebinds logger levels if any + * changed. + * + * @author Dave Syer + * + */ +public class LoggingRebinder implements ApplicationListener, + EnvironmentAware { + + private final Log logger = LogFactory.getLog(getClass()); + + private Environment environment; + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public void onApplicationEvent(EnvironmentChangeEvent event) { + if (environment == null) { + return; + } + LoggingSystem system = LoggingSystem.get(LoggingSystem.class.getClassLoader()); + setLogLevels(system, environment); + } + + protected void setLogLevels(LoggingSystem system, Environment environment) { + Map levels = new RelaxedPropertyResolver(environment) + .getSubProperties("logging.level."); + for (Entry entry : levels.entrySet()) { + setLogLevel(system, environment, entry.getKey(), entry.getValue().toString()); + } + } + + private void setLogLevel(LoggingSystem system, Environment environment, String name, + String level) { + try { + if (name.equalsIgnoreCase("root")) { + name = null; + } + level = environment.resolvePlaceholders(level); + system.setLogLevel(name, LogLevel.valueOf(level)); + } + catch (RuntimeException ex) { + this.logger.error("Cannot set level: " + level + " for '" + name + "'"); + } + } + +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories index 0345563f..372a099f 100644 --- a/src/main/resources/META-INF/spring.factories +++ b/src/main/resources/META-INF/spring.factories @@ -2,4 +2,17 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.client.CommonsClientAutoConfiguration,\ org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration,\ -org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration +org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\ +org.springframework.cloud.autoconfigure.RefreshAutoConfiguration,\ +org.springframework.cloud.autoconfigure.LifecycleMvcEndpointAutoConfiguration + +# Application Listeners +org.springframework.context.ApplicationListener=\ +org.springframework.cloud.bootstrap.BootstrapApplicationListener,\ +org.springframework.cloud.context.restart.RestartListener + +# Bootstrap components +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\ +org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\ +org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration \ No newline at end of file diff --git a/src/test/java/org/springframework/cloud/bootstrap/BootstrapDisabledAutoConfigurationIntegrationTests.java b/src/test/java/org/springframework/cloud/bootstrap/BootstrapDisabledAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000..0a11c681 --- /dev/null +++ b/src/test/java/org/springframework/cloud/bootstrap/BootstrapDisabledAutoConfigurationIntegrationTests.java @@ -0,0 +1,35 @@ +package org.springframework.cloud.bootstrap; + +import static org.junit.Assert.assertFalse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.bootstrap.BootstrapDisabledAutoConfigurationIntegrationTests.Application; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Application.class) +@IntegrationTest("spring.cloud.bootstrap.enabled:false") +public class BootstrapDisabledAutoConfigurationIntegrationTests { + + @Autowired + private ConfigurableEnvironment environment; + + @Test + public void noBootstrapProperties() { + assertFalse(environment.getPropertySources().contains("bootstrap")); + } + + @EnableAutoConfiguration + @Configuration + protected static class Application { + + } + +} diff --git a/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingAutoConfigurationIntegrationTests.java b/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000..74a4a66c --- /dev/null +++ b/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingAutoConfigurationIntegrationTests.java @@ -0,0 +1,48 @@ +package org.springframework.cloud.bootstrap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.bootstrap.BootstrapOrderingAutoConfigurationIntegrationTests.Application; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Application.class) +@IntegrationTest("encrypt.key:deadbeef") +@ActiveProfiles("encrypt") +public class BootstrapOrderingAutoConfigurationIntegrationTests { + + @Autowired + private ConfigurableEnvironment environment; + + @Test + public void bootstrapPropertiesExist() { + assertTrue(environment.getPropertySources().contains("bootstrap")); + } + + @Test + public void normalPropertiesDecrypted() { + assertEquals("foo", environment.resolvePlaceholders("${foo}")); + } + + @Test + public void bootstrapPropertiesDecrypted() { + assertEquals("bar", environment.resolvePlaceholders("${bar}")); + } + + @EnableAutoConfiguration + @Configuration + protected static class Application { + + } + +} diff --git a/src/test/java/org/springframework/cloud/bootstrap/config/BootstrapConfigurationTests.java b/src/test/java/org/springframework/cloud/bootstrap/config/BootstrapConfigurationTests.java new file mode 100644 index 00000000..c40022d6 --- /dev/null +++ b/src/test/java/org/springframework/cloud/bootstrap/config/BootstrapConfigurationTests.java @@ -0,0 +1,323 @@ +/* + * Copyright 2013-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.cloud.bootstrap.config; + +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * @author Dave Syer + * + */ +public class BootstrapConfigurationTests { + + private ConfigurableApplicationContext context; + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @After + public void close() { + // Expected.* is bound to the PropertySourceConfiguration below + System.clearProperty("expected.name"); + System.clearProperty("expected.fail"); + // Used to test system properties override + System.clearProperty("bootstrap.foo"); + PropertySourceConfiguration.MAP.clear(); + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void pickupExternalBootstrapProperties() { + String externalPropertiesPath = getExternalProperties(); + + this.context = new SpringApplicationBuilder().web(false) + .sources(BareConfiguration.class) + .properties("spring.cloud.bootstrap.location:" + externalPropertiesPath) + .run(); + assertEquals("externalPropertiesInfoName", this.context.getEnvironment() + .getProperty("info.name")); + assertTrue(this.context.getEnvironment().getPropertySources() + .contains("bootstrap")); + } + + /** + * Running the test from maven will start from a different directory then starting it + * from intellij + * + * @return + */ + private String getExternalProperties() { + String externalPropertiesPath = ""; + File externalProperties = new File( + "src/test/resources/external-properties/bootstrap.properties"); + if (externalProperties.exists()) { + externalPropertiesPath = externalProperties.getAbsolutePath(); + } + else { + externalProperties = new File( + "spring-cloud-config-client/src/test/resources/external-properties/bootstrap.properties"); + externalPropertiesPath = externalProperties.getAbsolutePath(); + } + return externalPropertiesPath; + } + + @Test + public void picksUpAdditionalPropertySource() { + PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); + System.setProperty("expected.name", "bootstrap"); + this.context = new SpringApplicationBuilder().web(false) + .sources(BareConfiguration.class).run(); + assertEquals("bar", this.context.getEnvironment().getProperty("bootstrap.foo")); + assertTrue(this.context.getEnvironment().getPropertySources() + .contains("bootstrap")); + } + + @Test + public void failsOnPropertySource() { + System.setProperty("expected.fail", "true"); + this.expected.expectMessage("Planned"); + this.context = new SpringApplicationBuilder().web(false) + .sources(BareConfiguration.class).run(); + } + + @Test + public void overrideSystemPropertySourceByDefault() { + PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); + System.setProperty("bootstrap.foo", "system"); + this.context = new SpringApplicationBuilder().web(false) + .sources(BareConfiguration.class).run(); + assertEquals("bar", this.context.getEnvironment().getProperty("bootstrap.foo")); + } + + @Test + public void systemPropertyOverrideFalse() { + PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); + PropertySourceConfiguration.MAP.put( + "spring.cloud.config.overrideSystemProperties", "false"); + System.setProperty("bootstrap.foo", "system"); + this.context = new SpringApplicationBuilder().web(false) + .sources(BareConfiguration.class).run(); + assertEquals("system", this.context.getEnvironment().getProperty("bootstrap.foo")); + } + + @Test + public void systemPropertyOverrideWhenOverrideDisallowed() { + PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); + PropertySourceConfiguration.MAP.put( + "spring.cloud.config.overrideSystemProperties", "false"); + // If spring.cloud.config.allowOverride=false is in the remote property sources + // with sufficiently high priority it always wins. Admins can enforce it by adding + // their own remote property source. + PropertySourceConfiguration.MAP.put("spring.cloud.config.allowOverride", "false"); + System.setProperty("bootstrap.foo", "system"); + this.context = new SpringApplicationBuilder().web(false) + .sources(BareConfiguration.class).run(); + assertEquals("bar", this.context.getEnvironment().getProperty("bootstrap.foo")); + } + + @Test + public void applicationNameInBootstrapAndMain() { + System.setProperty("expected.name", "main"); + this.context = new SpringApplicationBuilder() + .web(false) + .properties("spring.cloud.bootstrap.name:other", + "spring.config.name:plain").sources(BareConfiguration.class) + .run(); + assertEquals("app", + this.context.getEnvironment().getProperty("spring.application.name")); + // The parent is called "main" because spring.application.name is specified in + // other.properties (the bootstrap properties) + assertEquals( + "main", + this.context.getParent().getEnvironment() + .getProperty("spring.application.name")); + // The bootstrap context has a different "bootstrap" property source + assertNotSame( + this.context.getEnvironment().getPropertySources().get("bootstrap"), + ((ConfigurableEnvironment) this.context.getParent().getEnvironment()) + .getPropertySources().get("bootstrap")); + assertEquals("app", this.context.getId()); + } + + @Test + public void applicationNameNotInBootstrap() { + System.setProperty("expected.name", "main"); + this.context = new SpringApplicationBuilder() + .web(false) + .properties("spring.cloud.bootstrap.name:application", + "spring.config.name:other").sources(BareConfiguration.class) + .run(); + assertEquals("main", + this.context.getEnvironment().getProperty("spring.application.name")); + // The parent is called "application" because spring.application.name is not + // defined in the bootstrap properties + assertEquals("application", this.context.getParent().getEnvironment() + .getProperty("spring.application.name")); + } + + @Test + public void applicationNameOnlyInBootstrap() { + System.setProperty("expected.name", "main"); + this.context = new SpringApplicationBuilder().web(false) + .properties("spring.cloud.bootstrap.name:other") + .sources(BareConfiguration.class).run(); + // The main context is called "main" because spring.application.name is specified + // in other.properties (and not in the main config file) + assertEquals("main", + this.context.getEnvironment().getProperty("spring.application.name")); + // The parent is called "main" because spring.application.name is specified in + // other.properties (the bootstrap properties this time) + assertEquals( + "main", + this.context.getParent().getEnvironment() + .getProperty("spring.application.name")); + assertEquals("main", this.context.getId()); + } + + @Test + public void environmentEnrichedOnceWhenSharedWithChildContext() { + PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); + this.context = new SpringApplicationBuilder().sources(BareConfiguration.class) + .environment(new StandardEnvironment()).child(BareConfiguration.class) + .web(false).run(); + assertEquals("bar", this.context.getEnvironment().getProperty("bootstrap.foo")); + assertEquals(this.context.getEnvironment(), this.context.getParent() + .getEnvironment()); + MutablePropertySources sources = this.context.getEnvironment() + .getPropertySources(); + PropertySource bootstrap = sources.get("bootstrap"); + assertNotNull(bootstrap); + assertEquals(0, sources.precedenceOf(bootstrap)); + } + + @Test + public void environmentEnrichedInParentContext() { + PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); + this.context = new SpringApplicationBuilder().sources(BareConfiguration.class) + .child(BareConfiguration.class).web(false).run(); + assertEquals("bar", this.context.getEnvironment().getProperty("bootstrap.foo")); + assertNotSame(this.context.getEnvironment(), this.context.getParent() + .getEnvironment()); + assertTrue(this.context.getEnvironment().getPropertySources() + .contains("bootstrap")); + assertTrue(((ConfigurableEnvironment) this.context.getParent().getEnvironment()) + .getPropertySources().contains("bootstrap")); + } + + @Test + public void differentProfileInChild() { + PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); + // Profiles are always merged with the child + ConfigurableApplicationContext parent = new SpringApplicationBuilder() + .sources(BareConfiguration.class).profiles("parent").web(false).run(); + this.context = new SpringApplicationBuilder(BareConfiguration.class) + .profiles("child").parent(parent).web(false).run(); + assertNotSame(this.context.getEnvironment(), this.context.getParent() + .getEnvironment()); + // The ApplicationContext merges profiles (profiles and property sources), see + // AbstractEnvironment.merge() + assertTrue(this.context.getEnvironment().acceptsProfiles("child", "parent")); + // But the parent is not a child + assertFalse(this.context.getParent().getEnvironment().acceptsProfiles("child")); + assertTrue(this.context.getParent().getEnvironment().acceptsProfiles("parent")); + assertTrue(((ConfigurableEnvironment) this.context.getParent().getEnvironment()) + .getPropertySources().contains("bootstrap")); + assertEquals("bar", this.context.getEnvironment().getProperty("bootstrap.foo")); + // The "bootstrap" property source is not shared now, but it has the same + // properties in it because they are pulled from the PropertySourceConfiguration + // below + assertEquals("bar", + this.context.getParent().getEnvironment().getProperty("bootstrap.foo")); + // The parent property source is there in the child because they are both in the + // "parent" profile (by virtue of the merge in AbstractEnvironment) + assertEquals("parent", this.context.getEnvironment().getProperty("info.name")); + } + + @Configuration + @EnableConfigurationProperties + protected static class BareConfiguration { + } + + @Configuration + @ConfigurationProperties("expected") + // This is added to bootstrap context as a source in bootstrap.properties + protected static class PropertySourceConfiguration implements PropertySourceLocator { + + public static Map MAP = new HashMap( + Collections. singletonMap("bootstrap.foo", "bar")); + + private String name; + + private boolean fail = false; + + @Override + public PropertySource locate(Environment environment) { + if (this.name != null) { + assertEquals(this.name, + environment.getProperty("spring.application.name")); + } + if (this.fail) { + throw new RuntimeException("Planned"); + } + return new MapPropertySource("testBootstrap", MAP); + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isFail() { + return this.fail; + } + + public void setFail(boolean fail) { + this.fail = fail; + } + } + +} diff --git a/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfigurationTests.java b/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfigurationTests.java new file mode 100644 index 00000000..52c654ba --- /dev/null +++ b/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfigurationTests.java @@ -0,0 +1,24 @@ +package org.springframework.cloud.bootstrap.encrypt; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.security.crypto.encrypt.TextEncryptor; + +public class EncryptionBootstrapConfigurationTests { + + @Test + public void rsaKeyStore() { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + EncryptionBootstrapConfiguration.class).web(false).properties( + "encrypt.keyStore.location:classpath:/server.jks", + "encrypt.keyStore.password:letmein", + "encrypt.keyStore.alias:mytestkey", "encrypt.keyStore.secret:changeme") + .run(); + TextEncryptor encryptor = context.getBean(TextEncryptor.class); + assertEquals("foo", encryptor.decrypt(encryptor.encrypt("foo"))); + } + +} diff --git a/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationListenerTests.java b/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationListenerTests.java new file mode 100644 index 00000000..d9fb86ab --- /dev/null +++ b/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationListenerTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013-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.cloud.bootstrap.encrypt; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.security.crypto.encrypt.Encryptors; + +/** + * @author Dave Syer + * + */ +public class EnvironmentDecryptApplicationListenerTests { + + private EnvironmentDecryptApplicationInitializer listener = new EnvironmentDecryptApplicationInitializer(Encryptors.noOpText()); + + @Test + public void decryptCipherKey() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(context, "foo: {cipher}bar"); + listener.initialize(context); + assertEquals("bar", context.getEnvironment().getProperty("foo")); + } + + @Test(expected=IllegalStateException.class) + public void errorOnDecrypt() { + listener = new EnvironmentDecryptApplicationInitializer(Encryptors.text("deadbeef", "AFFE37")); + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(context, "foo: {cipher}bar"); + listener.initialize(context); + assertEquals("bar", context.getEnvironment().getProperty("foo")); + } + + @Test + public void errorOnDecryptWithEmpty() { + listener = new EnvironmentDecryptApplicationInitializer(Encryptors.text("deadbeef", "AFFE37")); + listener.setFailOnError(false); + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(context, "foo: {cipher}bar"); + listener.initialize(context); + // Empty is safest fallback for undecryptable cipher + assertEquals("", context.getEnvironment().getProperty("foo")); + } + +} diff --git a/src/test/java/org/springframework/cloud/context/environment/EnvironmentManagerIntegrationTests.java b/src/test/java/org/springframework/cloud/context/environment/EnvironmentManagerIntegrationTests.java new file mode 100644 index 00000000..d2f6f61b --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/environment/EnvironmentManagerIntegrationTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2006-2007 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.cloud.context.environment; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import javax.servlet.ServletException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.cloud.context.environment.EnvironmentManagerIntegrationTests.TestConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringApplicationConfiguration(classes = TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +public class EnvironmentManagerIntegrationTests { + + @Autowired + private TestProperties properties; + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @Before + public void setUp() { + this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + } + + @Test + public void testRefresh() throws Exception { + assertEquals("Hello scope!", properties.getMessage()); + // Change the dynamic property source... + this.mvc.perform(post("/env").param("message", "Foo")).andExpect(status().isOk()).andExpect( + content().string("{\"message\":\"Foo\"}")); + assertEquals("Foo", properties.getMessage()); + } + + @Test + public void testRefreshFails() throws Exception { + try { + this.mvc.perform(post("/env").param("delay", "foo")).andExpect( + status().is5xxServerError()); + fail("expected ServletException"); + } catch (ServletException e) { + // The underlying BindException is not handled by the dispatcher servlet + } + assertEquals(0, properties.getDelay()); + } + + public static void main(String[] args) { + SpringApplication.run(TestConfiguration.class, args); + } + + @Configuration + @EnableAutoConfiguration + protected static class TestConfiguration { + + @Bean + protected TestProperties properties() { + return new TestProperties(); + } + + } + + @ConfigurationProperties + protected static class TestProperties { + + private String message; + + private int delay; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getDelay() { + return delay; + } + + public void setDelay(int delay) { + this.delay = delay; + } + } + +} diff --git a/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java b/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java new file mode 100644 index 00000000..f7ec3697 --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2006-2007 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.cloud.context.properties; + +import static org.junit.Assert.assertEquals; + +import javax.annotation.PostConstruct; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinderIntegrationTests.TestConfiguration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@SpringApplicationConfiguration(classes=TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class ConfigurationPropertiesRebinderIntegrationTests { + + @Autowired + private TestProperties properties; + + @Autowired + private ConfigurationPropertiesRebinder rebinder; + + @Autowired + private ConfigurableEnvironment environment; + + @Test + @DirtiesContext + public void testSimpleProperties() throws Exception { + assertEquals("Hello scope!", properties.getMessage()); + // Change the dynamic property source... + EnvironmentTestUtils.addEnvironment(environment, "message:Foo"); + // ...but don't refresh, so the bean stays the same: + assertEquals("Hello scope!", properties.getMessage()); + assertEquals(1, properties.getCount()); + } + + @Test + @DirtiesContext + public void testRefresh() throws Exception { + assertEquals(1, properties.getCount()); + assertEquals("Hello scope!", properties.getMessage()); + // Change the dynamic property source... + EnvironmentTestUtils.addEnvironment(environment, "message:Foo"); + // ...and then refresh, so the bean is re-initialized: + rebinder.rebind(); + assertEquals("Foo", properties.getMessage()); + assertEquals(2, properties.getCount()); + } + + @Configuration + @EnableConfigurationProperties + @Import({RefreshAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class}) + protected static class TestConfiguration { + + @Bean + protected TestProperties properties() { + return new TestProperties(); + } + + } + + @ConfigurationProperties + protected static class TestProperties { + private String message; + private int delay; + private int count = 0; + public int getCount() { + return count; + } + public String getMessage() { + return message; + } + public void setMessage(String message) { + this.message = message; + } + public int getDelay() { + return delay; + } + public void setDelay(int delay) { + this.delay = delay; + } + @PostConstruct + public void init() { + this.count ++; + } + } + +} diff --git a/src/test/java/org/springframework/cloud/context/restart/RestartIntegrationTests.java b/src/test/java/org/springframework/cloud/context/restart/RestartIntegrationTests.java new file mode 100644 index 00000000..139d584b --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/restart/RestartIntegrationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2006-2007 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.cloud.context.restart; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; + +public class RestartIntegrationTests { + + private ConfigurableApplicationContext context; + + @After + public void close() { + if (context != null) { + context.close(); + } + } + + @Test + public void testRestartTwice() throws Exception { + + context = SpringApplication.run(TestConfiguration.class, "--endpoints.restart.enabled=true", "--server.port=0"); + RestartEndpoint endpoint = context.getBean(RestartEndpoint.class); + assertNotNull(context.getParent()); + assertNull(context.getParent().getParent()); + context = endpoint.restart(); + + assertNotNull(context); + assertNotNull(context.getParent()); + assertNull(context.getParent().getParent()); + + RestartEndpoint next = context.getBean(RestartEndpoint.class); + assertNotSame(endpoint, next); + context = next.restart(); + + assertNotNull(context); + assertNotNull(context.getParent()); + assertNull(context.getParent().getParent()); + + } + + public static void main(String[] args) { + SpringApplication.run(TestConfiguration.class, args); + } + + @Configuration + @EnableAutoConfiguration + protected static class TestConfiguration { + } + +} diff --git a/src/test/java/org/springframework/cloud/context/scope/refresh/ImportRefreshScopeIntegrationTests.java b/src/test/java/org/springframework/cloud/context/scope/refresh/ImportRefreshScopeIntegrationTests.java new file mode 100644 index 00000000..76cb9a74 --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/scope/refresh/ImportRefreshScopeIntegrationTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2006-2007 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.cloud.context.scope.refresh; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.scope.refresh.ImportRefreshScopeIntegrationTests.TestConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@SpringApplicationConfiguration(classes = TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class ImportRefreshScopeIntegrationTests { + + @Autowired + private ConfigurableListableBeanFactory beanFactory; + + @Autowired + private ExampleService service; + + @Autowired + private org.springframework.cloud.context.scope.refresh.RefreshScope scope; + + @Test + public void testSimpleProperties() throws Exception { + assertEquals("Hello scope!", service.getMessage()); + assertEquals("refresh", beanFactory.getBeanDefinition("scopedTarget.service").getScope()); + assertEquals("Hello scope!", service.getMessage()); + } + + @Configuration("service") + @RefreshScope + public static class ExampleService { + + public String getMessage() { + return "Hello scope!"; + } + + } + + @Configuration + @Import({ RefreshAutoConfiguration.class, ExampleService.class }) + protected static class TestConfiguration { + } + +} diff --git a/src/test/java/org/springframework/cloud/context/scope/refresh/MoreRefreshScopeIntegrationTests.java b/src/test/java/org/springframework/cloud/context/scope/refresh/MoreRefreshScopeIntegrationTests.java new file mode 100644 index 00000000..b4d54305 --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/scope/refresh/MoreRefreshScopeIntegrationTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2006-2007 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.cloud.context.scope.refresh; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.aop.framework.Advised; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.scope.refresh.MoreRefreshScopeIntegrationTests.TestConfiguration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@SpringApplicationConfiguration(classes = TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class MoreRefreshScopeIntegrationTests { + + @Autowired + private TestService service; + + @Autowired + private TestProperties properties; + + @Autowired + private org.springframework.cloud.context.scope.refresh.RefreshScope scope; + + @Autowired + private ConfigurableEnvironment environment; + + @Before + public void init() { + TestService.reset(); + } + + @Test + @DirtiesContext + public void testSimpleProperties() throws Exception { + assertEquals("Hello scope!", service.getMessage()); + assertTrue(service instanceof Advised); + // Change the dynamic property source... + EnvironmentTestUtils.addEnvironment(environment, "message:Foo"); + // ...but don't refresh, so the bean stays the same: + assertEquals("Hello scope!", service.getMessage()); + assertEquals(1, TestService.getInitCount()); + assertEquals(0, TestService.getDestroyCount()); + } + + @Test + @DirtiesContext + public void testRefresh() throws Exception { + assertEquals("Hello scope!", service.getMessage()); + String id1 = service.toString(); + // Change the dynamic property source... + EnvironmentTestUtils.addEnvironment(environment, "message:Foo"); + // ...and then refresh, so the bean is re-initialized: + scope.refreshAll(); + String id2 = service.toString(); + assertEquals("Foo", service.getMessage()); + assertEquals(2, TestService.getInitCount()); + assertEquals(1, TestService.getDestroyCount()); + assertNotSame(id1, id2); + } + + @Test + @DirtiesContext + public void testRefreshFails() throws Exception { + assertEquals("Hello scope!", service.getMessage()); + // Change the dynamic property source... + EnvironmentTestUtils.addEnvironment(environment, "message:Foo", "delay:foo"); + // ...and then refresh, so the bean is re-initialized: + scope.refreshAll(); + try { + // If a refresh fails (e.g. a binding error in this case) the application is + // basically hosed. + assertEquals("Hello scope!", service.getMessage()); + fail("expected BeanCreationException"); + } catch (BeanCreationException e) { + } + // But we can fix it by fixing the binding error: + EnvironmentTestUtils.addEnvironment(environment, "delay:0"); + // ...and then refresh, so the bean is re-initialized: + scope.refreshAll(); + assertEquals("Foo", service.getMessage()); + } + + public static class TestService implements InitializingBean, DisposableBean { + + private static Log logger = LogFactory.getLog(TestService.class); + + private volatile static int initCount = 0; + + private volatile static int destroyCount = 0; + + private String message = null; + + private volatile long delay = 0; + + public void setDelay(long delay) { + this.delay = delay; + } + + public void afterPropertiesSet() throws Exception { + logger.debug("Initializing message: " + message); + initCount++; + } + + public void destroy() throws Exception { + logger.debug("Destroying message: " + message); + destroyCount++; + message = null; + } + + public static void reset() { + initCount = 0; + destroyCount = 0; + } + + public static int getInitCount() { + return initCount; + } + + public static int getDestroyCount() { + return destroyCount; + } + + public void setMessage(String message) { + logger.debug("Setting message: " + message); + this.message = message; + } + + public String getMessage() { + logger.debug("Getting message: " + message); + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + logger.info("Returning message: " + message); + return message; + } + + } + + @Configuration + @EnableConfigurationProperties + @Import({ RefreshAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + protected static class TestConfiguration { + + @Bean + @RefreshScope + protected TestProperties properties() { + return new TestProperties(); + } + + @Bean + @RefreshScope + public TestService service() { + TestService service = new TestService(); + service.setMessage(properties().getMessage()); + service.setDelay(properties().getDelay()); + return service; + } + + } + + @ConfigurationProperties + @ManagedResource + protected static class TestProperties { + + private String message; + + private int delay; + + @ManagedAttribute + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @ManagedAttribute + public int getDelay() { + return delay; + } + + public void setDelay(int delay) { + this.delay = delay; + } + } + +} diff --git a/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshEndpointIntegrationTests.java b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshEndpointIntegrationTests.java new file mode 100644 index 00000000..ec321706 --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshEndpointIntegrationTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-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.cloud.context.scope.refresh; + +import static org.junit.Assert.assertEquals; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.scope.refresh.RefreshEndpointIntegrationTests.ClientApp; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Dave Syer + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = ClientApp.class) +@IntegrationTest("server.port:0") +@WebAppConfiguration +public class RefreshEndpointIntegrationTests { + + @Value("${local.server.port}") + private int port; + + @Test + public void webAccess() throws Exception { + TestRestTemplate template = new TestRestTemplate(); + template.exchange( + getUrlEncodedEntity("http://localhost:" + port + "/env", "message", + "Hello Dave!"), String.class); + template.postForObject("http://localhost:" + port + "/refresh", "", String.class); + String message = template.getForObject("http://localhost:" + port + "/", + String.class); + assertEquals("Hello Dave!", message); + } + + private RequestEntity getUrlEncodedEntity(String uri, String key, String value) + throws URISyntaxException { + MultiValueMap env = new LinkedMultiValueMap( + Collections.singletonMap("message", Arrays.asList("Hello Dave!"))); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + RequestEntity> entity = new RequestEntity>( + env, headers, HttpMethod.POST, new URI(uri)); + return entity; + } + + @Configuration + @EnableAutoConfiguration + protected static class ClientApp { + + @Bean + @RefreshScope + public Controller controller() { + return new Controller(); + } + + public static void main(String[] args) { + SpringApplication.run(ClientApp.class, args); + } + + } + + @RestController + protected static class Controller { + + @Value("${message:Hello World!}") + String message; + + @RequestMapping("/") + public String hello() { + return message; + } + + } + + +} diff --git a/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeConcurrencyTests.java b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeConcurrencyTests.java new file mode 100644 index 00000000..1aef5713 --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeConcurrencyTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2006-2007 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.cloud.context.scope.refresh; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.scope.refresh.RefreshScopeConcurrencyTests.TestConfiguration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.Repeat; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@SpringApplicationConfiguration(classes=TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class RefreshScopeConcurrencyTests { + + private static Log logger = LogFactory.getLog(RefreshScopeConcurrencyTests.class); + + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + @Autowired + private Service service; + + @Autowired + private TestProperties properties; + + @Autowired + private org.springframework.cloud.context.scope.refresh.RefreshScope scope; + + @Test + @Repeat(10) + @DirtiesContext + public void testConcurrentRefresh() throws Exception { + + assertEquals("Hello scope!", service.getMessage()); + properties.setMessage("Foo"); + properties.setDelay(500); + final CountDownLatch latch = new CountDownLatch(1); + Future result = executor.submit(new Callable() { + public String call() throws Exception { + logger.debug("Background started."); + try { + return service.getMessage(); + } finally { + latch.countDown(); + logger.debug("Background done."); + } + } + }); + assertTrue(latch.await(1500, TimeUnit.MILLISECONDS)); + logger.info("Refreshing"); + scope.refreshAll(); + assertEquals("Foo", service.getMessage()); + /* + * This is the most important assertion: we don't want a null value because that means the bean was destroyed + * and not re-initialized before we accessed it. + */ + assertNotNull(result.get()); + assertEquals("Hello scope!", result.get()); + } + + public static interface Service { + + String getMessage(); + + } + + public static class ExampleService implements Service, InitializingBean, DisposableBean { + + private static Log logger = LogFactory.getLog(ExampleService.class); + + private String message = null; + private volatile long delay = 0; + + public void setDelay(long delay) { + this.delay = delay; + } + + public void afterPropertiesSet() throws Exception { + logger.debug("Initializing message: " + message); + } + + public void destroy() throws Exception { + logger.debug("Destroying message: " + message); + message = null; + } + + public void setMessage(String message) { + logger.debug("Setting message: " + message); + this.message = message; + } + + public String getMessage() { + logger.debug("Getting message: " + message); + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + logger.info("Returning message: " + message); + return message; + } + + } + + @Configuration + @EnableConfigurationProperties(TestProperties.class) + @Import({RefreshAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class}) + protected static class TestConfiguration { + + @Autowired + private TestProperties properties; + + @Bean + @RefreshScope + public ExampleService service() { + ExampleService service = new ExampleService(); + service.setMessage(properties.getMessage()); + service.setDelay(properties.getDelay()); + return service; + } + + } + + @ConfigurationProperties + @ManagedResource + protected static class TestProperties { + private String message; + private int delay; + @ManagedAttribute + public String getMessage() { + return message; + } + public void setMessage(String message) { + this.message = message; + } + @ManagedAttribute + public int getDelay() { + return delay; + } + public void setDelay(int delay) { + this.delay = delay; + } + } + +} diff --git a/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeConfigurationTests.java b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeConfigurationTests.java new file mode 100644 index 00000000..b94f6cbc --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeConfigurationTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2013-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.cloud.context.scope.refresh; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.environment.EnvironmentManager; +import org.springframework.cloud.context.scope.refresh.RefreshScopeConfigurationTests.NestedApp.NestedController; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Dave Syer + * + */ +public class RefreshScopeConfigurationTests { + + private AnnotationConfigApplicationContext context; + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @After + public void init() { + if (context!=null) { + context.close(); + } + } + + private void refresh() { + EnvironmentManager environmentManager = context.getBean(EnvironmentManager.class); + environmentManager.setProperty("message", "Hello Dave!"); + org.springframework.cloud.context.scope.refresh.RefreshScope scope = context.getBean(org.springframework.cloud.context.scope.refresh.RefreshScope.class); + scope.refreshAll(); + } + + /** + * See gh-43 + */ + @Test + public void configurationWithRefreshScope() throws Exception { + context = new AnnotationConfigApplicationContext(Application.class, + PropertyPlaceholderAutoConfiguration.class, RefreshAutoConfiguration.class); + Application application = context.getBean(Application.class); + assertEquals("refresh", context.getBeanDefinition("scopedTarget.application").getScope()); + application.hello(); + refresh(); + String message = application.hello(); + assertEquals("Hello Dave!", message); + } + + @Test + public void refreshScopeOnBean() throws Exception { + context = new AnnotationConfigApplicationContext(ClientApp.class, + PropertyPlaceholderAutoConfiguration.class, RefreshAutoConfiguration.class); + Controller application = context.getBean(Controller.class); + application.hello(); + refresh(); + String message = application.hello(); + assertEquals("Hello Dave!", message); + } + + @Test + public void refreshScopeOnNested() throws Exception { + context = new AnnotationConfigApplicationContext(NestedApp.class, + PropertyPlaceholderAutoConfiguration.class, RefreshAutoConfiguration.class); + NestedController application = context.getBean(NestedController.class); + application.hello(); + refresh(); + String message = application.hello(); + assertEquals("Hello Dave!", message); + } + + // WTF? Maven can't compile without the FQN on this one (not the others). + @org.springframework.context.annotation.Configuration + protected static class NestedApp { + + @RestController + @RefreshScope + protected static class NestedController { + + @Value("${message:Hello World!}") + String message; + + @RequestMapping("/") + public String hello() { + return message; + } + + } + + public static void main(String[] args) { + SpringApplication.run(ClientApp.class, args); + } + + } + + @Configuration("application") + @RefreshScope + protected static class Application { + + @Value("${message:Hello World!}") + String message = "Hello World"; + + @RequestMapping("/") + public String hello() { + return message; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + @Configuration + protected static class ClientApp { + + @Bean + @RefreshScope + public Controller controller() { + return new Controller(); + } + + public static void main(String[] args) { + SpringApplication.run(ClientApp.class, args); + } + + } + + @RestController + protected static class Controller { + + @Value("${message:Hello World!}") + String message; + + @RequestMapping("/") + public String hello() { + return message; + } + + } + +} diff --git a/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeIntegrationTests.java b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeIntegrationTests.java new file mode 100644 index 00000000..e540a779 --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeIntegrationTests.java @@ -0,0 +1,229 @@ +/* + * Copyright 2006-2007 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.cloud.context.scope.refresh; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.aop.framework.Advised; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.scope.GenericScope; +import org.springframework.cloud.context.scope.refresh.RefreshScopeIntegrationTests.TestConfiguration; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.*; + +@SpringApplicationConfiguration(classes = TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class RefreshScopeIntegrationTests { + + @Autowired + private Service service; + + @Autowired + private TestProperties properties; + + @Autowired + private org.springframework.cloud.context.scope.refresh.RefreshScope scope; + + @Before + public void init() { + ExampleService.reset(); + } + + @Test + @DirtiesContext + public void testSimpleProperties() throws Exception { + assertEquals("Hello scope!", service.getMessage()); + assertTrue(service instanceof Advised); + // Change the dynamic property source... + properties.setMessage("Foo"); + // ...but don't refresh, so the bean stays the same: + assertEquals("Hello scope!", service.getMessage()); + assertEquals(1, ExampleService.getInitCount()); + assertEquals(0, ExampleService.getDestroyCount()); + } + + @Test + @DirtiesContext + public void testRefresh() throws Exception { + assertEquals("Hello scope!", service.getMessage()); + String id1 = service.toString(); + // Change the dynamic property source... + properties.setMessage("Foo"); + // ...and then refresh, so the bean is re-initialized: + scope.refreshAll(); + String id2 = service.toString(); + assertEquals("Foo", service.getMessage()); + assertEquals(2, ExampleService.getInitCount()); + assertEquals(1, ExampleService.getDestroyCount()); + assertNotSame(id1, id2); + assertNotNull(ExampleService.event); + assertEquals(RefreshScopeRefreshedEvent.DEFAULT_NAME, + ExampleService.event.getName()); + } + + @Test + @DirtiesContext + public void testRefreshBean() throws Exception { + assertEquals("Hello scope!", service.getMessage()); + String id1 = service.toString(); + // Change the dynamic property source... + properties.setMessage("Foo"); + // ...and then refresh, so the bean is re-initialized: + scope.refresh("service"); + String id2 = service.toString(); + assertEquals("Foo", service.getMessage()); + assertEquals(2, ExampleService.getInitCount()); + assertEquals(1, ExampleService.getDestroyCount()); + assertNotSame(id1, id2); + assertNotNull(ExampleService.event); + assertEquals(GenericScope.SCOPED_TARGET_PREFIX + "service", + ExampleService.event.getName()); + } + + public static interface Service { + + String getMessage(); + + } + + public static class ExampleService implements Service, InitializingBean, + DisposableBean, ApplicationListener { + + private static Log logger = LogFactory.getLog(ExampleService.class); + + private volatile static int initCount = 0; + private volatile static int destroyCount = 0; + private volatile static RefreshScopeRefreshedEvent event; + + private String message = null; + private volatile long delay = 0; + + public void setDelay(long delay) { + this.delay = delay; + } + + public void afterPropertiesSet() throws Exception { + logger.debug("Initializing message: " + message); + initCount++; + } + + public void destroy() throws Exception { + logger.debug("Destroying message: " + message); + destroyCount++; + message = null; + } + + public static void reset() { + initCount = 0; + destroyCount = 0; + event = null; + } + + public static int getInitCount() { + return initCount; + } + + public static int getDestroyCount() { + return destroyCount; + } + + public void setMessage(String message) { + logger.debug("Setting message: " + message); + this.message = message; + } + + public String getMessage() { + logger.debug("Getting message: " + message); + try { + Thread.sleep(delay); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + logger.info("Returning message: " + message); + return message; + } + + @Override + public void onApplicationEvent(RefreshScopeRefreshedEvent e) { + event = e; + } + } + + @Configuration + @EnableConfigurationProperties(TestProperties.class) + @Import({ RefreshAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + protected static class TestConfiguration { + + @Autowired + private TestProperties properties; + + @Bean + @RefreshScope + public ExampleService service() { + ExampleService service = new ExampleService(); + service.setMessage(properties.getMessage()); + service.setDelay(properties.getDelay()); + return service; + } + + } + + @ConfigurationProperties + @ManagedResource + protected static class TestProperties { + private String message; + private int delay; + + @ManagedAttribute + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @ManagedAttribute + public int getDelay() { + return delay; + } + + public void setDelay(int delay) { + this.delay = delay; + } + } + +} diff --git a/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeWebIntegrationTests.java b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeWebIntegrationTests.java new file mode 100644 index 00000000..8bce8fa3 --- /dev/null +++ b/src/test/java/org/springframework/cloud/context/scope/refresh/RefreshScopeWebIntegrationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2013-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.cloud.context.scope.refresh; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.environment.EnvironmentManager; +import org.springframework.cloud.context.scope.refresh.RefreshScopeWebIntegrationTests.Application; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Dave Syer + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Application.class) +public class RefreshScopeWebIntegrationTests { + + @Autowired + private org.springframework.cloud.context.scope.refresh.RefreshScope scope; + + @Autowired + private EnvironmentManager environmentManager; + + @Autowired + private Client application; + + @Autowired + private ConfigurableListableBeanFactory beanFactory; + + @Test + public void scopeOnBeanDefinition() throws Exception { + assertEquals("refresh", beanFactory.getBeanDefinition("scopedTarget.application").getScope()); + } + + @Test + public void beanAccess() throws Exception { + application.hello(); + environmentManager.setProperty("message", "Hello Dave!"); + scope.refreshAll(); + String message = application.hello(); + assertEquals("Hello Dave!", message); + } + + @Configuration + @EnableAutoConfiguration + protected static class Application { + + @Bean + @RefreshScope + public Client application() { + return new Client(); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + @RestController + protected static class Client { + + @Value("${message:Hello World!}") + String message; + + @RequestMapping("/") + public String hello() { + return message; + } + + } + +} diff --git a/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java b/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java new file mode 100644 index 00000000..c8b1e14d --- /dev/null +++ b/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-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.cloud.logging; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; + +import org.junit.After; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; + +/** + * @author Dave Syer + * + */ +public class LoggingRebinderTests { + + private LoggingRebinder rebinder = new LoggingRebinder(); + private Logger logger = LoggerFactory.getLogger("org.springframework.web"); + + @After + public void reset() { + LoggingSystem.get(getClass().getClassLoader()).setLogLevel("org.springframework.web", LogLevel.INFO); + } + + @Test + public void logLevelsChanged() { + assertFalse(logger.isTraceEnabled()); + StandardEnvironment environment = new StandardEnvironment(); + EnvironmentTestUtils.addEnvironment(environment, "logging.level.org.springframework.web=TRACE"); + rebinder.setEnvironment(environment); + rebinder.onApplicationEvent(new EnvironmentChangeEvent(Collections.singleton("logging.level.org.springframework.web"))); + assertTrue(logger.isTraceEnabled()); + } + +} diff --git a/src/test/resources/application-encrypt.properties b/src/test/resources/application-encrypt.properties new file mode 100644 index 00000000..b1329f64 --- /dev/null +++ b/src/test/resources/application-encrypt.properties @@ -0,0 +1 @@ +foo: {cipher}e4e061f9fe39ba5b14d8012d2f17d39775606039409b71ed4be0fdd033d5324a diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 00000000..961e7d22 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,5 @@ +message: Hello scope! +delay: 0 +debug: true +#logging.level.org.springframework.web: DEBUG +#logging.level.org.springframework.context.annotation: DEBUG \ No newline at end of file diff --git a/src/test/resources/bootstrap-encrypt.properties b/src/test/resources/bootstrap-encrypt.properties new file mode 100644 index 00000000..3035e009 --- /dev/null +++ b/src/test/resources/bootstrap-encrypt.properties @@ -0,0 +1 @@ +bar: {cipher}6154ca04d4bb6144d672c4e3d750b5147116dd381946d51fa44f8bc25dc256f4 \ No newline at end of file diff --git a/src/test/resources/bootstrap-parent.properties b/src/test/resources/bootstrap-parent.properties new file mode 100644 index 00000000..0c36bbdd --- /dev/null +++ b/src/test/resources/bootstrap-parent.properties @@ -0,0 +1 @@ +info.name: parent \ No newline at end of file diff --git a/src/test/resources/bootstrap.properties b/src/test/resources/bootstrap.properties new file mode 100644 index 00000000..9ec10bf9 --- /dev/null +++ b/src/test/resources/bootstrap.properties @@ -0,0 +1,2 @@ +spring.main.sources: org.springframework.cloud.bootstrap.config.BootstrapConfigurationTests.PropertySourceConfiguration +info.name: child diff --git a/src/test/resources/external-properties/bootstrap.properties b/src/test/resources/external-properties/bootstrap.properties new file mode 100644 index 00000000..925265df --- /dev/null +++ b/src/test/resources/external-properties/bootstrap.properties @@ -0,0 +1 @@ +info.name: externalPropertiesInfoName \ No newline at end of file diff --git a/src/test/resources/other.properties b/src/test/resources/other.properties new file mode 100644 index 00000000..dc756282 --- /dev/null +++ b/src/test/resources/other.properties @@ -0,0 +1 @@ +spring.application.name: main \ No newline at end of file diff --git a/src/test/resources/plain.properties b/src/test/resources/plain.properties new file mode 100644 index 00000000..aa8f5408 --- /dev/null +++ b/src/test/resources/plain.properties @@ -0,0 +1 @@ +spring.application.name: app diff --git a/src/test/resources/server.jks b/src/test/resources/server.jks new file mode 100644 index 00000000..560be5fe Binary files /dev/null and b/src/test/resources/server.jks differ