Dynamic Configuration: Mastering File Templating in Java

Configuration file templating is a powerful technique for generating dynamic configuration files from templates with variable substitution, conditional logic, and looping constructs. Instead of maintaining multiple static configuration files, you can create smart templates that adapt to different environments, deployment scenarios, or runtime conditions. This article explores various approaches to configuration file templating in Java, from simple variable replacement to advanced template engines.


Why Configuration Templating?

Common Use Cases:

  • Environment-specific configurations (dev, staging, production)
  • Multi-tenant applications with tenant-specific settings
  • Dynamic configuration based on deployment infrastructure
  • Internationalization and localization of configuration
  • Generating configuration for cloud resources and containers

Benefits:

  • Reduced duplication: Single template vs multiple config files
  • Consistency: Uniform structure across environments
  • Maintainability: Changes propagate through all generated configurations
  • Dynamic values: Incorporate runtime information and calculations

Approach 1: Simple Property Replacement with Java Built-ins

For basic variable substitution, Java's built-in capabilities may be sufficient.

public class SimpleTemplateEngine {
public String processTemplate(String template, Map<String, String> variables) {
String result = template;
for (Map.Entry<String, String> entry : variables.entrySet()) {
String placeholder = "${" + entry.getKey() + "}";
result = result.replace(placeholder, entry.getValue());
}
return result;
}
// Usage example
public static void main(String[] args) {
SimpleTemplateEngine engine = new SimpleTemplateEngine();
String template = """
server:
host: ${server.host}
port: ${server.port}
database:
url: jdbc:mysql://${db.host}:${db.port}/${db.name}
username: ${db.user}
""";
Map<String, String> variables = Map.of(
"server.host", "localhost",
"server.port", "8080",
"db.host", "db-server",
"db.port", "3306",
"db.name", "myapp",
"db.user", "admin"
);
String config = engine.processTemplate(template, variables);
System.out.println(config);
}
}

Output:

server:
host: localhost
port: 8080
database:
url: jdbc:mysql://db-server:3306/myapp
username: admin

Approach 2: Apache Commons Text StringSubstitutor

For more robust variable substitution with escape characters and default values.

import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.lookup.StringLookup;
public class CommonsTextTemplating {
public String processTemplate(String template, Map<String, String> variables) {
StringSubstitutor substitutor = new StringSubstitutor(variables);
substitutor.setEnableSubstitutionInVariables(true);
substitutor.setEscapeChar('\\');
return substitutor.replace(template);
}
// Advanced: With default values and environment variables
public String processWithDefaults(String template, Map<String, String> variables) {
StringLookup variableLookup = key -> {
// Check provided variables first
if (variables.containsKey(key)) {
return variables.get(key);
}
// Then check system properties
String systemValue = System.getProperty(key);
if (systemValue != null) {
return systemValue;
}
// Then check environment variables
String envValue = System.getenv(key.toUpperCase().replace('.', '_'));
if (envValue != null) {
return envValue;
}
// Return default pattern or empty
return "${" + key + "}"; // Keep unresolved
};
StringSubstitutor substitutor = new StringSubstitutor(variableLookup);
return substitutor.replace(template);
}
// Usage example with default values in template
public static void main(String[] args) {
CommonsTextTemplating engine = new CommonsTextTemplating();
String template = """
app:
name: ${app.name:MyApplication}
version: ${app.version:1.0.0}
environment: ${ENVIRONMENT:development}
max_memory: ${app.memory:512MB}
logging:
level: ${log.level:INFO}
file: /var/log/${app.name}/app.log
""";
Map<String, String> variables = Map.of(
"app.name", "OrderService",
"ENVIRONMENT", "production"
// app.version and log.level will use defaults
);
String config = engine.processWithDefaults(template, variables);
System.out.println(config);
}
}

Maven Dependency:

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>

Approach 3: Freemarker Template Engine

For complex templates with conditionals, loops, and advanced logic.

import freemarker.template.*;
import java.io.*;
import java.util.*;
public class FreemarkerTemplateEngine {
private final Configuration configuration;
public FreemarkerTemplateEngine() throws IOException {
this.configuration = new Configuration(Configuration.VERSION_2_3_31);
this.configuration.setDefaultEncoding("UTF-8");
this.configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
this.configuration.setLogTemplateExceptions(false);
this.configuration.setWrapUncheckedExceptions(true);
this.configuration.setFallbackOnNullLoopVariable(false);
}
// Process template from string
public String processTemplate(String templateContent, Map<String, Object> dataModel) {
try {
Template template = new Template("name", new StringReader(templateContent), configuration);
StringWriter writer = new StringWriter();
template.process(dataModel, writer);
return writer.toString();
} catch (TemplateException | IOException e) {
throw new RuntimeException("Template processing failed", e);
}
}
// Process template from file
public String processTemplateFile(String templatePath, Map<String, Object> dataModel) {
try {
Template template = configuration.getTemplate(templatePath);
StringWriter writer = new StringWriter();
template.process(dataModel, writer);
return writer.toString();
} catch (TemplateException | IOException e) {
throw new RuntimeException("Template processing failed", e);
}
}
// Usage example with complex data
public static void main(String[] args) throws Exception {
FreemarkerTemplateEngine engine = new FreemarkerTemplateEngine();
String template = """
<#-- Freemarker template with conditionals and loops -->
application:
name: "${app.name}"
version: "${app.version}"
environment: "${environment}"
<#if environment == "production">
server:
port: 443
ssl:
enabled: true
keystore: "/etc/ssl/prod-keystore.jks"
<#else>
server:
port: 8080
ssl:
enabled: false
</#if>
database:
primary:
url: "jdbc:mysql://${db.primary.host}:${db.primary.port}/${db.primary.name}"
username: "${db.primary.user}"
<#if db.replicas?has_content>
replicas:
<#list db.replicas as replica>
replica${replica?index}:
url: "jdbc:mysql://${replica.host}:${replica.port}/${replica.name}"
</#list>
</#if>
features:
<#list features as feature>
${feature.name}: ${feature.enabled?c}
</#list>
""";
// Complex data model
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("app.name", "ECommercePlatform");
dataModel.put("app.version", "2.1.0");
dataModel.put("environment", "production");
dataModel.put("db.primary.host", "primary-db.cluster.local");
dataModel.put("db.primary.port", "3306");
dataModel.put("db.primary.name", "ecommerce");
dataModel.put("db.primary.user", "app_user");
// List of replicas
List<Map<String, String>> replicas = new ArrayList<>();
replicas.add(Map.of("host", "replica1-db.cluster.local", "port", "3306", "name", "ecommerce"));
replicas.add(Map.of("host", "replica2-db.cluster.local", "port", "3306", "name", "ecommerce"));
dataModel.put("db.replicas", replicas);
// Features list
List<Map<String, Object>> features = new ArrayList<>();
features.add(Map.of("name", "caching", "enabled", true));
features.add(Map.of("name", "analytics", "enabled", false));
features.add(Map.of("name", "notifications", "enabled", true));
dataModel.put("features", features);
String config = engine.processTemplate(template, dataModel);
System.out.println(config);
}
}

Maven Dependency:

<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>

Generated Output:

application:
name: "ECommercePlatform"
version: "2.1.0"
environment: "production"
server:
port: 443
ssl:
enabled: true
keystore: "/etc/ssl/prod-keystore.jks"
database:
primary:
url: "jdbc:mysql://primary-db.cluster.local:3306/ecommerce"
username: "app_user"
replicas:
replica0:
url: "jdbc:mysql://replica1-db.cluster.local:3306/ecommerce"
replica1:
url: "jdbc:mysql://replica2-db.cluster.local:3306/ecommerce"
features:
caching: true
analytics: false
notifications: true

Approach 4: Thymeleaf for Configuration Templating

While typically used for HTML, Thymeleaf is excellent for any text-based templating.

