Skip to content

Repository files navigation

rwConfig

Maven Central Java Tests Runtime deps Status

Catching a typo before the app runs

What is rwConfig?

rwConfig is a simple, lightweight library that provides a unified and fast interface for reading configuration information from a wide range of config sources.

What distinguishes rwConfig from the pack is its design philosophy: define what you need and what it should look like ahead of time, and validate that you have what you need at coding/compile time (with the Maven plugin and/or the VS Code extension) and on startup. This means that you can rest easier knowing that you won't be surprised by configuration issues when it's too late to do something about it.

In A Nutshell

Declare what properties your app needs and (if necessary) where to find them--all in one file. For example:

int port = 8000

port is an int and its value defaults to 8000.

Suppose only some values are valid for the property port. You can define what values are allowed on the same line:

int[80, 1024..65535] port = 8000

Now the value of port must be either 80 or between 1024 and 65535. If you attempt to set a value that is not allowed then the app will not start. This is by design.

Next, add a property that whose value must be supplied rather than defaulted, and a couple of lines to say where values may come from:

int[80, 1024..65535] port = 8000
DBPassword

rwc.sources = environment
rwc.environment.type = environmentVariables

DBPassword deliberately has no value here--it has to come from a config source, which in this case means the environment variable DB_PASSWORD. Miss it and the application refuses to start, rather than running with a blank password. Sources can equally be the command line, a file you don't commit, a directory, or a database; you list the ones you want in rwc.sources, best first.

Read it with no ceremony:

int port = config.getInt("port");

No Optional. No cast. No default value at the call site. No exception to handle. The type, the default, and the allowed values were all settled before your first line of code ran.

And when the configuration is wrong, you hear about it as you write your code or at startup--not at 3am:

value is not allowed for property `port` (in source `args`): 500
property `DBPassword` is not set by any config source, and has no default value defined in the `rwconfig` file

How Fast Is It?

reading an int by name
rwConfig 3.3 ns
avaje-config 4.1 ns
java.util.Properties 7.3 ns
SmallRye Config 10.2 ns
Typesafe Config 16.5 ns
Spring Environment 35.1 ns
Commons Configuration 44.1 ns
Owner 248.2 ns

Fastest of the libraries that look a value up by name. Some libraries are faster still by not looking anything up at read time--the benchmark README is honest about which, and why that isn't the same question.

For arbitrary reads it's already faster than the other popular config libraries tested. The reason is this library validates everything up front; it trades up-front cost for a reduced cost at retrieval time. And that cost is spent only once--not on every read of a property. If you're creating a long-lived app that may ultimately do a lot of reads, this definitely doesn't hurt.

As a bonus, read times are consistent no matter what you're reading or where you're getting it from.

Don't take my word for it. Run the benchmarks yourself! Or just look at the takeaway.

Features

  • Information about every property, declared in one file - name, type, allowed values, default value, and where to look for it. The rwconfig file is a single source of truth, and doubles as your configuration documentation.

  • Errors at startup, not at 3am - missing values, unparsable values, and values outside their allowed range. Wrong types on the Java side and requests for properties that no declaration mentions are caught as you write your code.

  • All of your config sources in one place - get all of your config info through one interface with built-in support for command line arguments, environment variables, system properties, .properties files, .env files, Kubernetes secrets and config maps, and directories. Plus YAML, JSON, XML, HOCON, and databases via the bundled plugins. Document-based sources can load from the file system, a jar file, http(s), or the classpath.

    Need something else? Create your own source type with a small plugin API!

  • Layered sources, with precedence you declare - Want an environment variable to trump the same property in your config file? No problem! You're in control.

  • VS Code and Maven plugins - with these tools, even more configuration errors are caught as you code or at compile time. Most of the rest are caught at startup.

  • Fast, uniform reads - a read is one HashMap lookup of an already-parsed value: ~2.3 ns whether it's an int, a String, or a list.

  • No Optionals, no fluent chains - config.getInt("port") returns an int, because types, defaults, and validation were settled at startup. On the API side it's a similar API to java.util.Map, with types tacked on.

  • Types and lists - boolean, int, long, double, string, size, duration, and timestamp. Also booleanList, intList, longList, doubleList, stringList, sizeList, durationList, and timestampList.

  • Nearly dependency-free - the Java Base module and slf4j, which itself only requires Java Base.

  • Secure by omission - values your app never declared are not added to the Config object, even when the config source contains them.

  • Change detection, opt in - ask for it and rwConfig watches the sources that can be watched, then tells you when one changes. It notifies rather than reloads: the Config you hold stays the snapshot it was, and you decide when to build a new one.

