Apache Tamaya as a Toolkit

I mentioned in my previous post that Apache Tamaya is not a framework, but a flexible toolkit that can be used in a variety of ways. This requires Tamaya to be adaptable in various ways. This blog shows how Tamaya deals with such requirements in more detail. Summarizing Tamaya provides the following hooks and plugin mechanisms:

  • Tamaya provides a global access to a shared Configuration instance via it’s ConfigurationProvider singleton. This singleton is backed up by a ConfigurationProviderSpi, which allows to exchange the provider implementation used completely.
  • Instead of sharing a single Configuration instance you can also define your own lifecycle and configuration scope. To support these scenarios Tamaya provides a ConfigurationContextBuilder, which can be used to create your own completely independent Configuration instances.
  • By default Tamaya uses the Java ServiceLoader for locating services such as SPI implementations, PropertySources etc. But Tamaya internally indirects these accesses via it’s own ServiceContext, which is managed as a service by the ServiceContextManager service locator. This makes Tamaya flexible enough to integrate seemlessly with OSGI services or IoC containers.
  • Tamaya comes with a lean small core library, which only comes with a few dependencies (refer also to my last blog post here). So integration of Tamaya does only add a few bytes to your application size. If you want to support external configuration with Tamaya, but nevertheless you don’t want to ship your library or product with Tamaya, you can still use the tamaya-optional module, that allows you to integrate with Tamaya automatically, when it available on your classpath.
  • Finally, because Tamaya is built in a very modular way, you can exactly add and use the features you want. There is no need to add tens of Mega-Bytes of dependencies you do not require in your application.

This blog will discuss the first two concepts in more detail. The other points will follow in subsequent blog post(s).

Managing the shared Configuration context

Tamaya provides access to a shared default Configuration instance with it’s ConfigurationProvider singleton:

Configuration config = ConfigurationProvider.getConfiguration();

The default implementation in tamaya-core actually shares one single instance globally. This is completely enough for microservice-styled applications, but for more complex environments such as Java EE or OSGI, it might be not the best match. Fortunately you can replace the default implementation by registering your own implementation of org.apache.tamaya.spi.ConfigurationProviderSpi:

public interface ConfigurationProviderSpi {

    Configuration getConfiguration();
    Configuration createConfiguration(ConfigurationContext context);
    ConfigurationContextBuilder getConfigurationContextBuilder();
    void setConfiguration(Configuration config);
    boolean isConfigurationSettable();

    @Deprecated
    default ConfigurationContext getConfigurationContext(){...}
    @Deprecated
    default void setConfigurationContext(ConfigurationContext context){...}
    @Deprecated
    default boolean isConfigurationContextSettable(){...}
}

Above already the new interface version, which will be shipped  with 0.4-incubating is shown. In version 0.3-incubating and earlier you also have to implement the deprecated methods (fortunately the implementation can easily delegate to the other methods of the interface, so this is a no brainer).

Looking at the method you see the most important plugin APIs of the Tamaya API:

  • Configuration getConfiguration() defines, which Configuration instance is the shared default instance returned by ConfigurationProvider.getConfiguration(). You can return a single shared instance or manage multiple instances related to context data not known to Tamaya.
  • Configuration createConfiguration(ConfigurationContext) defines the logic that implements a Configuration instance based on a given ConfigurationContext (the instance containing the underlying property sources, filters, converters, policies etc; including their ordering and significance). In most cases you will reuse an existing implementation, such as org.apache.tamaya.core.internal.DefaultConfiguration (tamaya-core) or org.apache.tamaya.spisupport.DefaultConfiguration (tamaya-spisupport).
  • ConfigurationContextBuilder getConfigurationContextBuilder() creates a new builder for creating a ConfigurationContext. This builder allows you to assemble your own configuration setup, including the property sources, converters and filters to be used, as well as their ordering and significance. Given a ConfigurationContext you can use the createConfiguration factory method (see next section) to create a new Configuration instance.
    The builder API will be discussed in more detail later in this post.
  • void setConfiguration(Configuration) allows you to explicitly replace the current shared default instance, with an instance of your choice. Since this might be an operation not wanted to be available in production scenarios, the implementation of the SPI active might declare the operation to be not available using the boolean isConfigurationSettable() method.

Example: ConfigurationProvider for multithreaded testing

To demonstrate the power of this SPI let’s implement a ConfigurationProviderSPI, which supports Configuration isolation on Thread level. This allows us to set Configuration for isolated testing similar to the following snippet:

public class MyTest{

  private Configuration config = buildConfiguration();
  private PropertySource testPropertySource = new Config();
 
  /** Installs the test configuration for this thread. */
  @Before
  public void initConfig(){
    ConfigurationProvider.setConfiguration(config);
  }

