How to Implement a Custom ZooKeeper Loader from the Owner-Extras Module
Implement the Loader interface from org.aeonbits.owner.loaders, handle the zookeeper URI scheme in accept(URI), extract properties using Apache Curator in load(Properties, URI), and register your class via ConfigFactory.registerLoader() before creating the configuration instance.
The owner-extras module in the matteobaccan/owner repository ships with a reference ZooKeeperLoader that demonstrates how to bridge Owner’s type-safe configuration interfaces with Apache ZooKeeper. Creating a custom ZooKeeper loader lets you customize node traversal, connection retry policies, and serialization while preserving the clean @Sources annotation-driven configuration model.
Understanding the Loader Contract in Owner
Every configuration loader in the Owner framework must implement the Loader interface located at owner/src/main/java/org/aeonbits/owner/loaders/Loader.java. This contract defines three methods that control how Owner resolves and reads external configuration sources:
boolean accept(URI uri)– Returnstrueonly for URIs your loader can handle (e.g., schemezookeeper).void load(Properties result, URI uri)– Connects to the external source, reads data, and populates the suppliedPropertiesobject.String defaultSpecFor(String urlPrefix)– Optionally returns a default URL when none is declared in@Sources; returnnullif unsupported.
The Properties object passed into load acts as a mutable map; keys and values you insert become accessible through the typed configuration interface.
Anatomy of the Built-In ZooKeeperLoader
The reference implementation in owner-extras/src/main/java/org/aeonbits/owner/loaders/ZooKeeperLoader.java demonstrates the canonical pattern for ZooKeeper integration. Study its structure before extending it:
- Scheme detection – The
acceptmethod checksuri.getScheme().equals("zookeeper"). - Client construction – The private
getClient(URI)method builds a CuratorCuratorFrameworkusing the host and port from the URI. - Connection lifecycle – Inside
load, the client starts withclient.start()and blocks until connected usingclient.blockUntilConnected(timeout, TimeUnit.SECONDS), respecting the system propertyowner.zookeeper.connection.timeout.seconds(default 30 seconds). - Node iteration – The loader lists children of the base path (
uri.getPath), retrieves data for each child viaclient.getData().forPath(), and stores entries inresult.put(key, value). - Resource cleanup – The client closes in a
finallyblock to prevent connection leaks.
If the base path does not exist, the loader returns an empty Properties map, causing all configuration methods to yield null.
Creating Your Custom ZooKeeper Loader
To build a specialized loader—perhaps one that supports recursive node traversal or custom authentication—follow this implementation pattern.
Step 1: Implement the Loader Interface
Create a new class that implements org.aeonbits.owner.loaders.Loader. Import org.apache.curator.framework.CuratorFramework and CuratorFrameworkFactory to handle ZooKeeper connectivity.
package com.example.config;
import org.aeonbits.owner.loaders.Loader;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.utils.ZKPaths;
import java.io.IOException;
import java.net.URI;
import java.util.Properties;
public class RecursiveZooKeeperLoader implements Loader {
private static final String SCHEME = "zookeeper";
@Override
public boolean accept(URI uri) {
return SCHEME.equals(uri.getScheme());
}
// ... remaining methods
}
Step 2: Define Scheme Acceptance
The accept method must strictly filter URIs to avoid conflicts with other loaders. Return true only when the scheme matches your target protocol.
@Override
public boolean accept(URI uri) {
return "zookeeper".equals(uri.getScheme());
}
Step 3: Implement Property Loading
In load(Properties result, URI uri), instantiate the Curator client, connect, and populate the properties map. Always wrap the connection in a try-finally block to ensure client.close() runs.
@Override
public void load(Properties result, URI uri) throws IOException {
String connectString = uri.getHost() + (uri.getPort() == -1 ? "" : ":" + uri.getPort());
CuratorFramework client = CuratorFrameworkFactory.newClient(connectString,
(retry, elapsed, sleeper) -> false);
try {
client.start();
client.blockUntilConnected(30, java.util.concurrent.TimeUnit.SECONDS);
String basePath = uri.getPath();
for (String child : client.getChildren().forPath(basePath)) {
String fullPath = ZKPaths.makePath(basePath, child);
byte[] data = client.getData().forPath(fullPath);
if (data != null) {
result.put(child, new String(data));
}
}
} catch (Exception e) {
throw new IOException("Failed to load from ZooKeeper: " + uri, e);
} finally {
client.close();
}
}
Step 4: Optional Default URL Support
Override defaultSpecFor only if your loader should supply a fallback URL when the @Sources annotation is omitted. The built-in ZooKeeperLoader returns null, forcing explicit source declaration.
@Override
public String defaultSpecFor(String urlPrefix) {
return null; // No default ZooKeeper ensemble
}
Registering and Consuming the Loader
Before creating a configuration instance, you must register your custom loader with a ConfigFactory. The factory located at owner/src/main/java/org/aeonbits/owner/ConfigFactory.java maintains a registry of loaders and dispatches to them based on URI schemes.
import org.aeonbits.owner.ConfigFactory;
// Define the configuration contract
@Sources("zookeeper://127.0.0.1:2181/appConfig")
public interface AppConfig extends Config {
String databaseUrl();
Integer connectionPoolSize();
}
// Register the custom loader and instantiate
ConfigFactory factory = ConfigFactory.newInstance();
factory.registerLoader(new RecursiveZooKeeperLoader());
AppConfig cfg = factory.create(AppConfig.class);
System.out.println(cfg.databaseUrl());
The test suite in owner-extras/src/test/java/org/aeonbits/owner/loaders/ZooKeeperLoaderTest.java demonstrates this registration flow using an embedded ZooKeeper server, verifying that properties correctly map from ZNodes to interface methods.
Summary
- Implement
org.aeonbits.owner.loaders.Loaderand handle thezookeeperscheme inaccept(URI). - Use Apache Curator (
CuratorFramework) to connect, read children nodes, and populate thePropertiesmap insideload(Properties, URI). - Close the Curator client in a
finallyblock to prevent connection leaks. - Register your implementation via
ConfigFactory.registerLoader()before callingcreate(Class). - Return
nullfromdefaultSpecForunless you require a default ZooKeeper connection string.
Frequently Asked Questions
What interface must a custom ZooKeeper loader implement?
Any custom loader must implement org.aeonbits.owner.loaders.Loader, defining accept(URI), load(Properties, URI), and defaultSpecFor(String). This interface allows Owner to delegate URI resolution and property extraction to your code.
How do I register my custom loader with the Owner framework?
Call ConfigFactory.newInstance().registerLoader(new YourLoader()) prior to creating the configuration interface. The factory caches registered loaders and routes @Sources URIs to the first loader whose accept method returns true.
Can I configure the ZooKeeper connection timeout in my custom loader?
Yes. You can respect the standard property owner.zookeeper.connection.timeout.seconds (default 30 seconds) as shown in the reference ZooKeeperLoader, or implement custom timeout logic inside load by adjusting the parameters passed to client.blockUntilConnected().
What happens if the ZooKeeper base path does not exist?
The loader returns an empty Properties map. Consequently, all methods on the configuration interface return null (or default values if specified via @DefaultValue), and no exception is thrown during proxy creation.
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 →