Quick Start

1. Jar Installation (via Maven)

Add this to your project's pom.xml:

<dependencies>
    ...
    <dependency>
        <groupId>net.rabbitware.config</groupId>
        <artifactId>config</artifactId>
        <version>0.2.0</version>
    </dependency>
</dependencies>

2. Create a File Called rwconfig

Place this file in the working directory where your app will run, or in the resources folder of your Maven project. You can optionally define a custom path to this file by setting the environment variable RW_CONFIG_PATH, setting the Java system property rw.config.path, or adding the command line argument rw.config.path=/path/to/rwconfig.

A file of nothing but declarations is valid - every property then takes the default declared for it:

# a complete rwconfig file
int[80, 1024..65535] port = 8000

Declare config sources when you want values to come from somewhere else. They are listed highest precedence first, and each one says what type it is:

# sample rwconfig file
rwc.sources = args, system, environment
rwc.args.type = commandLineArguments
rwc.system.type = systemProperties
rwc.environment.type = environmentVariables

# example properties; your properties can be anything you want
int[80, 1024..65535] port = 8000
DBPassword

Two files in the example project sit at either end of the scale: a minimal rwconfig that is close to the smallest one worth writing, and a heavily commented rwconfig that exercises nearly every feature. The format is documented in full in The rwconfig File.

3. Basic Usage

Creating the Config Object

If you want to allow configuration properties to be overridden on the command line:

import net.rabbitware.config.*;

...

Config config = ConfigFactory.create(commandLineArguments);

If you don't want to allow configuration properties to be overridden on the command line, use the below code to create the Config object instead.

Note that the rwconfig example above defines the command-line arguments as a config source, so for that example to work you must use the above version of create:

import net.rabbitware.config.*;

...

Config config = ConfigFactory.create();

Retrieving a Value

Retrieving a property is painless:

// get the value of the property `port`
int port = config.getInt("port");

// get the type of the property `port`
Config.PropertyType propertyType = config.getType("port");

System.out.println("type of `port`: " + propertyType.name);
System.out.println("value of `port`: " + port);

There's no need to deal with Optionals here because the library handles property declarations, default values, and value types at startup. This means:

  • default values are declared in the rwconfig file--not in the code
  • missing or incorrect property values are caught at startup
  • expecting an incorrect property type in Java code is always an error
  • requesting an unknown property in Java code is always an error

These errors are treated as unchecked exceptions because (a) they are avoidable at coding time, and (b) encountering them at runtime guarantees that the code is not working as the developer intended from that point on.

3½. Install the Helpers

Maven Plugin

Why wait until startup to catch any issues? The Maven plugin catches those issues --and even more--at compile time. Using the plugin as an additional safeguard is strongly recommended.

<build>
  <plugins>
    <plugin>
        <groupId>net.rabbitware.config</groupId>
        <artifactId>rwconfig-maven-plugin</artifactId>
        <version>0.2.0</version>
        <executions>
            <execution><goals><goal>check</goal></goals></execution>
        </executions>
    </plugin>
  </plugins>
</build>

VS Code Extension

The VS Code extension catches the same issues as the Maven plugin even earlier (whenever you save), and highlights them directly in the editor.

To install it, Search for "rwConfig" in the VS Code Extensions Marketplace, or download it directly from the "Releases" section of this project page. If you have VS Code added to your path, you can also type this:

code --install-extension rabbitware.rwconfig

Should I Rip Out My Old Config System And Use It?

The short answer is "probably not". The more accurate answer is "it depends, but probably not". It's generally not worth the effort to make that kind of change in an existing project, frameworks normally have their own "blessed" config systems and it's best not to swim against the tide, and if you recommend an immature library for a new production-level project people will correctly think you're crazy. Maybe just play around with it on your home projects and give me some feedback. See Choosing a Configuration Library.

Does It Matter How Fast A Config System Is?

Not really. But some people like numbers. What really matters is the design philosophy. It's better to define up-front what your config properties should look like and how they should be loaded. And it's much better to test if your config is up to snuff as you code and at app start (when you're in the office), than to find out at 3am.

Project Goals

  • a simple interface with virtually no learning curve
  • lightweight
  • high-speed retrieval of property values
  • fail-fast behavior - detect errors at startup, at compile time, and as you code
  • sensible security for secrets (and possible secrets)

Project Non-goals

  • a "one-size fits all" approach
    • This is how I prefer to configure my apps. I'm not going to make the API or configuration more complex to cover use cases that would be of marginal value to me.
  • a way to set properties within the app
    • That would require the Config object to be mutable, and that creates many "what if" scenarios involving thread synchronization, guaranteeing atomic behavior to clients of the API, etc.
    • The closest to supporting this that I come is notifying clients of the API when a configuration source has changed. The client can then choose to discard the old Config object and create a new one. Yes, that's not very close at all.
  • support for an in-memory hierarchical data structure
    • hierarchical data sources (like YAML, JSON, and XML) are "flattened" during ingestion
      • reads are far faster this way
      • there is effectively no difference to clients of the API when retrieving a value by its key
    • see the "Design Choices" section for more details
  • anything that would greatly increase size, or add dependencies to this project outside of Java Base

Changelog

What changed in each release is in CHANGELOG.md.

TODOs

  • APIs for other languages

Design Choices and Miscellaneous Rants

  • effectively immutable and atomic configuration
  • "flat" data structure
    • WARNING: screed follows:

      I have no idea why many config systems work with a "node/graph" structure instead of a flattened structure. It is more efficient overall to "flatten" graph structures (like JSON files) than it is to do the reverse (convert structures like .properties files to nodes).

      The absolute worst-case performance of retrieving values from a flat HashMap is O(log n), and it is often O(1). That worst-case performance can get a bit better if you make n smaller by sorting different property types into their own buckets--something you can't do with the "node" structure.

      On the other hand, the worst-case performance of a node structure is O(log n1 + log n2... + log nx) and the best case is O(x), where x is the number of levels. This doesn't cover any cost of parsing the keys while doing the node navigation. Any edge-case advantage of keeping the hierarchy intact is (IMO) smaller than the performance gains from a flattened data structure. Plus, node navigation makes the code more complicated. Bad!

      Google's Gmail has done this for decades. As far as everything but the last bit of UI is concerned, your email folder structure is just a bunch of flat, opaque tags that happen to have forward slashes in them. The end result is virtually identical.

      Hierarchies and taxonomies are for human consumption. Computers don't need to care about them. If you try to force computers to work the way that humans do, you will get unnecessarily weaker performance.

  • fail-fast behavior
  • no dependencies
  • compatibility with the most popular configuration formats
  • simple, easy to learn rwconfig file syntax
  • declarative rather than code-based configuration
    • avoid Optionals and long method chains in the code by declaring types and default values in the rwconfig file
      • the use of Optional and orElse encourages the "magic number" anti-pattern
    • avoid the need to recompile just to tweak a config source or change a default value
    • no need to search through the code to find out where a value came from

Documentation

  • Getting Started - what the library is for, and a working configuration in a few minutes
  • The rwconfig File - the file format in full: types, allowed values, ranges, escapes, and splitting long lines
  • Config Sources - the built-in source types, how precedence works, and how to keep secrets out of shared files
  • The Java API - everything on the Config object
  • Writing a Plugin - adding a config source type of your own
  • Error Messages - what each startup error means and how to fix it
  • Choosing a Configuration Library - how rwConfig compares with the alternatives, and when to use one of them instead
  • Plugins - the YAML, JSON, XML, HOCON, JDBC, and prefix plugins that ship with the project

There is also a minimal rwconfig and a heavily commented sample rwconfig file that exercises nearly every feature, plus a runnable example application that loads it.

About

rwConfig is a simple, lightweight library that provides a unified interface for reading configuration information from a wide range of sources.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages