javaspringspring-bootsnakeyaml

Snake Yaml: How to load yaml file resolving property placeholders


We have a spring boot application with configuration being driven from application.yml file. In this configuration file we use the feature of defining a property by referring to another property inside the same application.yml file:

my-games-app:
  base-property: foo
  games:
    - game-one:
        game-name: ${my-games-app.base-property}one
        game-location: ${my-games-app.base-property}/one
    - game-two:
        game-name: ${my-games-app.base-property}two
        game-location: ${my-games-app.base-property}/two

And we have a @ConfigurationProperties bean loading games configuration:

@Configuration
@ConfigurationProperties(prefix = "my-games-app.games")
public class GamesConfig {
    private Map<String, Game> games;
    ...
}

Useless to say the above is just an example, in reality it is a very complex setup with GamesConfig bean being used as a constructor argument for many other beans inside our application:

@Component
public class GamesRunner {
    private final GamesConfig gamesConfig;
    ...
}

Everything works as expected. The problem we have is related to testing the beans where GamesConfig is injected; in the above example GamesRunner. At the moment we use @SpringBootTest to get hold of the beans we want to test. This again, works OK but the main inconvenient is that the whole application needs to be started in order to access the GamesConfig bean. This means setting up a lot of infrastructure such as a Database a JMS message broker and a Kafka broker. This takes time and makes our CI builds longer to run which started to become a bit of an inconvenient. Because the beans we want to test don't need any other setup than having the GamesConfig constructor argument provided we would prefer to have unit tests in place rather than integration tests as they are much faster to run.

In other words, we want to be able to recreate GamesConfig by hand by parsing our application.yml with a test helper method. To do this we use snakeyaml library:

public final class TestHelper {
    public static GamesConfig getGamesConfig() {
        var yaml = new Yaml();
        var applicationYaml = (Map<String, Object>) yaml.load(readResourceAsString("application.yml");
        return createGamesConfig(applicationYaml.get("games");
   }
   
   private static GamesConfig createGamesConfig(Object config) {
      // The config Object passed here is a `Map<String, Map<String, String>>`
      // as defeined in our `games` entry in our `application.yml`.
      // The issue is that game name and game locations are loaded exactly like 
      // configured without property place holders being resolved
     return gamesConfig;
   }
}

We resolved the issue by manually parsing the property placeholders and looking up their values in the application.yml file. Even if our own property placeholder implementation is quite generic, my feeling is that this extra work is not needed as it should be a basic expectation the library would have some specific set up to do this out of the box. Being very new to snakeyaml I hope someone else hit the same problem and knows how to do it.

We use snakeyaml because it just happened to be in the class path as a transitive dependency, we are open to any suggestions that would achieve the same thing.

Thank you in advance.


Solution

  • To my knowledge, SnakeYAML only supports substitution of environment variables, which is why what you want is not possible as far as I know. What you can do instead, of course, is simply use Spring's classes without setting up a full ApplicationContext.

    For example, assuming your game config from above, you could use:

    final var loader = new YamlPropertySourceLoader();
    final var sources = loader.load(
            "games-config.yml",
            new ClassPathResource("games-config.yml")
    );
    final var mutablePropertySources = new MutablePropertySources();
    sources.forEach(mutablePropertySources::addFirst);
    
    final var resolver = new PropertySourcesPropertyResolver(mutablePropertySources);
    resolver.setIgnoreUnresolvableNestedPlaceholders(true);
    
    System.out.println(resolver.getProperty("my-games-app.games[0].game-one.game-name"));
    System.out.println(resolver.getProperty("my-games-app.games[0].game-one.game-location"));
    System.out.println(resolver.getProperty("my-games-app.games[1].game-two.game-name"));
    System.out.println(resolver.getProperty("my-games-app.games[1].game-two.game-location"));
    

    which outputs:

    fooone
    foo/one
    footwo
    foo/two
    

    If you are actually interested in how Spring does it, a good starting point is the source code of the PropertySourcesPlaceholderConfigurer class.