Configuration library based on annotation processing
The Java library with the goal of minimizing the code required to handle application configuration.
The inspiring idea for the project comes from OWNER. OWNER is a nice Java library for the same purpose, but it’s not factually maintained anymore, and it’s not really support “new” language features from Java 8+.
So, this project is providing library with similar with OWNER API, but
Core is plain Java 11 without any external dependencies
Uses no reflection or runtime bytecode generation; generates plain Java source code.
Small (< 100KB) & lightweight core runtime part
Ready to use with OSGi
Supports multiple configuration sources: files, classpath, URLs, environment variables, system properties, META-INF/MANIFEST.MF, Apache ZooKeeper
Supports files in Multiple formats:
Supports multiple loading strategies (configuration sources fallback/merging)
Expandable with custom source loaders
Powerful type conversions: collections, maps, enums, etc.
Parameterized type converters
Expandable with custom type converters
Special support for java.util.Optional
, java.time.*
, byte-size settings (e.g. 10Mb
), Jasypt password-based encryption
Caching
Seamless integration with DI containers
Thread-safe
Reloading, Periodical auto reloading, Reload event listeners
Download: Maven Central Repository.
Download: GitHub Packages.
In order to use the library in a project, it’s need to add the dependency to the pom.xml:
<dependency>
<groupId>net.cactusthorn.config</groupId>
<artifactId>config-core</artifactId>
<version>0.81</version>
</dependency>
It’s also need to include the compiler used to convert annotated “source”-interfaces into the code:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>net.cactusthorn.config</groupId>
<artifactId>config-compiler</artifactId>
<version>0.81</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
FYI: With this configuration, Maven will output the generated code into target/generated-sources/annotations
.
Same with Gradle:
api 'net.cactusthorn.config:config-core:0.81'
annotationProcessor 'net.cactusthorn.config:config-compiler:0.81'
To access properties it’s need to define a convenient Java interface, e.g. :
@Config
@Prefix("app")
public interface MyConfig {
@Default("unknown")
String val();
@Key("number")
int intVal();
Optional<URI> uri();
@Disable(PREFIX)
List<UUID> ids();
@Split("[,:;]")
@Default("DAYS;HOURS")
Set<TimeUnit> units();
@LocalDateParser({ "dd.MM.yyyy", "yyyy-MM-dd" })
LocalDate date();
}
@Config
.Based on the interface, the annotation processor will generate an implementation, that can be obtained using ConfigFactory
:
MyConfig myConfig =
ConfigFactory.builder()
.setLoadStrategy(LoadStrategy.MERGE)
.addSource("file:~/myconfig.xml")
.addSource("classpath:config/myconfig-owner.xml")
.addSource("jar:file:path/to/some.jar!/path/to/myconfig.properties")
.addSource("https://somewhere.com/myconfig.toml")
.addSource("file:./myconfig.json")
.addSource("file:./myconfig.yaml")
.build()
.create(MyConfig.class);
e.g. “myconfig.properties”:
app.val=ABC
app.number=10
app.uri=http://java.sun.com/j2se/1.3/
ids=f8c3de3d-1fea-4d7c-a8b0-29f63c4c3454,123e4567-e89b-12d3-a456-556642440000
app.units=DAYS:HOURS;MICROSECONDS
app.date=12.11.2005
e.g. “myconfig.xml” (properties style xml):
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="app.val">ABC</entry>
<entry key="app.number">10</entry>
<entry key="app.uri">http://java.sun.com/j2se/1.3/</entry>
<entry key="ids">f8c3de3d-1fea-4d7c-a8b0-29f63c4c3454,123e4567-e89b-12d3-a456-556642440000</entry>
<entry key="app.units">DAYS;HOURS;MICROSECONDS</entry>
<entry key="app.date">12.11.2005</entry>
</properties>
e.g. “myconfig-owner.xml” (OWNER xml format):
<?xml version="1.0" encoding="UTF-8"?>
<app>
<val>ABC</val>
<number>10</number>
<uri>http://java.sun.com/j2se/1.3/</uri>
<units>DAYS;HOURS;MICROSECONDS</units>
<date>12.11.2005</date>
</app>
e.g. “myconfig.toml” (TOML format):
ids = ["f8c3de3d-1fea-4d7c-a8b0-29f63c4c3454","123e4567-e89b-12d3-a456-556642440000"]
[app]
val = "ABC"
number = 10
uri = "http://java.sun.com/j2se/1.3/"
units = ["DAYS", "HOURS", "MICROSECONDS"]
date = 2005-11-12
e.g. “myconfig.json” (JSON format):
{
"ids" : ["f8c3de3d-1fea-4d7c-a8b0-29f63c4c3454", "123e4567-e89b-12d3-a456-556642440000"],
"app" : {
"val" : "ABC",
"number" : 10,
"uri" : "http://java.sun.com/j2se/1.3/",
"units" : ["DAYS", "HOURS", "MICROSECONDS"],
"date" : "2005-11-12"
}
}
e.g. “myconfig.yaml” (YAML format):
ids:
- f8c3de3d-1fea-4d7c-a8b0-29f63c4c3454
- 123e4567-e89b-12d3-a456-556642440000
app:
val: ABC
number: 10
uri: http://java.sun.com/j2se/1.3/
units:
- DAYS
- HOURS
- MICROSECONDS
date: '2005-11-12'
@Target(TYPE)
@Target(TYPE)
@Target(TYPE)
@Target(METHOD)
@Target(METHOD)
Optional
return type.@Target({TYPE, METHOD})
@Target({TYPE, METHOD})
,
@Target({METHOD, ANNOTATION_TYPE})
@LocalDateParser
, @LocalDateTimeParser
, @LocalTimeParser
, @ZonedDateTimeParser
, @OffsetDateTimeParser
, @OffsetTimeParser
, @YearParser
, @YearMonthParser
, @MonthDayParser
@Target(METHOD)
@Default
or Optional
There are three ways for dealing with properties that are not found in sources:
If method return type is not Optional
and the method do not annotated with @Default
, the ConfigFactory.create
method will throw runtime exception “property … not found”
If method return type is Optional
-> method will return Optional.empty()
If method return type is not Optional
, but the method do annotated with @Default
-> method will return converted to return type default value.
FYI:
@Default
annotation can’t be used with a method that returns Optional
.@Config
annotation parametersThere are two optional parameters sources
and loadStrategy
which can be used to override these settings from ConfigFactory
.
e.g.
@Config(sources = {"classpath:config/testconfig2.properties","nocache:system:properties"},
loadStrategy = LoadStrategy.FIRST)
public interface ConfigOverride {
String string();
}
sources
parameter is present, all sources added in the ConfigFactory
(using ConfigFactory.Builder.addSource
methods) will be ignored.loadStrategy
parameter is present, it will be used instead of loadStrategy from ConfigFactory
.ConfigFactory.Builder.setSource(Map<String, String> properties)
method) are highest priority anyway. These properties will be merged in any case.@Key
and/or @Prefix
The @Key
and @Prefix
annotations can refer to system properties or environment variables.
This feature makes it possible to store, for example, settings for different environments in a single configuration file. e.g. (TOML):
host = "https://www.google.com/"
port = 80
[dev]
host = "https://github.com/"
port = 90
[prod]
host = "https://www.wikipedia.org/"
port = 100
Syntax: {name} or {name:default-value}
e.g.
@Config
public interface MyServer {
@Key("{env}.host") URL host();
@Key("{env}.port") int port();
}
or (with same result)
@Config
@Prefix("{env}")
public interface MyServer {
URL host();
int port();
}
usage e.g.:
java -Denv=dev -jar myapp.jar
FYI:
.
will be dropped....
) inside the key name will be substituted to single .
.system property value | key config | resulting key |
---|---|---|
dev | {env}.host | dev.host |
{env}.host | host | |
dev | server.{env}.host | server.dev.host |
server.{env}.host | server.host | |
dev | host.{env} | host.dev |
host.{env} | host |
system property value | key config | resulting key |
---|---|---|
dev | {env:test}.host | dev.host |
{env:test}.host | test.host | |
dev | server.{env:test}.host | server.dev.host |
server.{env:test}.host | server.test.host | |
dev | host.{env:test} | host.dev |
host.{env:test} | host.test |
ConfigFactory
The ConfigFactory
class is thread-safe, but not stateless.
It stores loaded properties in the internal cache (see Caching), and also control auto reloading.
Therefore, it certainly makes sense to create and use one single instance of ConfigFactory
for the whole application.
It’s possible to get loaded properties without define config-interface.
ConfigHolder holder =
ConfigFactory.builder()
.setLoadStrategy(LoadStrategy.FIRST)
.addSource("file:./myconfig.properties")
.addSource("classpath:config/myconfig.properties", "system:properties")
.build()
.configHolder();
String val = holder.getString("app.val", "unknown");
int intVal = holder.getInt("app.number");
Optional<List<UUID>> ids = holder.getOptionalList(UUID::fromString, "ids", ",");
Set<TimeUnit> units = holder.getSet(TimeUnit::valueOf, "app.units", "[:;]", "DAYS:HOURS");
@Factory
annotationThere is one place where Java-reflection is used: ConfigFactory.create
method.@Factory
annotation provides the ability to generate “Factory”-class(es) which helps to avoid reflection completely.
@Config
public interface MyConfig {
String val();
}
@Factory
public interface MyFactory {
MyConfig createMyConfig();
}
MyConfig myConfig = Factory_MyFactory.builder().addSource("file:./myconfig.properties").build().createMyConfig();
As you can see, based on the MyFactory
-interface annotated by @Factory
, the class Factory_MyFactory
will be generated, which has same API with ConfigFactory
but instead of create
-method it provides “create”-methods for the interface annotated by @Factory
.
Restrictions:
@Factory
must contains at least one method@Factory
must contains only methods without parameters@Factory
must return only types annotated by @Config
The ConfigFactory.Builder
contains a method for adding properties manually: setSource(Map<String, String> properties)
.
Manually added properties are highest priority always: loaded by URIs properties merged with manually added properties, independent of loading strategy.
In other words: the manually added properties will always override (sure, when the property keys are same) properties loaded by URI(s).
There is two major use-cases for the feature: unit-tests & console applications.
For console applications, it is convenient to provide command line arguments to the ConfigFactory
using this feature.
By default, ConfigFactory
caches loaded properties using source-URI (after resolving system properties and/or environment variable in it) as a cache key.
To not cache properties related to the URI(s), use URI-prefix nocache:
this will switch off caching for the URI.
e.g.
nocache
properties
nocache
~/my.properties
The ConfigFactory.Builder
provide the method to set global prefix setGlobalPrefix(String prefix)
, which will be used for all “config”-interfaces that will be created using the factory:
app.val=ABC
@Config
public interface MyConfig {
String val();
}
MyConfig myConfig = ConfigFactory.builder().setGlobalPrefix("app").build().create(MyConfig.class);
This makes it possible to avoid @Prefix
or/and @Key
annotations, in case several “config”-interfaces are created based on the same source(s).
FYI:
@Prefix
annotation. They can be used together.@Prefix
and @Key
)@Disable(Disable.Feature.GLOBAL_PREFIX)
The return type of the interface methods must either:
Be a primitive type
Have a public constructor that accepts a single String
argument
Have a public static method named valueOf
or fromString
that accepts a single String
argument
enum
valueOf
used unless the type is an enum
in which case fromString
used.Be
java.net.URL
java.net.URI
java.nio.file.Path
java.util.Currency
java.util.Locale
java.util.regex.Pattern
java.time.Instant
java.time.Duration
java.time.Period
java.time.LocalDate
java.time.LocalDateTime
java.time.LocalTime
java.time.ZonedDateTime
java.time.OffsetDateTime
java.time.OffsetTime
java.time.Year
java.time.YearMonth
java.time.MonthDay
java.time.ZoneId
java.time.ZoneOffset
net.cactusthorn.config.core.converter.bytesize.ByteSize
Be List<T>
, Set<T>
or SortedSet<T>
, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.
Be Map<K,V>
or SortedMap<K,V>
, where
Be Optional<T>
, where T satisfies 2, 3 or 4 above
Maps support is limited to two restrictions:
|
(pipe character)e.g. “myconfig.properties”:
map=A|10,BBB|20
map2=10000|10;20000|20
@Config(sources="classpath:/myconfig.properties")
public interface ConfigMap {
Map<String, Integer> map();
@Split(";") Optional<Map<Integer, Byte>> map2();
@Default("123e4567-e89b-12d3-a456-556642440000|https://github.com") Map<UUID, URL> map3();
}
FYI:
@Split
annotation set splitter for key+value “entries” (default “splitter” is comma : ,
).@ConverterClass
, @ZonedDateTimeParser
etc.) only affect the Map values.java.util.Locale
formatThe string must be well-formed BCP 47 language tag.
https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html#forLanguageTag-java.lang.String-
e.g. de-DE
java.time.Instant
formatThe string must represent a valid instant in UTC and is parsed using DateTimeFormatter.ISO_INSTANT
e.g. 2011-12-03T10:15:30Z
java.time.Duration
formatsStandard ISO 8601 format, as described in the JavaDoc for java.time.Duration. e.g. P2DT3H4M
“unit strings” format:
Bare numbers are taken to be in milliseconds: 10
Strings are parsed as a number plus an optional unit string: 10ms
, 10 days
The supported unit strings for duration are case sensitive and must be lowercase. Exactly these strings are supported:
ns
, nano
, nanos
, nanosecond
, nanoseconds
us
, µs
, micro
, micros
, microsecond
, microseconds
ms
, milli
, millis
, millisecond
, milliseconds
s
, second
, seconds
m
, minute
, minutes
h
, hour
, hours
d
, day
, days
java.time.Period
formatsStandard ISO 8601 format, as described in the JavaDoc for java.time.Period. e.g. P1Y2M3W4D
“unit strings” format:
Bare numbers are taken to be in days: 10
Strings are parsed as a number plus an optional unit string: 10y
, 10 days
The supported unit strings for duration are case sensitive and must be lowercase. Exactly these strings are supported:
d
, day
, days
w
, week
, weeks
m
, mo
, month
, months
y
, year
, years
net.cactusthorn.config.core.converter.bytesize.ByteSize
formatIt based on OWNER classes to represent data sizes.
usage:
@Config
public interface MyByteSize {
@Default("10 megabytes")
net.cactusthorn.config.core.converter.bytesize.ByteSize size();
}
The supported unit strings for ByteSize
are case sensitive and must be lowercase. Exactly these strings are supported:
byte
, bytes
, b
kilobyte
, kilobytes
, k
, ki
, kib
kibibyte
, kibibytes
, kb
megabyte
, megabytes
, m
, mi
, mib
mebibyte
, mebibytes
, mb
gigabyte
, gigabytes
, g
, gi
, gib
gibibyte
, gibibytes
, gb
terabyte
, terabytes
, t
, ti
, tib
tebibyte
, tebibytes
, tb
petabyte
, petabytes
, p
, pi
, pib
pebibyte
, pebibytes
, pb
exabyte
, exabytes
, e
, ei
, eib
exbibyte
, exbibytes
, eb
zettabyte
, zettabytes
, z
, zi
, zib
zebibyte
, zebibytes
, zb
yottabyte
, yottabytes
, y
, yi
, yib
yobibyte
, yobibytes
, yb
If it’s need to deal with class which is not supported “by default” (see Supported method return types), a custom converter can be implemented and used.
public class MyClassConverter implements Converter<MyClass> {
@Override public MyClass convert(String value, String[] parameters) {
...
}
}
The @ConverterClass
annotation allows to specify the Converter
-implementation for the config-interface method:
@Config public interface MyConfigWithConverter {
@ConverterClass(MyClassConverter.class) @Default("some super default value") MyClass theValue();
@ConverterClass(MyClassConverter.class) Optional<MyClass> mayBeValue();
@ConverterClass(MyClassConverter.class) Optional<List<MyClass>> values();
@ConverterClass(MyClassConverter.class) Optional<Map<Path, MyClass>> map();
}
FYI:
Converter
-implementation must be stateless and must have a default(no-argument) public
constructor.Converter
-implementation can be used instead “standard” converting, so it’s possible to implement specific converting for supported by default types.Converter
-implementation can be used for interface or abstract classConverter
-implementation are using only for values(not for keys)Sometimes it’s convenient to set several constant parameters for the custom converter.
For example, to provide format(s) with a converter for date-time types.
This can be achieved with converter-annotation for the custom-converter:
@Retention(SOURCE)
@Target(METHOD)
@ConverterClass(MyClassConverter.class) //converter implementation
public @interface MySuperParser {
String[] value() default "";
}
FYI:
String[] value() default ""
parameter, otherwise parameters will be ignored by compilerusage:
@Config
public interface MyConfig {
@MySuperParser({"param1", "param1"})
MyClass myValue();
}
Several of these annotations shipped with the library:
@LocalDateParser
@LocalDateTimeParser
@LocalTimeParser
@ZonedDateTimeParser
@OffsetDateTimeParser
@OffsetTimeParser
@YearParser
@YearMonthParser
@MonthDayParser
System properties: system:properties
Environment variables: system:env
properties file from class-path : classpath:relative-path-to-name.properties[#charset]
classpath:config/my.properties#ISO-5589-1
properties file from any URI convertable to URL: whatever-what-supported.properties[#charset]
file:./my.properties
file:///C:/my.properties
https://raw.githubusercontent.com/Gmugra/net.cactusthorn.config/main/core/src/test/resources/test.properties
jar
path/to/some.jar!/path/to/your.properties
XML file from class-path : classpath:relative-path-to-name.xml[#charset]
classpath:config/my.xml#ISO-5589-1
XML file from any URI convertable to URL: whatever-what-supported.xml[#charset]
file:./my.xml
META-INF/MANIFEST.MF: classpath
manifest?attribute[=value]
classpath
manifest?Bundle-Name=JUnit%20Jupiter%20API
classpath
manifest?exotic-unique-attribute
It’s possible to implement custom loaders using Loader
interface.
This makes it possible to load properties from specific sources (e.g. Database) or to support alternative configuration-file formats.
e.g.
public final class SinglePropertyLoader implements Loader {
@Override public boolean accept(URI uri) {
return uri.toString().equals("single:property");
}
@Override public Map<String, String> load(URI uri, ClassLoader classLoader) {
Map<String, String> result = new HashMap<>();
result.put("key", "value");
return result;
}
}
ConfigFactory factory =
ConfigFactory.builder()
.addLoader(SinglePropertyLoader.class)
.addSource("single:property")
.build();
FYI:
ConfigFactory.Builder.addLoader
method: last added -> first used.public
constructor.Service-provider loading facility (introduced in JDK 1.6) can be used to automatically add custom loader implementation to the ConfigFactory
. Simple add file META-INF\services\net.cactusthorn.config.core.loader.Loader with full-class-name of custom-loader implementation(s) in the class path.
e.g.
Syntax: {name} or {name:default-value}
e.g.
file:/{config-path}/my.properties
classpath:{config-path:home}/my.properties#{charset}
FYI:
Special use-case user home directory: The URIs with file:~/
(e.g. file:~/my.xml
or jar
) always correctly resolved to user home directory independent from OS.~/some.jar!/your.properties
file:~/my.xml
will be replaced to file:///C:/Users/UserName/my.xml
.ConfigFactory saves the sequence in which the sources URIs were added.
MyConfig myConfig =
ConfigFactory.builder()
.setLoadStrategy(LoadStrategy.FIRST)
.addSource("file:/myconfig.properties", "classpath:config/myconfig.properties")
.build()
.create(MyConfig.class);
Loading strategies:
“Relaxed”:
.
(dot), -
(minus) and _
(underscore) characters are ignoredWarning: Manually added properties (which added using ConfigFactory.Builder.setSource(Map<String, String> properties)
method) are highest priority always. So, loaded by URIs properties merged with manually added properties, independent of loading strategy.
ConfigFactory can automatically reload configurations which extends net.cactusthorn.config.core.Reloadable
interface.
To activate auto-reloading need to set “periodInSeconds” using autoReload
method:
ConfigFactory factory =
ConfigFactory.builder()
.addSource("file:/myconfig.properties")
.autoReload(5) //reload every 5 seconds
.build();
Warning: If you do not call autoReload
method, auto reloading will not work.
But, the source will be reloaded only if it changed.Loader
-implementation should implement contentHashCode
method which return hash-code. (The method return value should be changed, when URI related content is changed).
If Loader
-implementation do not support auto-reloading (which is default behavior) the method is returns always same value (e.g. 0
).
As result, for the moment, auto reloading only supported for:
system:properties
Warning: Be careful, non-cached(nocache:
) sources will always be reloaded, whether they are modified or not.
It is possible to disable auto reloading for the “config”-interface, even if it is activated:
@Config
@Disable(Disable.Feature.AUTO_RELOAD)
public interface MyConfig extends Reloadable {
String value();
}
Filesystems quirks
The date resolution vary from filesystem to filesystem.
For instance, for Ext3, ReiserFS and HSF+ the date resolution is of 1 second.
For FAT32 the date resolution for the last modified time is 2 seconds.
For Ext4 the date resolution is in nanoseconds.
It would be nice to know which properties has changed as result of reloading, so that you can e.g. re-configure only the affected services.
It’s possible to achieve using “Reload event listeners” feature.
Example how to do it: ListenerTest
Interfaces inheritance is supported.
e.g.
interface MyRoot {
@Key(rootVal) String value();
}
@Config
interface MyConfig extends MyRoot {
int intValue();
}
@Prefix
) on super-interfaces will be ignored.java.io.Serializable
“config”-interface can extends (directly or over super-interface) java.io.Serializable
.
In this case generated class will also get private static final long serialVersionUID
attribute.
@Config
public interface MyConfig extends java.io.Serializable {
long serialVersionUID = 100L;
String val();
}
The interface (as in the example before) can, optionally, contains long serialVersionUID
constant.
If the constant is present, the value will be used for the private static final long serialVersionUID
attribute in the generated class.
Otherwise generated class will be generated with private static final long serialVersionUID = 0L
.
net.cactusthorn.config.core.Accessible
“config”-interface can extends (directly or over super-interface) net.cactusthorn.config.core.Accessible
.
In this case generated class will also get methods for this interface:
Set<String> keys();
Object get(String key);
Map<String, Object> asMap();
net.cactusthorn.config.core.Reloadable
“config”-interface can extends (directly or over super-interface) net.cactusthorn.config.core.Reloadable
.
In this case generated class will also get methods for this interface:
void reload();
boolean autoReloadable();
void addReloadListener(ReloadListener listener);
FYI: The method always reload not cached sources, even if they not changed (see Caching)
“Extras” are optional extensions (converters and loaders) that need external dependencies and therefore can’t be integrated into the core library.
@PBEDecryptor
annotation which decrypt properties that were encrypted with Jasypt Password-Based Encryption.The runtime part of the library is using System.Logger.
This way, you can use any logging library you prefer:
There is no specific support for profiles, but it is easy to achieve similar behavior using System properties and/or environment variables in sources URIs,
e.g.:
ConfigFactory.builder()
.addSource("file:~/myconfig-{myapp.profile}.properties")
.addSource("file:./myconfig-{myapp.profile}.properties")
.addSource("classpath:myconfig.properties")
.build();
and get profile from, for example, system property:
java -Dmyapp.profile=DEV -jar myapp.jar
Example with Dagger 2:
It does not have annotation-processing enabled by default. To get it, you must install m2e-apt from the eclipse marketplace: https://immutables.github.io/apt.html
net.cactusthorn.config is released under the BSD 3-Clause license. See LICENSE file included for the details.