Konfiguration

Ein Programm - 100 Umgebungen

Gunter Winkler
München, 2014-07-28

usecase

DataSource

^1^external.service.url         = https://service.example.com/api^^
^2^external.service.timeout_sec = 60^^
^3^external.service.user        = TheUser^^
^4^external.service.pass        = *********^^
was tun?
http://spring.io
Spring modules; source: spring.io
Spring core modules; source: spring.io
In software engineering, inversion of control (IoC) is a programming technique [...] in which object coupling is bound at run time by an assembler object and is typically not known at compile time [...].

see "The Dependency Inversion principle" by Robert C. Martin

and "Inversion of Control Containers and the Dependency Injection Pattern" by Martin Fowler

Spring IoC Container; source: spring.io

Java-based metadata (seit 3.0)

^1^@Configuration^^
public class DaoConfig {

  ^2^@Bean^^(^3^name="runtimeDataSource"^^)
  public DataSource dataSource() {
    return new JndiObjectFactoryBean()...getObject();
  }

  @Bean
  public EntityService entityService(
      ^4^@Qualifier("runtimeDataSource")^^ DataSource ds) {
    return new BasicEntityService(ds);
  }
}
@Test
public void verifyThatSpringContextLoads() {
  try (ConfigurableApplicationContext context =
      ^1^new AnnotationConfigApplicationContext(DaoConfig.class))^^ {

    EntityService service = ^2^context.getBean(EntityService.class)^^;

    assertThat(service, is(notNullValue()));
  }
}
^1^@Component^^ // or @Service, @Repository, @Controller
public class SomeComponent {

	^2^@Autowire^^ // Spring
	private EntityService entityService;

	^3^@Inject^^ // JSR 330
	private Environment env;
}
Beispiel: Konfiguration eines WebService-client

WeatherClient.java

public class WeatherClient implements WeatherProvider {

  public WeatherClient(^1^String baseUrl^^, ^2^String userName^^, RestOperations restTemplate) {
    this.baseUrl = baseUrl;
    this.userName = userName;
    this.restTemplate = restTemplate;
  }
  // ...
}

weather/default.properties

weather.baseUrl  = http://api.geonames.org/weatherJSON
weather.username = demo
connect.timeout  = 10000

WeatherConfig.java

@Configuration
^1^@PropertySource({ "classpath:weather/default.properties" })^^
public class WeatherConfig {

  @Bean
  public WeatherProvider client(^2^Environment env^^, RestOperations restOps) {
    String baseUrl = ^3^env.getRequiredProperty^^("weather.baseUrl");
    String userName = env.getRequiredProperty("weather.username");
    return new WeatherClient(baseUrl, userName, restOps);
  }

  @Bean
  public RestOperations restTemplate(Environment env) {
    Integer connectTimeout = 
      ^4^env.getProperty^^("connect.timeout", Integer.class, ^5^30000^^);
    // ...
  }
}

WeatherClientTest

staging?

DEV ⇒ CI ⇒ TEST ⇒ LIVE

WeatherConfig.java

@Configuration
@PropertySource({
    "classpath:weather/default.properties",
    ^1^"classpath:weather/${stage}.properties"^^
})
public class WeatherConfig {

  @Bean
  public WeatherProvider client(Environment env, RestOperations restOps) {
    String baseUrl = env.getRequiredProperty("weather.baseUrl");
    String userName = env.getRequiredProperty("weather.username");
    return new WeatherClient(baseUrl, userName, restOps);
  }
  // ...
}
Woher kommt der Wert für ${stage}?

o.s.core.env.StandardEnvironment

  1. system properties
  2. system environment *

 

* smart matching: FOO_BAR = FOO.BAR = foo_bar = foo.bar

o.s.web.context.support.StandardServletEnvironment

  1. system properties
  2. system environment
  3. ServletConfig (servlet init-param)
  4. ServletContext (context-param)
  5. JNDI "java:comp/env/the.property.name"
Wohin mit den properties?

best practice: "fail fast"

best practice: "alles an eine Stelle"

best practice: "Sonderbehandlung für Passwörter"

WeatherConfig.java

@Configuration
@PropertySource({
    "classpath:weather/default.properties",
    "classpath:weather/${stage}.properties",
    "file://${user.home}/config/common-${stage}.properties",
    "file://${user.home}/config/secret.properties"
})
public class WeatherConfig {

  // ...

}
zero config?
Voraussetzungen:

WeatherConfig.java (v1)

@Configuration
@PropertySource({
    "classpath:weather-default.properties",
    "classpath:weather-${stage}.properties",
    "classpath:weather-${user.name}.properties",
    "classpath:weather-${hostname}.properties",
    "file://${user.home}/weather-default.properties",
    "file://${user.home}/weather-${stage}.properties",
    "file://${user.home}/weather-${user.name}.properties",
    "file://${user.home}/weather-${hostname}.properties"
})
public class WeatherConfig {

  // ...

}
java.lang.IllegalArgumentException: Could not resolve placeholder 'stage' in string value "classpath:weather-${stage}.properties"
java.io.FileNotFoundException: class path resource [weather-dev.properties] cannot be opened because it does not exist

WeatherConfig.java (Spring 4)

