Skip to content

Commit c0b267f

Browse files
committed
Polish "Fix detection logic for embedded databases"
See gh-23693
1 parent ab02084 commit c0b267f

File tree

8 files changed

+142
-66
lines changed

8 files changed

+142
-66
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -322,13 +322,13 @@ public void setUsername(String username) {
322322
* @since 1.4.0
323323
*/
324324
public String determineUsername() {
325-
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())
326-
&& !StringUtils.hasText(this.username)) {
327-
return "sa";
328-
}
329-
else {
325+
if (StringUtils.hasText(this.username)) {
330326
return this.username;
331327
}
328+
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) {
329+
return "sa";
330+
}
331+
return null;
332332
}
333333

334334
/**
@@ -350,13 +350,13 @@ public void setPassword(String password) {
350350
* @since 1.4.0
351351
*/
352352
public String determinePassword() {
353-
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())
354-
&& !StringUtils.hasText(this.password)) {
355-
return "";
356-
}
357-
else {
353+
if (StringUtils.hasText(this.password)) {
358354
return this.password;
359355
}
356+
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) {
357+
return "";
358+
}
359+
return null;
360360
}
361361

362362
public String getJndiName() {

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ void createDataSourceFallbackToEmbeddedProperties() {
127127
assertThat(context).hasSingleBean(Flyway.class);
128128
DataSource dataSource = context.getBean(Flyway.class).getConfiguration().getDataSource();
129129
assertThat(dataSource).isNotNull();
130+
assertThat(dataSource).hasFieldOrPropertyWithValue("user", "sa");
130131
assertThat(dataSource).hasFieldOrPropertyWithValue("password", "");
131132
});
132133
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourcePropertiesTests.java

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -78,32 +78,6 @@ void determineUrlWithExplicitConfig() throws Exception {
7878
assertThat(properties.determineUrl()).isEqualTo("jdbc:mysql://mydb");
7979
}
8080

