diff --git a/spring-cloud-consul-config/pom.xml b/spring-cloud-consul-config/pom.xml
index 8b678f04..2d402cf9 100644
--- a/spring-cloud-consul-config/pom.xml
+++ b/spring-cloud-consul-config/pom.xml
@@ -22,6 +22,11 @@
spring-boot-starter-web
true
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+ true
+
org.springframework.retry
spring-retry
diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java
new file mode 100644
index 00000000..6d7c8cbc
--- /dev/null
+++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2013-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.consul.config;
+
+import java.io.Closeable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.PostConstruct;
+
+import com.ecwid.consul.v1.ConsulClient;
+import com.ecwid.consul.v1.QueryParams;
+import com.ecwid.consul.v1.Response;
+import com.ecwid.consul.v1.kv.model.GetValue;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.ApplicationEventPublisherAware;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import lombok.Data;
+import lombok.extern.apachecommons.CommonsLog;
+
+/**
+ * @author Spencer Gibb
+ */
+@CommonsLog
+public class ConfigWatch implements Closeable, ApplicationEventPublisherAware {
+ private final List contexts;
+ private final ConsulClient consul;
+ private AtomicBoolean running = new AtomicBoolean(false);
+ private ApplicationEventPublisher publisher;
+ private HashMap consulIndexes = new HashMap<>();
+
+ public ConfigWatch(List contexts, ConsulClient consul) {
+ this.contexts = contexts;
+ this.consul = consul;
+ }
+
+ @Override
+ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
+ this.publisher = publisher;
+ }
+
+ @PostConstruct
+ public void start() {
+ this.running.compareAndSet(false, true);
+ }
+
+ @Scheduled(fixedDelayString = "${spring.cloud.consul.config.watch.delay:10}")
+ public void watchConfigKeyValues() {
+ if (this.running.get()) {
+ for (String context : this.contexts) {
+ if (!context.endsWith("/")) {
+ context = context + "/";
+ }
+
+ try {
+ Long currentIndex = this.consulIndexes.get(context);
+ if (currentIndex == null) {
+ currentIndex = -1L;
+ }
+
+ Response> response = this.consul.getKVValues(context, new QueryParams(2, currentIndex));
+
+ Long newIndex = response.getConsulIndex();
+
+ if (newIndex != null && !newIndex.equals(currentIndex)) {
+ // don't publish the same index again, don't publish the first time (-1) so index can be primed
+ if (!this.consulIndexes.containsValue(newIndex) && !currentIndex.equals(-1L)) {
+ this.publisher.publishEvent(new ConsulConfigRefreshEvent(this, new RefreshEventData(context, currentIndex, newIndex)));
+ }
+ this.consulIndexes.put(context, newIndex);
+ }
+
+ } catch (Exception e) {
+ log.error("Error initializing listener for context " + context, e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ this.running.compareAndSet(true, false);
+ }
+
+ @Data
+ static class RefreshEventData {
+ private final String context;
+ private final Long prevIndex;
+ private final Long newIndex;
+ }
+}
diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java
new file mode 100644
index 00000000..8841ac13
--- /dev/null
+++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2013-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.consul.config;
+
+import com.ecwid.consul.v1.ConsulClient;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cloud.endpoint.RefreshEndpoint;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Spencer Gibb
+ */
+@Configuration
+public class ConsulConfigAutoConfiguration {
+
+ @Configuration
+ @ConditionalOnClass(RefreshEndpoint.class)
+ protected static class ConsulRefreshConfiguration {
+ @Bean
+ @ConditionalOnBean(RefreshEndpoint.class)
+ public ConsulConfigRefreshListener zookeeperConfigRefreshListener(
+ RefreshEndpoint refreshEndpoint) {
+ return new ConsulConfigRefreshListener(refreshEndpoint);
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled", matchIfMissing = true)
+ public ConfigWatch configWatch(ConsulPropertySourceLocator locator,
+ ConsulClient consul) {
+ return new ConfigWatch(locator.getContexts(), consul);
+ }
+ }
+}
diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java
index c5c12baf..9e4d4e73 100644
--- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java
+++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java
@@ -52,6 +52,16 @@ public class ConsulConfigProperties {
private String aclToken;
+ private Watch watch = new Watch();
+
+ @Data
+ public class Watch {
+ private int waitTime = 2;
+ private boolean enabled = true;
+ private int delay = 10;
+
+ }
+
/**
* There are many ways in which we can specify configuration in consul i.e.,
*
@@ -76,7 +86,7 @@ public class ConsulConfigProperties {
*
* @author srikalyan.swayampakula
*/
- public static enum Format {
+ public enum Format {
/**
* Indicates that the configuration specified in consul is of type native key values.
*/
diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigRefreshEvent.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigRefreshEvent.java
new file mode 100644
index 00000000..9aa661c9
--- /dev/null
+++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigRefreshEvent.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.consul.config;
+
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * @author Spencer Gibb
+ */
+public class ConsulConfigRefreshEvent extends ApplicationEvent {
+
+ private Object event;
+
+ /**
+ * Create a new ApplicationEvent.
+ *
+ * @param source the object on which the event initially occurred (never {@code null})
+ */
+ public ConsulConfigRefreshEvent(Object source, Object event) {
+ super(source);
+ this.event = event;
+ }
+
+ public Object getEvent() {
+ return event;
+ }
+
+ public String getEventDesc() {
+ return event.toString();
+ }
+}
diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigRefreshListener.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigRefreshListener.java
new file mode 100644
index 00000000..0ecfd554
--- /dev/null
+++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigRefreshListener.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.consul.config;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.cloud.endpoint.RefreshEndpoint;
+import org.springframework.context.event.EventListener;
+
+import lombok.extern.apachecommons.CommonsLog;
+
+/**
+ * @author Spencer Gibb
+ */
+@CommonsLog
+public class ConsulConfigRefreshListener {
+ private RefreshEndpoint refresh;
+ private AtomicBoolean ready = new AtomicBoolean(false);
+
+ public ConsulConfigRefreshListener(RefreshEndpoint refresh) {
+ this.refresh = refresh;
+ }
+
+ @EventListener
+ public void handle(ApplicationReadyEvent event) {
+ this.ready.compareAndSet(false, true);
+ }
+
+ @EventListener
+ public void handle(ConsulConfigRefreshEvent event) {
+ if (this.ready.get()) { // don't handle events before app is ready
+ log.info("Event received " + event.getEventDesc());
+ if (this.refresh != null) {
+ String[] keys = this.refresh.refresh();
+ log.info("Refresh keys changed: " + Arrays.asList(keys));
+ }
+ }
+ }
+}
diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocator.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocator.java
index 93570c55..3b386fdb 100644
--- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocator.java
+++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocator.java
@@ -41,11 +41,17 @@ public class ConsulPropertySourceLocator implements PropertySourceLocator {
private ConsulConfigProperties properties;
+ private List contexts = new ArrayList<>();
+
public ConsulPropertySourceLocator(ConsulClient consul, ConsulConfigProperties properties) {
this.consul = consul;
this.properties = properties;
}
+ public List getContexts() {
+ return contexts;
+ }
+
@Override
@Retryable(interceptor = "consulRetryInterceptor")
public PropertySource> locate(Environment environment) {
@@ -55,21 +61,20 @@ public class ConsulPropertySourceLocator implements PropertySourceLocator {
List profiles = Arrays.asList(env.getActiveProfiles());
String prefix = this.properties.getPrefix();
- List contexts = new ArrayList<>();
String defaultContext = prefix + "/" + this.properties.getDefaultContext();
- contexts.add(defaultContext + "/");
- addProfiles(contexts, defaultContext, profiles);
+ this.contexts.add(defaultContext + "/");
+ addProfiles(this.contexts, defaultContext, profiles);
String baseContext = prefix + "/" + appName;
- contexts.add(baseContext + "/");
- addProfiles(contexts, baseContext, profiles);
+ this.contexts.add(baseContext + "/");
+ addProfiles(this.contexts, baseContext, profiles);
CompositePropertySource composite = new CompositePropertySource("consul");
- Collections.reverse(contexts);
+ Collections.reverse(this.contexts);
- for (String propertySourceContext : contexts) {
+ for (String propertySourceContext : this.contexts) {
ConsulPropertySource propertySource = create(propertySourceContext);
propertySource.init();
composite.addPropertySource(propertySource);
diff --git a/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories b/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories
index ea9d49cd..d1699b34 100644
--- a/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories
@@ -1,3 +1,7 @@
+# Auto Configuration
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.springframework.cloud.consul.config.ConsulConfigAutoConfiguration
+
# Bootstrap Configuration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.consul.config.ConsulConfigBootstrapConfiguration
diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocatorTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocatorTests.java
new file mode 100644
index 00000000..63048e5a
--- /dev/null
+++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocatorTests.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2013-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.consul.config;
+
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.ecwid.consul.v1.ConsulClient;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.cloud.consul.ConsulProperties;
+import org.springframework.cloud.context.scope.refresh.RefreshScope;
+import org.springframework.cloud.endpoint.RefreshEndpoint;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author Spencer Gibb
+ */
+public class ConsulPropertySourceLocatorTests {
+ public static final String PREFIX = "_propertySourceLocatorTests_config__";
+ public static final String ROOT = PREFIX + UUID.randomUUID();
+ private ConfigurableApplicationContext context;
+ public static final String KEY = ROOT + "/application/testProp";
+
+ @Configuration
+ @EnableAutoConfiguration
+ static class Config {
+ @Bean
+ public RefreshEndpoint refreshEndpoint(ConfigurableApplicationContext context,
+ RefreshScope scope) {
+ RefreshEndpoint endpoint = new TestRefreshEndpoint(context, scope);
+ return endpoint;
+ }
+ }
+
+ static class TestRefreshEndpoint extends RefreshEndpoint {
+ private CountDownLatch successLatch = new CountDownLatch(1);
+ private CountDownLatch toManyLatch = new CountDownLatch(1);
+ private AtomicInteger count = new AtomicInteger();
+
+ public TestRefreshEndpoint( ConfigurableApplicationContext context, RefreshScope scope) {
+ super(context, scope);
+ }
+
+ @Override
+ public synchronized String[] refresh() {
+ String[] keys = super.refresh();
+ if (this.count.incrementAndGet() == 1) {
+ this.successLatch.countDown();
+ } else {
+ this.toManyLatch.countDown();
+ }
+ return keys;
+ }
+ }
+
+ private ConfigurableEnvironment environment;
+ private ConsulClient client;
+ private ConsulProperties properties;
+
+ @Before
+ public void setup() {
+ this.properties = new ConsulProperties();
+ this.client = new ConsulClient(properties.getHost(), properties.getPort());
+ this.client.deleteKVValues(PREFIX);
+ this.client.setKVValue(KEY, "testPropVal");
+
+
+ this.context = new SpringApplicationBuilder(Config.class)
+ .web(false)
+ .run("--spring.spring.application.name=testConsulPropertySourceLocator",
+ "--spring.cloud.consul.config.prefix="+ROOT,
+ "spring.cloud.consul.config.watch.delay=1");
+
+ this.client = context.getBean(ConsulClient.class);
+ this.properties = context.getBean(ConsulProperties.class);
+ this.environment = context.getEnvironment();
+ }
+
+ @After
+ public void teardown() {
+ this.client.deleteKVValues(PREFIX);
+ }
+
+ @Test
+ public void propertyLoadedAndUpdated() throws Exception {
+ String testProp = this.environment.getProperty("testProp");
+ assertThat("testProp was wrong", testProp, is(equalTo("testPropVal")));
+
+ this.client.setKVValue(KEY, "testPropValUpdate");
+
+ TestRefreshEndpoint endpoint = this.context.getBean(TestRefreshEndpoint.class);
+ boolean receivedEvent = endpoint.successLatch.await(5, TimeUnit.SECONDS);
+ assertThat("listener didn't receive event", receivedEvent, is(true));
+
+ testProp = this.environment.getProperty("testProp");
+ assertThat("testProp was wrong after update", testProp, is(equalTo("testPropValUpdate")));
+
+ boolean receivedExtraEvent = endpoint.toManyLatch.await(500, TimeUnit.MILLISECONDS);
+ assertThat("refresh called to many times", receivedExtraEvent, is(false));
+ }
+}