Skip to content
Aviv C edited this page Jun 27, 2017 · 4 revisions

ConfEager is a configuration library for Java, designed to be as simple and lightweight as possible, to allow easy integration, and to maximize dynamic capabilities.

ConfEager data is eagerly loaded, hence it's name, making your configuration super fast and up-to-date at runtime, since everything is loaded on startup, and is stored in memory.

ConfEager library is designed to be easily extended and customized to enable working with whatever technologies you like to use to store configuration. It provides very little out-of-the-box library support, thus it has no dependencies, and it adds tiny footprint to your project.

Getting Started

The Main Entities

ConfEager library provides 3 main entities:

  • Configuration Class (ConfEager) which declare the properties we want to read into memory.
  • Configuration Property (ConfEagerProperty) which declares the actual name and type of each property.
  • Configuration Source (ConfEagerSource) which connects to a data source and populates the data into the configuration class properties.

To show this in action, let's start with super simple example in which we would like to read and validate 2 configuration properties from the process System Properties.

First, let's define the properties we would like to read by declaring our Configuration Class, which contains the two Configuration Properties:

public class LocalConfiguration extends ConfEager {
    public final ConfEagerPropertyBoolean enableLogs = new ConfEagerPropertyBoolean();
    public final ConfEagerPropertyString logDirectory = new ConfEagerPropertyString();
}

This declares the properties we would like to read, their type and their property names. Everything here is fully customizable, and we've used boolean and String out-of-the-box properties (more on that later). Now in order to load them from the process System Properties, to that end, we use the out-of-the-box System Properties configuration source:

LocalConfiguration localConfiguration = ConfEagerSourceSystemProperties.INSTANCE.bind(LocalConfiguration.class);

This will look for properties named enableLogs and logDirectory in the System Properties, if one of them is not found, it will throw a ConfEagerPropertiesMissingException. Then it maps the extracted values into types boolean and String, if one of them fails to parse due to illegal value, it will throw a ConfEagerIllegalPropertyValueException. Then it binds the localConfiguration instance to the source (in this case - ConfEagerSourceSystemProperties instance). This means that if the source changes, the in-memory values of localConfiguration properties gets immediately updated.

Then in order to read the configuration:

boolean enableLogs = localConfiguration.enableLogs.get();
String logDirectory = localConfiguration.logDirectory.get();

Few notes before moving on:

Customization

As mentioned above, by default, ConfEager provides ConfEagerSource implementations which read system properties and environment variables. In order to read configuration from MySQL, for example, we may use an implementation like this one:

public class ConfEagerMySQLSource extends ConfEagerSource {

    private final Map<String, String> data;

    public ConfEagerMySQLSource(String dbURL, String username, String password) {
        data = new HashMap<>();
        try{
            Class.forName("com.mysql.jdbc.Driver");
            Connection connection = DriverManager.getConnection(dbURL, username, password);
            Statement statement = connection.createStatement();
            String sql;
            sql = "SELECT * FROM `configuration`";
            ResultSet rs = statement.executeQuery(sql);
            while (rs.next()) {
                data.put(rs.getString("key"), rs.getString("value"));
            }
            rs.close();
            statement.close();
            connection.close();
        } catch(SQLException se){
            e.printStackTrace();
        } catch(Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public String getValueOrNull(String propertyName) {
        return data.get(propertyName);
    }

}

Then in order to use it we can do:

ConfEagerMySQLSource source = new ConfEagerMySQLSource("mysql://example.com", "username", "password");
LocalConfiguration localConfiguration = source.bind(LocalConfiguration.class);

Few notes before moving on:

  • The same way, we may use any other source, such as Consul KV, ZooKeeper, or any other local or remote source we like.
  • This is just an example implementation, it may be implemented using an async client, it may move the initialization phase from the constructor to an init method, or it may have any other form, as long as we fully initialize it before calling bind method.
  • Note that the url, username and password needed to connect, may be taken from some local configuration source, e.g. system properties, local configuration file, etc. We just need to define another configuration class with some local configuration source, initialize it, and then use the resulted configuration to connect to the remove configuration source. This actually is considered a good practice, in which we separate our local configuration (i.e. paths on current machine, machine identity, etc...) from cluster configuration (i.e. business logic parameters, or other configuration that relates to other nodes in the cluster).
  • Note that this implementation does not support updates. This means that if the data changes on the remote SQL tables, it will not get updated. To support that we need to choose update mechanism (i.e. periodic read, pub/sub, etc...), and then notify each update by calling the inherited notifyUpdate() method. More on that in ConfEagerSource section.

In More Detail

Configuration Class

A ConfEager configuration class is denoted by the class ConfEager. The purpose of this class is to declare the properties we want to read.

This is done by extending ConfEager, and adding property class fields. Preferably, property fields should be declared public final so that they will be read-only, and thus no getters will be needed and no outside modification may be possible.

Field Filter

When binding a configuration instance to a configuration source, the configuration instance is scanned for all fields which inherit ConfEagerProperty and are not static to be initialized. The not-static filter may be changed, by overriding the defaultFieldFilter method, which receive a field and either approve or disapprove it.

Environment

Additionally, all the configuration properties may receive a textual prefix to their property name. This allows for the support of environments, i.e. we can make a configuration class add staging/ to all property names, thus controlling which environment should be loaded. This is done by overriding the defaultEnvironment method and returning the prefix string which by default is an empty string.

Configuration Property

A ConfEager configuration property is denoted by the class ConfEagerProperty The purpose of this class is to declare the actual name and type of each property.

Required vs. Optional Properties

By default, a configuration property is required, and if missing from the source, a ConfEagerPropertiesMissingException will be thrown. To denote a property as optional, we need to pass a default values to it's constructor using the defaultValue method:

public class Configuration extends ConfEager {
    public final ConfEagerPropertyBoolean enableLogs = new ConfEagerPropertyBoolean(defaultValue(true));
}

Note that the defaultValue method is inherited from ConfEager. At this point, if enableLogs property is missing from the source, it will receive the value of true.

Property Keys

By default, a configuration property value is looked up in the source by the name of the property field. For example, if we declare:

public class Configuration extends ConfEager {
    public final ConfEagerPropertyBoolean enableLogs = new ConfEagerPropertyBoolean();
}

then the source data is scanned to contain a property named enableLogs. To override it, we need to pass a different property name to the property constructor using the propertyName method:

public class Configuration extends ConfEager {
    public final ConfEagerPropertyBoolean enableLogs = new ConfEagerPropertyBoolean(propertyName("ENABLE_LOGS"));
}

Note that the propertyName method is inherited from ConfEager. At this point, the source will be scanned for a property named ENABLE_LOGS, and it's values will be populated and bound to the enableLogs property field.

Property Types

Property values are extracted from the source as strings. The responsibility of parsing this string to the actual property value lies upon the ConfEagerPropery. For instance, if we have a boolean property, then valid values of the extracted string may be "true" and "false", any other case should not be acceptable. This is done through the ConfEagerPropery T map(String value) method. This means that we can actually create a property class for any type we want. For example, easily implement a java.net.URL property:

public class ConfEagerPropertyURL extends ConfEagerProperty<URL> {

    @Override
    protected URL map(String value) throws Exception {
        return new URL(value);
    }

}

Now when we do:

public class Configuration extends ConfEager {
    public final ConfEagerPropertyURL someURL = new ConfEagerPropertyURL();
}

and then bind this configuration to a source, we actually validate the someURL property to be a valid URL, then we can read it's value using configuration.someURL.get();.

This way we may implement any property type we want, and parse them in any way we want.

Note that in order to support optional properties and explicit property keys, we need to inherit all of ConfEagerProperty constructors:

public class ConfEagerPropertyURL extends ConfEagerProperty<URL> {

    public ConfEagerPropertyURL(ConfEager.DefaultValue<URL> defaultValue, ConfEager.PropertyName propertyName) {
        super(defaultValue, propertyName);
    }

    public ConfEagerPropertyURL(ConfEager.PropertyName propertyName, ConfEager.DefaultValue<URL> defaultValue) {
        super(propertyName, defaultValue);
    }

    public ConfEagerPropertyURL(ConfEager.PropertyName propertyName) {
        super(propertyName);
    }

    public ConfEagerPropertyURL(ConfEager.DefaultValue<URL> defaultValue) {
        super(defaultValue);
    }

    public ConfEagerPropertyURL() {}

    @Override
    protected URL map(String value) throws Exception {
        return new URL(value);
    }

}
Out-of-the-Box Property Types

ConfEager provides out-of-the-box property types for all Java primitives (boolean, double, float, int, long) and their arrays (boolean[], double[], float[], int[], long[]), String and String[] and for any enum. All of those are available under ConfEagerProperty*.

To use enums we can do:

public class Configuration extends ConfEager {
    public final ConfEagerPropertyEnum<Example> example = new ConfEagerPropertyEnum<>(Example.class, false);
    public enum Example {
        VALUE1, VALUE2
    }
}

Note the false value on the constructor call which denotes whether or not to enforce case sensitivity on the extracted value.

Configuration Source

A ConfEager configuration source is denoted by the class ConfEagerSource. The purpose of this class is to connect to a data source and to populate the data into the configuration class properties.

To implement a custom configuration source, one must implement a single method String getValueOrNull(String propertyName):

public class ConfEagerCustomSource extends ConfEagerSource {

    @Override
    public String getValueOrNull(String propertyName) {
        return "value";
    }

}

This is a dummy source which return the value "value" for every property name. This way, it's very simple to connect to any local or remove data source and retrieve all it's values. Then, when getValueOrNull is called, return the matching value, or null if no value is found. A MySQL source implementation example can be found above.

Updating Data

Configuration sources support live updating of data, and this is up for the implementer to manage. Upon the updating of data, the inherited notifyUpdate() method must be called in order to propagate the updates to all bound configuration instances.

For example, if we want to use a pub/sub client which provides a registerForUpdates() method, we can do:

public class ConfEagerCustomSource extends ConfEagerSource {

    private Map<String, String> data;

    private Client client;

    public ConfEagerCustomSource() {
        client = ...
        data = client.getData();
        client.registerForUpdates(() -> {
            data = client.getData();
            this.notifyUpdate();
        });
    }

    @Override
    public String getValueOrNull(String propertyName) {
        return data.get(propertyName);
    }

}

Note how we must first update the data, and then call the notifyUpdate() method.

Similarly, we can implement a scheduled task to be executed at a constant interval, extract all data and the notify update.

Binding

Once we've initialized all the data in the source, we may use the source to populate and bind any configuration class. This is done by calling either <T extends ConfEager> T bind(Class<T> confEagerObjectClass) or void bind(ConfEager confEagerObject) methods. For example:

public class LocalConfiguration extends ConfEager {
    public final ConfEagerPropertyBoolean enableLogs = new ConfEagerPropertyBoolean();
}

and then either:

LocalConfiguration localConfiguration = source.bind(LocalConfiguration.class);

or:

LocalConfiguration localConfiguration = new LocalConfiguration();
source.bind(localConfiguration);

The first method is the recommended one, but it may be used only in case where the ConfEager class has an empty constructor. In any other case, we must manually initialize it, and then use the second method.

Out-of-the-Box Configuration Sources

ConfEager currently provides 3 out-of-the-box configuration sources:

  • ConfEagerSourceSystemProperties.INSTANCE which read data from the process System Properties.
  • ConfEagerSourceEnvironmentVariables.INSTANCE which read data from the Environment Variables.
  • ConfEagerSourceCombinator which receive other sources, and chain them one after the other for each property, until it is found in either of them.