Salta ai contenuti

Questa pagina non è ancora tradotta in italiano: quello che segue è il testo inglese.

Basic usage

Questi contenuti non sono ancora disponibili nella tua lingua.

The approach used by OWNER APIs, is to define a Java interface associated to a properties file.

Suppose your properties file is defined as ServerConfig.properties:

port=80
hostname=foobar.com
maxThreads=100

To access this properties file you need to define a convenient Java interface ServerConfig.java in the same package:

import org.aeonbits.owner.Config;
public interface ServerConfig extends Config {
int port();
String hostname();
@DefaultValue("42")
int maxThreads();
}

Notice that the above interface extends from Config, that is a marker interface recognized by OWNER as valid to work with.

We’ll call this interface the Properties Mapping Interface or just Mapping Interface since its goal is to map Properties into a an easy to use piece of code.

Since the properties file does have the same name as the Java class, and they are located in the same package, the OWNER API will be able to automatically associate them.
For instance, if your mapping interface is called com.foo.bar.ServerConfig, OWNER will try to associate it to com.foo.bar.ServerConfig.properties, loading from the classpath.

The properties names defined in the properties file will be associated to the methods in the Java class having the same name.
For instance, the property port defined in the properties file will be associated to the method int port() in the Java class, the property hostname will be associated to the method String hostname() and the appropriate type conversion will apply automatically, so the method port() will return an int while the method hostname() will return a Java string, since the interface is defined in this way.

The mapping mechanism is fully customizable, as well the automatic type conversion we just introduced is flexible enough to cover most of the Java types as well as object types defined by the user.
You can see how in the next chapters.

At this point, you can create the ServerConfig object and use it in your code:

ServerConfig cfg = ConfigFactory.create(ServerConfig.class);
System.out.println("Server " + cfg.hostname() + ":" + cfg.port() +
" will run " + cfg.maxThreads());

Did you notice that in the above example it is specified @DefaultValue("42") annotation?

public interface ServerConfig extends Config {
int port();
String hostname();
@DefaultValue("42") // here!!!
int maxThreads();
}

It is used in case the maxThread key is missing from the properties file.

This annotation gets automatically converted to int, since maxThreads() returns an int.

Using the annotations, you can also customize the property keys:

# Example of property file 'ServerConfig.properties'
server.http.port=80
server.host.name=foobar.com
server.max.threads=100

This time, as commonly happens in Java applications, the properties names are separated by dots. Instead of just “port” we have “server.http.port”, so we need to map this property name to the associated method using the @Key annotation.

/*
* Example of ServerConfig.java interface mapping the previous
* properties file.
*/
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();
}

The @DefaultValue and @Key annotations are the basics to start using the OWNER API.

Notice that the three keys above share the same server. prefix, as it commonly happens. Since version 2.0.0 you can state that prefix once, on the interface, instead of repeating it on every method: see Key prefix.

You can leave the properties file away during development!

During the development you may decide to just use the `@DefaultValue` to provide a default configuration, without really adding the properties file. You can add the properties file later or leave this task to the end user.

The same setting gets spelled differently depending on where it is written. Java code says firstName, a .properties or a .yaml file usually says first-name, a shell says FIRST_NAME. Since version 2.0.0 a method finds its property under any of them, which is what #116 asked for:

public interface Person extends Config {
String firstName();
}
firstName = Luigi # the key the method resolves to
first-name = Luigi # kebab-case
first_name = Luigi # snake_case
FIRST_NAME = Luigi # the environment variable form

The fourth is a form like the others and is looked for in every source, not only in system:env: one rule, the same in every context, is what makes it possible to say where a value will be read from without running the program.

Any one of those four answers firstName().

Four forms, and no more. The set is closed: firstname, FirstName, first.name and FIRST-NAME are not spellings of this key. Spring Boot 1 matched loosely — separators dropped, case ignored, several spellings collapsing onto one property — and Boot 2 deliberately narrowed it, because with the loose version you could not say which key a value would be read from without running the program. This is the narrow side of that split.

The forms are derived from the key, not from the method name, so @Key takes part in it: a method annotated @Key("first-name") is equally found under firstName or FIRST_NAME. @Key still means exactly what it says — it is the key that is tried first, the one every error message names, and the one the key report prints.

One form applies to the whole key at once, prefixes and nesting included. server.maxThreads is looked for as server.max-threads and server.max_threads — never as a mixture such as server.MAX_THREADS, since a file is written in one convention throughout. That is also what makes a whole section work: with myDb() returning a nested interface, my-db.user-name is simply one of the spellings of myDb.userName.

The environment form is the one exception, and it has to be. A dot separates the segments of a key and cannot appear in the name of an environment variable a shell can set, so that form replaces every character that is not a letter or a digit with an underscore, and upper-cases the rest:

The key As an environment variable
firstName FIRST_NAME
server.maxThreads SERVER_MAX_THREADS
core.thread_number CORE_THREAD_NUMBER
servers[0].host SERVERS_0__HOST

