The whole API is one factory and one interface. There is no builder, no
annotation processing, and nothing to configure in code - all of that lives in
the rwconfig file.
import net.rabbitware.config.*;
Config config = ConfigFactory.create(args); // command line arguments usable
Config config = ConfigFactory.create(); // notUse the args parameter if your rwconfig declares a commandLineArguments
source; it is an error to declare that source and then call a version of
create() that does not provide command line arguments.
Everything happens here: the file is read, the sources are loaded, every value
is parsed and checked against its type and allowed values, and any property
without a value is reported. If create returns, the configuration is complete
and valid.
Create it once at startup and hold on to it. The result is effectively
immutable, so it can be shared across threads freely. A Config is a snapshot
and stays one: if configuration changes, build a new one.
Speaking of changes, rwConfig can tell you when your config has changed if you
set the enableChangeDetection flag to true:
Config config = ConfigFactory.create(true, args);It is up to you to reload your config when you decide that has become worth doing - see Noticing that a source has changed.
There is also an optional name parameter, which is what appears in log
messages and in change events. It is worth setting when an application builds
more than one Config object. If you don't set your own name, the default name
is a random UUID:
Config config = ConfigFactory.create("app config", true, args);One method per type, each returning a primitive or a List:
boolean debug = config.getBoolean("debugMode");
int port = config.getInt("port");
long maxSize = config.getLong("maxUploadSize");
double rate = config.getDouble("samplingRate");
String name = config.getString("appName");
List<Boolean> flags = config.getBooleanList("featureFlags");
List<Integer> ports = config.getIntList("workerPorts");
List<Long> ids = config.getLongList("accountIds");
List<Double> weights = config.getDoubleList("weights");
List<String> hosts = config.getStringList("hosts");No Optional, no default parameter, no cast. The type and the default were
settled in the rwconfig file, and the value was validated at startup.
Every method has a short alias, if you prefer terse code at the call site:
| long | short | long | short | |
|---|---|---|---|---|
getBoolean |
getb |
getBooleanList |
getbl |
|
getInt |
geti |
getIntList |
getil |
|
getLong |
getl |
getLongList |
getll |
|
getDouble |
getd |
getDoubleList |
getdl |
|
getString |
gets |
getStringList |
getsl |
Returned lists are unmodifiable.
Reads are fast enough not to think about - a read is a single hash lookup and a field access, and returning primitives avoids boxing. There is no reason to copy values into fields "for speed", though there are plenty of good reasons to copy them into well-named ones.
boolean exists = config.has("port"); // is it declared?
Config.PropertyType type = config.getType("port"); // INT
Set<String> names = config.getPropertyNames(); // every declared namegetPropertyNames() returns an unmodifiable, alphabetically sorted set. It
contains only your application's properties - library settings
itself are never included.
PropertyType is an enum: BOOLEAN, INT, LONG, DOUBLE, STRING,
BOOLEAN_LIST, INT_LIST, LONG_LIST, DOUBLE_LIST, STRING_LIST. Each has
a name field holding the spelling used in the rwconfig file (intList and
so on).
These are the run-time types, which is not quite the list of types you can
write in the file. duration, size, timestamp, and their list forms are parsed into
longs. For example, a property declared as duration timeout is read with
var duration = config.getLong("duration").
Iterating everything, which is what the example application does:
for (String name : config.getPropertyNames()) {
System.out.println(name + " (" + config.getType(name).name + ")");
}Change detection is off unless you ask for it, because it starts a thread:
Config config = ConfigFactory.create(true, args); // `true` turns it onThen register a listener. It is called when a watched source changes - not when a particular property changes, since rwConfig does not re-read the source for you:
config.addChangeListener("reloader", new Config.ChangeListener() {
@Override
public void onChange(Config.ChangeEvent event) {
logger.info("config source `{}` changed at {}", event.source(), event.timestamp());
Config replacement = ConfigFactory.create(true, args);
Config.Instance.replace(replacement); // discards the old one for you
}
@Override
public void onError(Config.ErrorEvent event) {
logger.warn("could not check `{}` for changes", event.source(), event.exception());
}
});addChangeListener(sourceName, listenerName, listener) narrows a listener to a
single source, and removeChangeListener takes it off again.
Asking for a listener on a Config built without change detection is an error
rather than a listener that can never fire.
A source can be watched only if its location can be:
| source | watched |
|---|---|
a file: location |
yes, through the filesystem's watch service |
a jar:file: location |
yes - the jar itself is watched |
an http: or https: location |
yes - a HEAD request comparing Last-Modified, if the server sends it |
a classpath: location |
no - it cannot change while the JVM runs |
a jdbc.plugin source |
only if it declares a changeQuery |
environmentVariables, systemProperties, commandLineArguments |
no - fixed at startup |
Sources that cannot be watched are simply never reported as changed. Nothing fails.
Sources are checked every five seconds by default. To change it, set the
library setting in the rwconfig file:
rwc.changeDetectionPollingInterval = 30000
The value is milliseconds and must be positive. Setting it without asking for change detection logs a warning, since nothing would use it.
Detection is not instant, and cannot be: on top of this interval, a filesystem watch has a latency of its own - on macOS the JDK polls the filesystem, which adds a second or so.
A Config with change detection running holds a polling thread, and the library
holds the Config. Neither is collected while that is true, so a config you
have replaced has to be told it is finished:
config.discard();After that it stops watching and stops firing events. Its values stay readable,
so anything still holding it keeps working - discard retires the watching, not
the configuration.
Config.Instance.replace(...) calls discard() on the instance it swaps out,
so the common case is handled for you.
Forgetting
discardleaks. AConfigbuilt with change detection and never discarded keeps its thread and its memory for the life of the process. If you never replace your configuration, you never need to call it.
All of them extend ConfigException, which extends RuntimeException. None
are checked, on the grounds that they are bugs to fix rather than conditions to
recover from - there is nothing sensible to do at runtime about asking for a
property that does not exist.
| exception | when |
|---|---|
PropertyNotFoundException |
the name is not declared in the rwconfig file |
IncorrectTypeException |
the property exists but is a different type |
ConfigException |
anything wrong during create |
config.getInt("prot"); // PropertyNotFoundException - typo
config.getInt("appName"); // IncorrectTypeException - it is a string
config.getIntList("port"); // IncorrectTypeException - it is a scalarThe first two can only happen because the Java code and the rwconfig file
disagree, which is why they are unchecked: the fix is to change one of them,
not to catch anything.
ConfigException from create is different - it means the configuration is
wrong, not the code, and its message names the property or source at fault. See
Error messages for a guide to them.
public class Main {
public static void main(String[] args) {
Config config = ConfigFactory.create(args);
var server = new Server(
config.getInt("port"),
config.getInt("workerThreads"),
config.getStringList("allowedOrigins")
);
if (config.getBoolean("debugMode")) {
server.enableDebugEndpoints();
}
server.start();
}
}Read what you need where you need it and pass values on as ordinary arguments.