Tamaya Configuration Injection

In this post we are gonna have a deeper look into Apache Tamaya‘s options to inject configuration. Tamaya has a very modularized design. As a consequence the first decision is about selecting the right dependencies:

Of course, the starter is Tamaya’s main API and it’s core implementation:

<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-api</artifactId>
  <version>0.4-incubating-SNAPSHOT</version>
</dependency>
<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-core</artifactId>
  <version>0.4-incubating-SNAPSHOT</version>
</dependency>

Depending on the configuration features you want to use you may add additional extensions.

Tamaya’s API package only contains the SE based API. So, for using injection, you need additionally Tamaya’s injection API module:

<dependency>
  <groupId>org.apache.tamaya.ext</groupId>
  <artifactId>tamaya-injection-api</artifactId>
  <version>0.4-incubating-SNAPSHOT</version>
</dependency>

IMPORTANT: The feature discussed in this blog are also available with the current 0.3-incubating release. Nevertheless some of the features shown will be included from 0.4-incubating and later. You can try out the latest snapshots (0.4-incubating-SNAPSHOT) by including the Apache snapshot repository:

<repository>
     <id>ApacheSnapshots</id>
     <name>Apache Snapshot Repository</name>
     <url>https://repository.apache.org/content/groups/snapshots/</url>
<repository>

 

The Tamaya Injection API

Tamaya’s injection API is compatible with Java SE 7, but since Tamaya will be based on Java 8, it will also provide injection of Optional<T> and Supplier<T> values. To illustrate it’s functionality let’s have a look at the following POJO:

public class AnnotatedConfigBean {

    @Config(value = {"foo.bar.key1", "foo.bar-old.key1", "key2"}, defaultValue = "ET")
    public String myParameter;

    @Config("simple_value")
    public String simpleValue;

    @Config
    String anotherValue;

    @Config("host.name")
    private String hostName;

    @Config("host")
    private DynamicValue<URL> dynamicHost;

    @NoConfig
    public String javaVersion;

    @NoConfig
    private List<String> events = new ArrayList<>();

    @Config("java.version")
    void setJavaVersion(String version){
        this.javaVersion = version;
    }

}

As we see the main annotation is named @Config. Hereby the annotation is defined as follows:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
public @interface Config {

    /** Value that is set by default as default, so it is possible to use empty Strings as default values. */
    String UNCONFIGURED_VALUE = "org.apache.tamaya.config.configproperty.unconfigureddvalue";

    /**
     * Defines the configuration property keys to be used. Hereby the first non null value evaluated is injected as
     * property value.
     *
     * @return the property keys, not null. If empty, the field or property name (of a setter method) being injected
     * is used by default.
     */
    @Nonbinding
    String[] value() default {};

    /**
     * The default value to be injected, if none of the configuration keys could be resolved. If no key has been
     * resolved and no default value is defined, it is, by default, handled as a deployment error. Depending on the
     * extension loaded default values can be fixed Strings or even themselves resolvable. For typed configuration of
     * type T entries that are not Strings the default value must be a valid input to a corresponding
     * {@link org.apache.tamaya.spi.PropertyConverter}.
     * 
     * @return default value used in case resolution fails.
     */
    @Nonbinding
    String defaultValue() default UNCONFIGURED_VALUE;

    /**
     * Flag that defines if a configuration property is required. If a required
     * property is missing, a {@link org.apache.tamaya.ConfigException} is raised.
     * Default is {@code true}.
     * @return the flag value.
     */
    @Nonbinding
    boolean required() default true;
}

Hereby

  • The annotation is modelled as CDI Qualifier annotation. In case no CDI is used this doesn’t affect any functionality. In case of CDI it ensures the properties are not used as binding attributes of the qualifier.
  • The annotation can be used on fields, (setter-)methods and as parameters (e.g. when used in a CDI container).
  • value is the most commonly used attribute. It can define one or more lookup keys to evaluate a configuration value. In case of multiple keys the first key that evaluates to a non-null value is used to determine the final configuration value. If no value is present the possible keys are derived based on the injected member. By default (there are additional options, see later in this post) for myPackage.MyClass.member the following keys are evaluated in the order as given:
    • myPackage.MyClass.member 
    • MyClass.member 
    • member 
  • defaultValue allows to define a default value. This value is used if none of the evaluated candidate keys (see above) can be evaluated to a non-null value. The defaultValue must be convertible by one of the registered converters to the required target type.
  • required allows to configure if Tamaya should throw an ConfigException if no candidate key evaluates to a non-null value, and no defaultValue has been provided. This feature especially works fine together with default assignments:
@Config(required=false)
boolean performAny = true;

Tamaya SE Based Injection

Injection is not necessarily bound to CDI. We already mentioned that Tamaya is designed as a flexible toolkit, which is usable in a variety of use cases. Therefore Tamaya offers injection services without any dependency injection framework installed with the SE injection extension. The extension provides an injection singleton, which allows simply to pass an object of any type and let Tamaya “configure” it. So our AnnotatedConfigBean can be configured in any Java environment as follows:

AnnotatedConfigBean bean = new AnnotatedConfigBean();
ConfigurationInjection.getConfigurationInjector().configure(bean);

Despite Injection of instances Tamaya also support the usage of interface that define a type-safe variant of a configuration object. Such an interface can be annotated with the same annotations that also apply for classes. The difference is that Tamaya will implement the interface based on it’s configuration. So let’s rewrite our example for using Tamaya’s template feature:

public interface AnnotatedConfigTemplate {

  @Config(value = {"foo.bar.key1", "foo.bar-old.key1", "key2"}, defaultValue = "ET")
  String myParameter();

  @Config("simple_value")
  String simpleValue();

  @Config
  String anotherValue();

  @Config("host")
  URL getHost();
 
  @Config("host")
  DynamicValue<URL> dynamicHost();

  @Config("java.version")
  String javaVersion();

}

Similarly to class injection we can use the injector to implement our template:

AnnotatedConfigTemplate template = new AnnotatedConfigTemplate ();
ConfigurationInjection.getConfigurationInjector().createTemplate(template );

As expected we now can access configuration fully type safe:

URL host = template.getHost();

Advanced Features

You have now seen the basic features Tamaya’s injection API provides. Nevertheless here are additional options to customized configuration evaluation:

  • We already have see the @NoConfig annotation in the previous example. This is a small marker annotations to inform Tamaya that the annotated element should never be injected with configured data. This is useful because Tamaya can also try to lookup and inject configuration also by using property or method names without annotations (see next point).
  • Classes and templates also can be annotated with @ConfigAutoInject. This annotation tells Tamaya to try to inject/configure all fields and methods matching with configuration. For simple configuration beans this is convenient, since you then can omit all the @Config annotations on the fields/methods. Nevertheless members still can be explicitly excluded from injection by annotating them with @NoConfig. By default Tamaya does not throw any ConfigException if a value cannot be evaluated for a given property. This can be set explicitly by setting the failOnError attribute on the @ConfigAutoInject annotation.
  • Injection points also can be annotated with @WithConfigOperator, which allows applying an unary function f(Configuration)->Configuration to be applied, before any keys are evaluated.
  • Injection points also can be annotated with @WithPropertyConverter, which allows setting the PropertyConverter to be used explicitly.
  • Finally @ConfigDefaultSection allows you to define the root package for the keys on evaluation. This feature will be discussed in the next section.

Configuring default sections

As mentioned in the previous section @ConfigDefaultSection allows you to define the root package for the keys on evaluation. To illustrate this feature refer to the example below:

package myPackage;

@ConfigDefaultSection("mysection")
public class SectionAnnotatedConfigBean {
  
  @Config(value = {"key1", "key2"})
  private String stage;

  ...
}

Adding the section annotation changes the way how the candidate keys are evaluated. For evaluating the configuration for the field stage, the following candidate keys are used:

  1. mysection.key1
  2. mysection.key2

Finally with Tamaya you can also define absolute keys by putting them in brackets. To illustrate let’s rewrite our example:

package myPackage;

@ConfigDefaultSection("mysection")
public class SectionAnnotatedConfigBean {
 
 @Config(value = {"key1", "key2", "[foo.bar.myAbsoluteKey]"})
 private String stage;

 ...
}

This example will evaluate to the following candidate keys:

  • mysection.key1
  • mysection.key2
  • foo.bar.myAbsoluteKey

Dynamic Configuration Injection

Tamaya also provides several options how configuration can be injected that is allowed to change during runtime:

  • Tamaya allows also to define injection points of type Supplier<T>.
  • Tamaya allows also to define injection points of type Provider<T>.
  • Tamaya allows also to define injection points of type Optional<T>.
  • Tamaya allows also to define injection points of type DynamicValue<T>.

DynamicValue hereby provides the most powerful model:

public interface DynamicValue<T> {

