Original post is http://blog.ik.am/#/entries/290 (Japanese)
Please correct my poor English ;)
What is Spring Cloud Config?
Spring Cloud Config project provides a mechanism of configuration in a distributed system.
My most interested topic in SpringOne 2gx 2014.
Spring Cloud Config consists of Client and Server.
Sever manages the external configuration such as Git and properties file, provides the centralized configuration to all of Client.
Client holds the configuration from Server using the abstraction mechanism Spring Framework originally has such as Environment
andPropertySource
.
It also enables reloading the configuration.
The minimum system using them is as follows:
"Normal application" requires the configuration is a Client. Even though there are many Clients, Server can mange them centrally.
How to use
Note that the following contents are for 1.0.0.M1 version, these can be changed drastically in the future.
Setup Config Server
Building Config Server is very simple. Add dependency on org.springframework.cloud:spring-cloud-config-server
and just put EnableConfigServer
to the entry point class.
Setting example of pom.xml is described later.
Entry point class is like as follows:
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableConfigServer // important!!
@ComponentScan
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Describe the configuration where to fetch the configuration in bootstrap.yml
just under the classpath (Note that it's not application.yml
).
As a default, spring-cloud-samples/config-repo is configured to fetch. In this case, because I want to change properties, I'm using my making/config-repo forked from it and set the url of Git share repository url in spring.platform.config.server.uri
property like:
spring.platform.config.server.uri: https://github.com/making/config-repo
Finally, the setting of the pom.xml. Since the official version has not been released yet, it has a little redundant configuration, it would be more simple after 1.0.0.RELEASE. The important part is the dependency on org.springframework.cloud:spring-cloud-config-server
.
<?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>demo</groupId>
<artifactId>configserver</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.5.RELEASE</version>
<relativePath/>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starters</artifactId>
<version>1.0.0.M1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>demo.App</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>${spring-loaded.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
The project structure is as follows:
Server runs on 8888 port when you run App
class. Defaut configured application.yml
is included in spring-cloud-config-server-1.0.0.M1.jar.
By accessing http://localhost:8888/{name}/{env}/{label}
, you can get the configuration for each environment(profile) of each application.
Probably you can regard
-
name
as application name -
env
as profile name (default isdefault
) -
label
as branch name (default ismaster
)
label
can be omitted.
In the making/config-repo example, these are the following pic:
- By accessing http://localhost:8888/foo/default ,you can get the configuration of foo.properties.
- By accessing http://localhost:8888/foo/development ,you can get the configuration foo.properties overwritten by foo-development.properties.
$ curl -X GET http://localhost:8888/foo/default | jq .
{
"propertySources": [
{
"source": {
"foo": "b",
"test": "This is a test",
"bar": "123456"
},
"name": "https://github.com/making/config-repo/foo.properties"
},
{
"source": {
"info.url": "https://github.com/spring-cloud-samples",
"info.description": "Spring Cloud Samples"
},
"name": "https://github.com/making/config-repo/application.yml"
}
],
"label": "master",
"name": "default"
}
Then set development
to env
and send a request:
$ curl -X GET http://localhost:8888/foo/development | jq .
{
"propertySources": [
{
"source": {
"bar": "spam"
},
"name": "https://github.com/making/config-repo/foo-development.properties"
},
{
"source": {
"foo": "b",
"test": "This is a test",
"bar": "123456"
},
"name": "https://github.com/making/config-repo/foo.properties"
},
{
"source": {
"info.url": "https://github.com/spring-cloud-samples",
"info.description": "Spring Cloud Samples"
},
"name": "https://github.com/making/config-repo/application.yml"
}
],
"label": "master",
"name": "development"
}
Returned both values default
anddevelopment
. Which to use is determined at Client side. (this case,development
is prioritized)
Let's change foo-development.properties
on Github as follows:
bar: Updated!
foo: Added!
Then send a request tohttp://localhost:8888/foo/development
again
$ curl -X GET http://localhost:8888/foo/development | jq .
{
"propertySources": [
{
"source": {
"foo": "Added!",
"bar": "Updated!"
},
"name": "https://github.com/making/config-repo/foo-development.properties"
},
{
"source": {
"foo": "b",
"test": "This is a test",
"bar": "123456"
},
"name": "https://github.com/making/config-repo/foo.properties"
},
{
"source": {
"info.url": "https://github.com/spring-cloud-samples",
"info.description": "Spring Cloud Samples"
},
"name": "https://github.com/making/config-repo/application.yml"
}
],
"label": "master",
"name": "development"
}
Returned latest value though pulling Git(In the following description, the original contents are reverted with git push -f origin HEAD^:master
)
How to configure authentication/authorization and encrypt/decrypt are described in the official document.
Setup Client
Let's move on to the Client side. Client is a Spring Boot application with the dependency on org.springframework.cloud:spring-cloud-config-client
, which enables to connect Config Server automatically and use properties via Config Server.
Also add the dependency on org.springframework.boot:spring-boot-starter-actuator
.
The application name of Client can be defined with spring.application.name
key in bootstrap.yml
.
spring:
application:
name: foo
By default, Config Server connects to http://localhost:8888
with env=default
and label=master
. To override, configure as bellow:
spring:
application:
name: foo
cloud:
config:
env: default # optional
label: master # optional
uri: http://localhost:8888 # optional
In ClientApp
class which is the entry point for Client, implement a simple controller uses bar
property.
package demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@EnableAutoConfiguration
@ComponentScan
@RestController
public class ClientApp {
@Value("${bar:World!}")
String bar;
@RequestMapping("/")
String hello() {
return "Hello " + bar + "!";
}
public static void main(String[] args) {
SpringApplication.run(ClientApp.class, args);
}
}
pom.xml is as follows
<?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>demo</groupId>
<artifactId>configclient</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.5.RELEASE</version>
<relativePath/>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starters</artifactId>
<version>1.0.0.M1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>demo.ClientApp</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>${spring-loaded.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
Project structure is as follows. (application.yml
is described later.)
You can find properties are obtained from Config Server by listing PropertiesSource
using the feature of Spring Boot Actuator (http://localhost:8080/env).
You can also see a single property.
$ curl http://localhost:8080/env/bar
123456
When you access to the controller,
$ curl http://localhost:8080
Hello 123456!
you can find the property on Config Server is injected.
Change configuration dynamically
Then rewrite a property value on the Config Server as follows:
bar: Spring Boot
When you access a single property,
$ curl http://localhost:8080/env/bar
123456
the change on the Config Server is not reflected at this stage. In order to reflect it, accessing refresh
endpoint with POST method is required.
$ curl -X POST http://localhost:8080/refresh
["bar"]
$ curl http://localhost:8080/env/bar
Spring Boot
You can find PropertySource
is reflected in the client.
Again,
$ curl http://localhost:8080
Hello 123456!
It is not possible to refresh the properties which are already "DI"ed (because this Controller is a singleton scope).
In order to re"DI", restart DI container is required by accessing restart
endpoint with POST method.
$ curl -X POST http://localhost:8080/restart
{"message":"Restarting"}
You can find DI container restarted from log (but it takes a few seconds).
$ curl http://localhost:8080
Hello Spring Boot!
At last, I could refresh the configuration without restarting the application!
By the way, restart
endpoint is disabled by default. To enable it application.yml
is needed to set as follows:
endpoints:
restart:
enabled: true
Ad-hoc configuration change
Without rewriting the Config Server, it is possible to change the configuration in the Client side ad hoc.
POST properties to env
endpoint as follows:
$ curl -X POST http://localhost:8080/env -d bar="Spring Cloud"
{"bar":"Spring Cloud"}
At this point, PropertySource
of this application is re-written, updated value can be obtained by GETting the property.
$ curl http://localhost:8080/env/bar
Spring Cloud
However, the result of the controller is unchanged since re-DI is not performed.
$ curl http://localhost:8080
Hello Spring Boot!
Also refesh
cannot change the result.
$ curl -X POST http://localhost:8080/refresh
[]
$ curl http://localhost:8080
Hello Spring Boot!
Again restart
by restarting the DI container, it is possible to rewrite the result of the controller.
$ curl -X POST http://localhost:8080/restart
{"message":"Restarting"}
$ curl http://localhost:8080
Hello Spring Cloud!
Introduction of Refresh scope
So far, you may feel unhappy to restart DI container because it takes a long time.
So refresh scope is introduced. A bean annotated with@RefreshScope
are re-instantiated without restarting the DI container by POSTing refresh
endpoint.
Modify ClientApp
package demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@EnableAutoConfiguration
@ComponentScan
@RestController
@RefreshScope // important!
public class ClientApp {
@Value("${bar:World!}")
String bar;
@RequestMapping("/")
String hello() {
return "Hello " + bar + "!";
}
public static void main(String[] args) {
SpringApplication.run(ClientApp.class, args);
}
}
When the application is restarted, it can be seen that ad hoc changes has been lost.
$ curl http://localhost:8080
Hello Spring Boot!
Again, change the value
$ curl -X POST http://localhost:8080/env -d bar="Spring Cloud Config"
{"bar":"Spring Cloud Config"}
The send a POST request to refresh
endpoint.
$ curl -X POST http://localhost:8080/refresh
[]
Although a little while ago I did not changed this controller anything, this time @RefreshScope
is anontated.
$ curl http://localhost:8080
Hello Spring Cloud Config!
It was possible to reflect the properties!
By the way,
- The beans annotated
@ConfigurationProperties
- The properties for the log level logging.level. *
has a refresh scope from the beginning.
Sample code is here。
I thought again Spring's DI container was excellent!
This feature should be prepared in Spring core.
Introduction of @Conditional
(rather than Spring Boot) was revolutionary for Spring which provides like this functionality without almost any settings.