81-
@Test
82-
void determineIsEmbeddedWithExplicitConfigforH2() throws Exception {
83-
DataSourceProperties properties = new DataSourceProperties();
84-
properties.setUrl("jdbc:h2:~/test");
85-
properties.setUsername("");
86-
properties.setPassword("");
87-
properties.afterPropertiesSet();
88-
assertThat(properties.getUrl()).isEqualTo("jdbc:h2:~/test");
89-
assertThat(properties.determineUrl()).isEqualTo("jdbc:h2:~/test");
90-
assertThat(properties.determineUsername()).isEqualTo("");
91-
assertThat(properties.determinePassword()).isEqualTo("");
92-
}
93-
94-
@Test
95-
void determineWithExplicitConfigforH2WithCustomJdbcUrl() throws Exception {
96-
DataSourceProperties properties = new DataSourceProperties();
97-
properties.setUrl("jdbc:h2:~/test");
98-
properties.setUsername("as");
99-
properties.setPassword("as");
100-
properties.afterPropertiesSet();
101-
assertThat(properties.getUrl()).isEqualTo("jdbc:h2:~/test");
102-
assertThat(properties.determineUrl()).isEqualTo("jdbc:h2:~/test");
103-
assertThat(properties.determineUsername()).isEqualTo("as");
104-
assertThat(properties.determinePassword()).isEqualTo("as");
105-
}
106-
10781
@Test
10882
void determineUrlWithGenerateUniqueName() throws Exception {
10983
DataSourceProperties properties = new DataSourceProperties();
@@ -151,11 +125,21 @@ void determineUsernameWithExplicitConfig() throws Exception {
151125
assertThat(properties.determineUsername()).isEqualTo("foo");
152126
}
153127

128+
@Test
129+
void determineUsernameWithNonEmbeddedUrl() throws Exception {
130+
DataSourceProperties properties = new DataSourceProperties();
131+
properties.setUrl("jdbc:h2:~/test");
132+
properties.afterPropertiesSet();
133+
assertThat(properties.getPassword()).isNull();
134+
assertThat(properties.determineUsername()).isNull();
135+
}
136+
154137
@Test
155138
void determinePassword() throws Exception {
156139
DataSourceProperties properties = new DataSourceProperties();
157140
properties.afterPropertiesSet();
158141
assertThat(properties.getPassword()).isNull();
142+
assertThat(properties.determinePassword()).isEqualTo("");
159143
}
160144

161145
@Test
@@ -167,6 +151,15 @@ void determinePasswordWithExplicitConfig() throws Exception {
167151
assertThat(properties.determinePassword()).isEqualTo("bar");
168152
}
169153

154+
@Test
155+
void determinePasswordWithNonEmbeddedUrl() throws Exception {
156+
DataSourceProperties properties = new DataSourceProperties();
157+
properties.setUrl("jdbc:h2:~/test");
158+
properties.afterPropertiesSet();
159+
assertThat(properties.getPassword()).isNull();
160+
assertThat(properties.determinePassword()).isNull();
161+
}
162+
170163
@Test
171164
void determineCredentialsForSchemaScripts() {
172165
DataSourceProperties properties = new DataSourceProperties();

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ void overrideDataSourceAndFallbackToEmbeddedProperties() {
246246
.run(assertLiquibase((liquibase) -> {
247247
DataSource dataSource = liquibase.getDataSource();
248248
assertThat(((HikariDataSource) dataSource).isClosed()).isTrue();
249+
assertThat(((HikariDataSource) dataSource).getUsername()).isEqualTo("sa");
249250
assertThat(((HikariDataSource) dataSource).getPassword()).isEqualTo("");
250251
}));
251252
}

spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,8 +1982,8 @@ This is controlled through two external properties:
19821982
You can set `spring.jpa.hibernate.ddl-auto` explicitly and the standard Hibernate property values are `none`, `validate`, `update`, `create`, and `create-drop`.
19831983
Spring Boot chooses a default value for you based on whether it thinks your database is embedded.
19841984
It defaults to `create-drop` if no schema manager has been detected or `none` in all other cases.
1985-
An embedded database is detected by looking at the `Connection` type.
1986-
`hsqldb`, `h2`, and `derby` are embedded, and others are not.
1985+
An embedded database is detected by looking at the `Connection` type and JDBC url.
1986+
`hsqldb`, `h2`, and `derby` are candidates, and others are not.
19871987
Be careful when switching from in-memory to a '`real`' database that you do not make assumptions about the existence of the tables and data in the new platform.
19881988
You either have to set `ddl-auto` explicitly or use one of the other mechanisms to initialize the database.
19891989

spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/TestDatabaseAutoConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ private void process(BeanDefinitionRegistry registry, ConfigurableListableBeanFa
101101

102102
private BeanDefinition createEmbeddedBeanDefinition(boolean primary) {
103103
BeanDefinition beanDefinition = new RootBeanDefinition(EmbeddedDataSourceFactoryBean.class);
104-
beanDefinition.setPrimary(true);
104+
beanDefinition.setPrimary(primary);
105105
return beanDefinition;
106106
}
107107

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
package org.springframework.boot.jdbc;
1818

1919
import java.sql.Connection;
20+
import java.sql.DatabaseMetaData;
2021
import java.sql.SQLException;
2122
import java.util.Locale;
23+
import java.util.function.Predicate;
24+
import java.util.stream.Stream;
2225

2326
import javax.sql.DataSource;
2427

@@ -44,32 +47,33 @@ public enum EmbeddedDatabaseConnection {
4447
/**
4548
* No Connection.
4649
*/
47-
NONE(null, null, null),
50+
NONE(null, null, null, (url) -> false),
4851

4952
/**
5053
* H2 Database Connection.
5154
*/
5255
H2(EmbeddedDatabaseType.H2, DatabaseDriver.H2.getDriverClassName(),
53-
"jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"),
56+
"jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", (url) -> url.contains(":h2:mem")),
5457

5558
/**
5659
* Derby Database Connection.
5760
*/
58-
DERBY(EmbeddedDatabaseType.DERBY, DatabaseDriver.DERBY.getDriverClassName(), "jdbc:derby:memory:%s;create=true"),
61+
DERBY(EmbeddedDatabaseType.DERBY, DatabaseDriver.DERBY.getDriverClassName(), "jdbc:derby:memory:%s;create=true",
62+
(url) -> true),
5963

6064
/**
6165
* HSQL Database Connection.
6266
* @deprecated since 2.4.0 in favor of {@link EmbeddedDatabaseConnection#HSQLDB}.
6367
*/
6468
@Deprecated
6569
HSQL(EmbeddedDatabaseType.HSQL, DatabaseDriver.HSQLDB.getDriverClassName(), "org.hsqldb.jdbcDriver",
66-
"jdbc:hsqldb:mem:%s"),
70+
"jdbc:hsqldb:mem:%s", (url) -> url.contains(":hsqldb:mem:")),
6771

6872
/**
6973
* HSQL Database Connection.
7074
*/
7175
HSQLDB(EmbeddedDatabaseType.HSQL, DatabaseDriver.HSQLDB.getDriverClassName(), "org.hsqldb.jdbcDriver",
72-
"jdbc:hsqldb:mem:%s");
76+
"jdbc:hsqldb:mem:%s", (url) -> url.contains(":hsqldb:mem:"));
7377

7478
private final EmbeddedDatabaseType type;
7579

@@ -79,15 +83,20 @@ public enum EmbeddedDatabaseConnection {
7983

8084
private final String url;
8185

82-
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String url) {
83-
this(type, driverClass, null, url);
86+
private final Predicate<String> embeddedUrl;
87+
88+
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String url,
89+
Predicate<String> embeddedUrl) {
90+
this(type, driverClass, null, url, embeddedUrl);
8491
}
8592

86-
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String fallbackDriverClass, String url) {
93+
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String fallbackDriverClass, String url,
94+
Predicate<String> embeddedUrl) {
8795
this.type = type;
8896
this.driverClass = driverClass;
8997
this.alternativeDriverClass = fallbackDriverClass;
9098
this.url = url;
99+
this.embeddedUrl = embeddedUrl;
91100
}
92101

