Externalized Properties makes the best use of Java's strong typing by using Java's dynamic proxy feature.
It works by creating dynamic/configurable proxy instances (created at runtime by Java) that implement user-defined interfaces as facade to resolve properties.
Properties are mapped to proxy interface methods by using the @ExternalizedProperty annotation.
public interface ApplicationProperties {
@ExternalizedProperty("java.home")
String javaHome();
@ExternalizedProperty("java.version")
String javaVersion();
}
Externalized Properties supports default values by using Java's default interface methods e.g.
public interface ApplicationProperties {
@ExternalizedProperty("my.property")
default String myProperty() {
// If "my.property" cannot be resolved,
// "Default Value" will be returned.
return "Default Value";
}
@ExternalizedProperty("my.property")
default String myPropertyOrDefault(String defaultValue) {
// If "my.property" cannot be resolved,
// The variable defaultValue will be returned.
return defaultValue;
}
}
Externalized Properties supports resolution of properties whose names are not known at compile time. This is made possible by the @ResolverFacade annotation e.g.
(Kindly see @ResolverFacade documentation to learn more about the rules of defining a resolver facade.)
public interface ApplicationProperties {
@ResolverFacade
String resolve(String propertyName);
@ResolverFacade
int resolveInt(String propertyName);
@ResolverFacade
<T> T resolve(String propertyName, Class<T> targetType);
@ResolverFacade
<T> T resolve(String propertyName, TypeReference<T> targetType);
}
Externalized Properties can support any configuration file/resource format via the ResourceResolver and ResourceReader classes.
public class App {
public static void main(String[] args) throws IOException {
ExternalizedProperties externalizedProperties = ExternalizedProperties.builder()
.resolvers(
propertiesFileResolver(),
yamlFileResolver(),
jsonFileResolver(),
xmlFileResolver()
)
.build();
}
private static ResourceResolver propertiesFileResolver() throws IOException {
// By default, ResourceResolver expects the resource to be in properties format.
return ResourceResolver.fromUrl(
App.class.getResource("/properties-sample/application.properties"));
}
private static ResourceResolver yamlFileResolver() throws IOException {
return ResourceResolver.fromUrl(
App.class.getResource("/yaml-sample/application.yaml"),
new YamlReader());
}
private static ResourceResolver jsonFileResolver() throws IOException {
return ResourceResolver.fromUrl(
App.class.getResource("/json-sample/application.json"),
new JsonReader());
}
private static ResourceResolver xmlFileResolver() throws IOException {
return ResourceResolver.fromUrl(
App.class.getResource("/xml-sample/application.xml"),
new XmlReader());
}
}
// Example uses Jackson ObjectMapper, but any other libraries will do.
private class YamlReader implements ResourceReader {
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
@Override
public Map<String, Object> read(String resourceContents) throws IOException {
return yamlMapper.readValue(
resourceContents,
new TypeReference<Map<String, Object>>() {});
}
}
// Example uses Jackson ObjectMapper, but any other libraries will do.
private class JsonReader implements ResourceReader {
private final ObjectMapper jsonMapper = new ObjectMapper();
@Override
public Map<String, Object> read(String resourceContents) throws IOException {
return jsonMapper.readValue(
resourceContents,
new TypeReference<Map<String, Object>>() {});
}
}
// Example uses Jackson ObjectMapper, but any other libraries will do.
private class XmlReader implements ResourceReader {
private final ObjectMapper xmlMapper = new ObjectMapper(new XmlFactory());
@Override
public Map<String, Object> read(String resourceContents) throws IOException {
return xmlMapper.readValue(
resourceContents,
new TypeReference<Map<String, Object>>() {});
}
}
Caching is enabled by default, but when not using defaults, it can be enabled via the ExternalizedProperties builder. All proxies created by the resulting ExternalizedProperties instance will cache resolved properties.
public static void main(String[] args) {
ExternalizedProperties externalizedProperties = ExternalizedProperties.builder()
.defaults()
// Cache initialized proxy instances.
.enableInitializeCaching()
// Cache results of proxy method invocations.
.enableInvocationCaching()
// Default is 30 minutes.
.cacheDuration(Duration.ofMinutes(10))
.build();
// This proxy will cache any resolved properties.
ApplicationProperties appProperties =
externalizedProperties.initialize(ApplicationProperties.class);
}
Eager loading is opt-in and can be enabled via the ExternalizedProperties builder. All proxies created by the resulting ExternalizedProperties instance will eagerly load properties on initialization.
private static void main(String[] args) {
ExternalizedProperties externalizedProperties = ExternalizedProperties.builder()
.defaults()
// Eager load properties.
.enableEagerLoading()
// Default is 30 minutes.
.cacheDuration(Duration.ofMinutes(10))
.build();
// This proxy should already have its properties loaded.
ApplicationProperties appProperties =
externalizedProperties.initialize(ApplicationProperties.class);
}
At the heart of Externalized Properties are the Resolvers. Instances of these interface are responsible for resolving requested properties.
Creating a custom resolver is as easy as implementing the Resolver interface and registering the resolver via the ExternalizedProperties builder.
public class MyCustomResolver implements Resolver {
@Override
public Optional<String> resolve(InvocationContext context, String propertyName) {
return Optional.ofNullable(...);
}
}
public interface ApplicationProperties {
@ExternalizedProperty("my.property")
MyCustomType myProperty();
}
private static void main(String[] args) {
ExternalizedProperties externalizedProperties = ExternalizedProperties.builder()
// Register custom resolvers here.
.resolvers(new MyCustomResolver())
.build();
ApplicationProperties appProperties =
externalizedProperties.initialize(ApplicationProperties.class);
// Resolved from MyCustomResolver.
String myProperty = appProperties.myProperty();
}