Spring Boot Externalized Config on Command Line

Spring Boot applications do not always have to serve as a web service located on the Internet. You can also use Spring Boot (or Spring without the Boot) for a command-line utility. I was recently faced with this task, and one requirement for the tool was to support setting a profile-specific configuration on the command line. This isn’t earth-shattering per se since that is a regular Spring feature. The goal was to provide a profile-specific configuration file on the command line that is not bundled in the application.

Imagine developing a Cloud service and running different environments for the different phases of your project – one for development tests, a staging environment, and, finally, the production environment. Connecting to the different environments may require secrets you do not want to be bundled in the application – and, thus, the source tree.

Now, you could roll your own configuration file reader. But wouldn’t it be nice to make full support of Spring’s @Value annotation or @ConfigurationProperties classes?

Setup

In typical Spring fashion, there are multiple ways to do it. First, a bit of setup, namely the configuration class and the configuration itself. You can find the complete source code on GitHub.

I prefer @ConfigurationProperties classes over @Value, so here we go.

@Data
@NoArgsConstructor
@AllArgsConstructor
@ConfigurationProperties("external-config")
public class ExternalConfigProperties {

    private Location input;
    private Location output;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Location {
        private String path;
    }
}

The @Configuration class tells Spring Boot to consider using the config-properties class. Nothing special. The critical bit is the @EnableConfigurationProperties(ExternalConfigProperties.class) annotation.

@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(ExternalConfigProperties.class)
public class AppConfig {

    private final ExternalConfigProperties externalConfigProperties;

    @Bean
    public AppRunner appRunner() {
        return new AppRunner(externalConfigProperties);
    }
}

And finally, the bundled application.yml configuration file.

logging:
  level:
    root: INFO
    com.thecodeslinger.externalconfig.ExternalConfigApplication: WARN

external-config:
  input:
    path: /home/bundled/thecode
  output:
    path: /home/bundled/slinger

Since it is just a demo, the values are merely dummies. I tried to add a bit of spice by creating a nested structure.

Usage

How would you specify a configuration file on the command line?

Example #1: Use a “config” folder

In the current working directory, create a folder named “config” and add a file named “application.yml” to it. You do not have to specify anything on the command line using this solution. Spring Boot will automatically pick up the additional configuration.

src % ls
config	main

src % ls config 
application.yml

src % java -jar ../target/external-config-1.0.0.jar param1
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.2)

-> AppRunner.run() Command Line Arguments
Argument: param1
-> ExternalConfigProperties
Input path: /Users/bundled/thecode
Output path: /Users/bundled/slinger

The “config” folder can be in a completely different location from the executed JAR file. If you require more than one configuration file, e.g., to support multiple profiles, you can typically append the profile name to the application.yml.

src % ls config 
application-mac.yml

src % java -jar ../target/external-config-1.0.0.jar param1 --spring.profiles.active=mac
-> AppRunner.run() Command Line Arguments
Argument: param1
Argument: --spring.profiles.active=mac
-> ExternalConfigProperties
Input path: /Users/mac/thecode
Output path: /Users/mac/slinger

Note the differences in the config:

  • It starts with “/Users” in the “mac” profile, whereas the bundled default is “/home”.
  • The external config disables the Spring banner.

There is a second option for setting the profile configuration: a JVM parameter instead of an application parameter. The Spring option will not appear as a command-line argument for your application when you employ this method.

src % java -Dspring.profiles.active=mac -jar ../target/external-config-1.0.0.jar param1
-> AppRunner.run() Command Line Arguments
Argument: param1
-> ExternalConfigProperties
Input path: /Users/mac/thecode
Output path: /Users/mac/slinger

Notice that you now use “-D” instead of “--” to precede the configuration setting spring.profiles.active=mac.

Example #2: Set a config folder

Instead of relying on an implicit “config” folder in the current working directory, you can set any arbitrarily named folder on the command line.

Important: The folder must end with a “/”.

Spring supports two configuration options to achieve this:

  • spring.config.location
  • spring.config.additional-location

The first option replaces the complete bundled file with a file in the folder you set on the command line. The second option augments the bundled file and only overwrites the bundled values with those found in the folder’s file.

Therefore, the first option must be a complete file that cannot rely on defaults. With the second option, you only set the values you want to change.

The file’s name must follow the same pattern as in Example #1application.yml

 or application-<profile>.yml.

Because Spring is complicated, you can also set either of these options using JVM or application parameters.

Assuming the filename is application.yml and using a JVM parameter:

% java -Dspring.config.additional-location=src/config/ -jar target/external-config-1.0.0.jar param1 param2

-> AppRunner.run() Command Line Arguments
Argument: param1
Argument: param2
-> ExternalConfigProperties
Input path: /Users/mac/thecode
Output path: /Users/mac/slinger

Assuming the filename is application-mac.yml and using application parameters:

% java -jar target/external-config-1.0.0.jar param1 param2 --spring.config.additional-location=src/config/ --spring.profiles.active=mac

-> AppRunner.run() Command Line Arguments
Argument: param1
Argument: param2
Argument: --spring.config.location=src/config/
Argument: --spring.profiles.active=mac
-> ExternalConfigProperties
Input path: /Users/mac/thecode
Output path: /Users/mac/slinger

The same applies to the spring.config.location configuration option.

Example #3: Specify a file explicitly

As before, you can use spring.config.location or spring.config.additional-location, and the same rules regarding replacing and augmenting apply. Only this time, you do not set a folder but a file. A profile is not required anymore.

% java -Dspring.config.location=src/config/application-mac.yml -jar target/external-config-1.0.0.jar param1 param2

Or

% java -jar target/external-config-1.0.0.jar param1 param2 \
       --spring.config.location=src/config/application-mac.yml

Famous Last Words

As you can see, Spring allows for a lot of flexibility, and I have not even shown all possible permutations on how you can combine these options. The most compatible way is to use JVM arguments since the Spring configuration options are not passed to your application’s list of arguments. For a simple demo utility, this is a non-issue. However, if you rely on a 3rd party command line parser, like Apache Commons CLI, or build your own, you might run into issues. The parser of your choice may not recognize the additional parameters and complain or trip during parsing. I plan to tackle this problem with Apache Commons CLI in another blog post, where I noticed this behavior.

Thank you very much for reading.

My Year in Video Gaming 2021

2021 has been a challenging year, for obvious reasons, but also in other personal aspects that are not part of this little essay. Despite all the trials and tribulations, I have probably never played so many games in just one year – some of them in Coop and others all on my lonesome. Many of them I finished, others I, or we, aborted. But not only that, I have also managed to transition from PC gaming to console gaming – a long-held goal of mine.

As always, I am pretty late to the party because I have trouble motivating myself to write stuff, despite having the ideas and mentally developing concepts for them. Much thinking, few doing. One of my 2021 issues.

(I am surprised I managed to get this huge Halo Infinite review out the door.)Here is how this will go. I am starting with a story about why I replaced my gaming PC with consoles and a laptop. Then I transition into my experience with said consoles, and I conclude this gaming year review with the list of games I have played in lonely-mode or Coop. Don’t worry. I didn’t go Halo Infinite on every game. I kept it short-ish because the list is astonishingly long.

Read More »