Skip to content

Commit 54ee498

Browse files
committed
Auto-configure the Postgres application_name when using Docker Compose
1 parent 301816e commit 54ee498

File tree

9 files changed

+405
-23
lines changed

9 files changed

+405
-23
lines changed

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

Lines changed: 25 additions & 3 deletions
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

Lines changed: 31 additions & 8 deletions
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

Lines changed: 16 additions & 1 deletion
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

Lines changed: 7 additions & 3 deletions
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

@@ -41,6 +42,7 @@
4142
* @author Moritz Halbritter
4243
* @author Andy Wilkinson
4344
* @author Phillip Webb
45+
* @author Dmytro Nosan
4446
*/
4547
class DockerComposeServiceConnectionsApplicationListener
4648
implements ApplicationListener<DockerComposeServicesReadyEvent> {
@@ -59,13 +61,15 @@ class DockerComposeServiceConnectionsApplicationListener
5961
public void onApplicationEvent(DockerComposeServicesReadyEvent event) {
6062
ApplicationContext applicationContext = event.getSource();
6163
if (applicationContext instanceof BeanDefinitionRegistry registry) {
62-
registerConnectionDetails(registry, event.getRunningServices());
64+
Environment environment = applicationContext.getEnvironment();
65+
registerConnectionDetails(registry, environment, event.getRunningServices());
6366
}
6467
}
6568

66-
private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) {
69+
private void registerConnectionDetails(BeanDefinitionRegistry registry, Environment environment,
70+
List<RunningService> runningServices) {
6771
for (RunningService runningService : runningServices) {
68-
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService);
72+
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService, environment);
6973
this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> {
7074
register(registry, runningService, connectionDetailsType, connectionDetails);
7175
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

Lines changed: 46 additions & 3 deletions
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,41 @@ public String getJdbcUrl() {
7886
return this.jdbcUrl;
7987
}
8088

89+
private static String getJdbcUrl(RunningService service, String database, Environment environment) {
90+
String jdbcUrl = jdbcUrlBuilder.build(service, database);
91+
return appendApplicationNameIfNecessary(jdbcUrl, environment);
92+
}
93+
94+
private static String appendApplicationNameIfNecessary(String jdbcUrl, Environment environment) {
95+
if (hasParameter(jdbcUrl, APPLICATION_NAME)) {
96+
return jdbcUrl;
97+
}
98+
String applicationName = environment.getProperty("spring.application.name");
99+
if (!StringUtils.hasText(applicationName)) {
100+
return jdbcUrl;
101+
}
102+
return appendParameter(jdbcUrl, APPLICATION_NAME, applicationName);
103+
}
104+
105+
private static boolean hasParameter(String jdbcUrl, String parameterName) {
106+
return jdbcUrl.contains("?%s=".formatted(parameterName))
107+
|| jdbcUrl.contains("&%s=".formatted(parameterName));
108+
}
109+
110+
private static String appendParameter(String jdbcUrl, String name, String value) {
111+
StringBuilder jdbcUrlBuilder = new StringBuilder(jdbcUrl);
112+
if (!jdbcUrl.contains("?")) {
113+
jdbcUrlBuilder.append("?");
114+
}
115+
else if (!jdbcUrl.endsWith("&")) {
116+
jdbcUrlBuilder.append("&");
117+
}
118+
return jdbcUrlBuilder.append(name)
119+
.append("=")
120+
.append(URLEncoder.encode(value, StandardCharsets.UTF_8))
121+
.toString();
122+
}
123+
81124
}
82125

83126
}

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

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
package org.springframework.boot.docker.compose.service.connection.postgres;
1818

1919
import io.r2dbc.spi.ConnectionFactoryOptions;
20+
import io.r2dbc.spi.Option;
2021

2122
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
2223
import org.springframework.boot.docker.compose.core.RunningService;
2324
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
2425
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
2526
import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder;
27+
import org.springframework.core.env.Environment;
28+
import org.springframework.util.StringUtils;
2629

2730
/**
2831
* {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails}
@@ -32,6 +35,7 @@
3235
* @author Andy Wilkinson
3336
* @author Phillip Webb
3437
* @author Scott Frederick
38+
* @author Dmytro Nosan
3539
*/
3640
class PostgresR2dbcDockerComposeConnectionDetailsFactory
3741
extends DockerComposeConnectionDetailsFactory<R2dbcConnectionDetails> {
@@ -44,7 +48,7 @@ class PostgresR2dbcDockerComposeConnectionDetailsFactory
4448

4549
@Override
4650
protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
47-
return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService());
51+
return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService(), source.getEnvironment());
4852
}
4953

5054
/**
@@ -53,23 +57,43 @@ protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerCompose
5357
static class PostgresDbR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
5458
implements R2dbcConnectionDetails {
5559

60+
private static final Option<String> APPLICATION_NAME = Option.valueOf("applicationName");
61+
5662
private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder(
5763
"postgresql", 5432);
5864

5965
private final ConnectionFactoryOptions connectionFactoryOptions;
6066

61-
PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service) {
67+
PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service, Environment environment) {
6268
super(service);
63-
PostgresEnvironment environment = new PostgresEnvironment(service.env());
64-
this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(),
65-
environment.getUsername(), environment.getPassword());
69+
this.connectionFactoryOptions = getConnectionFactoryOptions(service, environment);
6670
}
6771

6872
@Override
6973
public ConnectionFactoryOptions getConnectionFactoryOptions() {
7074
return this.connectionFactoryOptions;
7175
}
7276

77+
private static ConnectionFactoryOptions getConnectionFactoryOptions(RunningService service,
78+
Environment environment) {
79+
PostgresEnvironment env = new PostgresEnvironment(service.env());
80+
ConnectionFactoryOptions connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service,
81+
env.getDatabase(), env.getUsername(), env.getPassword());
82+
return appendApplicationNameIfNecessary(connectionFactoryOptions, environment);
83+
}
84+
85+
private static ConnectionFactoryOptions appendApplicationNameIfNecessary(
86+
ConnectionFactoryOptions connectionFactoryOptions, Environment environment) {
87+
if (connectionFactoryOptions.hasOption(APPLICATION_NAME)) {
88+
return connectionFactoryOptions;
89+
}
90+
String applicationName = environment.getProperty("spring.application.name");
91+
if (!StringUtils.hasText(applicationName)) {
92+
return connectionFactoryOptions;
93+
}
94+
return connectionFactoryOptions.mutate().option(APPLICATION_NAME, applicationName).build();
95+
}
96+
7397
}
7498

7599
}

0 commit comments

Comments
 (0)