  /** Resets the test configuration for this thread. */
  @After
  public void resetConfig(){
    ConfigurationProvider.setConfiguration(null);
  }

  /** Tests ... */
  @Test
  public void testFoo(){
    // execute any tests on any classes that consume the sharewd configuration:
    System.out.println("Thread: " + Thread.currentThread().getId() 
                       + " - config: " 
                       + ConfigurationProvider.getConfiguration().getProperties());
  }

  /** Builds the temporal configuration used by this test class. */
  private Configuration buildConfiguration(){
    ConfigurationContext context = ConfigurationProvider.getConfigurationContextBuilder()
                  .addDefaultConverter()
                  .addPropertySource(testPropertySource)
                  .build();
    return ConfigurationProvider.createConfiguration(context);
  }

  /** Configuration for this test. */
  private static final class Config extends BasePropertySource{
    ...
  }
}

The idea is that you are use parallel multi-threaded test execution in your test framework. The default shared provider instance of Tamaya will lead to conflicts in your test for different configurations. Our implementation now will allow isolating the configurations on a per thread level. To make live more easier we directly inherit from the default implementation based class and just add an optional ThreadLocal layer on top:

/**
 * Configuration provider that allows to set and reset a configuration
 * different per thread.
 */
public class TestConfigProvider extends DefaultConfigurationProvider{

    private ThreadLocal<Configuration> threadedConfig = new ThreadLocal<>();

    @Override
    public Configuration getConfiguration() {
        Configuration config = threadedConfig.get();
        if(config!=null){
            return config;
        }
        return super.getConfiguration();
    }

    @Override
    public void setConfiguration(Configuration config) {
        if(config==null){
            threadedConfig.remove();
        }else {
            threadedConfig.set(config);
        }
    }
}

Finally we have to register our new service implementation using the Java ServiceLoader. So let’s add a classpath resource to

   META-INF/org.apache.tamaya.spi.ConfigProviderSpi

with the following contents:

com.mycompany.test.TestConfigProvider

Given that the new provider is picked up by the API on startup and ensures configurations can be set and reset for each test thread running. By default, if no configuration has been explicitly set, still the default shared configuration instance is returned.

Using the ConfigurationContextBuilder

The ConfigurationContext is the container that manages all required resources to build up a Configuration such as property sources, converters, filters etc. By default, Tamaya creates a new Configuration based on the service provided by default through it’s current ServiceContext (more details are discussed later in this post).  Tamaya also comes with a builder, which allows you to assemble your own custom ConfigurationContext. The default bootstrap code gives you a first impression of the capabilities of this API:

ConfigurationContext context = new DefaultConfigurationContextBuilder()
    .addDefaultPropertyConverters()
    .addDefaultPropertyFilters()
    .addDefaultPropertySources()
   .build();

