knalli
knalli

Reputation: 1993

How can I build a database-based Spring Boot environment / property source?

The goal is running Spring Boot application with an Environment containing keys & values loaded and generated by a database connection (DataSource).

Or, more abstract defined: While a configuration by files only should be preferred (faster, easier, more tolerant, ...), sometimes you will find use cases where a non-static files based configuration is required.


Spring 3.1 introduces Environment which is actually a property resolver (extends PropertyResolver) and is based on a list of objects PropertySource. Such a source is a wrapper/adapter for a properties (file or object), a map or something else. It really looks like this is the way how to get.

Properties properties = new Properties();
properties.put("mykey", "in-config");
PropertiesPropertySource propertySource = new PropertiesPropertySource("myProperties", properties);

However, this cannot be done in @Configuration classes since it must be available for the configuration phase. Think about something like

@Bean public MyService myService() {
  if ("one".equals(env.getProperty("key")) {
    return new OneService();
  } else {
    return new AnotherService();
  }
}

// alternatively via
@Value("${key}")
private String serviceKey;

Additionally, the more recent Spring releases support Condition as well.

With a OneCondition like

public class OneCondition implements Condition {
  @Override
  public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
    return "one".equals(context.getEnvironment().getProperty("key"));
  }
}

This can be used like

@Bean
@Conditional(OneCondition.class)
public MyService myService() {
    return new OneService();
}

My non working ideas:

Option 1: @PropertySource

The corresponding annotation processor handles files only. This is fine, but not for this use case.

Option 2: PropertySourcesPlaceholderConfigurer

An example with a custom property source would be

@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() throws IOException {
  PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
  pspc.setIgnoreUnresolvablePlaceholders(Boolean.TRUE);

  // create a custom property source and apply into pspc
  MutablePropertySources propertySources = new MutablePropertySources();
  Properties properties = new Properties();
  properties.put("key", "myvalue");
  final PropertiesPropertySource propertySource = new PropertiesPropertySource("pspc", properties);
  propertySources.addFirst(propertySource);
  pspc.setPropertySources(propertySources);

  pspc.setLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:application.properties"));
    return pspc;
}

However, this only configures the placeholders (i.e. @Value. Any environment.getProperty() will not profit.

This is more or less the same as Option 1 (less magic, more options).


Do you know a better option? Ideally, the solution would use the context datasource. However, this is conceptually an issue since the datasource bean creation relies on properties itself...

Upvotes: 6

Views: 7147

Answers (1)

knalli
knalli

Reputation: 1993

Spring Boot provides some different extensions point for this early processing step: http://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-boot-application.html#howto-customize-the-environment-or-application-context

Internally, these options are realised with implementations of standard Spring ApplicationContextInitializer.

Depending on the priority of the source, the key/value will be available both in environment.getProperty() as well in property placeholders.

Because these is a pre-config-context listeners, no other beans are available, like a DataSource. So if the properties should be read from a database, the datasource and connection have to be build manually (eventually a separated datasource connection lookup).


Option: ApplicationListener for ApplicationEnvironmentPreparedEvent

Build an implementation of an application listener consuming ApplicationEnvironmentPreparedEvents and

register it in META-INF/spring.factories and the key org.springframework.context.ApplicationListener

- or -

use the SpringApplicationBuilder:

new SpringApplicationBuilder(App.class)
        .listeners(new MyListener())
        .run(args);

Example

public class MyListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
  @Override
  public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
    final ConfigurableEnvironment env = event.getEnvironment();
    final Properties props = loadPropertiesFromDatabaseOrSo();
    final PropertiesPropertySource source = new PropertiesPropertySource("myProps", props);
    environment.getPropertySources().addFirst(source);
  }
}

Reference: http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-spring-application.html#boot-features-application-events-and-listeners


Option: SpringApplicationRunListener

Besides the special event, there is also a more general event listener containing hooks for several types of events.

Build an implementation of SpringApplicationRunListener and register it in META-INF/spring.factories and the key org.springframework.boot.SpringApplicationRunListener.

Example

public class MyAppRunListener implements SpringApplicationRunListener {

  // this constructor is required!
  public MyAppRunListener(SpringApplication application, String... args) {}

  @Override
  public void environmentPrepared(final ConfigurableEnvironment environment) {

    MutablePropertySources propertySources = environment.getPropertySources();

    Properties props = loadPropertiesFromDatabaseOrSo();
    PropertiesPropertySource propertySource = new PropertiesPropertySource("myProps", props);
    propertySources.addFirst(propertySource);
  }

  // and some empty method stubs of the interface…

}

Reference: http://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-boot-application.html#howto-customize-the-environment-or-application-context


Option: ApplicationContextInitializer

This is an old friend for all "non Boot" Spring developers. However, SpringApplication mocks a configuration away -- at first.

Build an implementation of ApplicationContextInitializer and

register it in META-INF/spring.factories and the key org.springframework.context.ApplicationContextInitializer.

- or -

use the SpringApplicationBuilder:

new SpringApplicationBuilder(App.class)
        .initializers(new MyContextInitializer())
        .run(args);

Example

public class MyContextInitializer implements ApplicationContextInitializer {
  @Override
  public void initialize(final ConfigurableApplicationContext context) {
    ConfigurableEnvironment environment = context.getEnvironment();

    MutablePropertySources propertySources = environment.getPropertySources();

    Properties props = loadPropertiesFromDatabaseOrSo();
    PropertiesPropertySource propertySource = new PropertiesPropertySource("myProps", props);
    propertySources.addFirst(propertySource);
  }

}

Upvotes: 8

Related Questions