Configuration Overrides
Ocular supports overriding configuration values at runtime using environment variables and JVM system properties. This is useful for deploying the same application in different environments (e.g. development, staging, production) without changing configuration files.
How It Works
The override system has two phases:
- Compile time — An annotation processor scans your configuration classes and generates helper code that knows which environment variables and system properties to read.
- Runtime — After a configuration file is loaded, Ocular automatically checks for override values and applies them to the configuration object.
You annotate fields in your configuration class, and Ocular takes care of the rest.
Setup
Add the annotation processor to your build so it runs during compilation:
Annotating Configuration Fields
Use the @Overwrite annotation on any field you want to be overridable. Inside @Overwrite, you specify the sources to check:
@Env— read from an environment variable@Prop— read from a JVM system property (-Dflag)
Basic Example
import dev.chojo.ocular.override.Overwrite;
import dev.chojo.ocular.override.Env;
import dev.chojo.ocular.override.Prop;
public class AppConfig {
@Overwrite(env = @Env("APP_HOST"), prop= @Prop("app.host"))
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 configuration:
- Setting the environment variable
APP_HOST=example.comwill overridehostto"example.com". - Running with
-Dapp.port=9090will overrideportto9090. - If neither an environment variable nor a system property is set, the original value from the configuration file is kept.
Automatic Name Derivation
If you don't provide an explicit name, Ocular derives one automatically:
@Env()(no name) →CLASSNAME_FIELDNAMEin uppercase. For example, fieldhostin classAppConfigbecomesAPPCONFIG_HOST.@Prop()(no name) →classname.fieldNamein lowercase dot notation. For example, fieldhostin classAppConfigbecomesappconfig.host.
public class AppConfig {
@Overwrite(env = @Env(), prop= @Prop())
private String host = "localhost";
// Checks env var APPCONFIG_HOST and system property appconfig.host
}
Precedence
The order of env and prop inside @Overwrite determines priority. The first source declared wins — once a value is found, later sources are ignored.
// System property is checked first, then environment variable.
// If both are set, the system property wins (declared first).
@Overwrite(prop= @Prop("app.host"), env = @Env("APP_HOST"))
private String host;
// Environment variable is checked first, then system property.
// If both are set, the environment variable wins (declared first).
@Overwrite(env = @Env("APP_HOST"), prop= @Prop("app.host"))
private String host;
Custom Prefix with @OverwritePrefix
By default, automatically derived names use the class name as a prefix (e.g. APPCONFIG_HOST). You can replace this prefix by annotating the class with @OverwritePrefix:
import dev.chojo.ocular.override.OverwritePrefix;
@OverwritePrefix("myapp")
public class AppConfig {
@Overwrite(env = @Env(), prop= @Prop())
private String host = "localhost";
// Checks env var MYAPP_HOST and system property myapp.host
}
When an explicit name is provided in @Env or @Prop, the prefix is not applied by default:
@OverwritePrefix("myapp")
public class AppConfig {
@Overwrite(env = @Env("CUSTOM_HOST"))
private String host = "localhost";
// Checks env var CUSTOM_HOST (not MYAPP_CUSTOM_HOST)
}
Forcing the Prefix
Set force = true to always prepend the prefix, even when an explicit name is given:
@OverwritePrefix(value = "myapp", force = true)
public class AppConfig {
@Overwrite(env = @Env("HOST"), prop= @Prop("port"))
private String host = "localhost";
// Checks env var MYAPP_HOST and system property myapp.port
}
With force = true:
- Env names become
PREFIX_NAME(e.g.MYAPP_HOST). - Prop names become
prefix.name(e.g.myapp.port).
Supported Types
The override system supports the following field types. Values from environment variables and system properties (which are always strings) are automatically converted:
Stringint/Integerlong/Longdouble/Doublefloat/Floatboolean/Booleanshort/Shortbyte/Byte
What Happens Under the Hood
When you compile your project, the annotation processor:
- Finds every field annotated with
@Overwrite. - For each configuration class, generates a helper class named
<ClassName>_OcularOverridein the same package. - The generated class reads the specified environment variables and system properties and stores any found values in a map.
At runtime, after Ocular reads a configuration file:
- It looks for the generated
_OcularOverrideclass via reflection. - If found, it instantiates the class (which reads env vars and system properties).
- It applies any override values to the matching fields on the configuration object, converting types as needed.
If no override class is found (e.g. no fields were annotated), the configuration is used as-is.