Skip to content

Commit 3a5a70a

Browse files
committed
Auto-configure the Postgres application_name when using Docker Compose
1 parent 8ccf77b commit 3a5a70a

File tree

11 files changed

+555
-23
lines changed

11 files changed

+555
-23
lines changed

spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java

+25-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* @author Andy Wilkinson
3636
* @author Phillip Webb
3737
* @author Scott Frederick
38+
* @author Dmytro Nosan
3839
*/
3940
class PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests {
4041

@@ -57,22 +58,43 @@ void runWithBitnamiImageCreatesConnectionDetails(JdbcConnectionDetails connectio
5758
assertConnectionDetails(connectionDetails);
5859
}
5960

61+
@DockerComposeTest(composeFile = "postgres-application-name-compose.yaml", image = TestImage.POSTGRESQL)
62+
void runCreatesConnectionDetailsApplicationName(JdbcConnectionDetails connectionDetails)
63+
throws ClassNotFoundException {
64+
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
65+
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
66+
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://")
67+
.endsWith("?ApplicationName=spring+boot");
68+
checkApplicationName(connectionDetails, "spring boot");
69+
}
70+
6071
private void assertConnectionDetails(JdbcConnectionDetails connectionDetails) {
6172
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
6273
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
6374
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://").endsWith("/mydatabase");
6475
}
6576

66-
@SuppressWarnings("unchecked")
6777
private void checkDatabaseAccess(JdbcConnectionDetails connectionDetails) throws ClassNotFoundException {
78+
assertThat(queryForObject(connectionDetails, DatabaseDriver.POSTGRESQL.getValidationQuery(), Integer.class))
79+
.isEqualTo(1);
80+
}
81+
82+
private void checkApplicationName(JdbcConnectionDetails connectionDetails, String applicationName)
83+
throws ClassNotFoundException {
84+
assertThat(queryForObject(connectionDetails, "select current_setting('application_name')", String.class))
85+
.isEqualTo(applicationName);
86+
}
87+
88+
@SuppressWarnings("unchecked")
89+
private <T> T queryForObject(JdbcConnectionDetails connectionDetails, String sql, Class<T> result)
90+
throws ClassNotFoundException {
6891
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
6992
dataSource.setUrl(connectionDetails.getJdbcUrl());
7093
dataSource.setUsername(connectionDetails.getUsername());
7194
dataSource.setPassword(connectionDetails.getPassword());
7295
dataSource.setDriverClass((Class<? extends Driver>) ClassUtils.forName(connectionDetails.getDriverClassName(),
7396
getClass().getClassLoader()));
74-
JdbcTemplate template = new JdbcTemplate(dataSource);
75-
assertThat(template.queryForObject(DatabaseDriver.POSTGRESQL.getValidationQuery(), Integer.class)).isEqualTo(1);
97+
return new JdbcTemplate(dataSource).queryForObject(sql, result);
7698
}
7799

78100
}

spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java

+31-8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import io.r2dbc.spi.ConnectionFactories;
2222
import io.r2dbc.spi.ConnectionFactoryOptions;
23+
import io.r2dbc.spi.Option;
2324

2425
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
2526
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
@@ -36,6 +37,7 @@
3637
* @author Andy Wilkinson
3738
* @author Phillip Webb
3839
* @author Scott Frederick
40+
* @author Dmytro Nosan
3941
*/
4042
class PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests {
4143

@@ -60,21 +62,42 @@ void runWithBitnamiImageCreatesConnectionDetails(R2dbcConnectionDetails connecti
6062
assertConnectionDetails(connectionDetails);
6163
}
6264

65+
@DockerComposeTest(composeFile = "postgres-application-name-compose.yaml", image = TestImage.POSTGRESQL)
66+
void runCreatesConnectionDetailsApplicationName(R2dbcConnectionDetails connectionDetails) {
67+
assertConnectionDetails(connectionDetails);
68+
ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions();
69+
assertThat(options.getValue(Option.valueOf("applicationName"))).isEqualTo("spring boot");
70+
checkApplicationName(connectionDetails, "spring boot");
71+
}
72+
6373
private void assertConnectionDetails(R2dbcConnectionDetails connectionDetails) {
64-
ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions();
65-
assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=postgresql",
66-
"password=REDACTED", "user=myuser");
67-
assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret");
74+
ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions();
75+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isNotNull();
76+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isNotNull();
77+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase");
78+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("myuser");
79+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret");
80+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql");
6881
}
6982

