0

I have an application.yml file with following structure

app:
  tenants:
    default:
      property1: 1
      property2: 2
      property3: 3
      features:
        feature1: true
        feature2: false
    tenant1:
      property1: 5
      features:
        feature2: true
    tenant2:
      property3: 9

and following @ConfigurationProperties classes

@ConfigurationProperties("tenants")
class TenantsProperties {
  Map<String,Tenant> tenants
}

class Tenant {
  int property1 = 0;
  int property2 = 0;
  int property3 = 0;
  Map<String,Boolean> features = new HashMap<>();
}

I want to default the properties of tenant1 and tenant2 on what is in the default tenant.

@SpringBootTest
public class TenantsProeprtiesTest {
  @Autowired
  TenantsProperties tenants;

  @Test
  void test_default_properties() {
    Tenant tenant = tenants.getTenants().get("tenant1");
    assertEquals(2, tenant.getProperty2());
    assertEquals(5, tenant.getProperty1());
  }
}

I've registered a custom EnvironmentPostProcessor

@Order
public class DefaultTenantPostProcessor implements EnvironmentPostProcessor {
  @Override
  public void postProcessEnvironment(ConfigurableEnvironment environment, 
                                           SpringApplication application) {
    Map<String,Object> defaultProperties = getProperties("app.tenants.default", environment);
    Set<String> tenants = getDefinedTenants(environment);

    Map<String,Object> properties = combineTenantsAndDefaults(tenants, defaultProperties);

    environment.getPropertySource().addLast(new MapPeropertySource("defaultTenantProperties", properties);
  }
  ...
}

The EnvironmentPostProcessor works and adds properties to the end of the list, however by the time it gets to the test, the properties defined in the yaml file get moved beyond that, so they have lower precedence than the properties generated via the post processor and assertEquals(5, tenant.getProperty1()); fails.

Edit: so I did a bit of debugging and my default tenant properties get "upstaged" in org.springframework.boot.test.context.ConfigDataApplicationContextInitializer via ConfigDataEnvironmentPostProcessor.applyTo(environment, applicationContext, bootstrapContext). At the moment I have no idea how to work around this.

Peter
  • 400
  • 1
  • 13
  • Does `@Value("${app.tenants.default.property1:0}") int property1;` not work? – zouabi Aug 22 '23 at 01:38
  • @zouabi to be honest, I don't think I have tried that. From previous experience, other stack overflow comments and some articles I am unable to find now, `@Value` is not supported under a `@ConfigurationProperties` annotated class. I am not 100% sure this is transitive to nested classes though. I guess it is worth a try, but I don't think it is going to work. – Peter Aug 22 '23 at 02:23
  • @zouabi, I modified the question by adding a Map to the nested property class. I initially omitted it thinking it would make no difference, but it actually does as I don't think one can use `@Value` in any "reasonable" manner to populate a map. I found [this stackoverflow answer](https://stackoverflow.com/a/36794304/1373031), but I think that is more of a "hack" and it goes against the spirit of yaml in my opinion. At the minimum it would make unwieldy in our environment. – Peter Aug 22 '23 at 02:59

1 Answers1

1

Well, through debugging I stumbled upon DefaultPropertiesPropertySource and its static mehtod addOrMerge(...), which seem to be designed for this purpose, i.e. to be always the last property source. Unfortunately I am unable to find any examples, or documentation for this outside of the reference link. So I am not 100% sure this is ok to use.

Modifying the EnvironmentPostProcessor to use it resolved the issue:

@Order
public class DefaultTenantPostProcessor implements EnvironmentPostProcessor {
  @Override
  public void postProcessEnvironment(ConfigurableEnvironment environment, 
                                           SpringApplication application) {
    Map<String,Object> defaultProperties = getProperties("app.tenants.default", environment);
    Set<String> tenants = getDefinedTenants(environment);

    Map<String,Object> properties = combineTenantsAndDefaults(tenants, defaultProperties);

    DefaultPropertiesPropertySource.addOrMerge(properties, environment.getPropertySources());
  }
  ...
}

I am not accepting my answer yet to hopefully spur some more conversation or to be corrected.

Peter
  • 400
  • 1
  • 13