93102
/**
@@ -116,34 +125,41 @@ public String getUrl(String databaseName) {
116125
return (this.url != null) ? String.format(this.url, databaseName) : null;
117126
}
118127

128+
boolean isEmbeddedUrl(String url) {
129+
return this.embeddedUrl.test(url);
130+
}
131+
132+
boolean isDriverCompatible(String driverClass) {
133+
return (driverClass != null
134+
&& (driverClass.equals(this.driverClass) || driverClass.equals(this.alternativeDriverClass)));
135+
}
136+
119137
/**
120138
* Convenience method to determine if a given driver class name represents an embedded
121139
* database type.
122140
* @param driverClass the driver class
123141
* @return true if the driver class is one of the embedded types
142+
* @deprecated since 2.4.0 in favor of {@link #isEmbedded(String, String)}
124143
*/
125144
@Deprecated
126145
public static boolean isEmbedded(String driverClass) {
127-
return driverClass != null && (matches(HSQL, driverClass) || matches(H2, driverClass)
128-
|| matches(DERBY, driverClass) || matches(HSQLDB, driverClass));
146+
return isEmbedded(driverClass, null);
129147
}
130148

131149
/**
132-
* Convenience method to determine if a given driver class name and url represents an
133-
* embedded database type.The exception is made for the H2 database for embedded
134-
* types.
150+
* Convenience method to determine if a given driver class name and url represent an
151+
* embedded database type.
135152
* @param driverClass the driver class
136-
* @param url the jdbc url
137-
* @return true if the driver class is one of the embedded types
153+
* @param url the jdbc url (can be {@code null)}
154+
* @return true if the driver class and url refer to an embedded database
138155
*/
139156
public static boolean isEmbedded(String driverClass, String url) {
140-
return (driverClass != null
141-
&& (matches(HSQL, driverClass) || (matches(H2, driverClass) && url.contains(":h2:mem"))
142-
|| matches(DERBY, driverClass) || matches(HSQLDB, driverClass)));
157+
return driverClass != null && getEmbeddedDatabaseConnection(driverClass).isEmbeddedUrl(url);
143158
}
144159

