Loading strategies
The properties file for a mapping interface is automatically resolved by OWNER
API by matching the class name and the file name (appending .properties of
course).
But this logic can be tailored to your needs using some additional annotations:
@Sources({ "file:~/.myapp.config", "file:/etc/myapp.config", "classpath:foo/bar/baz.properties" })public interface ServerConfig extends Config { @Key("server.http.port") int port();
@Key("server.host.name") String hostname();
@Key("server.max.threads") @DefaultValue("42") int maxThreads();}In the above example, OWNER will try to load the properties from several @Sources:
- First, it will try to load the properties file from user’s home directory ~/.myapp.config, if this is found, this file alone will be used.
- If the previous attempt fails, then it will try to load the properties file from /etc/myapp.config, and if this is found, this one will be used.
- As last resort, it will try to load the properties from the classpath loading the resource identified by the path foo/bar/baz.properties.
- If none of the previous URL resources is found, then the Java interface will not be associated to any file, and only
@DefaultValuewill be used where specified. Where properties don’t have a default value,nullwill be returned (as it happens forjava.util.Properties).
In the above case, the properties values will be loaded from only one file: the first that is found. Only the first available properties file will be loaded, others will be ignored.
This load logic, is identified as “FIRST”, since only the first file found will be considered, and it is the default
logic adopted when the @Source annotation is specified with multiple URLs.
You can also specify this load policy explicitly using @LoadPolicy(LoadType.FIRST) on the interface declaration.
But what if you want to have some overriding between properties? This is definitely possible: you can do it with
the annotation @LoadPolicy(LoadType.MERGE):
@LoadPolicy(LoadType.MERGE)@Sources({ "file:~/.myapp.config", "file:/etc/myapp.config", "classpath:foo/bar/baz.properties", "system:properties", "system:env" })public interface ServerConfig extends Config { ...}In this case, for every property all the specified URLs will be queries, and the first resource defining the property will prevail. More in detail, this is what will happen:
- First, it will try to load the given property from ~/.myapp.config; if the given property is found the associated value will be returned.
- Then it will try to load the given property from /etc/myapp.config; if the property is found the value associated will be returned.
- Then it will try to load the given property from the classpath from the resource identified by the path foo/bar/baz.properties; if the property is found, the associated value is returned.
- Otherwise, it will try to load the given property from the Java system properties; if such property is defined, the associated value is returned.
- As last resort, it will try to load the given property from the operating system’s environment variables; if an environment variable with the same name is found, its value will be returned.
- If the given property is not found of any of the above cases, it will be returned the value specified by the
@DefaultValueif specified, otherwise null will be returned.
So basically we produce a merge between the properties files where the first property files overrides latter ones.
The @Sources annotation considers system properties and/or environment variables with the syntax
file:${user.home}/.myapp.config (this gets resolved by ‘user.home’ system property) or file:${HOME}/.myapp.config
(this gets resolved by the$HOME environment variable). The ~ used in the previous example is another example of
variable expansion, and it is equivalent to ${user.home}.
Sources and interface inheritance
Section titled “Sources and interface inheritance”When a mapping interface extends other interfaces, @Sources behaves
differently from the other interface-level annotations, and that is
deliberate: it does not pick one annotation and ignore the rest, it
accumulates. The URIs declared on the interface come first, followed by
those declared on each of its super-interfaces, and the resulting list is the
one the load policy is applied to.
@Sources("classpath:common.properties")public interface CommonConfig extends Config { String applicationName();}
@Sources("classpath:server.properties")@LoadPolicy(LoadType.MERGE)public interface ServerConfig extends CommonConfig { int port();}ServerConfig reads server.properties and common.properties, in that
order, so a property defined in both is taken from server.properties. This
is what makes it possible to describe a shared set of defaults once and let
each configuration override the part it cares about, which is the whole point
of extending a mapping interface.
Contrast this with @LoadPolicy and @HotReload, which describe a single
setting and therefore cannot be accumulated: for those, the first annotation
found wins — the one on the interface if it has one, otherwise the nearest one
above it.
All three are looked for on the whole hierarchy, and the order the interfaces
are visited in is what “nearest” means: the mapping interface first, then every
interface it extends directly, in the order of the extends clause, then
the ones those extend, and so on. Every direct parent is therefore asked before
any grandparent, and an interface reached by two paths is read once — so it
contributes its @Sources once.
Before 2.0.0 the lookup stopped at the direct super-interfaces.
An annotation sitting two levels up, on the parent of a parent, used to be silently ignored: its sources
were not loaded, and its policy or reload interval did not apply. It differed from
@Prefix, which has always counted at any depth, and each
of the three annotations had its own copy of the lookup, which is how the three came to disagree with a
fourth. They share one now, with every other annotation that describes the configuration object rather
than the methods of one interface — see
where an annotation counts.
If you were working around this by repeating an annotation on the interface handed to the
ConfigFactory, the repetition is no longer needed — and for @Sources it is
no longer harmless, since the same file would now be listed twice.
Declaring @Sources switches the convention off.
When nobody in the hierarchy declares @Sources, the properties are looked for by convention,
under the name of the mapping interface — see below. That is
a fallback for a configuration that names no source, not an extra source appended to the ones it
named: an interface carrying @Sources reads what it asked for and nothing else.
Before 2.0.0 the convention was appended to the declared sources as well, because the two were the same
lookup: every interface without the annotation contributed the default list, and Config itself
has none. So a MyConfig.properties on the classpath was read even by a configuration that had
declared its sources elsewhere. If that is what you want, declare it: @Sources({"file:my.properties",
"classpath:com/acme/MyConfig.properties"}) says so, and says it where it can be seen.
Where the properties come from
Section titled “Where the properties come from”A @Sources entry is a URI, and the loader that reads it is the first one that declares it accepts it.
Five are available out of the box, and they are consulted in this order:
| Loader | Accepts |
|---|---|
SystemLoader |
the system:properties and system:env pseudo-URIs, and nothing else |
IniLoader |
a URI whose path ends in .ini or .cfg — see File formats |
DotEnvLoader |
a URI whose path ends in .env — see File formats |
XMLLoader |
a URI whose path ends in .xml — see File formats |
PropertiesLoader |
anything that is a valid URL, in the standard properties format |
PropertiesLoader comes last because it accepts everything the others turned down: it is the
fallback, not a candidate among equals.
Three of them offer default file names, so a configuration with no @Sources looks for
MyConfig.properties, MyConfig.xml, MyConfig.ini and MyConfig.cfg and nothing more —
in that order, which is the subject of the next section. SystemLoader and DotEnvLoader offer
none: they answer when they are named and cost nothing when they are not, a .env being neither on
the classpath nor named after the configuration class.
The conventional sources
Section titled “The conventional sources”The files named after the mapping interface are the convention, and three questions come with them: which one wins when there is more than one, what happens when that is not what you meant, and how to ask for them by name from a configuration that also declares sources of its own.
Which one wins
Section titled “Which one wins”The order is the one above — .properties, .xml, .ini, .cfg, and then any file name offered by
a loader found on the classpath — and under the default LoadType.FIRST the
first one that exists is the one that answers.
It is the reverse of the table above, and deliberately:
| Question | Order | Why |
|---|---|---|
| Can you read this URI? | PropertiesLoader last |
it accepts every URL it can resolve, so asked earlier it would answer for the .ini and .xml files of the loaders after it |
| Which file is this configuration’s own? | PropertiesLoader first |
MyConfig.properties is the convention this library was built on and the file an application has been reading for years |
Being able to read anything is a liability in the first question and an asset in the second. .cfg
is last of the four for the same reason read backwards: it is the most generic of the names and the
likeliest to belong to somebody else’s tool.
Until 2.0.0 this order was never chosen.
It was the registration order of the loaders — which exists to answer the first question —
so MyConfig.ini and MyConfig.cfg silently outranked
MyConfig.properties. An application reading its .properties for years would
have stopped, without a word, the day somebody dropped a .cfg in the same directory.
When there is more than one, the library says so
Section titled “When there is more than one, the library says so”No order can do better than choose which of two silences you get: the file you did not expect being
read, or the file you did expect being ignored. So when more than one conventional file exists, it is
said out loud — a WARNING, naming both, saying which was read and how to end the ambiguity:
MyConfig: more than one conventional source exists: classpath:com/acme/MyConfig.properties,classpath:com/acme/MyConfig.xml. With LoadType.FIRST only classpath:com/acme/MyConfig.properties isread. Name the one you mean with @Sources, or owner:default.<extension>.Under owner.strict it is a refusal, like every other warning that has a
caller to refuse.
Asking for the convention by name
Section titled “Asking for the convention by name”owner:default — the constant Config.Sources.CONVENTIONAL — stands, where you write it, for
everything the configuration would look for if it declared no sources at all:
@LoadPolicy(LoadType.MERGE)@Sources({"file:~/myapp.conf", "system:env", Sources.CONVENTIONAL})public interface MyConfig extends Config { }Two things are worth knowing about it:
- the name is that of the interface you hand to the
ConfigFactory, not of the one carrying the annotation, so a base interface can tell twenty configurations to read their own conventional file and each of them reads its own; - followed by an extension —
owner:default.xml— it stands for that one conventional source instead of all of them. An extension no loader offers is refused rather than passed over: a source that resolves to nothing is skipped by design, so a misspeltowner:default.propertiswould otherwise leave a configuration reading nothing and saying nothing about it.
The alternative is to write classpath:com/acme/MyConfig.properties by hand. That works, and it has
two costs: it repeats the package, and it is a string no refactoring will follow — move the interface
to another package and that source stops resolving, silently.
The use cases, and how each is written
Section titled “The use cases, and how each is written”| You want | Write |
|---|---|
| your own sources and your conventional file, without spelling its path — #267 | @Sources({"file:~/foo.config", Sources.CONVENTIONAL}) |
| the environment to win, the conventional file as the base | @Sources({"system:env", Sources.CONVENTIONAL}) with @LoadPolicy(MERGE) |
| one base interface, twenty configurations, each reading its own file | the same @Sources on the base — the name follows the interface being created |
only the XML one, because a .cfg in that directory belongs to another tool |
@Sources("owner:default.xml") |
| YAML if it is there, properties otherwise | @Sources({"owner:default.yaml", "owner:default.properties"}) with FIRST |
to know why your new MyConfig.ini is not being read |
nothing: the WARNING above names the file that won |
A source over the network
Section titled “A source over the network”Every loader opens its source with uri.toURL().openStream(), so a @Sources entry may name any
protocol the JVM knows — and that has been true since 1.0.5 without ever being written down here:
@LoadPolicy(LoadType.MERGE)@Sources({ "https://config.example.com/myapp.properties", "classpath:myapp.properties" })public interface MyConfig extends Config { }The format is still chosen from the extension after the stream is open, so a .yaml or a .env
served over HTTPS is read by the loader for that format like any local file.
That covers most of what is usually meant by keeping configuration outside the artifact: a pre-signed S3 URL, an Azure Blob SAS, a signed Google Cloud Storage URL, a public bucket, a config server and a raw file from a Git host are all HTTPS. Nothing needs to be registered and no dependency is involved.
A remote source behaves like every other one under the loading
strategies: first-wins under FIRST, merged under MERGE.
Two cases are worth telling apart, and the library does:
| what the server answers | what happens |
|---|---|
| 404 | the source was not there, which is the network’s version of a missing file. Passed over in silence, owner.strict or not — refusing it would break the commonest shape a configuration has |
| 500, a refused connection, a timeout | the source is there and something is wrong with it. A WARNING naming it, and a refusal under owner.strict |
What is not covered, and what to do about it
A URL is all the authentication there is. A token in the query string works —
?token=abc is left alone, since the query belongs to the protocol — but anything needing
a signature or a header does not: s3://bucket/key signed with SigV4, a Vault token, Google
Application Default Credentials.
For those, teach the JVM the protocol rather than teaching OWNER — see below. Writing a loader of your own is the other way, and the right one when the source is not a file at all: a key/value API rather than a document.
A protocol the JVM does not know
Section titled “A protocol the JVM does not know”@Sources names a URI and every loader opens it with uri.toURL(), so the question is never whether
OWNER speaks a protocol — it is whether the JVM does. And that is extensible, by the application rather
than by us:
URL.setURLStreamHandlerFactory(protocol -> { if (!"s3".equals(protocol)) return null; // everything else: the JVM's own handlers, untouched return new S3UrlStreamHandler(s3Client); // yours, holding whatever credentials it needs});@Sources("s3://bucket/app.properties")public interface MyConfig extends Config { }Nothing is registered with OWNER and no loader is written. The source merges like any other, the format is
still chosen from the extension after the stream is open, and a handler that cannot produce the object
takes the same path as any unreadable source — a warning, or a refusal under owner.strict.
All of that is pinned down in
UnknownProtocolSourceTest,
which teaches the test JVM a made-up scheme and reads a configuration over it.
Once per JVM, which is why it is yours and not ours
URL.setURLStreamHandlerFactory may be called once for the life of a JVM. That is
exactly why a library must not call it on your behalf: it is a decision that belongs to the
application, next to the credentials such a handler needs — the same rule this library applies to an
encryption passphrase. Since Java 9 there is a tidier route with no
such limit, the java.net.spi.URLStreamHandlerProvider service, and a reader on 9 or later
should prefer it; the example above uses the older call because it works on the Java 8 baseline this
library still supports.
Where a loader of your own sits among them
Section titled “Where a loader of your own sits among them”Since 2.0.0. There are two ways in, and they land in different places:
| Matching a source | Guessing the file name | |
|---|---|---|
registered with registerLoader |
first of all | first of all |
| found on the classpath | after the registered ones, before the built-in ones | last of all |
| built in | last, PropertiesLoader last of those |
in the middle |
Both come before the built-in loaders when a source is being matched — otherwise PropertiesLoader, which
accepts every URL it can resolve, would take their files before they were asked.
The second column is deliberately not the same, and it is the one that matters when a configuration
declares no @Sources at all. Those names are tried in order, and under LoadType.FIRST the first that
resolves is the one that answers. If a loader found on the classpath contributed its name first, adding a
jar to a build would be enough to make a stray MyConfig.yaml start beating the MyConfig.properties an
application already reads — a change of behaviour nobody asked for and nothing announced. Placed last, it
can only answer for a name nothing else claimed. Registering a loader by hand is a different matter: that is
something the application said on purpose, so it keeps the front in both columns.
Reading from ZooKeeper
Section titled “Reading from ZooKeeper”The owner-extras artifact ships a further loader, ZooKeeperLoader, which reads the properties from
the children of a ZooKeeper node. It needs Apache Curator, which owner-extras declares as an
optional dependency and therefore does not bring along: declare that dependency yourself, and address the
source with the zookeeper scheme.
<dependency> <groupId>org.aeonbits.owner</groupId> <artifactId>owner-extras</artifactId> <version>2.0.0</version></dependency><dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>5.9.0</version></dependency>@Sources("zookeeper://zookeeper.example.com:2181/config/myapp")public interface ServerConfig extends Config { int port(); String hostname();}ServerConfig cfg = ConfigFactory.create(ServerConfig.class);Since 2.0.0 the loader is found on the classpath like every other, so there is nothing to register and
no factory of your own to create. Before then it had to be registered by hand, and code that still does
keeps working — registering a loader that was also discovered changes only where it sits in the order,
and it is already ahead of everything that could compete for a zookeeper: source.
Nothing is loaded until a zookeeper: source is read.
Being discovered means the loader is created in every application that carries
owner-extras, most of which will not have Curator. Nothing in it refers to Curator, so
that costs nothing and breaks nothing: a configuration reading any other source is unaffected, and it
contributes no file name to the ones tried when a configuration declares no @Sources.
Only reading a zookeeper: source without Curator fails, and it fails by naming the source
and the artifact to add rather than with a NoClassDefFoundError.
Each child of the node named by the path becomes a property: the name of the child is the key and its
data, read as a string, is the value. The port may be omitted, in which case the default of the client is
used. Connecting is given thirty seconds before it gives up, which the
owner.zookeeper.connection.timeout.seconds System Property changes; a failure to connect surfaces as an
IOException, so it behaves like any other unreachable source and
@LoadPolicy(MERGE) can fall back on another one.
The class moved in 2.0.0
ZooKeeperLoader is now in org.aeonbits.owner.extras.loaders, where it used to
be in org.aeonbits.owner.loaders. Only the import changes. The old package belongs to the
core artifact, and a package cannot live in two modules, which kept the two jars from being placed on
the module path together.
Writing your own loader
Section titled “Writing your own loader”A loader is an implementation of Loader, which answers three questions: whether it accepts a
given URI, how to read one into a java.util.Properties, and what file name — or names — to look for when
the configuration declares no @Sources at all.
public class YamlLoader implements Loader {
private static final String[] SUFFIXES = {".yaml", ".yml"};
@Override public boolean accept(URI uri) { return SourceOptions.hasExtension(uri, SUFFIXES); }
@Override public void load(Properties result, URI uri) throws IOException { SourceOptions.of(uri).refuseUnknown(); // this loader takes no options // ...read the source into result... }
@Override public String[] defaultSpecsFor(String uriPrefix) { return new String[] { uriPrefix + ".yaml", uriPrefix + ".yml" }; }}Register it on a factory as shown above and it takes part in the resolution like the built-in ones.
Letting it be found instead
Section titled “Letting it be found instead”Since 2.0.0. A loader declared in META-INF/services/org.aeonbits.owner.loaders.Loader is picked up when
a factory is created, without anyone calling registerLoader:
org.example.YamlLoaderIt has to be a public class with a public no-argument constructor — that is
ServiceLoader’s requirement, not OWNER’s. Being found enables it: it answers for its
formats at once, and its default file names join the ones looked for when an interface declares no
@Sources. That is what a jar shipping a format is for, so there is nothing further to turn on.
Which class loader does the looking, and when that matters
The context class loader of the thread that creates the factory, falling back on the one that loaded
OWNER when the thread has none. That is right in an ordinary application, where there is effectively one
class loader, and it is right in an application server, where OWNER sits in the shared libraries and
only the context class loader can see a jar in a web application's WEB-INF/lib — a parent
never sees a child's jars.
It is not right everywhere. On a thread a container or a pool set up, the context class loader may point
at something that knows nothing of your application, and the loader is not found although it is on the
class path. Under OSGi there is no class path to search and ServiceLoader needs help from
the container to work at all. In both cases, call registerLoader: that route depends on
nothing.
A loader that is not found does not look like an error
PropertiesLoader accepts every URL it can resolve and is consulted last, so a
app.yaml whose loader was not found is not left unread — it is read as a properties
file, and the configuration comes back holding almost nothing, with nothing said. When a format
seems to be ignored, put org.aeonbits.owner.level = CONFIG in a
logging.properties: OWNER then names the loaders it found, including when it found none,
which tells a loader that is absent from a loader that is broken.
Options on a source
Section titled “Options on a source”Since 2.0.0. A loader can be told something about one particular source, written in the fragment of its
URI — after the #, several separated by &:
@Sources("file:.env#dialect=dotenv"es=strip")The query belongs to the protocol and the fragment belongs to OWNER. A query is never touched, because
on a remote source it means something to the server; the fragment is never sent to one. It is also the only
place the options can be written for a resource inside a jar, whose URI has no query to speak of.
SourceOptions.of(uri) reads them and refuseUnknown(...) turns away the ones a loader does not
understand, naming the offender, the source and what would have been accepted — because an option that is
misspelt and ignored is a configuration that is wrong and says nothing.
Two built-in loaders have options of their own: the .env one, whose
dialect it is, and the XML one, which can be told
not to validate.
One option is not a loader’s at all. required is read by the library, before any loader is chosen,
and every loader therefore accepts it without declaring it — see below.
A source that was named and did not arrive
Section titled “A source that was named and did not arrive”Since 2.0.0. A source that cannot be read does not stop the others: that is what a fallback chain is for. But not every failure is a fallback, and until 2.0.0 they were all treated as one and passed over in silence. Three cases are now distinguished:
- A source that is simply not there says nothing. With
LoadType.FIRSTevery miss but the last is how the feature works, and a configuration with no@Sourcesprobes four names for every interface. A warning here would be noise, and noise is how a real warning stops being read. - A source that is there and cannot be read is a
WARNING. A wrong permission, a network source refusing, a host that does not answer: nobody designs a fallback on one of those, and what they produce is a configuration full of defaults. - Sources declared and not one of them readable is a
WARNINGof its own, because that is what a mistyped path looks like — every miss legitimate on its own, and only their sum wrong. A configuration that declares no@Sourcesis left alone: finding no file among the four probed names is how a configuration made entirely of defaults is written.
Each is said once, and again only if the failure changes, since a reload runs the whole load again and a hot reload runs it at its interval for as long as the process lives.
Which of the two it is, is not read off the exception.
FileInputStream throws FileNotFoundException for a file that is missing, for
a directory named where a file was meant, and for one it may not open — the three things this rule
exists to tell apart. For a file the filesystem is asked instead; only a source that is not a file
falls back on the exception, where a FileNotFoundException is a 404 or a missing entry in
a jar.
Saying that a source has to be there
Section titled “Saying that a source has to be there”Since 2.0.0. required=true on a source turns its absence from a fallback into an error, refused when the
configuration is created:
@Sources({"file:/etc/myapp.properties#required=true", "classpath:defaults.properties"})It is the counterpart of Spring’s optional: prefix, the other way up: their sources must exist unless
marked optional, ours may be missing unless marked required — which is what keeps every fallback chain
written so far working as it did.
It applies to a source that is absent, to one that cannot be read, and to a classpath: resource that
resolves to nothing, which is the case that never reaches a loader at all and would otherwise have been the
one place where the promise was quietly dropped.
Unlike a dialect or a validation flag, required is not an option of a loader — whether a source may be
missing is decided before a loader is chosen, and is the same question for all of them. That is why no
loader has to declare it and none of them refuses it.
Refusing everything that would only have been a warning
Section titled “Refusing everything that would only have been a warning”Since 2.0.0. required=true says it for one source. owner.strict says it for the whole
configuration, on the factory:
Factory factory = ConfigFactory.newInstance();factory.setProperty("owner.strict", "true");
ServerConfig cfg = factory.create(ServerConfig.class); // refuses instead of warningThe default is off, and off is how OWNER has always behaved. It is worth knowing why the switch exists at all: this library’s way of failing is to keep working. A source that cannot be read is passed over, the object is built out of default values, and the caller gets something that works and lies. That is a deliberate choice — a fallback is meant to work — but it is invisible until somebody reads the wrong value in production. Most of the field either throws or refuses to start; OWNER carries on, so the warnings above carry more weight here than they would elsewhere, and until 2.0.0 they were all a caller could get.
What counts as a failure is not a list of its own: it is the warnings. They were already chosen to leave the legitimate cases alone, so strict inherits that care instead of restating it and drifting from it. Five things are refused:
refused under owner.strict |
|
|---|---|
| a source that was named and could not be read | the second bullet above |
| not one of the declared sources could be read | the third bullet above |
@HotReload over a source nobody can watch |
see Reload |
a value built out of an @EncryptedValue one |
see Crypto support — and note that a marker does not have this problem at all, so this refusal is about the older annotation |
| a variable that resolves to nothing | see Variables expansion |
And what it leaves alone matters as much:
- A source that is merely absent stays silent, strict or not.
LoadType.FIRSTexpects misses by design, and refusing them would make the property unusable with the commonest shape a configuration has. - A configuration made entirely of defaults declares no sources, so nothing failed to be read.
- A reload that fails is outside this on purpose: it happens later, on a scheduled thread with nobody to refuse, and turning a transient failure into a crash is worse than the warning. The event API is where a reload problem is answered.
It raises the UnsupportedOperationException that required=true already raises for the identical case,
and it belongs to the factory rather than to the JVM: an application turning it on does not make a
library that happens to use OWNER strict as a side effect.
Two loaders warn and are not covered.
The XML one when the parser will not support the hardening, and the .env one when values
are quoted under a dialect that keeps quotes. Loader.load(Properties, URI) is a public SPI
with no way to reach the factory, so strictness cannot be handed to a loader without changing it. Both
already have a per-source answer in the fragment — #validate=false and
#dialect= — so neither is left without one.
What 2.0.0 added, and what it did not break
Section titled “What 2.0.0 added, and what it did not break”Everything above is additive. defaultSpecsFor and a default defaultSpecFor are default methods, so a
Loader written against 1.x compiles and runs unchanged, and does not need recompiling; a loader offering a
single file name may still say so with defaultSpecFor, and one whose format goes by two names overrides
defaultSpecsFor instead. Returning null from the first, or an empty array from the second, means the
loader adds nothing to what is looked for — which is what SystemLoader and DotEnvLoader both do.