    /**
     * Performs a commit, if necessary, and returns the current value.
     *
     * @return the non-null value held by this {@code DynamicValue}
     * @throws org.apache.tamaya.ConfigException if there is no value present
     *
     * @see DynamicValue#isPresent()
     */
    T commitAndGet();

    /**
     * Commits a new value that has not been committed yet, make it the new value of the instance. On change any
     * registered listeners will be triggered.
     */
    void commit();

    /**
     * Access the {@link UpdatePolicy} used for updating this value.
     * @return the update policy, never null.
     */
    UpdatePolicy getUpdatePolicy();

    /**
     * Add a listener to be called as weak reference, when this value has been changed.
     * @param l the listener, not null
     */
    void addListener(PropertyChangeListener l);

    /**
     * Removes a listener to be called, when this value has been changed.
     * @param l the listner to be removed, not null
     */
    void removeListener(PropertyChangeListener l);

    /**
     * If a value is present in this {@code DynamicValue}, returns the value,
     * otherwise throws {@code ConfigException}.
     *
     * @return the non-null value held by this {@code Optional}
     * @throws org.apache.tamaya.ConfigException if there is no value present
     *
     * @see DynamicValue#isPresent()
     */
    T get();

    /**
     * Method to check for and apply a new value. Depending on the {@link  UpdatePolicy}
     * the value is immediately or deferred visible (or it may even be ignored completely).
     * @return true, if a new value has been detected. The value may not be visible depending on the current
     * {@link UpdatePolicy} in place.
     */
    boolean updateValue();

    /**
     * Evaluates the current value dynamically from the underlying configuration.
     * @return the current actual value, or null.
     */
    T evaluateValue();

    /**
     * Sets a new {@link UpdatePolicy}.
     * @param updatePolicy the new policy, not null.
     */
    void setUpdatePolicy(UpdatePolicy updatePolicy);

    /**
     * Access a new value that has not yet been committed.
     * @return the uncommitted new value, or null.
     */
    T getNewValue();

    /**
     * Return {@code true} if there is a value present, otherwise {@code false}.
     *
     * @return {@code true} if there is a value present, otherwise {@code false}
     */
    boolean isPresent();

    /**
     * Return the value if present, otherwise return {@code other}.
     *
     * @param other the value to be returned if there is no value present, may
     * be null
     * @return the value, if present, otherwise {@code other}
     */
    T orElse(T other);

    /**
     * Return the value if present, otherwise invoke {@code other} and return
     * the result of that invocation.
     *
     * @param other a {@code ConfiguredItemSupplier} whose result is returned if no value
     * is present
     * @return the value if present otherwise the result of {@code other.get()}
     * @throws NullPointerException if value is not present and {@code other} is
     * null
     */
    T orElseGet(Supplier<? extends T> other);

    /**
     * Return the contained value, if present, otherwise throw an exception
     * to be created by the provided supplier.
     *
     * NOTE A method reference to the exception constructor with an empty
     * argument list can be used as the supplier. For example,
     * {@code IllegalStateException::new}
     *
     * @param <X> Type of the exception to be thrown
     * @param exceptionSupplier The supplier which will return the exception to
     * be thrown
     * @return the present value
     * @throws X if there is no value present
     * @throws NullPointerException if no value is present and
     * {@code exceptionSupplier} is null
     */
    <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X;

}

Summarizing DynamicValue allows you to register listeners for observing any configuration changes for the underlying value and let you define the behavior, when changes have been detected. Changes detected also can be explicitly applied by calling commit. The main policies are defined in UpdatePolicy:

/**
 * Policy to control how new values are applied to a {@link DynamicValue}.
 */
public enum UpdatePolicy {
    /** New values are applied immediately and registered listeners are informed about the change. */
    IMMEDIATE,
    /** New values or not applied, but stored in the newValue property. Explicit call to DynamicValue#commit
     of DynamicValue#commitAndGet are required to accept the change and inform the listeners about the change.
     * Registered listeners will be informed, when the commit was performed explicitly.
     */
    EXPLICIT,
    /**
     * @deprecated Use {@link #EXPLICIT} instead of.
     */
    @Deprecated
    EXPLCIT,
    /**
     * New values are always immediately discarded, listeners are not triggered.
     */
    NEVER,
    /**
     * All listeners are informed about the change encountered, but the value will not be applied.
     */
    LOG_ONLY
}

Configuration Injection with CDI

Using CDI to manage your component lifecycle is fully supported. To make it work you need a different extension to be added.

<dependency>
  <groupId>org.apache.tamaya.ext</groupId>
  <artifactId>tamaya-inject-cdi</artifactId>
  <version>0.4-incubating-SNNAPSHOT</version>