145-
private static boolean matches(EmbeddedDatabaseConnection candidate, String driverClass) {
146-
return driverClass.equals(candidate.driverClass) || driverClass.equals(candidate.alternativeDriverClass);
160+
private static EmbeddedDatabaseConnection getEmbeddedDatabaseConnection(String driverClass) {
161+
return Stream.of(H2, HSQLDB, DERBY).filter((connection) -> connection.isDriverCompatible(driverClass))
162+
.findFirst().orElse(NONE);
147163
}
148164

149165
/**
@@ -184,15 +200,16 @@ private static class IsEmbedded implements ConnectionCallback<Boolean> {
184200

185201
@Override
186202
public Boolean doInConnection(Connection connection) throws SQLException, DataAccessException {
187-
String productName = connection.getMetaData().getDatabaseProductName();
203+
DatabaseMetaData metaData = connection.getMetaData();
204+
String productName = metaData.getDatabaseProductName();
188205
if (productName == null) {
189206
return false;
190207
}
191208
productName = productName.toUpperCase(Locale.ENGLISH);
192209
EmbeddedDatabaseConnection[] candidates = EmbeddedDatabaseConnection.values();
193210
for (EmbeddedDatabaseConnection candidate : candidates) {
194211
if (candidate != NONE && productName.contains(candidate.name())) {
195-
return true;
212+
return candidate.isEmbeddedUrl(metaData.getURL());
196213
}
197214
}
198215
return false;

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnectionTests.java

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,24 @@
1616

1717
package org.springframework.boot.jdbc;
1818

19+
import java.sql.Connection;
20+
import java.sql.DatabaseMetaData;
21+
import java.sql.SQLException;
22+
23+
import javax.sql.DataSource;
24+
1925
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.MethodSource;
28+
29+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
30+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
31+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
2032

2133
import static org.assertj.core.api.Assertions.assertThat;
2234
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
35+
import static org.mockito.BDDMockito.given;
36+
import static org.mockito.Mockito.mock;
2337

2438
/**
2539
* Tests for {@link EmbeddedDatabaseConnection}.
@@ -78,15 +92,65 @@ void getUrlWithEmptyDatabaseNameForHsqldb() {
7892
.withMessageContaining("DatabaseName must not be empty");
7993
}
8094

95+
@ParameterizedTest(name = "{1}")
96+
@MethodSource("embeddedDriverAndUrlParameters")
97+
void isEmbeddedWithDriverAndUrl(EmbeddedDatabaseConnection connection, String url, boolean embedded) {
98+
assertThat(EmbeddedDatabaseConnection.isEmbedded(connection.getDriverClassName(), url)).isEqualTo(embedded);
99+
}
100+
101+
static Object[] embeddedDriverAndUrlParameters() {
102+
return new Object[] { new Object[] { EmbeddedDatabaseConnection.H2, "jdbc:h2:~/test", false },
103+
new Object[] { EmbeddedDatabaseConnection.H2, "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", true },
104+
new Object[] { EmbeddedDatabaseConnection.HSQLDB, "jdbc:hsqldb:hsql://localhost", false },
105+
new Object[] { EmbeddedDatabaseConnection.HSQLDB, "jdbc:hsqldb:mem:test", true },
106+
new Object[] { EmbeddedDatabaseConnection.DERBY, "jdbc:derby:memory:test", true } };
107+
}
108+
109+
@Test
110+
void isEmbeddedWithH2DataSource() {
111+
testEmbeddedDatabase(new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build());
112+
}
113+
81114
@Test
82-
void isEmbeddedForh2CustomDatabaseName() {
83-
assertThat(EmbeddedDatabaseConnection.isEmbedded("org.h2.Driver", "jdbc:h2:~/test")).isFalse();
115+
void isEmbeddedWithHsqlDataSource() {
116+
testEmbeddedDatabase(new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build());
84117
}
85118

86119
@Test
87-
void isEmbeddedForh2EmbeddedDatabaseName() {
88-
assertThat(EmbeddedDatabaseConnection.isEmbedded("org.h2.Driver",
89-
"jdbc:h2:mem:b3c7d078-1362-4be7-a088-e25dcc3aee32;DB_CLOSE_DELAY=-1")).isTrue();
120+
void isEmbeddedWithDerbyDataSource() {
121+
testEmbeddedDatabase(new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.DERBY).build());
122+
}
123+
124+
void testEmbeddedDatabase(EmbeddedDatabase database) {
125+
try {
126+
assertThat(EmbeddedDatabaseConnection.isEmbedded(database)).isTrue();
127+
}
128+
finally {
129+
database.shutdown();
130+
}
131+
}
132+
133+
@Test
134+
void isEmbeddedWithUnknownDataSource() throws SQLException {
135+
assertThat(EmbeddedDatabaseConnection.isEmbedded(mockDataSource("unknown-db", null))).isFalse();
136+
}
137+
138+
@Test
139+
void isEmbeddedWithH2File() throws SQLException {
140+
assertThat(EmbeddedDatabaseConnection
141+
.isEmbedded(mockDataSource(EmbeddedDatabaseConnection.H2.getDriverClassName(), "jdbc:h2:~/test")))
142+
.isFalse();
143+
}
144+
145+
DataSource mockDataSource(String productName, String connectionUrl) throws SQLException {
146+
DatabaseMetaData metaData = mock(DatabaseMetaData.class);
147+
given(metaData.getDatabaseProductName()).willReturn(productName);
148+
given(metaData.getURL()).willReturn(connectionUrl);
149+
Connection connection = mock(Connection.class);
150+
given(connection.getMetaData()).willReturn(metaData);
151+
DataSource dataSource = mock(DataSource.class);
152+
given(dataSource.getConnection()).willReturn(connection);
153+
return dataSource;
90154
}
91155

92156
}

0 commit comments

Comments
 (0)