Parsing Spring Variables Outside of the Spring Runtime

Dzeri, 25-06-2023, Programming

A proof-of-concept on how to mimic Spring Boot's approach to parsing variables from properties files.

Gradle, Java, Spring

The Problem

So I have a Java project that builds on Spring Boot, and uses Gradle as the build system. Recently I introduced Liquibase, which integrates with Spring Boot at application start time. That's fine and all, but when developing new migrations, I want to be able to run them without starting the application, by calling liquibase directly. This is indeed possible with the liquibase gradle plugin, however, having both options entails configuring them separately, which I think is bad practice, and might just make you give up if you use variable interpolation to slowly build up the final value.

This got me thinking: "There must be a nice, official way to parse properties like Spring Boot would, without having to start the entire application". Well to my surprise, it seems like I'm some kind of pioneer in this way of thought and everyone else is duplicating configuration variables. I don't even want to think about how many unexpected bugs this has lead to. I even opened an issue with Spring Boot, and the proposal was turned down as "too complex to implement". Well, there's no point to only complaining, so let's try to build a workaround!

A Partial Solution

I've made a proof of concept that supports multiple Spring profiles, precedence and variable interpolation together with ENV variable overriding. It's not the cleanest and doesn't mirror Spring Boot entirely, but it should have you covered for most cases. This is how I integrated it in my build.gradle file:

import org.springframework.core.env.StandardEnvironment
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.springframework.core.io.support.ResourcePropertySource

sourceSets {
    main {
        java {
            srcDir ("src/main/java")
            srcDir ("build/generated/sources")
        }
    }
}

// Rest of build.gradle

// Extract properties as Spring Boot would
StandardEnvironment springBootEnvironment = new StandardEnvironment();
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver()
String activeProfilesEnvVariable = "$System.env.spring_profiles_active"
String[] profiles = activeProfilesEnvVariable.split(",")
println "Active spring profiles: " + profiles
if (activeProfilesEnvVariable != "null" && profiles.length != 0) {
    for (final def profile in profiles) {
        for (final def resDir in sourceSets.main.getResources().srcDirs) {
            String searchPath = Paths.get("file:" + resDir.toString(), "application-" + profile + ".properties").toString()
            var resources = resourcePatternResolver.getResources(searchPath)
            for (final def res in resources) {
                springBootEnvironment.getPropertySources().addLast(new ResourcePropertySource(res))
            }
        }
    }
}
springBootEnvironment
        .getPropertySources()
        .addLast(
                new ResourcePropertySource(
                        resourcePatternResolver.getResource("file:src/main/resources/application.properties")
                )
        )

// Configure the liquibase plugin
liquibase {
    activities {
        main {
          username springBootEnvironment.getProperty("spring.datasource.username")
          password springBootEnvironment.getProperty("spring.datasource.password")
          // Other properties
        }
    }
}

You'd run gradle with the spring_profiles_active ENV variable set to whichever profile(s) you want, just like with your Spring Boot app, for example spring_profiles_active=dev gradle status. By default, it reads from the application.properties file.

As you can see, most of the code deals with the precedence and discovery of properties files. If you use yaml property files, you can use the YamlPropertySourceLoader.

To summarize, this piece of code will emulate the way Spring Boot parses properties according to active profiles. If you have a different approach, please share it in the comments!