How to Load Properties from Classpath, File System, and Custom URIs in OWNER
Use the @Config.Sources annotation with classpath:, file:, or custom protocol prefixes, and implement the Loader interface to add support for proprietary URI schemes.
The OWNER library—maintained in the matteobaccan/owner repository—provides a type-safe way to map Java properties files to interfaces. Understanding how to load properties from classpath, file system, and custom URIs allows you to externalize configuration while keeping your code clean and portable.
Loading Properties from the Classpath
Using the classpath: Protocol
OWNER treats the classpath: prefix as a first-class citizen. When you annotate an interface with @Config.Sources({"classpath:path/to/file.properties"}), the library delegates to ConfigURIFactory.newURI(String) in [ConfigURIFactory.java](https://github.com/matteobaccan/owner/blob/master/owner/src/main/java/org/aeonbits/owner/ConfigURIFactory.java). This method strips the prefix and invokes ClassLoader.getResource() to locate the resource relative to the root of the classpath (typically src/main/resources or inside a JAR).
import org.aeonbits.owner.Config;
import org.aeonbits.owner.ConfigFactory;
@Config.Sources({"classpath:org/aeonbits/owner/config.properties"})
public interface ServerConfig extends Config {
@Key("server.host")
String host();
@Key("server.port")
int port();
}
// Usage
ServerConfig cfg = ConfigFactory.create(ServerConfig.class);
System.out.println(cfg.host() + ":" + cfg.port());
Handling Spaces in Paths
The classpath loader correctly handles paths containing spaces. In [LoadPathsWithSpacesTest.java](https://github.com/matteobaccan/owner/blob/master/owner/src/test/java/org/aeonbits/owner/loadstrategies/LoadPathsWithSpacesTest.java), the test demonstrates loading classpath:org/aeonbits/owner/directory with spaces/simple.properties. ConfigURIFactory normalizes back-slashes to forward-slashes and preserves the space characters, which the class loader resolves correctly.
Loading Properties from the File System
Using the file: Protocol with Variable Expansion
To load from an absolute or relative file path, use the file: prefix. ConfigURIFactory expands ${...} variables (such as ${user.dir} or custom properties) before creating the URI. It also encodes spaces to %20 to ensure valid URI syntax.
@Config.Sources({
"file:${user.dir}/config/app.properties",
"file:/etc/myapp/production.properties"
})
public interface AppConfig extends Config {
@Key("database.url")
String dbUrl();
}
PropertiesManager (in [PropertiesManager.java](https://github.com/matteobaccan/owner/blob/master/owner/src/main/java/org/aeonbits/owner/PropertiesManager.java)) reads the @Sources annotation, iterates over each string, and delegates to ConfigURIFactory to produce a java.net.URI object. These URIs are then passed to LoadersManager for resolution.
Loading Properties from Custom URIs
When the built-in classpath: and file: schemes are insufficient, you can implement custom protocols (such as zookeeper:, consul:, or https: with special handling) by providing your own Loader.
Implementing the Loader Interface
The contract is defined in [Loader.java](https://github.com/matteobaccan/owner/blob/master/owner/src/main/java/org/aeonbits/owner/loaders/Loader.java). A valid implementation must provide three methods:
boolean accept(URI uri)– returnstrueif this loader handles the given scheme.void load(Properties result, URI uri)– reads the resource and populates thePropertiesinstance.String defaultSpecFor(String uriPrefix)– returns a default URI string for the given prefix.
import org.aeonbits.owner.loaders.Loader;
import java.net.URI;
import java.util.Properties;
import java.io.IOException;
public class HttpLoader implements Loader {
@Override
public boolean accept(URI uri) {
return "http".equals(uri.getScheme()) || "https".equals(uri.getScheme());
}
@Override
public void load(Properties result, URI uri) throws IOException {
// Implementation would perform HTTP GET and parse response
// For demonstration, we inject a dummy value:
result.setProperty("remote.config", "loadedFromHttp");
}
@Override
public String defaultSpecFor(String uriPrefix) {
return "https://default.example.com/config";
}
}
Registering Your Custom Loader
Before creating any configuration interface that uses the custom scheme, register the loader with ConfigFactory. This updates the internal LoadersManager registry (see [LoadersManager.java](https://github.com/matteobaccan/owner/blob/master/owner/src/main/java/org/aeonbits/owner/LoadersManager.java)).
import org.aeonbits.owner.ConfigFactory;
// Register once at application startup
ConfigFactory.registerLoader(new HttpLoader());
// Now the factory can resolve https:// URIs
@Config.Sources({"https://api.example.com/config/app.properties"})
public interface RemoteConfig extends Config {
String remoteConfig();
}
Real-World Example: ZooKeeperLoader
The owner-extras module provides a concrete reference implementation. [ZooKeeperLoader.java](https://github.com/matteobaccan/owner/blob/master/owner-extras/src/main/java/org/aeonbits/owner/loaders/ZooKeeperLoader.java) implements the zookeeper: protocol. It accepts URIs with that scheme, connects to the ZooKeeper ensemble, reads the znode data, and loads it into a Properties object. This demonstrates how to integrate OWNER with external configuration stores.
Summary
- Classpath loading uses the
classpath:prefix and resolves resources via the class loader, supporting spaces in paths. - Filesystem loading uses the
file:prefix with optional variable expansion (${user.dir}) handled byConfigURIFactory. - Custom protocols require implementing the
Loaderinterface, registering it viaConfigFactory.registerLoader(), and referencing the new scheme in@Config.Sources. - The architecture delegates URI creation to
ConfigURIFactory, loader selection toLoadersManager, and property reading to specificLoaderimplementations such asPropertiesLoader,XMLLoader, or your custom class.
Frequently Asked Questions
Can I load from multiple sources at once?
Yes. The @Config.Sources annotation accepts an array of URI strings. OWNER loads them in the order declared, and later sources override earlier ones for duplicate keys. This is managed internally by PropertiesManager which iterates over the source list and merges properties into a single configuration instance.
How does OWNER resolve conflicting properties from different sources?
OWNER uses a first-declared, last-wins strategy. When LoadersManager processes the list of URIs from @Sources, it loads each resource sequentially into the same Properties object. If a key exists in multiple sources, the value from the last loaded source overwrites the previous value. You can verify this behavior in the loading logic within PropertiesManager.
Can I use environment variables in file paths?
Yes. ConfigURIFactory performs variable expansion on source strings before converting them to URIs. You can reference any system property or environment variable using the ${variable.name} syntax. For example, file:${user.home}/app/config.properties resolves to the user's home directory, and file:${ENV_CONFIG_PATH} expands to the value of the ENV_CONFIG_PATH environment variable if defined.
Is it possible to reload properties at runtime?
OWNER creates immutable configuration instances by default, but you can achieve dynamic reloading by creating a new instance via ConfigFactory.create() whenever you need fresh values. For true hot-reloading, you would need to implement a custom Loader that caches values with a TTL or watches the underlying resource (file, HTTP endpoint, etc.) for changes, then exposes a method to invalidate the cache before re-creating the config interface.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →