External Configurations
We should all know that application configurations should not live inside your artefacts. I mean imagine having to rebuild and re-deploy your application every time some little property changes. Spring boot already has this feature and it is really easy to use. See Externalised Configuration in the documentation.
The simple solution for this is to have a config server serve these configurations out to all your applications. Great news again Spring has a solution. See Quick Start Page on how to make a quick config server.
This makes it simple to manage your configurations and even version control them.
So, whats the problem?
The problem with this approach is that your application does not pick up configuration changes when the repository has changed. The application will only get this new change when the application has been restarted. We want the application to refresh it’s own context and continue without exiting the JVM.
Build a config-server
I start off with a normal spring boot app and add the following dependency:
<properties> ... <spring-cloud.version>Edgware.RELEASE</spring-cloud.version> </properties> ... <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> ... <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency>
Then on the main Java class you just need to add `@EnableConfigServer`
package com.val.config.valconfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.config.server.EnableConfigServer; @SpringBootApplication @EnableConfigServer public class ValConfigApplication { public static void main(String[] args) { SpringApplication.run(ValConfigApplication.class, args); } }
Let’s add some configuration for this guy (application.yaml):
server: port: 8888 spring: cloud: config: server: git: searchPaths: '{application}' uri: /shared/config/config-repo/
Use searchPaths for the way the config server will interpret its URL call. In this case I assume that my config server will host configs for more than one app.Therefore the searchPaths will be set to [{application}] indicating that we will search per app. With spring.cloud.config.server.git.uri specify the location of your git repo that contains your configs. You can have a local or remote repo to handle this. Locally create a folder like /shared/config/val-service/ and git init it, or place a ‘https://github.com/whomever/whatever.git’ for remote. This will be cloned to the ‘baseDir’ path.
I have set up a directory /shared/config/config-repo/ and init a git repo for this demo:
mkdir -p /shared/config/config-repo/
cd /shared/config/config-repo/
git init
I then add a folder with my application name (for this demo its val-demo-service):
mkdir val-demo-service
cd val-demo-service
touch application.yaml
I edit the application.yaml and add a simple config:
application:
sample: This is my new string.
Then I commit
cd /shared/config/config-repo/
git add .
git commit -m "Init"
Now we can run the server application mvn spring-boot:run -pl val-demo-config and hit the URL http://localhost:8888/val-demo-service/default, this will return something like this (I used curl):
> curl http://localhost:8888/val-demo-service/default
# {"name":"val-demo-service","profiles":["default"],"label":null,"version":"88be013b25ebbdc69f07a3b88e3582d66673b119","state":null,"propertySources":[{"name":"/shared/config/config-repo/val-demo-service/application.yaml","source":{"application.sample":"This is my new string."}}]}
Build a simple Client
I start off with a normal spring boot app and add the following two dependencies:
<properties> ... <spring-cloud.version>Edgware.RELEASE</spring-cloud.version> </properties> ... <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> ... <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
- spring-cloud-starter-config will give us the ability to query the config server automatically.
- spring-boot-starter-web because we want to see the result on a rest endpoint.
Now create a rest endpoint to fetch a simple property:
@ConfigurationProperties("application") public class ApplicationProperties { private String sample; public String getSample() { return sample; } public void setSample(final String sample) { this.sample = sample; } }
@RestController @RequestMapping public class SampleOutputController { private ApplicationProperties applicationProperties; @Autowired public SampleOutputController(final ApplicationProperties applicationProperties) { this.applicationProperties = applicationProperties; } @GetMapping @RefreshScope public String getTheSampleProperty() { return applicationProperties.getSample(); } }
Next some configuration (bootstrap.yaml). It is important that the application has a name, as this is how it tries to get the config. Remember that bootstrap config is loaded before the normal application config. This iso that you app has a name before trying to fetch config:
spring: application: name: val-demo-service cloud: config: uri: http://localhost:8888/
- spring.cloud.config.uri Indicates where to fetch the config. Default is http://localhost:8888/
- spring.application.name is the application’s name
When you run the application you will find that the client application will pull the configuration on startup but will not update unless you restart the application.
Solution
While looking for a solution to this problem I came across this post. The author uses the /Refresh endpoint on spring-boot-starter-actuator. Which made me wonder if I can reuse the implementation to refresh the application on my own.
I added the spring-boot-actuator dependency to my client POM:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator</artifactId> </dependency>
And created a RestartEndpoint bean for the refreshing feature and a RestTemplate bean to be able to fetch config with on my application configuration class:
@Configuration public class ApplicationConfiguration { @LoadBalanced @Bean public RestTemplate restTemplate() { return new RestTemplate(); } @Bean public RestartEndpoint restartEndpoint() { return new RestartEndpoint(); } }
- We can also schedule this task using spring’s @Scheduled annotation which will execute that method every configured count of milliseconds.
- We can stop scheduled tasks using ScheduledAnnotationBeanPostProcessor.
- Since we know that config server will give us our configuration on an endpoint like: http://localhost:8888/{application-name}/{profiles} we can just use a rest call and detect changes.
- We know our servers address from the spring.cloud.config.uri property and the application name from the spring.application.name property.
- We can figure out our application’s active profiles by using the Environment bean and calling the environment.getActiveProfiles() method.
- And lastly we know that a spring application is stopped using SpringApplication.exit(applicationContext) method.
That left me with this possible solution:
@Service public class ConfigurationResetService { private static final Logger log = LoggerFactory.getLogger(ConfigurationResetService.class); private String config; private boolean firstFetch; @Value("${spring.cloud.config.uri}") private String configServerUri; @Value("${spring.application.name}") private String applicationName; private RestTemplate restTemplate; private ApplicationContext applicationContext; private RestartEndpoint restartEndpoint; private Environment environment; private ApplicationProperties applicationProperties; private ScheduledAnnotationBeanPostProcessor postProcessor; @Autowired public ConfigurationResetService(final RestTemplate restTemplate, final Environment environment, final ApplicationContext applicationContext, final RestartEndpoint restartEndpoint, final ApplicationProperties applicationProperties, ScheduledAnnotationBeanPostProcessor postProcessor) { this.restTemplate = restTemplate; this.applicationContext = applicationContext; this.restartEndpoint = restartEndpoint; this.environment = environment; this.applicationProperties = applicationProperties; this.postProcessor = postProcessor; } @Scheduled(fixedDelayString = "${application.refreshDelay:5000}") public void checkForConfigChange() { if (!applicationProperties.isEnableRefresh()) { postProcessor.postProcessBeforeDestruction(this, "checkForConfigChange"); log.info("The check for config change disabled."); return; } boolean shouldRefresh = shouldRefresh(); if (shouldRefresh) { if (applicationProperties.isRefreshOnConfigChange()) { log.info("The application will refresh...."); Thread restartThread = new Thread(() -> restartEndpoint.restart()); restartThread.setDaemon(false); restartThread.start(); } else { log.info("The application exiting due to a changed configuration."); SpringApplication.exit(applicationContext); } } } private boolean shouldRefresh() { boolean shouldRefresh = false; String environmentActiveProfiles = environment.getActiveProfiles().length == 0 ? "default" : String.join(",", environment.getActiveProfiles()); UriComponents uriComponents = UriComponentsBuilder .fromHttpUrl(configServerUri) .path("/{applicationName}/{profiles}") .buildAndExpand(applicationName, environmentActiveProfiles); URI uri = uriComponents.toUri(); log.debug("Checking for new config at [{}]", uri); try { ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class); if (response.getStatusCode().is2xxSuccessful()) { log.debug("Fetched configuration [{}]", response.getBody()); if (config == null) { log.debug("Configuration been set to [{}].", response.getBody()); config = response.getBody(); } else { if (firstFetch) { firstFetch = false; log.info("Configuration was not up when the application started."); shouldRefresh = true; } if (!response.getBody().equals(config)) { log.info("Configuration has changed to [{}]", response.getBody()); config = null; shouldRefresh = true; } } } else { log.error("Config server returned a non 2xx code [{}]. \nHeaders [{}]\n Body [{}]", response.getStatusCode(), response.getHeaders(), response.getBody()); } } catch (final ResourceAccessException e) { log.error("Couldn't fetch configuration, make sure that the host [{}] is available.", configServerUri); firstFetch = (config == null); } return shouldRefresh; } }
And a properties class:
@ConfigurationProperties("application") public class ApplicationProperties { private String sample; private boolean refreshOnConfigChange; private int refreshDelay; private boolean enableRefresh; public String getSample() { return sample; } public void setSample(final String sample) { this.sample = sample; } public boolean isRefreshOnConfigChange() { return refreshOnConfigChange; } public void setRefreshOnConfigChange(final boolean refreshOnConfigChange) { this.refreshOnConfigChange = refreshOnConfigChange; } public int getRefreshDelay() { return refreshDelay; } public void setRefreshDelay(final int refreshDelay) { this.refreshDelay = refreshDelay; } public boolean isEnableRefresh() { return enableRefresh; } public void setEnableRefresh(final boolean enableRefresh) { this.enableRefresh = enableRefresh; } }
The Demo
Make sure that the server is not running and start the service application mvn spring-boot:run -pl val-demo-config. Note that the logs say, this is because we can’t fetch our config:
...
2017-12-20 12:34:29.947 INFO 43792 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:8888/
2017-12-20 12:34:30.125 WARN 43792 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Could not locate PropertySource: I/O error on GET request for "http://localhost:8888/val-demo-service/default": Connection refused; nested exception is java.net.ConnectException: Connection refused
...
2017-12-20 12:34:33.028 ERROR 43792 --- [pool-1-thread-1] c.v.s.v.s.ConfigurationResetService : Couldn't fetch configuration, make sure that the host [http://localhost:8888/] is available.
2017-12-20 12:34:34.133 ERROR 43792 --- [pool-1-thread-1] c.v.s.v.s.ConfigurationResetService : Couldn't fetch configuration, make sure that the host [http://localhost:8888/] is available.
2017-12-20 12:34:35.237 ERROR 43792 --- [pool-1-thread-1] c.v.s.v.s.ConfigurationResetService : Couldn't fetch configuration, make sure that the host [http://localhost:8888/] is available.
...
The application will not be able to fetch config, so hitting the http://localhost:8080 endpoint will display the local config’s string:
> curl http://localhost:8080
# If set up correctly I should never see this one :D
Now in another terminal I run the server with mvn spring-boot:run -pl val-demo-config, the client terminal should pick up the change and refresh:
...
2017-12-20 12:38:28.705 INFO 43847 --- [pool-1-thread-1] c.v.s.v.s.ConfigurationResetService : Configuration was not up when the application started.
2017-12-20 12:38:28.705 INFO 43847 --- [pool-1-thread-1] c.v.s.v.s.ConfigurationResetService : The application will refresh....
2017-12-20 12:38:28.706 INFO 43847 --- [ Thread-20] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@ebaa6cb: startup date [Wed Dec 20 12:38:17 SAST 2017]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@4a94ee4
2017-12-20 12:38:28.708 INFO 43847 --- [ Thread-20] o.s.c.support.DefaultLifecycleProcessor : Stopping beans in phase 0
2017-12-20 12:38:28.709 INFO 43847 --- [ Thread-20] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
2017-12-20 12:38:28.709 INFO 43847 --- [ Thread-20] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans
2017-12-20 12:38:28.764 INFO 43847 --- [ Thread-20] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2017-12-20 12:38:28.816 INFO 43847 --- [ Thread-20] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@29aded56: startup date [Wed Dec 20 12:38:28 SAST 2017]; root of context hierarchy
2017-12-20 12:38:28.841 INFO 43847 --- [ Thread-20] trationDelegate$BeanPostProcessorChecker : Bean 'configurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$a195c2da] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
...
Now that the application has refreshed, lets see what hitting http://localhost:8080 endpoint gives us:
> curl http://localhost:8080
# This is my new string.
Now lets change the config in the repo and see the change. I modify the file /shared/config/config-repo/val-demo-service/application.yaml:
application:
sample: This is a changed property.
I commit these changes and watch the two terminals:
cd /shared/config/config-repo/
git add .
git commit -m "updated"
The client should now be refreshed:
...
2017-12-20 12:52:52.209 INFO 44237 --- [pool-1-thread-1] c.v.s.v.s.ConfigurationResetService : Configuration has changed to [{"name":"val-demo-service","profiles":["default"],"label":null,"version":"f824b17bea18b91aa4c94d46f4584e596aca6dda","state":null,"propertySources":[{"name":"/shared/config/config-repo/val-demo-service/application.yaml","source":{"application.sample":"This is a changed property."}}]}]
2017-12-20 12:52:52.210 INFO 44237 --- [pool-1-thread-1] c.v.s.v.s.ConfigurationResetService : The application will refresh....
2017-12-20 12:52:52.211 INFO 44237 --- [ Thread-21] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@ebaa6cb: startup date [Wed Dec 20 12:52:30 SAST 2017]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@4a94ee4
2017-12-20 12:52:52.212 INFO 44237 --- [ Thread-21] o.s.c.support.DefaultLifecycleProcessor : Stopping beans in phase 0
2017-12-20 12:52:52.213 INFO 44237 --- [ Thread-21] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
2017-12-20 12:52:52.213 INFO 44237 --- [ Thread-21] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans
2017-12-20 12:52:52.269 INFO 44237 --- [ Thread-21] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2017-12-20 12:52:52.319 INFO 44237 --- [ Thread-21] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@39350ef6: startup date [Wed Dec 20 12:52:52 SAST 2017]; root of context hierarchy
2017-12-20 12:52:52.343 INFO 44237 --- [ Thread-21] trationDelegate$BeanPostProcessorChecker : Bean 'configurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$a195c2da] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
...
Now that the application has refreshed, lets see what hitting http://localhost:8080 endpoint gives us:
> curl http://localhost:8080
# This is a changed property.
Conclusion
You can implement something like this to refresh your scope on the fly. Unfortunately you will still need to give at least some basic configurations. And this solution will not work for startup configuration dependencies like database connections. This sample does however have a some decent features such as:
- Application will refresh on a config change.
- Application can also shut down if you want on a config change.
- Application can start without a config server, then when the config server is up refresh the application.
- The config server can go down and up without affecting the client application.
- Configure how often the application checks for changes.
- Configure the shutdown/restart feature to be on/off.
Get the source code can be found here at my github repo Demo Spring Boot Config Server.