commit c6cee8a8ccc7cb1795bea96a016415eac0df0e15 Author: Dave Syer Date: Fri Jun 13 14:44:18 2014 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..84506aac --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*~ +.#* +*# +target +bin +.classpath +.project +.settings +.springBeans +.DS_Store +*.sw* diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..697a0e1b --- /dev/null +++ b/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + org.springframework.platform + spring-platform-config + 1.0.0.BUILD-SNAPSHOT + pom + Spring Platform Config + Spring Platform Config + http://projects.spring.io/spring-platform/ + + org.springframework.boot + spring-boot-starter-parent + 1.1.2.BUILD-SNAPSHOT + + + + Pivotal Software, Inc. + http://www.spring.io + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + + + https://github.com/spring-platform/spring-platform-config + + + + dsyer + Dave Syer + dsyer at gopivotal.com + Pivotal Software, Inc. + http://www.spring.io + + Project lead + + + + + 3.0.0 + + + ${basedir} + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + org.apache.maven.plugins + maven-surefire-plugin + 2.17 + + + com.google.code.maven-replacer-plugin + replacer + 1.5.3 + + + + + + + + default + + true + + + spring-platform-config-client + spring-platform-config-server + + + + full + + + diff --git a/spring-platform-config-client/README.md b/spring-platform-config-client/README.md new file mode 100644 index 00000000..d6329124 --- /dev/null +++ b/spring-platform-config-client/README.md @@ -0,0 +1,126 @@ +# Spring Platform Components + +This project contains boilerplate patterns for distributed application +components, making those patterns easy to implement with Spring (and +Spring Boot in particular). + +## Spring Boot Features + +**Bootstrap Listener** - A `SpringApplication` with the platform +components on its classpath will have an extended bootstrap phase, +allowing new strategies for configuration management and other +non-functional features to be added to the application before it is +initialized. The implementation is similar to +`@EnableAutoConfiguration`, allowing new jars to contribute additional +features by defining `BootstrapConfiguration` classes in the form of +`@Configuration`. Instead of being added to the application's context +they are added to a special "bootstrap" context, which is then used to +extract `ApplicationContextInitializers` to apply to the application +before it is created. A typical use case would be to locate and +install additional environment properties so that the application can +adapt to its environment in a completely flexible way. + +**Autoconfiguration** - An application using +`@EnableAutoConfiguration` will pick up beans for the components +below, notably `EnvironmentManager` and `RefreshScope`. + +## EnvironmentManager and EnvironmentManagerEndpoint + +The `EnvironmentManager` allows you to modify the Spring `Environment` +in a running application. It adds a `PropertySource` with high +priority to the `Environment` (so properties set there will take +precedence). If you have the Actuator and are in a webapp then the +`EnvironmentManagerEndpoint` will also expose "/env" with POST for +updating the `EnvironmentManager` remotely. E.g. run the app locally: + +``` +@Configuration +@EnableAutoConfiguration +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +then interact with the `Environment`: + +``` +$ curl localhost:8080/env/message +Hello +$ curl localhost:8080/env -d message=Foo +$ curl localhost:8080/env/message +Foo +``` + +Doing this (or the equivalent operation over JMX) will trigger an event +that leads to rebinding of `@ConfigurationProperties` (see below). + +## ConfigurationPropertiesRebinder + +Spring beans that are `@ConfigurationProperties` can be rebound if the `Environment` changes. Example: + +``` + @ConfigurationProperties + protected static class ServiceProperties { + private String message; + public String getMessage() { + return message; + } + public void setMessage(String message) { + this.message = message; + } + } +``` + +then inject `ServiceProperties` into an application component using `@Autowired` and use it to govern the behaviour. +When the `Environment` changes do this + +``` + @Autowired + private ConfigurationPropertiesRebinder rebinder; + + ... + + rebinder.rebind(); + +``` + +The `ConfigurationPropertiesRebinder` is also a `@ManagedResource` so you can ping the `rebind()` operation remotely. + +## RefreshScope + +Spring beans in scope="refresh" are re-initialized on the next method call after a refresh. Example: + +``` + @Configuration + @EnableConfigurationProperties(TestProperties.class) + @EnableAutoConfiguration + 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; + } + + } +``` + +then update the `TestProperties` (e.g. via JMX or something), and + +``` + @Autowired + private RefreshScope scope; + +... + + // ...and then refresh, so the bean is re-initialized: + scope.refresh("service"); +``` diff --git a/spring-platform-config-client/pom.xml b/spring-platform-config-client/pom.xml new file mode 100644 index 00000000..3eca7d3d --- /dev/null +++ b/spring-platform-config-client/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + spring-platform-config-client + jar + Spring Platform Config Client + + + org.springframework.platform + spring-platform-config + 1.0.0.BUILD-SNAPSHOT + .. + + + http://spring.io + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-aop + true + + + org.springframework.boot + spring-boot-starter-web + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + + diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/autoconfigure/RefreshAutoConfiguration.java b/spring-platform-config-client/src/main/java/org/springframework/platform/autoconfigure/RefreshAutoConfiguration.java new file mode 100644 index 00000000..075aaece --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/autoconfigure/RefreshAutoConfiguration.java @@ -0,0 +1,83 @@ +/* + * 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.platform.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.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.platform.context.environment.EnvironmentManager; +import org.springframework.platform.context.environment.EnvironmentManagerMvcEndpoint; +import org.springframework.platform.context.properties.ConfigurationPropertiesRebinder; +import org.springframework.platform.context.scope.refresh.RefreshScope; + +@Configuration +@ConditionalOnClass(RefreshScope.class) +@AutoConfigureAfter(EndpointAutoConfiguration.class) +public class RefreshAutoConfiguration { + + @Autowired + private ConfigurationPropertiesBindingPostProcessor binder; + + @Autowired + private ConfigurationBeanFactoryMetaData metaData; + + @Bean + @ConditionalOnMissingBean + public static RefreshScope refreshScope() { + return new RefreshScope(); + } + + @Bean + @ConditionalOnMissingBean + public EnvironmentManager environmentManager(ConfigurableEnvironment environment) { + return new EnvironmentManager(environment); + } + + @Bean + @ConditionalOnMissingBean + public ConfigurationPropertiesRebinder configurationPropertiesRebinder() { + ConfigurationPropertiesRebinder rebinder = new ConfigurationPropertiesRebinder(binder); + rebinder.setBeanMetaDataStore(metaData); + return rebinder; + } + + @Configuration + @ConditionalOnWebApplication + @ConditionalOnClass(EnvironmentEndpoint.class) + @ConditionalOnExpression("${endpoints.env.enabled:true}") + @ConditionalOnBean(EnvironmentEndpoint.class) + protected static class EnvironmentEndpointConfiguration { + + @Bean + public EnvironmentManagerMvcEndpoint environmentManagerEndpoint(EnvironmentEndpoint delegate, EnvironmentManager environment) { + return new EnvironmentManagerMvcEndpoint(delegate, environment); + } + + } + +} diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/BootstrapApplicationListener.java b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/BootstrapApplicationListener.java new file mode 100644 index 00000000..0aff9bad --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/BootstrapApplicationListener.java @@ -0,0 +1,135 @@ +/* + * 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.platform.bootstrap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.boot.SpringApplication; +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.context.event.ContextClosedEvent; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +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 org.springframework.core.io.support.SpringFactoriesLoader; + +/** + * 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 int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 5; + + private int order = DEFAULT_ORDER; + + @Override + public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { + Environment environment = event.getEnvironment(); + if (environment instanceof ConfigurableEnvironment) { + ConfigurableEnvironment configurable = (ConfigurableEnvironment) environment; + // don't listen to events in a bootstrap context + if (configurable.getPropertySources().contains("bootstrap")) { + return; + } + ConfigurableApplicationContext context = bootstrapServiceContext( + configurable, event.getSpringApplication()); + apply(context, event.getSpringApplication(), configurable); + } + } + + private ConfigurableApplicationContext bootstrapServiceContext( + ConfigurableEnvironment environment, final SpringApplication application) { + StandardEnvironment bootstrapEnvironment = new StandardEnvironment(); + MutablePropertySources bootstrapProperties = bootstrapEnvironment.getPropertySources(); + for (PropertySource source : bootstrapProperties) { + bootstrapProperties.remove(source.getName()); + } + for (PropertySource source : environment.getPropertySources()) { + bootstrapProperties.addLast(source); + } + bootstrapProperties.addFirst(new MapPropertySource("bootstrap", + Collections. singletonMap("spring.config.name", + "bootstrap"))); + 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().showBanner( + false).environment(bootstrapEnvironment).web(false).properties( + "spring.application.name:bootstrap"); + builder.sources(names.toArray()); + final ConfigurableApplicationContext context = builder.run(); + // Shutdown the bootstrap context when the app closes + // TODO: maybe make the bootstrap context a parent of the app context + application.addListeners(new ApplicationListener() { + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + context.close(); + } + }); + return context; + } + + private void apply(ConfigurableApplicationContext context, + SpringApplication application, ConfigurableEnvironment environment) { + @SuppressWarnings("rawtypes") + List initializers = getOrderedBeansOfType(context, + ApplicationContextInitializer.class); + application.addInitializers(initializers.toArray(new ApplicationContextInitializer[0])); + } + + 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; + } + +} diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/BootstrapConfiguration.java b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/BootstrapConfiguration.java new file mode 100644 index 00000000..49b5fc0c --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/BootstrapConfiguration.java @@ -0,0 +1,32 @@ +/* + * 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.platform.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; + + +@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/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/config/ConfigServiceBootstrapConfiguration.java b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/config/ConfigServiceBootstrapConfiguration.java new file mode 100644 index 00000000..a16102a6 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/config/ConfigServiceBootstrapConfiguration.java @@ -0,0 +1,78 @@ +/* + * 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.platform.bootstrap.config; + +import java.util.ArrayList; +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.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.PropertySource; + +/** + * @author Dave Syer + * + */ +@Configuration +@EnableConfigurationProperties +public class ConfigServiceBootstrapConfiguration implements + ApplicationContextInitializer { + + private static Log logger = LogFactory.getLog(ConfigServiceBootstrapConfiguration.class); + + @Autowired(required = false) + private List propertySourceLocators = new ArrayList(); + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + CompositePropertySource composite = new CompositePropertySource("bootstrap"); + AnnotationAwareOrderComparator.sort(propertySourceLocators); + boolean empty = true; + for (PropertySourceLocator locator : propertySourceLocators) { + PropertySource source = null; + try { + source = locator.locate(); + } catch (Exception e) { + logger.error("Could not locate PropertySource: " + e.getMessage()); + } + if (source == null) { + continue; + } + composite.addPropertySource(source); + empty = false; + } + if (!empty) { + applicationContext.getEnvironment().getPropertySources().addFirst(composite); + } + } + + @Bean + @ConfigurationProperties("spring.platform.config") + public ConfigServicePropertySourceLocator configServicePropertySource() { + return new ConfigServicePropertySourceLocator(); + } + +} diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/config/ConfigServicePropertySourceLocator.java b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/config/ConfigServicePropertySourceLocator.java new file mode 100644 index 00000000..cd1861e9 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/config/ConfigServicePropertySourceLocator.java @@ -0,0 +1,121 @@ +/* + * 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.platform.bootstrap.config; + +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Dave Syer + * + */ +public class ConfigServicePropertySourceLocator implements PropertySourceLocator { + + private String env = "development"; + + @Value("${spring.application.name:'application'}") + private String name; + + private String label = "master"; + + private String url = "http://localhost:8888"; + + private RestTemplate restTemplate = new RestTemplate(); + + @Override + public PropertySource locate() { + CompositePropertySource composite = new CompositePropertySource("configService"); + List result = restTemplate.exchange( + url + "/{name}/{env}/{label}", HttpMethod.GET, + new HttpEntity((Void) null), + new ParameterizedTypeReference>() { + }, name, env, label).getBody(); + for (SerializableMapPropertySource source : result) { + @SuppressWarnings("unchecked") + Map map = (Map) source.getSource(); + composite.addPropertySource(new MapPropertySource(source.getName(), map)); + } + return composite; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEnv() { + return env; + } + + public void setEnv(String env) { + this.env = env; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + protected static class SerializableMapPropertySource { + + private String name; + + private Map source; + + @JsonCreator + public SerializableMapPropertySource(@JsonProperty("name") String name, + @JsonProperty("source") Map source) { + this.name = name; + this.source = source; + } + + public String getName() { + return name; + } + + public Map getSource() { + return source; + } + + } +} diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/config/PropertySourceLocator.java b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/config/PropertySourceLocator.java new file mode 100644 index 00000000..fc4cf294 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/bootstrap/config/PropertySourceLocator.java @@ -0,0 +1,29 @@ +/* + * 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.platform.bootstrap.config; + +import org.springframework.core.env.PropertySource; + + +/** + * @author Dave Syer + * + */ +public interface PropertySourceLocator { + + PropertySource locate(); + +} diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/context/config/BeanLifecycleDecorator.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/config/BeanLifecycleDecorator.java new file mode 100644 index 00000000..1cdf49b1 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/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.platform.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/spring-platform-config-client/src/main/java/org/springframework/platform/context/config/StandardBeanLifecycleDecorator.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/config/StandardBeanLifecycleDecorator.java new file mode 100644 index 00000000..ca6abbc7 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/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.platform.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/spring-platform-config-client/src/main/java/org/springframework/platform/context/config/annotation/RefreshScope.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/config/annotation/RefreshScope.java new file mode 100644 index 00000000..6ce9676b --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/context/config/annotation/RefreshScope.java @@ -0,0 +1,37 @@ +/* + * 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.platform.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; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Scope("refresh") +@Documented +public @interface RefreshScope { + /** + * @see Scope#proxyMode() + */ + ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT; + +} diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/context/environment/EnvironmentChangeEvent.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/environment/EnvironmentChangeEvent.java new file mode 100644 index 00000000..7ddd6e80 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/context/environment/EnvironmentChangeEvent.java @@ -0,0 +1,43 @@ +/* + * 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.platform.context.environment; + +import java.util.Set; + +import org.springframework.context.ApplicationEvent; + +/** + * @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/spring-platform-config-client/src/main/java/org/springframework/platform/context/environment/EnvironmentManager.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/environment/EnvironmentManager.java new file mode 100644 index 00000000..b01b9e0a --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/context/environment/EnvironmentManager.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.platform.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.MapPropertySource; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.stereotype.Component; + +/** + * @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; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @ManagedOperation + public void reset() { + if (!map.isEmpty()) { + Set keys = map.keySet(); + map.clear(); + publish(new EnvironmentChangeEvent(keys)); + } + } + + @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/spring-platform-config-client/src/main/java/org/springframework/platform/context/environment/EnvironmentManagerMvcEndpoint.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/environment/EnvironmentManagerMvcEndpoint.java new file mode 100644 index 00000000..c65246cf --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/context/environment/EnvironmentManagerMvcEndpoint.java @@ -0,0 +1,70 @@ +/* + * 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.platform.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; + +/** + * @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; + } + + 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/spring-platform-config-client/src/main/java/org/springframework/platform/context/properties/ConfigurationPropertiesRebinder.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/properties/ConfigurationPropertiesRebinder.java new file mode 100644 index 00000000..1732a24f --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/context/properties/ConfigurationPropertiesRebinder.java @@ -0,0 +1,108 @@ +/* + * 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.platform.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.context.ApplicationListener; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.platform.context.environment.EnvironmentChangeEvent; +import org.springframework.stereotype.Component; + +/** + * @author Dave Syer + * + */ +@Component +@ManagedResource +public class ConfigurationPropertiesRebinder implements BeanPostProcessor, ApplicationListener { + + private ConfigurationBeanFactoryMetaData metaData; + + private ConfigurationPropertiesBindingPostProcessor binder; + + public ConfigurationPropertiesRebinder( + ConfigurationPropertiesBindingPostProcessor binder) { + this.binder = binder; + } + + private Map beans = new HashMap(); + + /** + * @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); + } + + @ManagedAttribute + public Set getBeanNames() { + return new HashSet(beans.keySet()); + } + + @Override + public void onApplicationEvent(EnvironmentChangeEvent event) { + rebind(); + } + +} diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/GenericScope.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/GenericScope.java new file mode 100644 index 00000000..b6a04c49 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/GenericScope.java @@ -0,0 +1,490 @@ +/* + * 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.platform.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.aop.scope.ScopedProxyUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.BeanDefinitionVisitor; +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.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.expression.BeanFactoryAccessor; +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.platform.context.config.BeanLifecycleDecorator; +import org.springframework.platform.context.config.BeanLifecycleDecorator.Context; +import org.springframework.platform.context.config.StandardBeanLifecycleDecorator; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; + +/** + *