</dependency>

This module requires the corresponding CDI extensions to be configured with the ServiceLoader:

org.apache.tamaya.cdi.TamayaCDIInjectionExtension
org.apache.tamaya.cdi.TamayaSEInjectionExtension

Hereby

  • TamayaCDIInjectionExtension supports injection using the CDI @Inject annotations.
  • TamayaSEInjectionExtension allows to inject members using the SE injection variant (without (news)

The annotations as described in the previous sections work the same. You only have to add the @Inject annotation, so CDI recognizes a member as a configurable extension standpoint.

Publishing Configuration Events

Beside observing change events on single configuration entries Tamaya also support observing for configuration changes on the whole configuration. This feature is provided by the tamaya-events module:

<dependency>
  <groupId>org.apache.tamaya.ext</groupId>
  <artifactId>tamaya-events</artifactId>
  <version>0.4-incubating-SNAPSHOT</version>
</dependency>

Config Events

The module publishes change events, which are defined as follows:

public interface ConfigEvent<T>{

    /**
     * Access the type of resource. This allows to easily determine the resource an event wants to observe.
     * @return the resource type.
     */
    Class<T> getResourceType();

    /**
     * Get the underlying property provider/configuration.
     * @return the underlying property provider/configuration, never null.
     */
    T getResource();

    /**
     * Get the version relative to the observed resource. The version is required to be unique for
     * each change emmitted for a resource. There is no further requirement how this uniqueness is
     * modelled, so returning a UUID is a completely valid strategy.
     * @return the base version.
     */
    String getVersion();

    /**
     * Get the timestamp in millis from the current epoch. it is expected that the timestamp and the version are unique to
     * identify a changeset.
     * @return the timestamp, when this changeset was created.
     */
    long getTimestamp();
}

In most of the cases you will listen for ConfigurationChange events:

/**
 * Event that contains a set current changes that were applied or could be applied.
 * This class is immutable and thread-safe. To create instances use
 * {@link PropertySourceChangeBuilder}.
 *
 * Created by Anatole on 22.10.2014.
 */
public final class ConfigurationChange implements ConfigEvent<Configuration>, Serializable{
    ...
}

Similarly there is also a PropertyChangeEvent defined.

As expected you can listen on the events using a listener:

@FunctionalInterface
public interface ConfigEventListener {
    /**
     * Called if an event occurred.
     * @param event the event, not null.
     */
    void onConfigEvent(ConfigEvent<?> event);
}

The ConfigEventManager

The module provides a simple SE API to add/remove and access listeners, the ConfigEventManager singleton:

public final class ConfigEventManager {
    
   public static void addListener(ConfigEventListener l) {...}
   public static <T extends ConfigEvent> void addListener(ConfigEventListener l, Class<T> eventType) {...}
   
   public static void removeListener(ConfigEventListener l) {...}
   public static <T extends ConfigEvent> void removeListener(ConfigEventListener l, Class<T> eventType) {...}
   
   public static <T extends ConfigEvent>
        Collection<? extends ConfigEventListener> getListeners(Class<T> type) {...}
   public static <T extends ConfigEvent>
    Collection<? extends ConfigEventListener> getListeners() {...}
   ...
}

As shown you can register listeners globally or for a certain configuration event type, E.g. for listening on ConfigurationChange events only do the following:

ConfigEventManager.addListener(testListener, ConfigurationChange.class);

Since Tamaya is designed as a Toolkit it will not perform any actions automatically. So to start configuration monitoring to regularly pull for configuration changes and publishing corresponding event you have to activate monitoring:

ConfigEventManager.enableChangeMonitoring(true);
System.out.println("Monitoring enabled: " + ConfigEventManager.isChangeMonitoringEnabled());

You can also define the period between your checks in milliseconds:

ConfigEventManager.setChangeMonitoringPeriod(20000L);
System.out.println("Monitoring period: " + ConfigEventManager.getChangeMonitorinPeriod()/10 + " seconds.");

Firing Events

By default the ConfigEventManager internally can synchronously and asynchronously publish events. The implementation can be adapted by registering your own instance of org.apache.tamaya.events.spi.ConfigEventManagerSpi. As an example Tamaya’s CDI injection module also registers an alternate implementation that relies on CDI to publish corresponding change events.,

 

Advertisements

About atsticks

Advanced Java Software Engineer and Architect.
This entry was posted in Architecture, Uncategorized and tagged , , , , , , . 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