This code snippet actually does the following:

  1. It Creates a new ConfigurationContextBuilder instance (this can also be done by use of the public Tamaya API:  ConfigurationProvider.getConfigurationContextBuilder().
  2. Load all available PropertyConverter instances from the ServiceContext and add them to the current chain of converters (one chain for each target type). Order them based on their @Priority annotations (if missing a priority of 0 is assumed).
  3. Load all available PropertyFilter instances from the ServiceContext and add them to the current chain of converters. Order them based on their @Priority annotations (if missing a priority of 0 is assumed).
  4. Load all available PropertySource instances from the ServiceContext and add them to the current chain of converters. Load all available PropertySourceProvider instances from the ServiceContext and add all provided (calling PropertySourceProvider.getPropertySources()PropertySource instances to the current chain of converters.
  5. Order the property sources based on their int getOrdinal() value (API method of PropertySource).
  6. By default the PropertyValueCombinationPolicy used is initialized with an overriding policy. This policy will ensure that non-null values returned from more significant PropertySources (being at higher positions in the chain order) will override any existing values for the same key.
  7. Finally the builder instances and lists are rendered into a read-only ConfigurationContext instance.

Give this context a Configuration can be easily created by calling the corresponding API method:

Configuration config = ConfigurationProvider.createConfiguration(context);

Given that, it should be obvious how easy it is, to apply his logic for different classloaders and reuse Tamaya’s bootstrapping logic for different classloader hierarchies. But the builder effectively provides quite a lot of additional functionality to manipulate/adapt a configuration context:

public interface ConfigurationContextBuilder {
 
  ConfigurationContextBuilder setContext(ConfigurationContext context);
  
  // setting up the property source chain
  ConfigurationContextBuilder addPropertySources(PropertySource... propertySources);
  ConfigurationContextBuilder addPropertySources(Collection<PropertySource> propertySources);
  ConfigurationContextBuilder addDefaultPropertySources();
  ConfigurationContextBuilder removePropertySources(PropertySource... propertySources);
  ConfigurationContextBuilder removePropertySources(Collection<PropertySource> propertySources);
  ConfigurationContextBuilder increasePriority(PropertySource propertySource);
  ConfigurationContextBuilder decreasePriority(PropertySource propertySource);
  ConfigurationContextBuilder highestPriority(PropertySource propertySource);
  ConfigurationContextBuilder lowestPriority(PropertySource propertySource);
  ConfigurationContextBuilder sortPropertySources(Comparator<PropertySource> comparator);
  List<PropertySource> getPropertySources();
  
  // Setting up property filters
  ConfigurationContextBuilder addPropertyFilters(PropertyFilter... filters); 
  ConfigurationContextBuilder addPropertyFilters(Collection<PropertyFilter> filters);
  ConfigurationContextBuilder addDefaultPropertyFilters();
  ConfigurationContextBuilder removePropertyFilters(PropertyFilter... filters);
  ConfigurationContextBuilder removePropertyFilters(Collection<PropertyFilter> filters);
  List<PropertyFilter> getPropertyFilters();
  ConfigurationContextBuilder sortPropertyFilter(Comparator<PropertyFilter> comparator);
  
  // Setting up converters
  <T> ConfigurationContextBuilder addPropertyConverters(TypeLiteral<T> typeToConvert, PropertyConverter<T>... propertyConverters);
  <T> ConfigurationContextBuilder addPropertyConverters(TypeLiteral<T> typeToConvert, Collection<PropertyConverter<T>> propertyConverters);
  ConfigurationContextBuilder addDefaultPropertyConverters();
  <T> ConfigurationContextBuilder removePropertyConverters(TypeLiteral<T> typeToConvert, PropertyConverter<T>... propertyConverters);
  <T> ConfigurationContextBuilder removePropertyConverters(TypeLiteral<T> typeToConvert, Collection<PropertyConverter<T>> propertyConverters);
  ConfigurationContextBuilder removePropertyConverters(TypeLiteral<?> typeToConvert);
  Map<TypeLiteral<?>, Collection<PropertyConverter<?>>> getPropertyConverter();
  
  // Setting the combination policy
  ConfigurationContextBuilder setPropertyValueCombinationPolicy(PropertyValueCombinationPolicy policy);
  
  // building a context
  ConfigurationContext build();

}

Summarizing with the builder you can:

  • loading the default instances of property sources, converters and filters reusing the default loading logic already implemented.
  • add, remove any kind of property sources, converters and filters.
  • reorder property sources programmatically regardless of their provided getOrdinal() value.
  • reorder property sources and filters based on your own custom comparator implementations.
  • changing the conbination policy programmatically as well.

Ĥereby you have full control about what is happening. Unless you explicitly call the sortXXX methods, the builder will exactly presume the ordering of artifacts (e.g. property sources) as they were added to the builder. Given that you can very easily define your configuration context based on your own requirements. As an example we would like to build a configuration (context) with the following property sources used:

  1. Lookup environment properties first.
  2. Check for system properties.
  3. Add CLI parameters as third source.
  4. And let configuration be overriden by a centrally managed etcd server.
  5. For conversion and filtering we are satisfied with the default logic.

In code we can build up a corresponding Context/Configuration instance and initialize our application as follows:

public static void main(String[] args) {
  CLIPropertySource.initMainArgs(args);
  ConfigurationContext ctx = ConfigurationProvider.getConfigurationContextBuilder()
    .addDefaultPropertyConverters()
    .addDefaultPropertyFilters()
    .addPropertySources(
      new EnvironmentPropertySource(),
      new SystemPropertySource(),
      new CLIPropertySource(),
      new EtcdPropertySource("localhost")
  );
  ConfigurationProvider.setConfiguration(ConfigurationProvider.createConfiguration(context));
  
  // start your application
}

Given this snippet you see that Tamaya is very flexible it does not tell you, how you have to write your applications or what framework you have to use. Nevertheless to make life easier Tamaya provides several extensions/integrations, for example with

  • Spring/Spring Boot
  • Vertx
  • CDI/Java EE
  • Microprofile.io (work in progress)
  • OSGI Configuration (work in progress)

This is why we call Tamaya to be a toolkit for Configuration. It should shine with ease of use and flexibility and not impose any constraints on developers.

Tamaya internally is built similar to a JSR, where an API is defined (and also a spec and TCK), which can be implemented by different parties. Tamaya defines it’s API in tamaya-api, and ships with a default implementation tamaya-core. The tamaya-spisupport module provides many implementation SPI classes indepedent of the currently active implementation, so it allows you to implement the SPI without depending on tamaya-core.

Advertisements

About atsticks

Advanced Java Software Engineer and Architect.
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s