import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.StringTemplateResolver;
public class ThymeleafTemplateEngine {
private final TemplateEngine templateEngine;
public ThymeleafTemplateEngine() {
this.templateEngine = new TemplateEngine();
StringTemplateResolver templateResolver = new StringTemplateResolver();
templateResolver.setTemplateMode(TemplateMode.TEXT);
templateResolver.setCacheable(false);
this.templateEngine.setTemplateResolver(templateResolver);
}
public String processTemplate(String template, Map<String, Object> variables) {
Context context = new Context();
variables.forEach(context::setVariable);
return templateEngine.process(template, context);
}
// Usage example
public static void main(String[] args) {
ThymeleafTemplateEngine engine = new ThymeleafTemplateEngine();
String template = """
application:
name: "[[${appName}]]"
version: "[[${version}]]"
environment: "[[${environment}]]"
[# th:if="${production}"]
security:
ssl: required
cors:
allowed_origins: ["https://example.com"]
[/]
[# th:unless="${production}"]
security:
ssl: optional
cors:
allowed_origins: ["*"]
[/]
endpoints:
[# th:each="endpoint : ${endpoints}"]
- name: "[[${endpoint.name}]]"
path: "[[${endpoint.path}]]"
enabled: "[[${endpoint.enabled}]]"
[/]
""";
Map<String, Object> variables = new HashMap<>();
variables.put("appName", "ApiGateway");
variables.put("version", "1.5.2");
variables.put("environment", "staging");
variables.put("production", false);
List<Map<String, Object>> endpoints = new ArrayList<>();
endpoints.add(Map.of("name", "health", "path", "/health", "enabled", true));
endpoints.add(Map.of("name", "metrics", "path", "/metrics", "enabled", true));
endpoints.add(Map.of("name", "admin", "path", "/admin", "enabled", false));
variables.put("endpoints", endpoints);
String config = engine.processTemplate(template, variables);
System.out.println(config);
}
}

Maven Dependency:

<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>

Approach 5: JSON/XML/YAML-Specific Templating

For structured configuration formats, specialized libraries can be more appropriate.

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
public class JsonYamlTemplating {
private final ObjectMapper jsonMapper = new ObjectMapper();
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
// Template JSON configuration
public String processJsonTemplate(String jsonTemplate, Map<String, Object> variables) {
try {
String processed = jsonTemplate;
for (Map.Entry<String, Object> entry : variables.entrySet()) {
String placeholder = "\\$\\{" + entry.getKey() + "\\}";
processed = processed.replaceAll(placeholder, 
jsonMapper.writeValueAsString(entry.getValue()).replace("\"", ""));
}
return processed;
} catch (Exception e) {
throw new RuntimeException("JSON template processing failed", e);
}
}
// Advanced: Using JsonPath for targeted replacements
public String processWithJsonPath(String jsonTemplate, Map<String, Object> pathValues) {
try {
DocumentContext document = JsonPath.parse(jsonTemplate);
for (Map.Entry<String, Object> entry : pathValues.entrySet()) {
document.set(JsonPath.compile(entry.getKey()), entry.getValue());
}
return document.jsonString();
} catch (Exception e) {
throw new RuntimeException("JsonPath processing failed", e);
}
}
// Usage examples
public static void main(String[] args) throws Exception {
JsonYamlTemplating engine = new JsonYamlTemplating();
// Simple replacement
String jsonConfig = """
{
"database": {
"host": "${db_host}",
"port": ${db_port},
"name": "${db_name}"
},
"features": {
"caching": ${feature_caching},
"logging": ${feature_logging}
}
}
""";
Map<String, Object> variables = Map.of(
"db_host", "localhost",
"db_port", 5432,
"db_name", "myapp",
"feature_caching", true,
"feature_logging", false
);
String result = engine.processJsonTemplate(jsonConfig, variables);
System.out.println("Simple JSON Template Result:");
System.out.println(result);
// JsonPath example
String existingConfig = """
{
"server": {
"port": 8080,
"host": "old-host"
},
"database": {
"connection": {
"max_pool_size": 10
}
}
}
""";
Map<String, Object> pathUpdates = Map.of(
"$.server.host", "new-production-host",
"$.server.port", 443,
"$.database.connection.max_pool_size", 50,
"$.database.connection.timeout", 30000  // Add new field
);
String updatedConfig = engine.processWithJsonPath(existingConfig, pathUpdates);
System.out.println("\nJsonPath Updated Config:");
System.out.println(updatedConfig);
}
}

Dependencies:

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.8.0</version>
</dependency>

Approach 6: Environment-Aware Template Manager

A comprehensive solution that combines multiple approaches.

