Introduction

This tutorial walks you through the process of consuming the configurations from the Spring cloud config server. So we 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 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 us to externalize our properties or configurations.

For example, you have database connection details which may be different depending on your 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

Eclipse 2019-12, At least Java 1.8, Spring Boot 2.3.1, Spring Cloud, Gradle 6.4.1, Maven 3.6.3

Spring Config Server App

We will build Spring config server app 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:

buildscript {
	ext {
		springBootVersion = '2.3.1.RELEASE'
	}
	
    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.9.RELEASE"
}

sourceCompatibility = 12
targetCompatibility = 12

repositories {
    mavenCentral()
}

dependencies {
	implementation("org.springframework.cloud:spring-cloud-config-server:2.2.3.RELEASE")
	
	//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:Hoxton.SR6'
    }
}

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

<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>

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

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-config-server</artifactId>
		</dependency>
		
		<!--required only if jdk 9 or higher version is used-->
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.4.0-b180830.0359</version>
		</dependency>
	</dependencies>
	
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Hoxton.SR6</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

    <build>
        <plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>at least 8</source>
					<target>at least 8</target>
				</configuration>
			</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 we 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 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 then you have to use 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 we are 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 Client App

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

Create a project in Eclipse 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.3.1.RELEASE'
	}
	
    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.9.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:2.2.3.RELEASE")
	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:Hoxton.SR6'
    }
}

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

<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>

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

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>
		<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>
		
		<!--required only if jdk 9 or higher version is used-->
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.4.0-b180830.0359</version>
		</dependency>
	</dependencies>
	
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Hoxton.SR6</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

    <build>
        <plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>at least 8</source>
					<target>at least 8</target>
				</configuration>
			</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 we need bootstrap.properties file under classpath directory src/main/resources. We 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.

#default config server url
spring.cloud.config.uri=http://localhost:8888
spring.application.name=external

We also want to enable the /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 we 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 we said earlier that configuration file is read once during bootstrap of the app and not again. So we 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.

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

Thanks for reading.

Tags:

Leave a Reply

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