Skip to content

Commit 8664d53

Browse files
committed
Refactor Liquibase integration to support environment-aware property pass-through
Signed-off-by: Dylan Miska <djmiska25@gmail.com>
1 parent 9ffd144 commit 8664d53

File tree

9 files changed

+341
-132
lines changed

9 files changed

+341
-132
lines changed

module/spring-boot-liquibase/src/main/java/org/springframework/boot/liquibase/autoconfigure/DataSourceClosingSpringLiquibase.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@
2323
import liquibase.exception.LiquibaseException;
2424
import liquibase.integration.spring.SpringLiquibase;
2525

26-
import org.springframework.beans.factory.DisposableBean;
2726
import org.springframework.util.ReflectionUtils;
2827

2928
/**
3029
* A custom {@link SpringLiquibase} extension that closes the underlying
3130
* {@link DataSource} once the database has been migrated.
3231
*
3332
* @author Andy Wilkinson
33+
* @author Dylan Miska
3434
* @since 4.0.0
3535
*/
36-
public class DataSourceClosingSpringLiquibase extends SpringLiquibase implements DisposableBean {
36+
public class DataSourceClosingSpringLiquibase extends EnvironmentAwareSpringLiquibase {
3737

3838
private volatile boolean closeDataSourceOnceMigrated = true;
3939

@@ -58,7 +58,8 @@ private void closeDataSource() {
5858
}
5959

6060
@Override
61-
public void destroy() throws Exception {
61+
public void destroy() {
62+
super.destroy();
6263
if (!this.closeDataSourceOnceMigrated) {
6364
closeDataSource();
6465
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2012-present 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.liquibase.autoconfigure;
18+
19+
import liquibase.Scope;
20+
import liquibase.exception.LiquibaseException;
21+
import liquibase.integration.spring.SpringLiquibase;
22+
import org.jspecify.annotations.Nullable;
23+
import org.springframework.beans.BeansException;
24+
import org.springframework.beans.factory.DisposableBean;
25+
import org.springframework.context.ApplicationContext;
26+
import org.springframework.context.ApplicationContextAware;
27+
28+
import java.util.HashMap;
29+
import java.util.Map;
30+
31+
/**
32+
* A {@link SpringLiquibase} subclass that creates a Liquibase child scope with a
33+
* reference to the Spring Environment, allowing the
34+
* {@link EnvironmentConfigurationValueProvider} to access the correct Environment for
35+
* configuration values.
36+
*
37+
* @author Dylan Miska
38+
*/
39+
class EnvironmentAwareSpringLiquibase extends SpringLiquibase implements ApplicationContextAware, DisposableBean {
40+
41+
private @Nullable String environmentId;
42+
43+
@Override
44+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
45+
this.environmentId = applicationContext.getId();
46+
}
47+
48+
@Override
49+
public void afterPropertiesSet() throws LiquibaseException {
50+
try {
51+
Map<String, Object> scopeValues = new HashMap<>();
52+
scopeValues.put(EnvironmentConfigurationValueProvider.SPRING_ENV_ID_KEY, this.environmentId);
53+
Scope.child(scopeValues, EnvironmentAwareSpringLiquibase.super::afterPropertiesSet);
54+
}
55+
catch (Exception ex) {
56+
if (ex instanceof LiquibaseException) {
57+
throw (LiquibaseException) ex;
58+
}
59+
throw new LiquibaseException(ex);
60+
}
61+
}
62+
63+
@Override
64+
public void destroy() {
65+
if (this.environmentId != null) {
66+
EnvironmentConfigurationValueProvider.unregisterEnvironment(this.environmentId);
67+
}
68+
}
69+
70+
}

module/spring-boot-liquibase/src/main/java/org/springframework/boot/liquibase/autoconfigure/EnvironmentConfigurationValueProvider.java

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import liquibase.configuration.ProvidedValue;
2121
import org.jspecify.annotations.Nullable;
2222
import org.springframework.core.env.Environment;
23-
import org.springframework.util.Assert;
23+
24+
import java.util.Map;
25+
import java.util.concurrent.ConcurrentHashMap;
2426

2527
/**
2628
* A Liquibase {@code ConfigurationValueProvider} that passes through properties defined
@@ -38,16 +40,23 @@
3840
* No relaxed binding or key transformation is performed. Keys are looked up exactly as
3941
* provided by Liquibase (including dots and casing), prefixed with
4042
* {@code spring.liquibase.properties.}.
43+
*
44+
* @author Dylan Miska
4145
*/
42-
final class EnvironmentConfigurationValueProvider extends AbstractConfigurationValueProvider {
46+
public final class EnvironmentConfigurationValueProvider extends AbstractConfigurationValueProvider {
4347

4448
private static final String PREFIX = "spring.liquibase.properties.";
4549

46-
private final Environment environment;
50+
public static final String SPRING_ENV_ID_KEY = "spring.environment.id";
4751

48-
EnvironmentConfigurationValueProvider(Environment environment) {
49-
Assert.notNull(environment, "Environment must not be null");
50-
this.environment = environment;
52+
private static final Map<String, Environment> environments = new ConcurrentHashMap<>();
53+
54+
static void registerEnvironment(String environmentId, Environment environment) {
55+
environments.put(environmentId, environment);
56+
}
57+
58+
static void unregisterEnvironment(String environmentId) {
59+
environments.remove(environmentId);
5160
}
5261

5362
@Override
@@ -60,15 +69,25 @@ public int getPrecedence() {
6069
if (keyAndAliases == null) {
6170
return null;
6271
}
72+
73+
String environmentId = liquibase.Scope.getCurrentScope().get(SPRING_ENV_ID_KEY, String.class);
74+
if (environmentId == null) {
75+
return null;
76+
}
77+
78+
Environment environment = environments.get(environmentId);
79+
if (environment == null) {
80+
return null;
81+
}
82+
6383
for (String requestedKey : keyAndAliases) {
64-
if (requestedKey == null) {
65-
continue;
66-
}
67-
String propertyName = PREFIX + requestedKey;
68-
String value = this.environment.getProperty(propertyName);
69-
if (value != null) {
70-
return new ProvidedValue(requestedKey, requestedKey, value,
71-
"Spring Environment property '" + propertyName + "'", this);
84+
if (requestedKey != null) {
85+
String propertyName = PREFIX + requestedKey;
86+
String value = environment.getProperty(propertyName);
87+
if (value != null) {
88+
return new ProvidedValue(requestedKey, requestedKey, value,
89+
"Spring Environment property '" + propertyName + "'", this);
90+
}
7291
}
7392
}
7493
return null;

module/spring-boot-liquibase/src/main/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfiguration.java

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616

1717
package org.springframework.boot.liquibase.autoconfigure;
1818

19+
import java.util.Objects;
20+
1921
import javax.sql.DataSource;
2022

2123
import liquibase.Liquibase;
22-
import liquibase.Scope;
2324
import liquibase.UpdateSummaryEnum;
2425
import liquibase.UpdateSummaryOutputEnum;
2526
import liquibase.change.DatabaseChange;
@@ -46,12 +47,12 @@
4647
import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration.LiquibaseAutoConfigurationRuntimeHints;
4748
import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration.LiquibaseDataSourceCondition;
4849
import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer;
50+
import org.springframework.context.ApplicationContext;
4951
import org.springframework.context.annotation.Bean;
5052
import org.springframework.context.annotation.Conditional;
5153
import org.springframework.context.annotation.Configuration;
5254
import org.springframework.context.annotation.Import;
5355
import org.springframework.context.annotation.ImportRuntimeHints;
54-
import org.springframework.core.env.Environment;
5556
import org.springframework.jdbc.core.ConnectionCallback;
5657
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
5758
import org.springframework.util.Assert;
@@ -73,6 +74,7 @@
7374
* @author Evgeniy Cheban
7475
* @author Moritz Halbritter
7576
* @author Ahmed Ashour
77+
* @author Dylan Miska
7678
* @since 4.0.0
7779
*/
7880
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@@ -104,8 +106,8 @@ PropertiesLiquibaseConnectionDetails liquibaseConnectionDetails(LiquibasePropert
104106
SpringLiquibase liquibase(ObjectProvider<DataSource> dataSource,
105107
@LiquibaseDataSource ObjectProvider<DataSource> liquibaseDataSource, LiquibaseProperties properties,
106108
ObjectProvider<SpringLiquibaseCustomizer> customizers, LiquibaseConnectionDetails connectionDetails,
107-
Environment environment) {
108-
registerLiquibaseConfigurationValueProvider(environment);
109+
ApplicationContext applicationContext) {
110+
registerLiquibaseConfigurationValueProvider(applicationContext);
109111
SpringLiquibase liquibase = createSpringLiquibase(liquibaseDataSource.getIfAvailable(),
110112
dataSource.getIfUnique(), connectionDetails);
111113
liquibase.setChangeLog(properties.getChangeLog());
@@ -151,24 +153,26 @@ private SpringLiquibase createSpringLiquibase(@Nullable DataSource liquibaseData
151153
@Nullable DataSource dataSource, LiquibaseConnectionDetails connectionDetails) {
152154
DataSource migrationDataSource = getMigrationDataSource(liquibaseDataSource, dataSource, connectionDetails);
153155
SpringLiquibase liquibase = (migrationDataSource == liquibaseDataSource
154-
|| migrationDataSource == dataSource) ? new SpringLiquibase()
156+
|| migrationDataSource == dataSource) ? new EnvironmentAwareSpringLiquibase()
155157
: new DataSourceClosingSpringLiquibase();
156158
liquibase.setDataSource(migrationDataSource);
157159
return liquibase;
158160
}
159161

160-
private void registerLiquibaseConfigurationValueProvider(Environment environment) {
161-
liquibase.configuration.LiquibaseConfiguration liquibaseConfiguration = Scope.getCurrentScope()
162+
private void registerLiquibaseConfigurationValueProvider(ApplicationContext applicationContext) {
163+
liquibase.configuration.LiquibaseConfiguration liquibaseConfiguration = liquibase.Scope.getCurrentScope()
162164
.getSingleton(liquibase.configuration.LiquibaseConfiguration.class);
163165

164-
// Remove any previously registered instance of our provider class
165-
liquibaseConfiguration.getProviders()
166+
boolean providerExists = liquibaseConfiguration.getProviders()
166167
.stream()
167-
.filter((provider) -> provider.getClass() == EnvironmentConfigurationValueProvider.class)
168-
.toList()
169-
.forEach(liquibaseConfiguration::unregisterProvider);
168+
.anyMatch((provider) -> provider.getClass() == EnvironmentConfigurationValueProvider.class);
169+
170+
if (!providerExists) {
171+
liquibaseConfiguration.registerProvider(new EnvironmentConfigurationValueProvider());
172+
}
170173

171-
liquibaseConfiguration.registerProvider(new EnvironmentConfigurationValueProvider(environment));
174+
EnvironmentConfigurationValueProvider.registerEnvironment(
175+
Objects.requireNonNull(applicationContext.getId()), applicationContext.getEnvironment());
172176
}
173177

174178
private DataSource getMigrationDataSource(@Nullable DataSource liquibaseDataSource,

module/spring-boot-liquibase/src/main/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseProperties.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* @author Eddú Meléndez
3737
* @author Ferenc Gratzer
3838
* @author Evgeniy Cheban
39+
* @author Dylan Miska
3940
* @since 4.0.0
4041
*/
4142
@ConfigurationProperties(prefix = "spring.liquibase", ignoreUnknownFields = false)

module/spring-boot-liquibase/src/main/java/org/springframework/boot/liquibase/endpoint/LiquibaseEndpoint.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
import java.util.HashMap;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.Objects;
2324
import java.util.Set;
2425

2526
import javax.sql.DataSource;
2627

28+
import liquibase.Scope;
2729
import liquibase.changelog.ChangeSet.ExecType;
2830
import liquibase.changelog.RanChangeSet;
2931
import liquibase.changelog.StandardChangeLogHistoryService;
@@ -36,6 +38,7 @@
3638
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
3739
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3840
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
41+
import org.springframework.boot.liquibase.autoconfigure.EnvironmentConfigurationValueProvider;
3942
import org.springframework.context.ApplicationContext;
4043
import org.springframework.util.Assert;
4144
import org.springframework.util.StringUtils;
@@ -45,6 +48,7 @@
4548
*
4649
* @author Eddú Meléndez
4750
* @author Nabil Fawwaz Elqayyim
51+
* @author Dylan Miska
4852
* @since 4.0.0
4953
*/
5054
@Endpoint(id = "liquibase")
@@ -58,18 +62,26 @@ public LiquibaseEndpoint(ApplicationContext context) {
5862
}
5963

6064
@ReadOperation
61-
public LiquibaseBeansDescriptor liquibaseBeans() {
65+
public LiquibaseBeansDescriptor liquibaseBeans() throws Exception {
6266
ApplicationContext target = this.context;
6367
Map<@Nullable String, ContextLiquibaseBeansDescriptor> contextBeans = new HashMap<>();
6468
while (target != null) {
65-
Map<String, LiquibaseBeanDescriptor> liquibaseBeans = new HashMap<>();
66-
DatabaseFactory factory = DatabaseFactory.getInstance();
67-
target.getBeansOfType(SpringLiquibase.class)
68-
.forEach((name, liquibase) -> liquibaseBeans.put(name, createReport(liquibase, factory)));
69-
ApplicationContext parent = target.getParent();
70-
contextBeans.put(target.getId(),
71-
new ContextLiquibaseBeansDescriptor(liquibaseBeans, (parent != null) ? parent.getId() : null));
72-
target = parent;
69+
Map<String, Object> scopeValues = new HashMap<>();
70+
scopeValues.put(EnvironmentConfigurationValueProvider.SPRING_ENV_ID_KEY,
71+
Objects.requireNonNull(target.getId()));
72+
ApplicationContext currentContext = target;
73+
Scope.child(scopeValues, () -> {
74+
Map<String, LiquibaseBeanDescriptor> liquibaseBeans = new HashMap<>();
75+
DatabaseFactory factory = DatabaseFactory.getInstance();
76+
currentContext.getBeansOfType(SpringLiquibase.class)
77+
.forEach((name, liquibase) -> liquibaseBeans.put(name, createReport(liquibase, factory)));
78+
ApplicationContext parent = currentContext.getParent();
79+
contextBeans.put(currentContext.getId(),
80+
new ContextLiquibaseBeansDescriptor(liquibaseBeans, (parent != null) ? parent.getId() : null));
81+
return null;
82+
});
83+
84+
target = target.getParent();
7385
}
7486
return new LiquibaseBeansDescriptor(contextBeans);
7587
}

module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/actuate/endpoint/LiquibaseEndpointTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
* @author Andy Wilkinson
5454
* @author Stephane Nicoll
5555
* @author Leo Li
56+
* @author Dylan Miska
5657
*/
5758
@WithResource(name = "db/changelog/db.changelog-master.yaml", content = """
5859
databaseChangeLog:
@@ -176,6 +177,26 @@ void whenMultipleLiquibaseBeansArePresentChangeSetsAreCorrectlyReportedForEachBe
176177
});
177178
}
178179

180+
@Test
181+
@WithResource(name = "db/create-custom-schema.sql", content = "CREATE SCHEMA \"CustomSchema\";")
182+
void customLiquibasePropertyIsAppliedDuringEndpointCall() {
183+
this.contextRunner.withUserConfiguration(Config.class, DataSourceWithSchemaConfiguration.class)
184+
.withPropertyValues("spring.liquibase.default-schema=CustomSchema",
185+
"spring.liquibase.properties.liquibase.preserveSchemaCase=true")
186+
.run((context) -> {
187+
Map<String, LiquibaseBeanDescriptor> liquibaseBeans = context.getBean(LiquibaseEndpoint.class)
188+
.liquibaseBeans()
189+
.getContexts()
190+
.get(context.getId())
191+
.getLiquibaseBeans();
192+
// If preserveSchemaCase wasn't applied, Liquibase would fail to find the
193+
// mixed-case schema
194+
assertThat(liquibaseBeans.get("liquibase").getChangeSets()).hasSize(1);
195+
assertThat(liquibaseBeans.get("liquibase").getChangeSets().get(0).getChangeLog())
196+
.isEqualTo("db/changelog/db.changelog-master.yaml");
197+
});
198+
}
199+
179200
private boolean getAutoCommit(DataSource dataSource) throws SQLException {
180201
try (Connection connection = dataSource.getConnection()) {
181202
return connection.getAutoCommit();

0 commit comments

Comments
 (0)