import java.util.*;
import java.util.regex.*;
public class EnvironmentAwareTemplateManager {
private final Map<String, Object> baseVariables;
private final TemplateEngine templateEngine;
public EnvironmentAwareTemplateManager() {
this.baseVariables = new HashMap<>();
this.templateEngine = new FreemarkerTemplateEngine(); // Or any other engine
// Initialize with system properties and environment variables
initializeBaseVariables();
}
private void initializeBaseVariables() {
// System properties
System.getProperties().forEach((key, value) -> 
baseVariables.put("system." + key, value));
// Environment variables  
System.getenv().forEach((key, value) -> 
baseVariables.put("env." + key.toLowerCase(), value));
// Runtime information
baseVariables.put("runtime.availableProcessors", Runtime.getRuntime().availableProcessors());
baseVariables.put("runtime.maxMemory", Runtime.getRuntime().maxMemory());
baseVariables.put("timestamp", System.currentTimeMillis());
}
public void addVariable(String key, Object value) {
baseVariables.put(key, value);
}
public String generateConfig(String templateContent, Map<String, Object> overrideVariables) {
Map<String, Object> allVariables = new HashMap<>(baseVariables);
if (overrideVariables != null) {
allVariables.putAll(overrideVariables);
}
return templateEngine.processTemplate(templateContent, allVariables);
}
public String generateConfigForEnvironment(String templateContent, String environment) {
Map<String, Object> envVariables = new HashMap<>(baseVariables);
envVariables.put("environment", environment);
envVariables.put("isProduction", "production".equals(environment));
envVariables.put("isDevelopment", "development".equals(environment));
envVariables.put("isStaging", "staging".equals(environment));
return generateConfig(templateContent, envVariables);
}
// Usage example
public static void main(String[] args) throws Exception {
EnvironmentAwareTemplateManager manager = new EnvironmentAwareTemplateManager();
// Add application-specific variables
manager.addVariable("app.name", "ConfigService");
manager.addVariable("app.version", "3.2.1");
String template = """
# Generated configuration for ${environment}
# Timestamp: ${timestamp?datetime}
application:
name: "${app.name}"
version: "${app.version}"
environment: "${environment}"
<#if isProduction>
server:
port: 443
ssl: true
workers: ${runtime.availableProcessors * 4}
<#elseif isStaging>
server:
port: 8443
ssl: true
workers: ${runtime.availableProcessors * 2}
<#else>
server:
port: 8080
ssl: false
workers: ${runtime.availableProcessors}
</#if>
system:
processors: ${runtime.availableProcessors}
max_memory: ${runtime.maxMemory}
""";
// Generate for different environments
System.out.println("=== DEVELOPMENT CONFIG ===");
String devConfig = manager.generateConfigForEnvironment(template, "development");
System.out.println(devConfig);
System.out.println("\n=== PRODUCTION CONFIG ===");
String prodConfig = manager.generateConfigForEnvironment(template, "production");
System.out.println(prodConfig);
}
}

Best Practices for Configuration Templating

  1. Separation of Concerns: Keep templates separate from business logic
  2. Validation: Validate generated configurations before use
  3. Error Handling: Provide clear error messages for missing variables
  4. Security: Avoid executing arbitrary code in templates
  5. Caching: Cache processed templates when appropriate
  6. Testing: Test templates with various input scenarios
  7. Documentation: Document available variables and template syntax
public class TemplateValidator {
public void validateTemplateResult(String result, String expectedPattern) {
// Validate that the result matches expected structure
if (result.contains("${")) {
throw new IllegalStateException("Template contains unresolved variables");
}
// Additional validation logic
if (expectedPattern != null && !result.matches(expectedPattern)) {
throw new ValidationException("Generated config doesn't match expected pattern");
}
}
}

Conclusion

Configuration file templating in Java offers powerful capabilities for dynamic configuration management:

  • Simple replacements with String.replace() or Apache Commons Text
  • Complex logic with Freemarker or Thymeleaf
  • Structured data handling with JSON/XML-specific tools
  • Environment awareness with system property and environment variable integration

Choose the approach that matches your complexity needs:

  • Basic: String replacement or Apache Commons Text
  • Intermediate: Freemarker for logic and loops
  • Advanced: Custom template managers with validation and environment awareness

By implementing configuration templating, you can create more maintainable, flexible, and environment-aware applications that adapt seamlessly to different deployment scenarios.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper