-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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.
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:
- Note the hierarchy of the main 3 entities:
- We declare a Configuration Class (which inherits
ConfEager
)- A configuration class contains Configuration Property fields (which inherit
ConfProperty
)
- A configuration class contains Configuration Property fields (which inherit
- Then we use a Configuration Source (which inherits
ConfEagerSource
) to bind and populate the configuration class instance.
- We declare a Configuration Class (which inherits
- Property names may be overridden (e.g. field
enableLogs
may be read from property namedenable_logs
), More on that in ConfEagerProperty section. - Properties are required by default, but may be optional. More on that in ConfEagerProperty section.
- Many other out-of-the-box property types are available, but also may easily be created. More on that in ConfEagerProperty section.
- A few out-of-the-box configuration sources (other than SystemProperties) are available, but also may easily be created. More on that in ConfEagerSource section.
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.
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.
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.
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.
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.
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
.
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 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);
}
}
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.
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.
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.
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.
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.