Skip to content

Commit 1b43d97

Browse files
authored
Add Cloud Environment Repository with MySQL example
1 parent ebcf4e1 commit 1b43d97

File tree

15 files changed

+387
-4
lines changed

15 files changed

+387
-4
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Repository contains a collection of [Spring Boot](https://spring.io/projects/spr
66
separate project.
77

88
# Topics
9-
| Topic | Description |
10-
|-----------------------------------------------|-------------------------------------------------------|
11-
| [Spring Data Envers Audit](data-envers-audit) | Enable with Entity Revisions using Spring Data Envers |
12-
| [Spring Data JPA Audit](data-jpa-audit) | Enable Audit with Spring Data JPA |
9+
| Topic | Description |
10+
|-----------------------------------------------|-----------------------------------------------------------------------------------------|
11+
| [Spring Cloud Environment Repository](cloud-jdbc-env-repo) | Store environment properties in MySQL database with Spring Cloud Environment Repository |
12+
| [Spring Data Envers Audit](data-envers-audit) | Enable with Entity Revisions using Spring Data Envers |
13+
| [Spring Data JPA Audit](data-jpa-audit) | Enable Audit with Spring Data JPA |

cloud-jdbc-env-repo/.gitignore

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
HELP.md
2+
.gradle
3+
build/
4+
!gradle/wrapper/gradle-wrapper.jar
5+
!**/src/main/**/build/
6+
!**/src/test/**/build/
7+
8+
### STS ###
9+
.apt_generated
10+
.classpath
11+
.factorypath
12+
.project
13+
.settings
14+
.springBeans
15+
.sts4-cache
16+
bin/
17+
!**/src/main/**/bin/
18+
!**/src/test/**/bin/
19+
20+
### IntelliJ IDEA ###
21+
.idea
22+
*.iws
23+
*.iml
24+
*.ipr
25+
out/
26+
!**/src/main/**/out/
27+
!**/src/test/**/out/
28+
29+
### NetBeans ###
30+
/nbproject/private/
31+
/nbbuild/
32+
/dist/
33+
/nbdist/
34+
/.nb-gradle/
35+
36+
### VS Code ###
37+
.vscode/

cloud-jdbc-env-repo/README.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Spring Cloud: JDBCEnvironmentRepository Sample Application
2+
Sample of [Spring Cloud Config Server][2] embedded application that uses database as backend for configuration properties.
3+
4+
## Background
5+
[Spring Cloud Config Server][2] provides several options to store configuration for an application. In general it is handled
6+
by [Environment Repository][3].
7+
8+
Available options are git, file system, vault, svn, and database. This application demonstrates usage of [JdbcEnvironmentRepository][1]
9+
which allows an application to store its configuration in database.
10+
11+
## Configuration
12+
In order to enable this feature we will include `spring-boot-starter-jdbc` as one of the dependencies for the application and
13+
include `jdbc` as one of its active profiles.
14+
15+
### Include JDBC as dependency
16+
This can be seen in [build.gradle](build.gradle).
17+
18+
```groovy
19+
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
20+
```
21+
22+
### Include `jdbc` as active profile
23+
This can be seen in [bootstrap.yml][4].
24+
25+
```yaml
26+
spring:
27+
profiles:
28+
active: jdbc
29+
```
30+
31+
## Creating table and populating data
32+
By default the [JdbcEnvironmentRepository][1] will look into a table called `PROPERTIES` which contains the following columns:
33+
34+
- KEY
35+
- VALUE
36+
- APPLICATION
37+
- PROFILE
38+
- LABEL
39+
40+
### Create table schema
41+
Schema to create the table can found in [init-script.sql][6]:
42+
43+
```sql
44+
CREATE TABLE `PROPERTIES` (
45+
`KEY` VARCHAR(128),
46+
`VALUE` VARCHAR(128),
47+
`APPLICATION` VARCHAR(128),
48+
`PROFILE` VARCHAR(128),
49+
`LABEL` VARCHAR(128),
50+
PRIMARY KEY (`KEY`, `APPLICATION`, `LABEL`)
51+
);
52+
```
53+
54+
In the script above KEY, APPLICATION, PROFILE, and LABEL are marked as composite key in order to avoid duplicated entry.
55+
56+
### Populate table
57+
For this demonstration will we have a configuration called `app.greet.name` and this will be populated upon start-up.
58+
Its script can be found in [init-script.sql][6].
59+
60+
```sql
61+
INSERT INTO PROPERTIES (`APPLICATION`, `PROFILE`, `LABEL`, `KEY`, `VALUE`) VALUES ('demo', 'default', 'master', 'app.greet.name', 'Demo');
62+
```
63+
64+
The script above explains that the configuration `app.greet.name` belongs to:
65+
66+
- an application called _demo_
67+
- with profile called _default_
68+
- and labelled _master_
69+
70+
## Configure Application Properties
71+
In order for [JdbcEnvironmentRepository][1] to retrieve properties for this application it will need to be informed on
72+
its name, profile, and label. This configurations can be found in [bootstrap.yml][4]
73+
74+
```yaml
75+
spring:
76+
application:
77+
name: demo
78+
```
79+
80+
We are not configuring `spring.cloud.profile` because its default value is `default`.
81+
82+
## Create Bootstrap Application Context
83+
Finally, we will need to inform Spring Cloud on what are the classes needed in order to build the
84+
bootstrap application context. This can found in [spring.factories][8]:
85+
86+
```text
87+
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
88+
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
89+
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
90+
```
91+
92+
These two classes will help us to build [JdbcTemplate][10] which is needed to construct [JdbcEnvironmentRepository][9].
93+
94+
## Verify Configuration Properties
95+
In order to ensure that the application will use configurations from database we will create same configuration in [application.yml][11]:
96+
97+
```yaml
98+
app:
99+
greet:
100+
name: Default
101+
```
102+
103+
## Configure Environment Properties Retrieval SQL
104+
By default, [there are two SQL statements that are used to retrieve properties from database](https://github.com/spring-cloud/spring-cloud-config/blob/main/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JdbcEnvironmentProperties.java#L30).
105+
However, these queries need to be modified to follow MySQL requirement and implemented in [bootstrap.yml][4]:
106+
107+
```yaml
108+
spring:
109+
cloud:
110+
config:
111+
server:
112+
jdbc:
113+
sql: SELECT `KEY`, `VALUE` from PROPERTIES where APPLICATION=? and PROFILE=? and LABEL=?
114+
sql-without-profile: SELECT `KEY`, `VALUE` from PROPERTIES where APPLICATION=? and PROFILE='default' and LABEL=?
115+
```
116+
117+
We will have [GreetResource][12] which will retrieve the value of `app.greet.name` from [GreetProperties][14].
118+
119+
```java
120+
@RestController
121+
class GreetResource {
122+
123+
private final GreetProperties properties;
124+
125+
GreetResource(GreetProperties properties) {
126+
this.properties = properties;
127+
}
128+
129+
@GetMapping("/greet")
130+
public String greet(@RequestParam String greeting) {
131+
return String.format("%s, my name is %s", greeting, properties.name());
132+
}
133+
134+
}
135+
```
136+
137+
Next we will have [CloudJdbcEnvRepoApplicationTests][13] class that verifies that the value for `app.greet.name` is **Demo** and not **Default**:
138+
139+
```java
140+
@Testcontainers
141+
@SpringBootTest(properties = "spring.datasource.url=jdbc:tc:mysql:8:///test?TC_INITSCRIPT=init-script.sql", webEnvironment = RANDOM_PORT)
142+
class CloudJdbcEnvRepoApplicationTests {
143+
144+
@Container
145+
private static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8");
146+
147+
@Autowired
148+
private TestRestTemplate restClient;
149+
150+
@Test
151+
@DisplayName("Given app.greet.name is configured to Demo in the database When I call greet Then I should get Hello, my name is Demo")
152+
void greet() {
153+
var response = restClient.getForEntity("/greet?greeting={0}", String.class, "Hello");
154+
155+
assertThat(response.getBody()).isEqualTo("Hello, my name is Demo");
156+
}
157+
158+
}
159+
```
160+
161+
By executing `greet()` we verify that the returned response is **Hello, my name is Demo** and not **Hello, my name is Default**.
162+
163+
[1]: https://cloud.spring.io/spring-cloud-config/single/spring-cloud-config.html#_jdbc_backend
164+
[2]: https://cloud.spring.io/spring-cloud-config/single/spring-cloud-config.html#_spring_cloud_config_server
165+
[3]: https://cloud.spring.io/spring-cloud-config/single/spring-cloud-config.html#_environment_repository
166+
[4]: src/main/resources/bootstrap.yml
167+
[6]: src/test/resources/init-script.sql
168+
[8]: src/main/resources/META-INF/spring.factories
169+
[9]: https://github.com/spring-cloud/spring-cloud-config/blob/master/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JdbcEnvironmentRepository.java
170+
[10]: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html
171+
[11]: src/main/resources/application.yml
172+
[12]: src/main/java/zin/rashidi/boot/cloud/jdbcenvrepo/greet/GreetResource.java
173+
[13]: src/test/java/zin/rashidi/boot/cloud/jdbcenvrepo/CloudJdbcEnvRepoApplicationTests.java
174+
[14]: src/main/java/zin/rashidi/boot/cloud/jdbcenvrepo/greet/GreetProperties.java

cloud-jdbc-env-repo/build.gradle

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot' version '3.1.1'
4+
id 'io.spring.dependency-management' version '1.1.1'
5+
}
6+
7+
group = 'zin.rashidi.boot'
8+
version = '0.0.1-SNAPSHOT'
9+
10+
java {
11+
sourceCompatibility = '17'
12+
}
13+
14+
configurations {
15+
compileOnly {
16+
extendsFrom annotationProcessor
17+
}
18+
}
19+
20+
repositories {
21+
mavenCentral()
22+
}
23+
24+
ext {
25+
set('springCloudVersion', "2022.0.3")
26+
}
27+
28+
dependencies {
29+
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
30+
implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
31+
implementation 'org.springframework.cloud:spring-cloud-config-server'
32+
runtimeOnly 'com.mysql:mysql-connector-j'
33+
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
34+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
35+
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
36+
testImplementation 'org.testcontainers:junit-jupiter'
37+
testImplementation 'org.testcontainers:mysql'
38+
}
39+
40+
dependencyManagement {
41+
imports {
42+
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
43+
}
44+
}
45+
46+
tasks.named('test') {
47+
useJUnitPlatform()
48+
}

cloud-jdbc-env-repo/settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'cloud-jdbc-env-repo'
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package zin.rashidi.boot.cloud.jdbcenvrepo;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class CloudJdbcEnvRepoApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(CloudJdbcEnvRepoApplication.class, args);
11+
}
12+
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package zin.rashidi.boot.cloud.jdbcenvrepo.greet;
2+
3+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4+
import org.springframework.context.annotation.Configuration;
5+
6+
/**
7+
* @author Rashidi Zin, GfK
8+
*/
9+
@Configuration
10+
@EnableConfigurationProperties(GreetProperties.class)
11+
class GreetConfiguration {
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package zin.rashidi.boot.cloud.jdbcenvrepo.greet;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
/**
6+
* @author Rashidi Zin, GfK
7+
*/
8+
@ConfigurationProperties(prefix = "app.greet")
9+
record GreetProperties(String name) {
10+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package zin.rashidi.boot.cloud.jdbcenvrepo.greet;
2+
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.RequestParam;
5+
import org.springframework.web.bind.annotation.RestController;
6+
7+
/**
8+
* @author Rashidi Zin, GfK
9+
*/
10+
@RestController
11+
class GreetResource {
12+
13+
private final GreetProperties properties;
14+
15+
GreetResource(GreetProperties properties) {
16+
this.properties = properties;
17+
}
18+
19+
@GetMapping("/greet")
20+
public String greet(@RequestParam String greeting) {
21+
return String.format("%s, my name is %s", greeting, properties.name());
22+
}
23+
24+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
2+
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
3+
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
app:
2+
greet:
3+
name: Default
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
spring:
2+
application:
3+
name: demo
4+
cloud:
5+
config:
6+
server:
7+
bootstrap: true
8+
jdbc:
9+
sql: SELECT `KEY`, `VALUE` from PROPERTIES where APPLICATION=? and PROFILE=? and LABEL=?
10+
sql-without-profile: SELECT `KEY`, `VALUE` from PROPERTIES where APPLICATION=? and PROFILE='default' and LABEL=?
11+
profiles:
12+
active: jdbc
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package zin.rashidi.boot.cloud.jdbcenvrepo;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
5+
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.context.SpringBootTest;
10+
import org.springframework.boot.test.web.client.TestRestTemplate;
11+
import org.testcontainers.containers.MySQLContainer;
12+
import org.testcontainers.junit.jupiter.Container;
13+
import org.testcontainers.junit.jupiter.Testcontainers;
14+
15+
@Testcontainers
16+
@SpringBootTest(properties = "spring.datasource.url=jdbc:tc:mysql:8:///test?TC_INITSCRIPT=init-script.sql", webEnvironment = RANDOM_PORT)
17+
class CloudJdbcEnvRepoApplicationTests {
18+
19+
@Container
20+
private static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8");
21+
22+
@Autowired
23+
private TestRestTemplate restClient;
24+
25+
@Test
26+
@DisplayName("Given app.greet.name is configured to Demo in the database When I call greet Then I should get Hello, my name is Demo")
27+
void greet() {
28+
var response = restClient.getForEntity("/greet?greeting={0}", String.class, "Hello");
29+
30+
assertThat(response.getBody()).isEqualTo("Hello, my name is Demo");
31+
}
32+
33+
}

0 commit comments

Comments
 (0)