It is the rule MicroProfile Config mandates for the same purpose and the one Spring Boot documents, so an application that already exports SERVER_MAX_THREADS for something else keeps exporting one variable. One consequence worth knowing: two different keys can collapse onto one variable — a.b and a_b are both A_B — and then both methods read it. That is inherent to the environment’s namespace, not to this library, and it is the reason the other three forms keep the shape of the key instead.

A value that was written beats one that was only defaulted, whichever spelling holds it — so max-threads=7 in the file beats the @DefaultValue("42") on maxThreads(), as anybody would expect. Among values that were all written, the key the method resolves to comes first, and the other spellings follow in the order they are listed above: kebab, snake, upper snake, camel.

That order only matters when a configuration holds two spellings of one property at once, and when it does, one of them is read and the other is inert. OWNER does not fail silently, so it says so:

WARNING: com.acme.Person: 'firstName' is written in 2 spellings at once - [firstName, first-name].
Relaxed binding reads 'firstName' and never looks at [first-name]. Keep one spelling, or switch the
feature off for the method with @DisableFeature(RELAXED_BINDING) if they are meant to be different
properties.

With owner.strict on, the same case is refused when the configuration is created instead of being reported. A @DefaultValue is never one of the two: it lives under the key of its own method and pairing it with what the file wrote would report every defaulted property in every kebab-case configuration.

  • The properties keep the names they were written with. store(), list(), propertyNames() and toString() show first-name, and nothing is ever added under the key of the method. See Accessible and Mutable.
  • The methods that take a key answer about that key. getProperty("firstName") returns null when the file says first-name, and a Traceable origin stays attached to the key that really exists. These are addressed by key, and relaxing them would mean getProperty(k) reading something that is not k.
  • The prefix of a group is matched as it is written. A method returning a Map, or an indexed list such as hostNames[0], reads everything below a prefix — there the prefix decides which keys are the group, and choosing among four of them could silently merge two groups or answer with the wrong one.

One thing does follow the spellings, and has to: what @Sensitive masks is matched by name against the properties as they were loaded, so a @Sensitive String dbPassword() reading DB_PASSWORD out of the environment is masked in list() and toString() under that name too.

The feature is on by default and is switched off per method or per interface with @DisableFeature(RELAXED_BINDING); see Disabling features. Switching it off is worth it where a file deliberately holds two spellings as two different properties, and where a property that is often absent is read in a tight loop: a key that is found costs nothing extra, while a key that is missing is looked for three more times before the answer is null.

The default covers a property that is missing. A property that is there but empty is a value like any other:

server.max.threads=

@DefaultValue("42") is not used here, and since an empty text is not a number the conversion fails with Cannot convert '' to int for property 'server.max.threads'. That is deliberate, and it is the same distinction drawn by MicroProfile Config, by Quarkus and by Spring Boot: leaving a property empty is a way of saying this is not set here, which with a MERGE load policy is how a value coming from another file is overridden. Falling back on the default whenever a conversion fails would also turn a typo like server.max.threads=4O — written with the letter O — into a silent 42, which is exactly the kind of quiet wrongness a default is not there to produce.

There is one case where the distinction gets in the way, though: a value left empty by a template that nobody filled in.

server.max.threads=${MAX_THREADS}

Since version 2.0.0 a single method can opt into having the default cover that case too:

@Key("server.max.threads")
@DefaultValue(value = "42", useOnEmpty = true)
int maxThreads();

With useOnEmpty = true an empty value — including one made of whitespace only, and one that becomes empty after the variables are expanded — is treated as if the property were not there at all, and the default is used in its place. A value that is wrong rather than empty still fails: useOnEmpty is about the absence of information, not about recovering from a mistake. Use @Mandatory when a property must be set.

The flag applies to the annotated method only, and it does not change what is stored: getProperty() and the other Accessible methods keep returning the empty value. The default replacing an empty value goes through variable expansion, preprocessing, decryption and parameter formatting exactly as the value it replaces, so the result is the same as if the property had been missing.

Suppose you have defined a method in your mapping interface that cannot be resolved to any property loaded from a properties file, and this method doesn’t define a @DefaultValue what happens? Simple: it will return null, or a NullPointerException;

Suppose our ServerConfig class was looking like this:

public static interface ServerConfig extends Config {
String hostname();
int port();
Boolean debugEnabled();
}

If we don’t have any ServerConfig.properties associated to it, when we call the method String hostname() it will return null, as well as when we call the method Boolean debugEnabled() since the return types String and Boolean are java objects. But if we call the method int port() then a NullPointerException will be raised.

You don't want the NullPointerException?

If you don't want to get the NullPointerException, you can just define a default value. For instance, you can set @DefaultValue("0") for an int return type, or a @DefaultValue("false") for a boolean return type, and so on...

Sometimes a configuration property is required and there is no sensible default for it: think of a database URL or an API key. Since version 2.0.0 you can mark such properties with the @Mandatory annotation:

public interface ServerConfig extends Config {
@Mandatory
String hostname();
@DefaultValue("8080")
int port();
}

When the Config object is created, OWNER verifies that every mandatory property can be resolved (from the loaded sources, the imports or a @DefaultValue): if any of them is missing, ConfigFactory.create() throws a MissingMandatoryPropertyException listing all the missing keys, so you can fix your configuration in a single pass. The exception also exposes the missing keys programmatically via getKeys().

