Configuration overrides
Ocular can override values from your files with environment variables and JVM system properties. That way you run the same application in development, staging and production without touching the files.
How it works
There are two parts:
- At compile time an annotation processor finds the fields you marked. For each class it generates a helper class that reads the environment variables and system properties.
- At runtime Ocular reads the file and then uses the helper class to replace values that were set from outside.
Overrides win over the values in the file. They are applied every time the file is read, also on reload.
Set up the annotation processor
The processor is part of the Ocular artifact.
- Gradle (Kotlin DSL)
- Gradle (Groovy)
- Maven
Gradle only runs processors you list in annotationProcessor:
dependencies {
implementation("dev.chojo:ocular:2.3.2")
annotationProcessor("dev.chojo:ocular:2.3.2")
}
Gradle only runs processors you list in annotationProcessor:
dependencies {
implementation 'dev.chojo:ocular:2.3.2'
annotationProcessor 'dev.chojo:ocular:2.3.2'
}
Maven runs the processors it finds on the class path, so the normal dependency is enough. If your build sets
annotationProcessorPaths on the compiler plugin, add Ocular there:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>dev.chojo</groupId>
<artifactId>ocular</artifactId>
<version>2.3.2</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Mark fields
Put @Overwrite on every field that may be overridden. Inside it, list the sources:
@Envreads an environment variable.@Propreads a system property, set with-Don the command line.
public class AppConfig {
@Overwrite(env = @Env("APP_HOST"), prop = @Prop("app.host"), description = "Host the server binds to")
private String host = "localhost";
@Overwrite(env = @Env("APP_PORT"), prop = @Prop("app.port"))
private int port = 8080;
@Overwrite(env = @Env("APP_DEBUG"))
private boolean debug = false;
}
With this class:
APP_HOST=example.comsetshosttoexample.com.-Dapp.port=9090setsportto9090.- If nothing is set, the value from the file stays.
description is optional. It shows up in the log and in the reference file.
An overridden value is a normal value of your object. When you save the file, the overridden value is written into it.
Order of sources
The sources are checked in the order you write them. The first one that has a value wins.
@Overwrite(prop = @Prop("app.host"), env = @Env("APP_HOST"))
private String host;
Here the system property wins when both are set. Swap prop and env to let the environment variable win.
Each attribute takes several sources, too. They are checked in order:
@Overwrite(env = {@Env("APP_HOST"), @Env("HOST")})
private String host;
Names without a value
Leave the name out and Ocular builds one from the class and the field:
| Source | Name | Example for field maxPlayers in AppConfig |
|---|---|---|
@Env | CLASSNAME_FIELDNAME, all upper case | APPCONFIG_MAXPLAYERS |
@Prop | classname.fieldName, class name in lower case, field name as written | appconfig.maxPlayers |
public class AppConfig {
@Overwrite(env = @Env, prop = @Prop)
private String host = "localhost";
}
This field reads the environment variable APPCONFIG_HOST and the system property appconfig.host.
The class name is the simple name of the class that declares the field.
Prefix
@OverwritePrefix on the class replaces the class name in generated names:
@OverwritePrefix("myapp")
public class AppConfig {
@Overwrite(env = @Env, prop = @Prop)
private String host = "localhost";
}
This field reads MYAPP_HOST and myapp.host.
The prefix is adjusted to each source. For @Env it is upper case and dots become underscores. For @Prop it is
lower case and underscores become dots. The prefix my_app gives MY_APP_HOST and my.app.host.
Names you write yourself are used as they are:
@OverwritePrefix("myapp")
public class AppConfig {
@Overwrite(env = @Env("CUSTOM_HOST"))
private String host = "localhost";
}
This field reads CUSTOM_HOST.
Force the prefix
With force = true the prefix is put in front of your own names, too:
@OverwritePrefix(value = "myapp", force = true)
public class AppConfig {
@Overwrite(env = @Env("HOST"), prop = @Prop("server.host"))
private String host = "localhost";
}
This field reads MYAPP_HOST and myapp.server.host. Only the prefix is changed. Your own part is used exactly as
you wrote it.
Supported types
Environment variables and system properties are text. Ocular converts them to the type of the field:
Stringint,long,double,float,short,byteand their wrapper classesbooleanandBoolean.truein any case means true, everything else means false.- enums. An exact match of the constant name wins, otherwise the case is ignored, so
warnreads asWARN. - arrays,
ListandSetof the types above, written as a comma separated value likeINFO,WARN. Spaces around the values are removed. AListbecomes anArrayList, aSetaHashSet.
If a value cannot be converted, for example abc for an int or an unknown enum constant, Ocular logs a warning and
keeps the value from the file. The same happens for field types that are not in the list.
Methods
@Overwrite also works on methods. The generated names use the method name instead of the field name.
- A method with one parameter is called with the converted value. Use it like a setter.
- A method without parameters is treated as a getter. Ocular writes the value into the field behind it. It looks
for a field with the same name as the method, then for the field of a
getXorisXmethod, then for the longest field name the method name starts with. The field must have the return type of the method.
public class AppConfig {
private String greeting = "hello";
@Overwrite(prop = @Prop("app.greeting"))
public String greeting() {
return greeting;
}
}
Nested objects
Overrides also work on fields of nested objects, like a database section in your main class:
public class AppConfig {
private Database database = new Database();
}
public class Database {
@Overwrite(env = @Env, prop = @Prop)
private String host = "localhost";
}
The generated names use the class that declares the field, so this reads DATABASE_HOST and database.host.
Ocular only looks inside objects whose class lies in the root package of the key. That is the package of the configuration class and its sub packages. Objects of other classes, such as JDK types, are left alone. Collections, maps, arrays and static fields are skipped too.
If your nested classes live in another package, widen the root package on the key:
Key<AppConfig> key = Key.builder(Path.of("config.yml"), AppConfig::new)
.rootPackage("com.example")
.build();
Classes nested inside another class work too, for example a public static class Database inside AppConfig.
Their default prefix is their own simple name, database.
Reference file
The annotation processor writes a list of all overrides to META-INF/ocular/overrides.md in your compiled classes,
so it also ends up in your jar. With Gradle you find it at build/classes/java/main/META-INF/ocular/overrides.md.
# Ocular Override Reference
This file is auto-generated by the Ocular annotation processor.
It lists all available configuration overrides.
## com.example.AppConfig
### host
Host the server binds to
Sources (in priority order):
- Environment: `APP_HOST`
- Property: `app.host`
Ocular also logs the available overrides of a class each time it applies them, in the order they are tried:
Available overrides for AppConfig:
Property: app.host - Host the server binds to
Environment: APP_HOST - Host the server binds to
Under the hood
For a class com.example.AppConfig the processor generates com.example.AppConfig_OcularOverride. Its constructor
reads the environment variables and system properties and keeps every value it finds.
After Ocular reads a file, it loads that class with the class loader of the Configurations instance. If there is
no such class, because no field was marked, the object stays as it is.