Problem
Liquibase has an expanding set of global configuration options (e.g. duplicateFileMode
, logLevel
). Currently, these can only be set via environment variables or JVM system properties (e.g. -Dliquibase.duplicateFileMode=WARN
).
In a Spring Boot application, this creates inconsistency. Most Liquibase settings live under spring.liquibase.*
(application.yml
) but in order to set global properties we must set global values elsewhere, such as through system properties, env vars, or through Customizer<Liquibase>
. In my own experience, this fragmented approach made it difficult to configure liquibase across environments and made testing more difficult.
Use case
My personal use case was that we had multiple Liquibase changelog files and frequently saw an error indicating that Liquibase found duplicates. The recommended fix is to set liquibase.duplicateFileMode=WARN
. The workaround we used was to set the variable in our Gradle file, which worked but led to issues I mentioned above.
Proposal
Introduce a generic pass-through mechanism for Liquibase global properties. This was the proposed approach on the long-standing Liquibase issue here. For example:
spring:
liquibase:
change-log: classpath:/db/changelog_master.xml
globals:
duplicate-file-mode: WARN
log-level: INFO
Behavior:
- At startup, entries under
spring.liquibase.globals.*
are applied to Liquibase’s global configuration before SpringLiquibase executes (internally via the existing customizer / LiquibaseConfiguration bridge). - Spring Boot does not hard-code or validate the option names; it simply passes them through. Liquibase remains the source of truth for supported keys and semantics.
Benefits
- Keeps all Liquibase config in
application.yml
as the single source of truth - Reduces production/test mismatches.
- Minimal maintenance burden: Spring Boot simply passes properties through; Liquibase defines the option set.
Proof of concept
I have a Kotlin project where I constructed a solution for this. It works, though there might be a better place to hook in to access the spring configuration. Note that I am not using spring.liquibase.globals
because spring.liquibase
maps to a static properties class. To implement that behavior, you'd need to add globals as a string map property. If this looks like something that your team would be interested in, I'd be happy to do the implementation or adapt it to be more consistent with the integration code.
src/main/resources/META-INF/services/liquibase.configuration.ConfigurationValueProvider
com.example.liquibase.config.SpringLiquibaseGlobalConfigProvider
src/main/resources/META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer=\
com.example.liquibase.config.LiquibaseEnvironmentInitializer
src/main/kotlin/com/example/liquibase/config/LiquibaseEnvironmentInitializer.kt
package com.example.liquibase.config
import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.core.Ordered
class LiquibaseEnvironmentInitializer :
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
override fun initialize(applicationContext: ConfigurableApplicationContext) {
SpringLiquibaseGlobalConfigProvider.environment = applicationContext.environment
}
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE
}
src/main/kotlin/com/example/liquibase/config/SpringLiquibaseGlobalConfigProvider.kt
package com.example.liquibase.config
import liquibase.configuration.AbstractConfigurationValueProvider
import liquibase.configuration.ProvidedValue
import org.springframework.core.env.ConfigurableEnvironment
class SpringLiquibaseGlobalConfigProvider : AbstractConfigurationValueProvider() {
companion object {
var environment: ConfigurableEnvironment? = null
}
override fun getPrecedence() = 10_000
override fun getProvidedValue(vararg keyAndAliases: String?): ProvidedValue? {
val env = environment ?: return null
for (key in keyAndAliases.filterNotNull()) {
val shortKey = key.substringAfterLast(".")
val candidates = listOf(
"app.liquibase.global-config.$shortKey",
"app.liquibase.global-config.${toKebabCase(shortKey)}"
)
for (candidate in candidates) {
env.getProperty(candidate)?.let { value ->
return ProvidedValue(key, key, value, "Spring Environment ($candidate)", this)
}
}
}
return null
}
private fun toKebabCase(s: String): String =
s.foldIndexed(StringBuilder()) { i, acc, ch ->
if (ch.isUpperCase() && i > 0) acc.append('-')
acc.append(ch.lowercaseChar())
}.toString()
}