The check is enforced on every access too: if a mandatory property becomes unavailable later — for instance after a hot reload or a removeProperty() on a Mutable config — reading it throws MissingMandatoryPropertyException instead of returning null.

@Mandatory can also be applied to the interface, making all the properties declared in that interface mandatory:

@Mandatory
public interface DatabaseConfig extends Config {
String url(); // mandatory
String username(); // mandatory
@DefaultValue("10")
int poolSize(); // mandatory, but satisfied by the default value
}
Methods taking parameters

Properties whose method takes parameters cannot be validated at creation time, since the property key may depend on the invocation arguments: for those, the check happens when the method is invoked.

A mapping interface can extend another one and re-declare one of its methods, to give it a different key or a different default value:

public interface BaseConfig extends Config {
@Key("feature.default.setting")
@DefaultValue("-1")
long setting();
}
public interface FeatureConfig extends BaseConfig {
@Key("feature.concrete.setting")
@DefaultValue("42")
@Override
long setting();
}

An override redirects the property rather than adding one: setting() reads feature.concrete.setting, and feature.default.setting is no longer part of this configuration — getProperty() returns null for it, and its @DefaultValue is not registered. There is one method, so there is one key. That follows from Java itself: an overriding declaration hides the one it overrides, and OWNER sees a single setting() method.

Two things are commonly wanted here, and both are written down explicitly rather than inferred from the override.

To keep reading the base key as well, declare an accessor for it instead of relying on the overridden declaration:

public interface FeatureConfig extends BaseConfig {
@Key("feature.concrete.setting")
@DefaultValue("42")
@Override
long setting();
@Key("feature.default.setting")
@DefaultValue("-1")
long baseSetting();
}

To make the concrete setting fall back to the base one — an overlay, rather than a replacement — say so with a variable:

@Key("feature.concrete.setting")
@DefaultValue("${feature.default.setting:-1}")
@Override
long setting();

setting() now returns feature.concrete.setting when it is defined, otherwise feature.default.setting, otherwise -1. Note that this is a chain of three and that the fallback works on the properties too, not only on the default values: setting feature.default.setting in a properties file changes the answer, which is not something an inherited @DefaultValue could ever do.

Several annotations are written on the interface rather than on a method, and once interfaces extend each other the question is where each of them counts. There are two answers, and which one applies follows from what the annotation is about.

The ones that describe the configuration object@Sources, @LoadPolicy, @HotReload, @DecryptorClass, the @Description of a saved file — are found wherever in the hierarchy they are written, nearest first: the configuration object is the interface you hand to the ConfigFactory together with everything it extends.

The ones that describe the methods an interface declares@Prefix, @Mandatory, @Sensitive, @Separator, @TokenizerClass, @PreprocessorClasses — are read off the interface that declares the method, and neither climb nor descend. A sub-interface saying @Sensitive does not mask the keys its parent declared, and one saying @Separator(";") does not change how a list its parent described is cut: an interface governs what it declares, or two interfaces would be describing one key.

@DisableFeature is asked both questions, and answers each with its own rule.

The tables — which annotation is in which family, how each propagates through a hierarchy, the order the interfaces are visited in, and the reason behind each of the eight rules — are in Where an annotation counts.

Keeping this library out of the API you publish

Section titled “Keeping this library out of the API you publish”

extends Config is how an interface is declared to be a configuration, and it is the reason ConfigFactory.create(MyConfig.class) is checked by the compiler rather than at run time. Config is a marker: it declares no methods, so extending it adds nothing to your type. Accessible, Mutable, Reloadable and Traceable do add methods, and you extend those only if you want them.

There is one case where the marker costs something, and it is worth knowing the way out. If you are writing a library of your own on top of this one and you publish the configuration interface to your users, then extends Config puts OWNER on their compile classpath: their code will not compile without it. Keep the two apart, and it does not:

// what your users compile against — nothing of ours on it
public interface AppSettings {
String host();
int port();
}
// what you keep to yourself
interface AppSettingsMapping extends AppSettings, Config {
@Override @Key("app.host") @DefaultValue("localhost")
String host();
@Override @Key("app.port") @DefaultValue("8080")
int port();
}
AppSettings settings = ConfigFactory.create(AppSettingsMapping.class);

The methods are redeclared in the mapping interface so that the annotations have somewhere to live that your users never see — an @Key on the published interface would put us back on their classpath just as surely as the supertype would. A method redeclared like that is the one whose annotations are read.

This is #66, and it is also the answer to the half of it that was about not exposing list(PrintStream) and the other debugging methods: those are on Accessible, which nobody has to extend. If you do want Accessible and do not want it showing the whole file, that is @DeclaredOnly.

Now you know the minimum to get productive with the OWNER API. But this is just the beginning. OWNER is a rich API that allows you to add additional behaviors and have more interesting features, so that you should be able to use this library virtually in any other context where you where using the java.util.Properties class.

Project maintained by Matteo Baccan, and the awesome contributors.

Developed with IntelliJ IDEA

Hosted on GitHub