How to Implement a Preprocessor to Transform Property Values Before Use in Owner
Implement the Preprocessor interface and register your class with the @PreprocessorClasses annotation to intercept and modify raw property strings before type conversion occurs.
The Owner configuration library (matteobaccan/owner) provides a powerful extension point that lets you sanitize, normalize, or decrypt property values immediately after they are read from the source but before they are converted to target types. By implementing a Preprocessor to transform property values before use, you can enforce consistent formatting across your entire configuration layer without changing the underlying property files.
Understanding the Preprocessor Pipeline
The transformation mechanism relies on four core components working in sequence. Understanding these files helps you debug and extend the behavior effectively.
Core Components
| Component | File Path | Purpose |
|---|---|---|
Preprocessor interface |
owner/src/main/java/org/aeonbits/owner/Preprocessor.java |
Defines the contract with a single process(String) method that receives the raw value and returns the transformed string. |
@PreprocessorClasses annotation |
owner/src/main/java/org/aeonbits/owner/Config.java (lines 58-68) |
Declares which preprocessor classes apply to a method or an entire config interface. |
PreprocessorResolver |
owner/src/main/java/org/aeonbits/owner/PreprocessorResolver.java |
Instantiates and orders the processors by reading annotations from the method first, then the declaring interface. |
PropertiesInvocationHandler |
owner/src/main/java/org/aeonbits/owner/PropertiesInvocationHandler.java (lines 97-103) |
Executes the preProcess method, feeding the raw property value through each resolved processor sequentially. |
Execution Flow
When you invoke a method on your config interface, Owner executes these steps:
- Read the raw property string from the underlying source (file, system property, environment variable).
- Resolve the list of preprocessors via
PreprocessorResolver.resolvePreprocessors. - Transform the value by calling
process(String)on each preprocessor in order. - Convert the final string to the declared return type (int, boolean, custom object, etc.).
Because preprocessing occurs before type conversion, you can modify the raw text without worrying about format exceptions, provided your preprocessor outputs a string compatible with the target converter.
Implementing a Custom Preprocessor to Transform Property Values
Creating your own preprocessor requires implementing the interface and registering it via annotations. Here is the complete workflow.
Step 1: Create the Preprocessor Class
Implement the org.aeonbits.owner.Preprocessor interface and override the process method. This example normalizes whitespace by collapsing multiple spaces and trimming the result.
package com.example.owner;
import org.aeonbits.owner.Preprocessor;
/**
* Normalizes whitespace in property values.
*/
public class WhitespaceNormalizer implements Preprocessor {
@Override
public String process(String input) {
if (input == null) {
return null;
}
return input.replaceAll("\\s+", " ").trim();
}
}
Step 2: Register with @PreprocessorClasses
Apply the @PreprocessorClasses annotation to your config interface or to individual methods. This example applies the preprocessor to the entire interface, affecting every property retrieval.
package com.example.owner;
import org.aeonbits.owner.Config;
import org.aeonbits.owner.Config.PreprocessorClasses;
@PreprocessorClasses({ WhitespaceNormalizer.class })
public interface ApplicationConfig extends Config {
@DefaultValue(" admin user ")
String adminUser(); // Returns "admin user"
@DefaultValue(" 127.0.0.1 ")
String serverHost(); // Returns "127.0.0.1"
}
Step 3: Method-Level vs Interface-Level Registration
You can scope preprocessors narrowly to specific methods or broadly to the entire configuration interface. Method-level annotations override interface-level behavior for that specific property.
@PreprocessorClasses({ GlobalPreprocessor.class }) // Applied to all methods
public interface MixedConfig extends Config {
@PreprocessorClasses({ SpecificPreprocessor.class }) // Applied first
@DefaultValue("value")
String specificProperty(); // Processing order: SpecificPreprocessor → GlobalPreprocessor
String normalProperty(); // Only GlobalPreprocessor applies
}
Preprocessor Execution Order
When both method-level and interface-level @PreprocessorClasses annotations are present, Owner concatenates the lists in a specific sequence defined in PreprocessorResolver.resolvePreprocessors.
The execution order is:
- Method-level preprocessors (declared directly on the invoked method)
- Interface-level preprocessors (declared on the config interface)
This ordering ensures that specific, local transformations occur before broader, global ones. For example, if you have a method-specific decryption preprocessor and an interface-wide trim preprocessor, the decryption runs first, then the trimming occurs.
Real-World Examples from the Owner Test Suite
The Owner library includes comprehensive tests demonstrating practical preprocessor implementations. The PreprocessorTest class in owner/src/test/java/org/aeonbits/owner/PreprocessorTest.java defines three useful examples as static inner classes:
Trim: Removes leading and trailing whitespace from property values.ToLowerCase: Converts the entire string to lowercase for case-insensitive configuration handling.SkipInlineComments: Strips inline comments (text following#or!) commonly found in property files.
These examples demonstrate that preprocessors can handle everything from simple string manipulation to complex parsing logic before Owner attempts type conversion.
Summary
Implementing a Preprocessor to transform property values before use in Owner involves these key steps:
- Implement the
org.aeonbits.owner.Preprocessorinterface and override theprocess(String)method to define your transformation logic. - Register your preprocessor class using the
@PreprocessorClassesannotation on either the config interface (global scope) or individual methods (local scope). - Understand that method-level preprocessors execute before interface-level ones, allowing you to chain specific and global transformations.
- Reference the source files
Preprocessor.java,Config.java,PreprocessorResolver.java, andPropertiesInvocationHandler.javato understand the internal execution pipeline.
Frequently Asked Questions
Can I apply multiple preprocessors to the same property?
Yes. The @PreprocessorClasses annotation accepts an array of classes, and they execute in the order declared. Additionally, you can combine method-level and interface-level annotations to create complex transformation chains. For example, you might declare { Trim.class, ToLowerCase.class } to first remove whitespace and then convert to lowercase.
What happens if my preprocessor returns null?
If your process method returns null, Owner passes that null value to the type converter. Depending on the target type, this may result in a null value for object types or a conversion exception for primitive types. Always ensure your preprocessor handles null inputs gracefully, typically by returning null unchanged or providing a default string representation.
Can preprocessors access the property key or configuration metadata?
No. The Preprocessor interface only receives the raw property value as a String. It does not have access to the property key, the method being invoked, or other metadata. If you need context-aware transformations, consider implementing a custom Converter instead, which receives the Method object and can access annotations and other metadata.
Are preprocessors instantiated once or on every property access?
Owner instantiates preprocessor classes once per configuration interface creation and caches the instances. The PreprocessorResolver creates the instances when resolving the preprocessor list, and these same instances are reused for all subsequent property retrievals on that config object. Therefore, preprocessors should be stateless or thread-safe if they maintain any internal state.
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 →