Monday, September 7, 2015

Apache Tamaya - the new Configuration API

It was quite s long time since I have blogged here, but now it is definitive time to reactivate this blog and get in contact with all interested people on the configuration topic.

History

After JavaOne 2014, when the configuration topic was cancelled from the EE8 list, David Blevins and others proposed to start an Apache project for several reasons:
  • let focus people with experience in the topic to identify a common feature set
  • implement the ideas as part of an Apache project to provide the ideas using a free nd redistributable licence
  • use a common proven organisation also capable of generating successful adoption.This was when Apache Tamaya was put to incubation. Following we had several discussions, hangouts, mails. As a result Apache Tamaya is now available as a first release 0.1-incubating, ready to be used.
Also to be mentioned is that also Mark Struberg and Gerhard Petracek, the guys behind Deltaspike, joined this project and actively contributed to it. Given that I think it is worth to have a deeper look at the project. This is what this blog is all about.

The Apache Tamaya Project

Like a Java Specification Request

Apache Tamaya is built-up similarly to a Java Specification Request (JSR). It as an API that defines the artifacts user's typically interact with and it provides a reference implementation that implements the API so it can be used for real world projects. The reason for doing so are the following:
  1. Separating an API from the implementation gives you a very clear and clean view on the problem. You must isolate the essence of your problem and omit all kind of over-specific aspects. If done in a good way, this leads to a simple and well comprehensive API, which at the same time is powerful enough to support at least most of all other requirements (e.g. using extension points or hooks for plugin in adapted or additional functionality (aka service provider interfaces/SPIs).
  2. The API may be more independent than the reference implementation regarding its compatibility requirements. As an example the Java 7 based API of Apache Tamaya in fact is also compatible Java 6 and Java ME platforms.
  3. You can start with a minimal set of functionality on the API and extending it step-by-step as needed. Very extension must be checked, if it is really necessary or if the requirement not also can be implemented using the existing API/SPI. This ensures your API is really focusing on the minimal aspects thefore getting lean and clear.
  4. Last but not least, somehow corresponding to the previous point, adding new functionality does not interfere with the basic API/implementation, making it very easy to add new functionality. The Apache Tamaya project also contains quite a few of so called extensions that only depend on the API, so the project has already proven being able to cover this aspect very efficiently.
The only difference to a JSR is the current lack of a Technical Compatibility Kit (TCK) that ensures that different implementations of the API are compatible with a common set of rules. Similarly we do not have something like a "specification" (but we have a very extensive documentation, somehow quite similar to a specification, also covering many of the aspects/discussions done during the evaluation phase for the Java EE JSR 2 years ago).

Compatibility

Apache Tamaya currently supports both Java 7 and Java 8. Reasons behind is that there is still plenty of code, especially in the enterprise context, running on Java 7. And we wanted people to be able to use Apache Tamaya also before they move to the Java 8 platform. Said that the API can be added to your maven build quite easily:

<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-java7-api</artifactId>
  <version>0.1-incubating</version>
</dependency>

Or, when with Java 8:

<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-api</artifactId>
  <version>0.1-incubating</version>
</dependency>

Similarly the implementation (called core), can be added similarly:

Compatible with Java 7 and beyond:

<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-java7-core</artifactId>
  <version>0.1-incubating</version>
</dependency>

Compatible  with Java 8:

<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-core</artifactId>
  <version>0.1-incubating</version>
</dependency>

The Main Concepts

Configuration Abstraction and Access 

One of the main objectives is to define an abstraction for configuration and define a common way of accessing it using simple Java code. So the first thing is to define a model for configuration:

public interface Configuration {

  default String get(String key) {...}
  default <T> T get(String key, Class<T> type) {...}
  default Configuration with(ConfigOperator operator) {...}
  default <T> T query(ConfigQuery<T> query) {...}

  <T> T get(String key, TypeLiteral<T> type);
  Map<String, String> getProperties();

  // not available for Java 7
  default Optional<String> getOptional(String key) {...}
  default <T> Optional<T> getOptional(String key,
                                      Class<T> type) {...}
  default <T> Optional<T> getOptional(String key,
                                      TypeLiteral<T> type) {...}
  default Optional<Boolean> getBoolean(String key) {...}
  default OptionalInt getInteger(String key) {...}
  default OptionalLong getLong(String key) {...}
  default OptionalDouble getDouble(String key) {...}
}

So looking at this interface some important key decisions can be identified:
  • Configuration entries are accessed using String keys.
  • Configuration values are basically modelled as Strings
  • Typed access is supported as well using Class or TypeLiteral.
  • Configuration can be accessed key by key or by accessing the full properties map (getProperties). Hereby there is a constraint that the returned map may not contain all entries that would also be available when accessing them individually. Reason is that some configuration sources may not be able to list all the entries (aka being scannable). Refer also the SPI part, when the PropertySource interface is discussed, for further details.
  • The methods with, query define so called functional extension points, allowing additional functionality being added as operators/queries that can be applied on a configuration.
  • Finally, only defined in the API version depending on Java 8, are all the methods returning Optional values. These add support for the new Optional artifact introduced with Java 8. Similarly all the default methods were replaced in the Java 7 variant with corresponding abstract base implementations shipped with the reference implementation.
Instances of Configuration can be accessed from a ConfigurationProvider singleton:

Configuration config = ConfigurationProvider.getConfiguration();

Hereby  always a valid instance must be returned. It is not required that always the same instance is returned. Especially when running in a contextual environment, such as Java EE, each context may return different configurations, also reflecting the configuration resources deployed in the different Java EE artifacts. Similarly also OSGI based environments have their own classloader hierarchies, that may require isolation of configuration along the classloader bounderies.

Functional Extension Points

In the previous section we already mentioned the methods with and query. These take as argument a ConfigurationOperator or a ConfigurationQuery<T>, which are defined as follows:

@FunctionalInterface
public interface ConfigOperator {
    Configuration operate(Configuration config);
}

@FunctionalInterface
public interface ConfigQuery<T> {
    T query(Configuration config);
}

So basically ConfigOperator acts as a mapping that derives a Configuration from another Configuration, whereas a ConfigurationQuery<T> can return any kind of result. Both constructs allow adding functionality in multiple ways without having to deal with it on the Configuration interface, e.g. aspects like:

  • Filtering of configuration for specific use cases, e.g. recombining entries, or removing entries out of scope for a certain use case
  • Masking of entries or sections for security reasons
  • Creating typed objects based on configuration
  • Statistical details on a given configuration, e.g. the defined sections
  • Configuration validation and documentation
  • Conversion of configuration, e.g. to a JSON representation
  • and much more.

For running examples you may consider having a look at the tamaya-functions extension module, which already implements quite a few of aspects.


A minimalistic Example

To clarify things a bit more let's create a small example, which just uses the base mechanism provided with Tamaya's core implementation. Let's assume we build a small node, that a micro-service performing a simple compound interest rate calculation (I will omit the financial details how this is achieved here). Such a calculation basically is defined as:

We assume that the interest rate is something that is configured for this component, so in our component we simply add the following code:

BigDecimal interestRate = ConfigurationProvider.getConfiguration()
                .get("com.mycomp.ratecalculator.rate",
                     BigDecimal.class);

When using Java 8 we could also easily combine it with a default value:

BigDecimal interestRate = ConfigurationProvider.getConfiguration()
                .getOptional("com.mycomp.ratecalculator.rate",
                             BigDecimal.class)
                .orElse(BigDecimal.of(0.05d));

Given that we can easily implement our business logic, also using the JSR 354 type (see http://javamoney.org):

public class MyRateCalculator implements RateCalculator{

  private BigDecimal interestRate = ConfigurationProvider
                .getConfiguration()
                .getOptional("com.mycomp.ratecalculator.rate",
                             BigDecimal.
class)
                .orElse(BigDecimal.of(0.05d));

  public MonetaryAmount calcRate(MonetaryAmount amt, int periods){
   ...
  }

}

Now given you have built your logic in a similar way you have multiple benefits:
  • You can deploy your calculator as part of a Desktop application.
  • You can deploy your calculator as part of a Java EE application.
  • You can deploy your calculator in an OSGI container.
  • You can deploy your calculator easily as a standalone micro-service (with an appropriate API, e.g. REST).



Making Tamaya Support Optional

Basically you can even use the Tamaya optional module to integrate with Tamaya only as an optional dependency. This extension module is a very simple module, adding basically only one class to your dependency path, which

  • Ensures Tamaya API is on your classpath
  • Optionally checks if a Configuration is accessible from a given context.
  • Delegates Configuration request to Tamaya, or - if not availalbe - to a delegate passed from your logic, when creating the delegate:
import org.apache.tamaya.ext.optional.OptionalConfiguration;

private BigDecimal interestRate = 
              Optional.ofNullable(
                 OptionalConfiguration
.of(
                    (k) -> MyConfigMechanism.get(k)
                              
// String get(String key);
                 )
                .get("com.mycomp.ratecalculator.rate",
                             BigDecimal.
class))
                .orElse(BigDecimal.of(0.05d));

This allows you to support Tamya Configuration, but you can still use your own default configuration logic as default, if Tamaya is not loaded in your target environment.


What else?

From an API perspective there is not much more needed. The TypeLiteral class used before is the same, which is also known well from Java EE (CDI) and the only other artifact not mentioned is the ConfigException class. Of course, this functionality per se is very minimalistic, but it exactly does, what it is supposed to: it provides a minimalistic access API for configuration. And why we think this is so important? Here is why
  1. Everybody writing components typically writes some configuration logic, but everybody does it different: different formats, locations, key schemes, overridings etc. Also Apache Tamaya neither wants to define what you configure, or where your configuration is located and how it can be overridden. But we define a common API for accessing the configuration.
  2. Given that components from different teams can be more easily integrated within a project, but also within a concrete enterprise context, since all components refer to the same configuration mechanism.
  3. Even better, when using Tamaya overriding rules of configuration can be more or less ignored, since the mechanisms of Tamaya (I will present the corresponding SPI in the next blog here) already provide these mechanisms, so they can be adapted as needed.
  4. Similarly the formats used for configuration and also the fact that configuration may be locally stored in the file system or be remotely distributed is not of importance anymore.
This per se should render Apache Tamaya to very interesting and crucial piece of any application or module architecture. Additionally its SPI brings additional benefits, especially within bigger entprise contexts. We will look at the SPI  and the extensions in the next blog posts here. So stay tuned!

As always comments are welcome. If anybody out there is also thinking of contributing to the project please get in contact with us under dev@tamaya.incubator.apache.org.

And of course, help us spreading words writing tweets, blogs, adopting it, using it, loving it! 

Want to hear more?

Want to know more about Apache Tamaya? Visit our project site or even better join and see us at 






Saturday, November 29, 2014

Java Configuration API - Java 7 or Java 8 ?

Java 7 vs Java 8 for Apache Tamaya Config

Since the Apache mailing list is preventing me at all cost my more comprehensive addition to the current Tamaya discussion about Java 7 vs. Java 8, I now decided to write a blog.

I will shortly show here a small example how a modern configuration API written in Java 8 significantly differs from one written in Java 7 and why it is worth to do it in Java 8, so read ahead:


  1. the target is to build a modern API.
  2. there are many developers that do not use Spring or other technologies, where adoption is much faster.
  3. Wildfly as well as Weblogic 12.1.3 are Java 8 certified AFAIK (to be verified, since EE7 TCK does not run on Java 8...)
  4. Adoption within one year will be great.
  5. Java 8 is more than Lambdas and streams!
  6. Java 8 is the future! And we design for the future, we do not want to be one additional config framework.
  7. We can still provide a backport for Java 7. The core of Tamaya will be quite small. It should be possible to provide a backport within a couple of hours.

I give you an example here, e.g. let us start on the PropertyProvider:


In Java 8:


public interface PropertyProvider {


   Optional<String> get(String key);
   boolean containsKey(String key);
   Map<String, String> toMap();
   MetaInfo getMetaInfo();


   default boolean hasSameProperties(PropertyProvider provider) {...}
   default Set<String> keySet(){...}
   default ConfigChangeSet load(){...}
   default boolean isMutable(){...}
   default void apply(ConfigChangeSet change){...}
}


In Java 7:


With Java 7 this would be (to provide similar comfort, but on the cost of implementation dependencies and limited flexibility because of missing behavioural inheritance):


public interface PropertyProvider {


   String get(String key); // @throws ConfigException if no value
   String getOrDefault(String key, String value);


   boolean containsKey(String key);
   Map<String, String> toMap();
   MetaInfo getMetaInfo();


   default boolean hasSameProperties(PropertyProvider provider) {...}
   default Set<String> keySet(){...}
   default ConfigChangeSet load(){...}
   default boolean isMutable(){...}
   default void apply(ConfigChangeSet change){...}
}


protected abstract class AbstractPropertyProvider implements PropertyProvider {
   public boolean hasSameProperties(PropertyProvider provider) {...}
   public Set<String> keySet(){...}
   public ConfigChangeSet load(){...}
   public boolean isMutable(){...}
   public void apply(ConfigChangeSet change){...}
}


Example 2: Configuration


Looking at Configuration and the singleton access there things get even worse:


In Java 8:


public interface Configuration extends PropertyProvider{


   <T> Optional<T> get(String key, Class<T> type);
   void addPropertyChangeListener(PropertyChangeListener l);
   void removePropertyChangeListener(PropertyChangeListener l);


   default OptionalBoolean getBoolean(String key){... }
   default OptionalInt getInteger(String key){... }
   default OptionalLong getLong(String key){... }
   default OptionalDouble getDouble(String key){...  }
   default <T> Optional<T> getAdapted(String key, PropertyAdapter<T> adapter){... }
   default Set<String> getAreas(){... }
   default Set<String> getTransitiveAreas(){... }
   default Set<String> getAreas(final Predicate<String> predicate){... }
   default Set<String> getTransitiveAreas(Predicate<String> predicate){... }
   default boolean containsArea(String key){... }
   default Configuration with(ConfigOperator operator){... }
   default <T> T query(ConfigQuery<T> query){...}
   default String getVersion(){...}


   public static boolean isDefined(String name){...}
   public static <T> T of(String name, Class<T> template){...}
   public static Configuration of(String name){...}
   public static Configuration of(){...}
   public static <T> T of(Class<T> type){... }
   public static void configure(Object instance){... }
   public static String evaluateValue(String expression){... }
   public static String evaluateValue(Configuration config, String expression){...  }
   public static void addGlobalPropertyChangeListener(PropertyChangeListener listener){... }
   public static void removeGlobalPropertyChangeListener(PropertyChangeListener listener){...}
}


In Java 7:


public interface Configuration extends PropertyProvider{
   <T> T get(String key, Class<T> type); // throws ConfigException
   <T> T getOrDefault(String key, Class<T> type, T instance);
   void addPropertyChangeListener(PropertyChangeListener l);
   void removePropertyChangeListener(PropertyChangeListener l);


   boolean getBoolean(String key){... } // throws ConfigException
   boolean getBooleanOrDefault(String key, boolean defaultVal){... }
   int getInteger(String key){... } // throws ConfigException
   int getIntegerOrDefault(String key, int defaultVal){... } 
// throws ConfigException
   long getLong(String key){... } // throws ConfigException
   long getLongOrDefault(String key, long defaultVal);
   double getDouble(String key){... } // throws ConfigException
   double getDoubleOrDefault(String key, double defaultVal);
   <T> getAdapted(String key, PropertyAdapter<T> adapter){... } 
// throws ConfigException
   <T> getAdaptedOrDefault(String key, PropertyAdapter<T> adapter, T defaultVal){... } // throws ConfigException
   Set<String> getAreas(){... }
   Set<String> getTransitiveAreas(){... }
   Set<String> getAreas(final Predicate<String> predicate){... }
// Duplicate predicate class, or introduce additional interface
   Set<String> getTransitiveAreas(Predicate<String> predicate){... } 
// Duplicate predicate class, or introduce additional interface
   boolean containsArea(String key){... }
   Configuration with(ConfigOperator operator){... }
   <T> T query(ConfigQuery<T> query){...}
   String getVersion(){...}
}


public final class ConfigManager{
  private ConfigManager(){}


   public static boolean isDefined(String name){...}
   public static <T> T of(String name, Class<T> template){...}
   public static Configuration of(String name){...}
   public static Configuration of(){...}
   public static <T> T of(Class<T> type){... }
   public static void configure(Object instance){... }
   public static String evaluateValue(String expression){... }
   public static String evaluateValue(Configuration config, String expression)
{... }
   public static void addGlobalPropertyChangeListener(
PropertyChangeListener listener){... }
   public static void removeGlobalPropertyChangeListener(
PropertyChangeListener listener){...}
}


protected abstract class AbstractConfiguration extends AbstractPropertyProvider implements Configuration{
   boolean getBoolean(String key){... } // throws ConfigException
   boolean getBooleanOrDefault(String key, boolean defaultVal){... }
   int getInteger(String key){... } // throws ConfigException
   int getIntegerOrDefault(String key, int defaultVal){... } // throws ConfigException if not found
   long getLong(String key){... } // throws ConfigException
   long getLongOrDefault(String key, long defaultVal);
   double getDouble(String key){... } // throws ConfigExceptio
   double getDoubleOrDefault(String key, double defaultVal);
   <T> getAdapted(String key, PropertyAdapter<T> adapter){... } 
// throws ConfigException
   <T> getAdaptedOrDefault(String key, PropertyAdapter<T> adapter, T defaultVal)  
{...} // throws ConfigException
   public Set<String> getAreas(){... }
   public Set<String> getTransitiveAreas(){... }
   public Set<String> getAreas(final Predicate<String> predicate){... }
   public Set<String> getTransitiveAreas(Predicate<String> predicate){... }
   public boolean containsArea(String key){... }
   public Configuration with(ConfigOperator operator){... }
   public <T> T query(ConfigQuery<T> query){...}
   public String getVersion(){...}
}


And even when looking from the client side:


Java 8:


String value = Configuration.of().get("a.b.c").orElse(MyClass::calculateDefault);


Java 7:


String value = ConfigurationManager.getConfiguration().getOrDefault("a.b.c", null);
if(value==null){
 value = calculateDefault();
}


So obviously the strength of Java 8 are far beyond Streams and Lambdas:
  • The API footprint for clients overall is half the size.
  • The implementations of APIs/SPIs is much more easier and does not introduce implementation dependencies on abstract classes
  • Users must known much less artefacts to use the API!
  • It is much more flexible and extendable (eg method references)
  • ...

The above case with the deferred calculation is additionally a simple but common use case for Lambda usage. Considering implementation use cases like filtering and mapping/combining of configuration to other things Streams are incredibly useful as well. Similarly we would loose for sure great support from some of the most communities like SouJava and LJC.

So I hope I have now convinced really everybody that it is NOT worth to stick on Java 7, just because we would have faster adoption ;-) ! Do the API right for Java 8 and if enough people ask for do a backport. With the current relative small size of Tamaya a backport should be doable in about 3-4 hours ;)