8

I'm in the process of building a library which provides an opinionated configuration for applications which use our Spring Cloud Config/Eureka setup. The idea is to deliver this configuration as a custom starter with little or no spring cloud-related boilerplate in individual microservice apps.

At this point, the majority of the shared configuration that I want to put in this library consists of stuff in bootstrap.yml. I'd like to provide bootstrap.yml in my custom starter, but applications using the library still need to be able to provide their own bootstrap.yml, even if only so they can set their spring.application.name properly.

Due to the way bootstrap.yml is loaded from the classpath, Spring seems to ignore the one in the shared lib if the application has its own bootstrap.yml. I can't even use an ApplicationContextInitializer to customize the Environment because of the special way the bootstrap context treats ApplicationContextInitializers.

Does anyone have any recommendations for an approach that would work here? I want to provide a drop-in lib that makes our opinionated bootstrap config work without having to duplicate a boilerplate bootstrap.yml in all of our projects.

shazbot
  • 153
  • 2
  • 9
  • 3
    I don't think you could do it with bootstrap.yml itself, but you could plug into the bootstrap mechanism itself and populate a property source with the defaults. – spencergibb Jun 17 '16 at 19:55
  • Why a yaml file? You could create Java configurations. That would be easier to distribute. – John Giotta Jun 22 '16 at 13:24
  • Certainly java would be easier to distribute. However, I would need to do a large amount of customization in order to accomplish things that can be done very easily with yaml. Simply providing some javaconfig to turn off spring cloud bus under a certain profile, for example, is pretty daunting in terms of configuration over convention, but it can be done in yaml with a simple spring.cloud.bus.enabled=false. Just to provide a default eureka defaultZone, I'm looking at configuring beans myself that otherwise benefit from autoconfig. – shazbot Jun 22 '16 at 13:46

2 Answers2

7

You can add a PropertySource in a shared library to the bootstrap properties by using the org.springframework.cloud.bootstrap.BootstrapConfiguration key in the META-INF/spring.factories file.

For example, you can create a library containing the following:

src/main/java/com/example/mylib/MyLibConfig.java

package com.example.mylib;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:mylib-config.properties")
public class MyLibConfig {
}

src/main/resources/mylib-config.properties

eureka.instance.public=true
# or whatever...

src/main/resources/META-INF/spring.factories

org.springframework.cloud.bootstrap.BootstrapConfiguration=com.example.mylib.MyLibConfig

More details: http://projects.spring.io/spring-cloud/spring-cloud.html#_customizing_the_bootstrap_configuration

daiscog
  • 11,441
  • 6
  • 50
  • 62
2

I was able to find a solution to this. The goals of this solution are:

  • Load the values from a yaml file in a shared library.
  • Allow applications using the library to introduce their own bootstrap.yml that is also loaded into the Environment.
  • Values in the bootstrap.yml should override values in the shared yaml.

The main challenge is to inject some code at the appropriate point in the application lifecycle. Specifically, we need to do it after the bootstrap.yml PropertySource is added to the environment (so that we can inject our custom PropertySource in the correct order relative to it), but also before the application starts configuring beans (as our config values control behavior).

The solution I found was to use a custom EnvironmentPostProcessor

public class CloudyConfigEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {

    private YamlPropertySourceLoader loader;

    public CloudyConfigEnvironmentPostProcessor() {
        loader = new YamlPropertySourceLoader();
    }

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication application) {
        //ensure that the bootstrap file is only loaded in the bootstrap context
        if (env.getPropertySources().contains("bootstrap")) {
            //Each document in the multi-document yaml must be loaded separately.
            //Start by loading the no-profile configs...
            loadProfile("cloudy-bootstrap", env, null);
            //Then loop through the active profiles and load them.
            for (String profile: env.getActiveProfiles()) {
                loadProfile("cloudy-bootstrap", env, profile);
            }
        }
    }

    private void loadProfile(String prefix, ConfigurableEnvironment env, String profile) {
        try {
            PropertySource<?> propertySource = loader.load(prefix + (profile != null ? "-" + profile: ""), new ClassPathResource(prefix + ".yml"), profile);
            //propertySource will be null if the profile isn't represented in the yml, so skip it if this is the case.
            if (propertySource != null) {
                //add PropertySource after the "applicationConfigurationProperties" source to allow the default yml to override these.
                env.getPropertySources().addAfter("applicationConfigurationProperties", propertySource);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public int getOrder() {
        //must go after ConfigFileApplicationListener
        return Ordered.HIGHEST_PRECEDENCE + 11;
    }

}

This custom EnvironmentPostProcessor can be injected via META-INF/spring.factories:

#Environment PostProcessors
org.springframework.boot.env.EnvironmentPostProcessor=\
com.mycompany.cloudy.bootstrap.autoconfig.CloudyConfigEnvironmentPostProcessor

A couple things to note:

  • The YamlPropertySourceLoader loads yaml properties by profile, so if you are using a multi-document yaml file you need to actually load each profile from it separately, including the no-profile configs.
  • ConfigFileApplicationListener is the EnvironmentPostProcessor responsible for loading bootstrap.yml (or application.yml for the regular context) into the Environment, so in order to position the custom yaml properties correctly relative to the bootstrap.yml properties precedence-wise, you need to order your custom EnvironmentPostProcessor after ConfigFileApplicationListener.

Edit: My initial answer did not work. I'm replacing it with this one, which does.

shazbot
  • 153
  • 2
  • 9