7083
private void checkDatabaseAccess(R2dbcConnectionDetails connectionDetails) {
84+
Integer result = queryForObject(connectionDetails, DatabaseDriver.POSTGRESQL.getValidationQuery(),
85+
Integer.class);
86+
assertThat(result).isEqualTo(1);
87+
}
88+
89+
private void checkApplicationName(R2dbcConnectionDetails connectionDetails, String applicationName) {
90+
assertThat(queryForObject(connectionDetails, "select current_setting('application_name')", String.class))
91+
.isEqualTo(applicationName);
92+
}
93+
94+
private <T> T queryForObject(R2dbcConnectionDetails connectionDetails, String sql, Class<T> result) {
7195
ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions();
72-
Object result = DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions))
73-
.sql(DatabaseDriver.POSTGRESQL.getValidationQuery())
74-
.map((row, metadata) -> row.get(0))
96+
return DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions))
97+
.sql(sql)
98+
.mapValue(result)
7599
.first()
76100
.block(Duration.ofSeconds(30));
77-
assertThat(result).isEqualTo(1);
78101
}
79102

80103
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
database:
3+
image: '{imageName}'
4+
ports:
5+
- '5432'
6+
environment:
7+
- 'POSTGRES_USER=myuser'
8+
- 'POSTGRES_DB=mydatabase'
9+
- 'POSTGRES_PASSWORD=secret'
10+
labels:
11+
org.springframework.boot.jdbc.parameters: 'ApplicationName=spring+boot'
12+
org.springframework.boot.r2dbc.parameters: 'applicationName=spring boot'

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.docker.compose.service.connection;
1818

1919
import org.springframework.boot.docker.compose.core.RunningService;
20+
import org.springframework.core.env.Environment;
2021