+ * 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); + + private BeanLifecycleWrapperCache cache = new BeanLifecycleWrapperCache( + new StandardScopeCache()); + + private String name = "generic"; + + private boolean proxyTargetClass = true; + + private boolean autoProxy = 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; + } + + /** + * Flag to indicate that all scoped beans should automatically be proxied. + * If true then scoped beans can be injected as dependencies of another + * component and the concrete target will only be instantiated when it is + * used. Proxying is a huge advantage if the context storage for the scope + * cache is not available at configuration time (e.g. for thread-based, or + * other transient scopes). If this flag is false you can expect maybe to + * have to add extra meta-data to the bean definitions individually (e.g. + * <aop:scoped-proxy/> for an XML configuration). + * + * @param autoProxy + * the flag value to set, default is true + */ + public void setAutoProxy(boolean autoProxy) { + this.autoProxy = autoProxy; + } + + /** + * 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; + } + + public void destroy() { + List errors = new ArrayList(); + Collection wrappers = 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 = cache.remove(name); + if (wrapper != null) { + wrapper.destroy(); + } + } + + public Object get(String name, ObjectFactory objectFactory) { + if (lifecycle == null) { + lifecycle = new StandardBeanLifecycleDecorator(proxyTargetClass); + } + BeanLifecycleWrapper value = cache.put(name, new BeanLifecycleWrapper( + name, objectFactory, lifecycle)); + return value.getBean(); + } + + public String getConversationId() { + return name; + } + + public void registerDestructionCallback(String name, Runnable callback) { + BeanLifecycleWrapper value = cache.get(name); + if (value == null) { + return; + } + value.setDestroyCallback(callback); + } + + public Object remove(String name) { + BeanLifecycleWrapper value = 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(); + } + + public Object resolveContextualObject(String key) { + Expression expression = parseExpression(key); + return expression.getValue(evaluationContext, 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; + } + } + + public void postProcessBeanFactory( + ConfigurableListableBeanFactory beanFactory) throws BeansException { + + beanFactory.registerScope(name, this); + setSerializationId(beanFactory); + + this.beanFactory = beanFactory; + + evaluationContext = new StandardEvaluationContext(); + evaluationContext.addPropertyAccessor(new BeanFactoryAccessor()); + + if (!autoProxy) { + // No need to try and create proxies + return; + } + + Assert.state(beanFactory instanceof BeanDefinitionRegistry, + "BeanFactory was not a BeanDefinitionRegistry, so RefreshScope cannot be used."); + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(beanName); + // Replace this or any of its inner beans with scoped proxy if it + // has this scope + boolean scoped = name.equals(definition.getScope()); + Scopifier scopifier = new Scopifier(registry, name, + proxyTargetClass, scoped); + scopifier.visitBeanDefinition(definition); + if (scoped) { + createScopedProxy(beanName, definition, registry, + proxyTargetClass); + } + } + + } + + /** + * 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 BeanDefinitionHolder createScopedProxy(String beanName, + BeanDefinition definition, BeanDefinitionRegistry registry, + boolean proxyTargetClass) { + BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy( + new BeanDefinitionHolder(definition, beanName), registry, + proxyTargetClass); + registry.registerBeanDefinition(beanName, + proxyHolder.getBeanDefinition()); + return proxyHolder; + } + + /** + * Helper class to scan a bean definition hierarchy and force the use of + * auto-proxy for scoped beans. + * + * @author Dave Syer + * + */ + private static class Scopifier extends BeanDefinitionVisitor { + + private final boolean proxyTargetClass; + + private final BeanDefinitionRegistry registry; + + private final String scope; + + private final boolean scoped; + + public Scopifier(BeanDefinitionRegistry registry, String scope, + boolean proxyTargetClass, boolean scoped) { + super(new StringValueResolver() { + public String resolveStringValue(String value) { + return value; + } + }); + this.registry = registry; + this.proxyTargetClass = proxyTargetClass; + this.scope = scope; + this.scoped = scoped; + } + + @Override + protected Object resolveValue(Object value) { + + BeanDefinition definition = null; + String beanName = null; + if (value instanceof BeanDefinition) { + definition = (BeanDefinition) value; + beanName = BeanDefinitionReaderUtils.generateBeanName( + definition, registry); + } else if (value instanceof BeanDefinitionHolder) { + BeanDefinitionHolder holder = (BeanDefinitionHolder) value; + definition = holder.getBeanDefinition(); + beanName = holder.getBeanName(); + } + + if (definition != null) { + boolean nestedScoped = scope.equals(definition.getScope()); + boolean scopeChangeRequiresProxy = !scoped && nestedScoped; + if (scopeChangeRequiresProxy) { + // Exit here so that nested inner bean definitions are not + // analysed + return createScopedProxy(beanName, definition, registry, + proxyTargetClass); + } + } + + // Nested inner bean definitions are recursively analysed here + value = super.resolveValue(value); + return value; + + } + + } + + private static class BeanLifecycleWrapperCache { + + private final ScopeCache cache; + + public BeanLifecycleWrapperCache(ScopeCache cache) { + this.cache = cache; + } + + public BeanLifecycleWrapper remove(String name) { + return (BeanLifecycleWrapper) cache.remove(name); + } + + public Collection clear() { + Collection values = cache.clear(); + Collection wrappers = new LinkedHashSet(); + for (Object object : values) { + wrappers.add((BeanLifecycleWrapper) object); + } + return wrappers; + } + + public BeanLifecycleWrapper get(String name) { + return (BeanLifecycleWrapper) cache.get(name); + } + + public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) { + return (BeanLifecycleWrapper) cache.put(name, (Object) 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 = lifecycle.decorateDestructionCallback(callback); + } + + @SuppressWarnings("unchecked") + public Object getBean() { + if (bean == null) { + bean = lifecycle.decorateBean(objectFactory.getObject(), + context); + } + return bean; + } + + public void destroy() { + if (context==null) { + return; + } + Runnable callback = context.getCallback(); + if (callback!=null) { + callback.run(); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : 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 (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + } + +} diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/ScopeCache.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/ScopeCache.java new file mode 100644 index 00000000..483fa052 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/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.platform.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/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/StandardScopeCache.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/StandardScopeCache.java new file mode 100644 index 00000000..cb46708b --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/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.platform.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/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/refresh/RefreshScope.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/refresh/RefreshScope.java new file mode 100644 index 00000000..b3e33521 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/refresh/RefreshScope.java @@ -0,0 +1,82 @@ +/* + * 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.platform.context.scope.refresh; + +import java.io.Serializable; + +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.platform.context.scope.GenericScope; + +/** + *

+ * 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 { + + /** + * 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("scopedTarget.")) { + // User wants to refresh the bean with this name but that isn't the one in the cache... + name = "scopedTarget." + name; + } + // Ensure lifecycle is finished if bean was disposable + super.destroy(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(); + } + +} diff --git a/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/thread/ThreadLocalScopeCache.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/thread/ThreadLocalScopeCache.java new file mode 100644 index 00000000..f228d913 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/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.platform.context.scope.thread; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.platform.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/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/thread/ThreadScope.java b/spring-platform-config-client/src/main/java/org/springframework/platform/context/scope/thread/ThreadScope.java new file mode 100644 index 00000000..a1ec0885 --- /dev/null +++ b/spring-platform-config-client/src/main/java/org/springframework/platform/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.platform.context.scope.thread; + +import org.springframework.platform.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/spring-platform-config-client/src/main/resources/META-INF/spring.factories b/spring-platform-config-client/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..dcfc9684 --- /dev/null +++ b/spring-platform-config-client/src/main/resources/META-INF/spring.factories @@ -0,0 +1,12 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.platform.autoconfigure.RefreshAutoConfiguration + +# Application Listeners +org.springframework.context.ApplicationListener=\ +org.springframework.platform.bootstrap.BootstrapApplicationListener + +# Bootstrap components +org.springframework.platform.bootstrap.BootstrapConfiguration=\ +org.springframework.platform.bootstrap.config.ConfigServiceBootstrapConfiguration,\ +org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration \ No newline at end of file diff --git a/spring-platform-config-client/src/test/java/org/springframework/platform/bootstrap/BootstrapConfigurationTests.java b/spring-platform-config-client/src/test/java/org/springframework/platform/bootstrap/BootstrapConfigurationTests.java new file mode 100644 index 00000000..1dfbbcb3 --- /dev/null +++ b/spring-platform-config-client/src/test/java/org/springframework/platform/bootstrap/BootstrapConfigurationTests.java @@ -0,0 +1,93 @@ +/* + * 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.platform.bootstrap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.platform.bootstrap.config.PropertySourceLocator; + +/** + * @author Dave Syer + * + */ +public class BootstrapConfigurationTests { + + private ConfigurableApplicationContext context; + + @After + public void close() { + if (context != null) { + context.close(); + } + } + + @Test + public void picksUpAdditionalPropertySource() { + context = new SpringApplicationBuilder().web(false).sources( + BareConfiguration.class).run(); + assertEquals("bar", context.getEnvironment().getProperty("bootstrap.foo")); + assertTrue(context.getEnvironment().getPropertySources().contains("bootstrap")); + } + + @Test + public void environmentEnrichedOnce() { + context = new SpringApplicationBuilder().sources(BareConfiguration.class).environment( + new StandardEnvironment()).child(BareConfiguration.class).web(false).run(); + assertEquals("bar", context.getEnvironment().getProperty("bootstrap.foo")); + assertEquals(context.getEnvironment(), context.getParent().getEnvironment()); + assertTrue(context.getEnvironment().getPropertySources().contains("bootstrap")); + } + + @Test + public void environmentEnrichedInParent() { + context = new SpringApplicationBuilder().sources(BareConfiguration.class).child( + BareConfiguration.class).web(false).run(); + assertEquals("bar", context.getEnvironment().getProperty("bootstrap.foo")); + assertNotSame(context.getEnvironment(), context.getParent().getEnvironment()); + assertTrue(context.getEnvironment().getPropertySources().contains("bootstrap")); + assertTrue(((ConfigurableEnvironment) context.getParent().getEnvironment()).getPropertySources().contains( + "bootstrap")); + } + + @Configuration + protected static class BareConfiguration { + } + + @Configuration + protected static class PropertySourceConfiguration implements PropertySourceLocator { + + @Override + public PropertySource locate() { + return new MapPropertySource("testBootstrap", + Collections. singletonMap("bootstrap.foo", "bar")); + } + } + +} diff --git a/spring-platform-config-client/src/test/java/org/springframework/platform/context/environment/EnvironmentManagerIntegrationTests.java b/spring-platform-config-client/src/test/java/org/springframework/platform/context/environment/EnvironmentManagerIntegrationTests.java new file mode 100644 index 00000000..eecf8d00 --- /dev/null +++ b/spring-platform-config-client/src/test/java/org/springframework/platform/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.platform.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.platform.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/spring-platform-config-client/src/test/java/org/springframework/platform/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java b/spring-platform-config-client/src/test/java/org/springframework/platform/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java new file mode 100644 index 00000000..6f235a0f --- /dev/null +++ b/spring-platform-config-client/src/test/java/org/springframework/platform/context/properties/ConfigurationPropertiesRebinderIntegrationTests.java @@ -0,0 +1,101 @@ +/* + * 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.platform.context.properties; + +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.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.platform.autoconfigure.RefreshAutoConfiguration; +import org.springframework.platform.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()); + } + + @Test + @DirtiesContext + public void testRefresh() throws Exception { + 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()); + } + + @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; + 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/spring-platform-config-client/src/test/java/org/springframework/platform/context/scope/refresh/MoreRefreshScopeIntegrationTests.java b/spring-platform-config-client/src/test/java/org/springframework/platform/context/scope/refresh/MoreRefreshScopeIntegrationTests.java new file mode 100644 index 00000000..ac98237d --- /dev/null +++ b/spring-platform-config-client/src/test/java/org/springframework/platform/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.platform.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.platform.autoconfigure.RefreshAutoConfiguration; +import org.springframework.platform.context.config.annotation.RefreshScope; +import org.springframework.platform.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.platform.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/spring-platform-config-client/src/test/java/org/springframework/platform/context/scope/refresh/RefreshScopeConcurrencyTests.java b/spring-platform-config-client/src/test/java/org/springframework/platform/context/scope/refresh/RefreshScopeConcurrencyTests.java new file mode 100644 index 00000000..18b7a981 --- /dev/null +++ b/spring-platform-config-client/src/test/java/org/springframework/platform/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.platform.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.platform.autoconfigure.RefreshAutoConfiguration; +import org.springframework.platform.context.config.annotation.RefreshScope; +import org.springframework.platform.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.platform.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 { + latch.countDown(); + return service.getMessage(); + } finally { + logger.debug("Background done."); + } + } + }); + assertTrue(latch.await(500, 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/spring-platform-config-client/src/test/java/org/springframework/platform/context/scope/refresh/RefreshScopeIntegrationTests.java b/spring-platform-config-client/src/test/java/org/springframework/platform/context/scope/refresh/RefreshScopeIntegrationTests.java new file mode 100644 index 00000000..e991d788 --- /dev/null +++ b/spring-platform-config-client/src/test/java/org/springframework/platform/context/scope/refresh/RefreshScopeIntegrationTests.java @@ -0,0 +1,211 @@ +/* + * 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.platform.context.scope.refresh; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +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.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.platform.autoconfigure.RefreshAutoConfiguration; +import org.springframework.platform.context.config.annotation.RefreshScope; +import org.springframework.platform.context.scope.refresh.RefreshScopeIntegrationTests.TestConfiguration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@SpringApplicationConfiguration(classes=TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class RefreshScopeIntegrationTests { + + @Autowired + private Service service; + + @Autowired + private TestProperties properties; + + @Autowired + private org.springframework.platform.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); + } + + @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); + } + + public static interface Service { + + String getMessage(); + + } + + public static class ExampleService implements Service, InitializingBean, DisposableBean { + + private static Log logger = LogFactory.getLog(ExampleService.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(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/spring-platform-config-client/src/test/resources/application.properties b/spring-platform-config-client/src/test/resources/application.properties new file mode 100644 index 00000000..9999b13c --- /dev/null +++ b/spring-platform-config-client/src/test/resources/application.properties @@ -0,0 +1,3 @@ +message: Hello scope! +delay: 0 +# debug: true \ No newline at end of file diff --git a/spring-platform-config-client/src/test/resources/bootstrap.properties b/spring-platform-config-client/src/test/resources/bootstrap.properties new file mode 100644 index 00000000..a7cc0b11 --- /dev/null +++ b/spring-platform-config-client/src/test/resources/bootstrap.properties @@ -0,0 +1 @@ +spring.main.sources: org.springframework.platform.bootstrap.BootstrapConfigurationTests.PropertySourceConfiguration \ No newline at end of file diff --git a/spring-platform-config-server/pom.xml b/spring-platform-config-server/pom.xml new file mode 100644 index 00000000..152ae86c --- /dev/null +++ b/spring-platform-config-server/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + spring-platform-config-server + jar + + spring-platform-config-server + spring-platform-config-server + + + org.springframework.platform + spring-platform-config + 1.0.0.BUILD-SNAPSHOT + .. + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + UTF-8 + org.springframework.platform.config.server.Application + 1.7 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/Application.java b/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/Application.java new file mode 100644 index 00000000..fdd13cf4 --- /dev/null +++ b/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/Application.java @@ -0,0 +1,81 @@ + +package org.springframework.platform.config.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.support.StandardServletEnvironment; + +@Configuration +@ComponentScan +@EnableAutoConfiguration +@RestController +public class Application { + + @Autowired + private ConfigurableEnvironment environment; + + 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)); + + @RequestMapping("/{name}/{env}") + public List master(@PathVariable String env) { + return properties(env, "master"); + } + + @RequestMapping("/{name}/{env}/{label}") + public List properties(@PathVariable String env, @PathVariable String label) { + List result = new ArrayList(); + for (PropertySource source : environment.getPropertySources()) { + String name = source.getName(); + if (!standardSources .contains(name) && source instanceof MapPropertySource) { + result.add(new SerializableMapPropertySource(name, (Map)source.getSource())); + } + } + return result; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + protected static class SerializableMapPropertySource { + + private String name; + private Map source; + + public SerializableMapPropertySource(String name, Map source) { + this.name = name; + this.source = source; + } + + public String getName() { + return name; + } + + public Map getSource() { + return source; + } + + } +} diff --git a/spring-platform-config-server/src/main/resources/application.yml b/spring-platform-config-server/src/main/resources/application.yml new file mode 100644 index 00000000..fa0246f7 --- /dev/null +++ b/spring-platform-config-server/src/main/resources/application.yml @@ -0,0 +1,7 @@ +info: + component: Config Server +spring: + application: + name: configserver +server: + port: 8888 \ No newline at end of file diff --git a/spring-platform-config-server/src/test/java/org/springframework/platform/config/server/ApplicationTests.java b/spring-platform-config-server/src/test/java/org/springframework/platform/config/server/ApplicationTests.java new file mode 100644 index 00000000..ade672e0 --- /dev/null +++ b/spring-platform-config-server/src/test/java/org/springframework/platform/config/server/ApplicationTests.java @@ -0,0 +1,18 @@ +package org.springframework.platform.config.server; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Application.class) +@WebAppConfiguration +public class ApplicationTests { + + @Test + public void contextLoads() { + } + +}