Spring Centralized Runtime Properties Configuration without refreshing the Client

Introduction

This tutorial walks you through the process of consuming the configurations from the Spring cloud config server. So I will setup a config server and build a client application consumes the configuration on startup and then refreshes the configuration without restarting the client.

Let’s say you have some configuration values, in application properties or yaml/yml files, which may be changed during runtime and it is not possible always to restart your application and change the configuration values. Here Spring configuration helps you to externalize our properties or configurations.

For example, you have database connection details which may be different depending on your different environments. Of course, according to your environment you can create separate properties file for configuration but what happens when you want to point to different datasource at runtime due to some reason, i.e., your existing datasource is no more required or it has some problems.

You may have other configuration properties which are changeable during run time only.

Spring Cloud Config is where you can have not only your all configurations centrally managed but also you can refresh them dynamically and which in turn can be picked by your referencing applications from the very next moment.

Spring Cloud Config provides server-side and client-side support for externalized configuration in a distributed system. With the Config Server, you have a central place to manage external properties for applications across all environments.

You can use Spring Cloud’s @EnableConfigServer to enable a config server that can communicate with other client applications.

Here in this example I am going to create one config server and one config client application. You can have multiple client applications to read config values from config server application.

The config server application will read the external properties file from local system, you can also read the external file from other location, for example, Git repository. The client applications will read the values from config server.

Prerequisites

Java at least 1.8, Spring Boot 2.4.3, Spring Cloud, Gradle 6.4.1 – 6.7.1, Maven 3.6.3

Spring Config Server App

I will build Spring config server app using Spring cloud here. So let’s get started with project setup.

The name of the project is spring-dynamic-runtime-configurations-server.

The build.gradle script is given below if you are using gradle as build tool:

buildscript {
	ext {
		springBootVersion = '2.4.3'
	}
	
    repositories {
    	mavenCentral()
    }
    
    dependencies {
    	classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

plugins {
    id 'java-library'
    id 'org.springframework.boot' version "${springBootVersion}"
    id "io.spring.dependency-management" version "1.0.11.RELEASE"
}

sourceCompatibility = 12
targetCompatibility = 12

repositories {
    mavenCentral()
}

dependencies {
	implementation("org.springframework.cloud:spring-cloud-config-server:3.0.2")
	
	//required for jdk 9 or above
	runtimeOnly('javax.xml.bind:jaxb-api:2.4.0-b180830.0359')
}

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2020.0.0'
    }
}

For maven based project you can use below pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.roytuts</groupId>
	<artifactId>spring-dynamic-runtime-configurations-server</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>12</maven.compiler.source>
		<maven.compiler.target>12</maven.compiler.target>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.3</version>
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-config-server</artifactId>
		</dependency>

		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<scope>runtime</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>2020.0.0</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

Application properties configuration is done to read the external configuration file and other details. Put a properties file application.properties under the class path directory src/main/resources.

spring.application.name=config-server
server.port=8888
spring.profiles.active=native
spring.cloud.config.server.native.search-locations=file:///C:/config

In the above configuration file I have put the application name, port on which the server will run, profile should be native for local configuration file read, the path of the local configuration file’s directory.

Note the three slashes (///) before C drive name in the location.

For default profile make sure either file name is external.yml/yaml or external.properties.

For other profiles e.g. dev, file name should be external-dev.yml or external-dev.properties (if all are in the same folder), then http://localhost:8888/external/dev would show both dev and default profile entries.

The name in the left side of the - is application name and name in right side is profile. So the endpoint for external-dev.yml will be
http://localhost:8888/external/dev.

If you want to read from Git repository instead of local system for external configuration the you can specify the Git repository path using the key spring.cloud.config.server.git.uri instead of spring.cloud.config.server.native.search-locations.

The local configuration file name is external.properties and kept under C:/config folder. The config file has the below key/value pair.

msg=Hello Dev

To enable config server as said earlier I am going to use @EnableConfigServer annotation as shown below:

package com.roytuts.spring.dynamic.runtime.configurations.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer
@SpringBootApplication
public class SpringDynamicRuntimeConfigServerApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringDynamicRuntimeConfigServerApp.class, args);
	}

}

Now if you run the config server application and hit the URL http://localhost:8888/external/default, you will get the following output:

