In projects I’ve been on, it’s very common to have a Common or Shared project/module where shared code is kept. For example, you might have a IngestData project and a DigestData project and they both require a Common project where some common business logic lives. In this post, I’ll show how to use Spring to load properties not only from your application project, but your common project as well, and honour Spring profiles at the same time.

First, why does this not work out of the box?

Spring automatically picks up your application.properties or application.yml file from your main application, and will even parse that file for Profiles, or load other Profile-specific properties files that share the same root (such as application-UAT.properties). But if you put an application.properties file in your common project, it will be ignored.

Your first thought might be to use @PropertySource or @PropertySources, but this annotation loads an exact file name only, and will not load your property files with Profiles in the names. There is an open issue to make this a feature request at https://github.com/spring-projects/spring-boot/issues/24688, but the Spring team has some understable concerns about this. @PropertySource is discouraged for this use anyway. The official documents says:

While using @PropertySource on your @SpringBootApplication may seem to be a convenient way to load a custom resource in the Environment, we do not recommend it. Such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.*which are read before refresh begins.

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto.application.customize-the-environment-or-application-context

But the docs do mention how to accomplish this with a custom EnvironmentPostProcessor, and combining this code with a custom YAML PropertySourceFactory (https://www.baeldung.com/spring-yaml-propertysource) we can accomplish our desired behaviour:

public MyEnvironmentPostProcessor implements EnvironmentPostProcessor {

  @Override
  public void postProcessEnvironment(final ConfigurableEnvironment, final SpringApplication application) {
      environment.getPropertySources().addLast(createYamlResource("common.yml");
      environment.getPropertySources().addLast(createPropertiesResource("common.properties");
environment.getPropertySources().addLast(createPropertiesResource("common-UAT.properties");
    }

  private Resource createYamlResource(String name) {
      Resource path = new ClassPathResource(name);
      EncodedResource encodedResource = new EncodedResource(path);
      YamlPropertySourceFactory factory = new YamlPropertySourceFactory();
      return factory.createPropertySource("name", encodedResource);
  }

  private Resource createPropertiesResource(String name) {
      return new ClassPathResource(name);
  }

  private Class YamlPropertySourceFactory {

    @Override
    public PropertySource<?> createPropertySource(@Nullable final String name, final EncodedResource resource) throws IOException {

      YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
      factory.setResources(resource.getResource());
      factory.afterPropertiesSet();
      Properties properties = factory.getObject();
      String sourceName = name != null ? name : resource.getResource().getFilename();
      return new PropertiesPropertySource(sourceName, properties);
  }

}

Make sure the properties file exist: common.yml, common.properties, common-UAT.properties because the code above will not fail gracefully.