@Configuration
@PropertySource(value={
    "classpath:weather-default.properties",
    "classpath:weather-${stage}.properties",
    "classpath:weather-${user.name}.properties",
    "classpath:weather-${hostname}.properties",
    "file://${user.home}/weather-default.properties",
    "file://${user.home}/weather-${stage}.properties",
    "file://${user.home}/weather-${user.name}.properties",
    "file://${user.home}/weather-${hostname}.properties"
}, ^1^ignoreResourceNotFound = true^^)
public class WeatherConfig {

  // ...

}
Woher kommt der Wert für ${hostname}? from openclipart.org; author nicubunu

Spring container life cycle

Spring container life cycle
public class SomeApplicationContextInitializer implements
    ApplicationContextInitializer<ConfigurableApplicationContext> {

  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {

    // ... do something with applicationContext

  }

}
public class HostnameProvidingApplicationContextInitializer implements
    ApplicationContextInitializer<ConfigurableApplicationContext> {

  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
    ^1^ConfigurableEnvironment env = applicationContext.getEnvironment();^^
    ^2^MutablePropertySources ps = env.getPropertySources();^^
    ps.^3^addLast^^(new MapPropertySource("hostNamePropertySource",
        ImmutableMap.of("hostname", tryHardToFindHostname())));
  }
  // ...
}
DEBUG log:
11:42:53.614 [main] DEBUG o.s.c.e.PropertySourcesPropertyResolver - Found key 'hostname' in [hostNamePropertySource] with type [String] and value 'TNG-WINKLER'
Wie aktiviert man den ApplicationContextInitializer?
web.xml
  <context-param>
    <param-name>contextClass</param-name>
    <param-value>
      o.s.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
  </context-param>
  <context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>
      some.package.HostnameProvidingApplicationContextInitializer
    </param-value>
  </context-param>
Komponententests
@RunWith(^1^SpringJUnit4ClassRunner.class^^)
^2^@ContextConfiguration^^(
    classes = { WeatherConfig.class },
^3^    initializers = { 
      HostnameProvidingApplicationContextInitializer.class 
    })^^
public class WeatherClientTest {
  // ...
}
standalone app
  private ApplicationContext bootSpringContext() {
    AnnotationConfigApplicationContext ac 
        = ^1^new AnnotationConfigApplicationContext()^^;
    ac.^2^register(WeatherConfig.class)^^;

    ^3^new HostnameProvidingApplicationContextInitializer().initialize(ac)^^;

    ^4^ac.refresh()^^;
    return ac;
  }
Fragen?
Beispiel: Konfiguration von DataSources
@Configuration
public class EmbeddedDataSourceConfig {
  // for development

  @Bean
  public DataSource embeddedDataSource(Environment env) {
    EmbeddedDataSource ds = new org.apache.derby.jdbc.EmbeddedXADataSource40();
    ds.setCreateDatabase("create");
    ds.setDatabaseName(env.getRequiredProperty("db.name"));
    ds.setUser(env.getRequiredProperty("db.user"));
    ds.setPassword(env.getRequiredProperty("db.password"));
    return ds;
  }

}
@Configuration
public class PostgresDataSourceConfig {
  // for component tests

  @Bean
  public DataSource embeddedDataSource(Environment env) {
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName("org.postgresql.Driver");
    ds.setUrl(env.getRequiredProperty("db.jdbc.url"));
    ds.setUsername(env.getRequiredProperty("db.user"));
    ds.setPassword(env.getRequiredProperty("db.password"));
    return ds;
  }

}
@Configuration
public class JndiDataSourceConfig {
  // for application container

  @Bean
  public DataSource jndiDataSource(Environment env) throws NamingException {
    JndiLocatorDelegate locator = 
        JndiLocatorDelegate.createDefaultResourceRefLocator();
    return locator.lookup(
        env.getRequiredProperty("datasource.jndiName"), 
        DataSource.class);
  }

}

Wie kann man umschalten?

@Profile

aktivierte Profile
@Configuration
public class DataSourceConfig {

  @Bean
  ^1^@Profile("embedded-ds")^^ // since Spring 4
  public DataSource embeddedDataSource(Environment env) {
    // ...
  }

  @Bean
  @Profile("postgres-ds")
  public DataSource directDataSource(Environment env) {
    // ...
  }

  @Bean
  @Profile({"jndi-ds", ^2^"default"^^})
  public DataSource jndiDataSource(Environment env) throws NamingException {
    // ...
  }
}

weitere Beispiele

Beispiel: Atomikos "on-demand"

Spring container life cycle

Spring container life cycle
@Configuration
public class DataSourceConfig {

  // beans: embeddedDataSource, directDataSource, jndiDataSource

  @Bean
  @Profile("atomikos-jta")
  public ^1^static^^ BeanPostProcessor wrapConnections() {
    return new WrapConnectionWithAtomikosBpp();
  }
}
public class WrapConnectionWithAtomikosBpp 
    implements BeanPostProcessor {

  @Override
  public Object ^1^postProcessAfterInitialization^^(Object bean, String beanName) throws BeansException {
    if (bean instanceof XADataSource) {
      AtomikosDataSourceBean wrapped = new AtomikosDataSourceBean();
      wrapped.setUniqueResourceName(beanName);
      wrapped.setXaDataSource((XADataSource) bean);
      return wrapped;
    } else {
      return bean;
    }
  }

  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    return bean;
  }

}
usecase

Alternativen?

 

https://github.com/TNG/property-loader

https://github.com/TNG/config-builder

Vielen Dank!

Fragen?

gunter.winkler@tngtech.com
guwi17@gmx.de