{
   "name":"external",
   "profiles":[
      "default"
   ],
   "label":null,
   "version":null,
   "state":null,
   "propertySources":[
      {
         "name":"file:///C:/config/external.properties",
         "source":{
            "msg":"Hello Dev"
         }
      }
   ]
}

So the in the config file external.properties the key msg will be consumed by the client application.

Spring Config Client App

Next I am going to build a client application that will consume the key msg from the external file external.properties.

Create a project in you favorite IDE or tool and the name of the project is spring-dynamic-runtime-configurations-client.

For gradle based project you can use below build.gradle script:

buildscript {
	ext {
		springBootVersion = '2.4.3'
	}
	
    repositories {
    	mavenCentral()
    }
    
    dependencies {
    	classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

plugins {
    id 'java-library'
    id 'org.springframework.boot' version "${springBootVersion}"
    id "io.spring.dependency-management" version "1.0.11.RELEASE"
}

sourceCompatibility = 12
targetCompatibility = 12

repositories {
    mavenCentral()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
	implementation("org.springframework.cloud:spring-cloud-starter-config:3.0.2")
	implementation("org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}")
	
	//required for jdk 9 or above
	runtimeOnly('javax.xml.bind:jaxb-api:2.4.0-b180830.0359')
}

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2020.0.0'
    }
}

For maven based project you can use below pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.roytuts</groupId>
	<artifactId>spring-dynamic-runtime-configurations-client</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>12</maven.compiler.source>
		<maven.compiler.target>12</maven.compiler.target>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.3</version>
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-config</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>

		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<scope>runtime</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>2020.0.0</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

The properties to configure the Config Client must necessarily be read in before the rest of the application’s configuration is read from the Config Server, during the bootstrap phase.

So I need application.properties file under classpath directory src/main/resources. I need to also mention the location of the config server using spring.cloud.config.uri.

Notice the name of the application is given same as the external file’s name.

spring.application.name=external
#default config server url
spring.config.import=optional:configserver:http://localhost:8888/

I also want to enable the /actuator/refresh endpoint, to allow client application to pick up the dynamic configuration changes. The following line has to be added into src/main/resources/application.properties:

management.endpoints.web.exposure.include=*

By default, the configuration values are read on the client’s startup and not again. You can force a bean to refresh its configuration by annotating the REST controller with the Spring Cloud Config @RefreshScope and then triggering a refresh (http://localhost:8080/actuator/refresh) endpoint. That’s why I added actuator dependency.

The refresh endpoint works on HTTP POST method. So you need to send blank body in the request to reflect the changes.

Now create a REST controller to get the value dynamically for the key msg.

package com.roytuts.spring.dynamic.runtime.configurations.client.rest.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RefreshScope
@RestController
public class SpringRestController {

	@Value("${msg:Hey}")
	private String msg;

	@GetMapping("/msg")
	public ResponseEntity<String> greeting() {
		return new ResponseEntity<String>(msg, HttpStatus.OK);
	}

}

The default value I have put here is Hey for the key msg. In case you don’t have any value for the key msg then you will see this default message.

Let’s create a main class to run our application.

package com.roytuts.spring.dynamic.runtime.configurations.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringDynamicRuntimeConfigClientApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringDynamicRuntimeConfigClientApp.class, args);
	}

}

Testing the Application

Once your server and client applications are running you can now check the value of the msg.

Bu hitting the URL http://localhost:8080/msg you will get value Hello Dev and not the default value, because the external properties file has been read during application startup.

Now update the key/value pair in the external.properties file as msg=.

As I said earlier that configuration file is read once during bootstrap of the app and not again. So I need to trigger the refresh endpoint to get the updated value from the external configuration.

Let’s trigger the endpoint the URL http://localhost:8080/actuator/refresh with HTTP POST method and with blank request body.

Now if you hit the URL http://localhost:8080/msg again you will nothing in the output or response:

spring centralized runtime properties configuration without refreshing the client

Now if you change the key/value pair as msg=Hello Developer in the external.properties then you will get output Hello Developer.

spring centralized runtime properties configuration without refreshing the client

That’s all about how to manage external configurations and change them dynamically at run time without restarting the application or JVM.

Source Code

Download

Leave a Reply

Your email address will not be published. Required fields are marked *