2122
/**
2223
* Passed to {@link DockerComposeConnectionDetailsFactory} to provide details of the
@@ -25,19 +26,24 @@
2526
* @author Moritz Halbritter
2627
* @author Andy Wilkinson
2728
* @author Phillip Webb
29+
* @author Dmytro Nosan
2830
* @since 3.1.0
2931
* @see DockerComposeConnectionDetailsFactory
3032
*/
3133
public final class DockerComposeConnectionSource {
3234

3335
private final RunningService runningService;
3436

37+
private final Environment environment;
38+
3539
/**
3640
* Create a new {@link DockerComposeConnectionSource} instance.
3741
* @param runningService the running Docker Compose service
42+
* @param environment environment in which the current application is running
3843
*/
39-
DockerComposeConnectionSource(RunningService runningService) {
44+
DockerComposeConnectionSource(RunningService runningService, Environment environment) {
4045
this.runningService = runningService;
46+
this.environment = environment;
4147
}
4248

4349
/**
@@ -48,4 +54,13 @@ public RunningService getRunningService() {
4854
return this.runningService;
4955
}
5056

57+
/**
58+
* Environment in which the current application is running.
59+
* @return the environment
60+
* @since 3.4.0
61+
*/
62+
public Environment getEnvironment() {
63+
return this.environment;
64+
}
65+
5166
}

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.boot.docker.compose.lifecycle.DockerComposeServicesReadyEvent;
3232
import org.springframework.context.ApplicationContext;
3333
import org.springframework.context.ApplicationListener;
34+
import org.springframework.core.env.Environment;
3435
import org.springframework.util.ClassUtils;
3536
import org.springframework.util.StringUtils;
3637

@@ -59,13 +60,15 @@ class DockerComposeServiceConnectionsApplicationListener
5960
public void onApplicationEvent(DockerComposeServicesReadyEvent event) {
6061
ApplicationContext applicationContext = event.getSource();
6162
if (applicationContext instanceof BeanDefinitionRegistry registry) {
62-
registerConnectionDetails(registry, event.getRunningServices());
63+
Environment environment = applicationContext.getEnvironment();
64+
registerConnectionDetails(registry, environment, event.getRunningServices());
6365
}
6466
}
6567

66-
private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) {
68+
private void registerConnectionDetails(BeanDefinitionRegistry registry, Environment environment,
69+
List<RunningService> runningServices) {
6770
for (RunningService runningService : runningServices) {
68-
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService);
71+
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService, environment);
6972
this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> {
7073
register(registry, runningService, connectionDetailsType, connectionDetails);
7174
this.factories.getConnectionDetails(connectionDetails, false)

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java

+25-3
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616

1717
package org.springframework.boot.docker.compose.service.connection.postgres;
1818

19+
import java.net.URLEncoder;
20+
import java.nio.charset.StandardCharsets;
21+
1922
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
2023
import org.springframework.boot.docker.compose.core.RunningService;
2124
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
2225
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
2326
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
27+
import org.springframework.core.env.Environment;
28+
import org.springframework.util.StringUtils;
2429

2530
/**
2631
* {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails}
@@ -30,6 +35,7 @@
3035
* @author Andy Wilkinson
3136
* @author Phillip Webb
3237
* @author Scott Frederick
38+
* @author Dmytro Nosan
3339
*/
3440
class PostgresJdbcDockerComposeConnectionDetailsFactory
3541
extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> {
@@ -42,7 +48,7 @@ protected PostgresJdbcDockerComposeConnectionDetailsFactory() {
4248

4349
@Override
4450
protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
45-
return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService());
51+
return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService(), source.getEnvironment());
4652
}
4753

4854
/**
@@ -53,14 +59,16 @@ static class PostgresJdbcDockerComposeConnectionDetails extends DockerComposeCon
5359

5460
private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("postgresql", 5432);
5561

62+
private static final String APPLICATION_NAME = "ApplicationName";
63+
5664
private final PostgresEnvironment environment;
5765

5866
private final String jdbcUrl;
5967

60-
PostgresJdbcDockerComposeConnectionDetails(RunningService service) {
68+
PostgresJdbcDockerComposeConnectionDetails(RunningService service, Environment environment) {
6169
super(service);
6270
this.environment = new PostgresEnvironment(service.env());
63-
this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase());
71+
this.jdbcUrl = getJdbcUrl(service, this.environment.getDatabase(), environment);
6472
}
6573

6674
@Override
@@ -78,6 +86,20 @@ public String getJdbcUrl() {
7886
return this.jdbcUrl;
7987
}
8088

89+
private static String getJdbcUrl(RunningService service, String database, Environment environment) {
90+
PostgresJdbcUrl jdbcUrl = new PostgresJdbcUrl(jdbcUrlBuilder.build(service, database));
91+
addApplicationNameIfNecessary(jdbcUrl, environment);
92+
return jdbcUrl.toString();
93+
}
94+
95+
private static void addApplicationNameIfNecessary(PostgresJdbcUrl jdbcUrl, Environment environment) {
96+
String applicationName = environment.getProperty("spring.application.name");
97+
if (StringUtils.hasText(applicationName)) {
98+
jdbcUrl.addParameterIfDoesNotExist(APPLICATION_NAME,
99+
URLEncoder.encode(applicationName, StandardCharsets.UTF_8));
100+
}
101+
}
102+
81103
}
82104

83105
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.docker.compose.service.connection.postgres;
18+
19+
import java.util.StringTokenizer;
20+
21+
import org.springframework.util.LinkedMultiValueMap;
22+
import org.springframework.util.MultiValueMap;
23+
24+
/**
25+
* Utility class to customize Postgres JDBC URL.
26+
*
27+
* @author Dmytro Nosan
28+
*/
29+
class PostgresJdbcUrl {
30+
31+
private final String url;
32+
33+
private final MultiValueMap<String, String> parameters;
34+
35+
/**
36+
* Creates new {@link PostgresJdbcUrl} instance.
37+
* @param jdbcUrl the JDBC URL
38+
*/
39+
PostgresJdbcUrl(String jdbcUrl) {
40+
this.url = getUrl(jdbcUrl);
41+
this.parameters = getParameters(jdbcUrl);
42+
}
43+
44+
/**
45+
* Adds value to the JDBC URL if a given name does not exist.
46+
* @param name the JDBC parameter name
47+
* @param value the JDBC parameter value
48+
*/
49+
void addParameterIfDoesNotExist(String name, String value) {
50+
if (this.parameters.containsKey(name)) {
51+
return;
52+
}
53+
this.parameters.add(name, value);
54+
}
55+
56+
/**
57+
* Build a JDBC URL.
58+
* @return a new JDBC URL
59+
*/
60+
@Override
61+
public String toString() {
62+
StringBuilder jdbcUrlBuilder = new StringBuilder(this.url);
63+
if (this.parameters.isEmpty()) {
64+
return jdbcUrlBuilder.toString();
65+
}
66+
jdbcUrlBuilder.append('?');
67+
this.parameters.forEach((name, values) -> values.forEach((value) -> {
68+
jdbcUrlBuilder.append(name);
69+
if (value != null) {
70+
jdbcUrlBuilder.append('=').append(value);
71+
}
72+
jdbcUrlBuilder.append('&');
73+
}));
74+
jdbcUrlBuilder.deleteCharAt(jdbcUrlBuilder.length() - 1);
75+
return jdbcUrlBuilder.toString();
76+
}
77+
78+
private static String getUrl(String jdbcUrl) {
79+
int index = jdbcUrl.indexOf('?');
80+
return (index != -1) ? jdbcUrl.substring(0, index) : jdbcUrl;
81+
}
82+
83+
private static MultiValueMap<String, String> getParameters(String jdbcUrl) {
84+
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
85+
int index = jdbcUrl.indexOf('?');
86+
if (index == -1) {
87+
return parameters;
88+
}
89+
StringTokenizer tokenizer = new StringTokenizer(jdbcUrl.substring(index + 1), "&");
90+
while (tokenizer.hasMoreTokens()) {
91+
String token = tokenizer.nextToken();
92+
int pos = token.indexOf('=');
93+
if (pos == -1) {
94+
parameters.add(token, null);
95+
}
96+
else {
97+
parameters.add(token.substring(0, pos), token.substring(pos + 1));
98+
}
99+
}
100+
return parameters;
101+
}
102+
103+
}

0 commit comments

Comments
 (0)