google-site-verification: google61fe8ba583a51912.html
Jsonnet for Configuration Management in Java

Introduction

Jsonnet is a powerful configuration language that generates JSON data. It extends JSON with features like variables, functions, conditionals, and imports, making it ideal for managing complex configurations in Java applications. This guide covers integrating Jsonnet with Java for dynamic configuration management.

Setup and Dependencies

1. Maven Configuration

<dependencies> <!-- Jsonnet Java implementation --> <dependency> <groupId>org.jsonnet</groupId> <artifactId>jsonnet</artifactId> <version>0.20.0</version> </dependency> <!-- Alternatively, Google's implementation --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.10.1</version> </dependency> <!-- For YAML output support --> <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> </dependencies>

2. Gradle Configuration

dependencies { implementation("org.jsonnet:jsonnet:0.20.0") implementation("com.google.code.gson:gson:2.10.1") implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2") }

Core Jsonnet Integration

1. Jsonnet Evaluation Service

@Service @Slf4j public class JsonnetService { private final Jsonnet jsonnet; private final ObjectMapper jsonMapper; private final ObjectMapper yamlMapper; public JsonnetService() { this.jsonnet = new Jsonnet(); this.jsonMapper = new ObjectMapper(); this.yamlMapper = new ObjectMapper(new YAMLFactory()); } public String evaluateJsonnet(String jsonnetCode) { try { return jsonnet.evaluateSnippet("snippet", jsonnetCode); } catch (JsonnetException e) { throw new JsonnetEvaluationException("Failed to evaluate Jsonnet", e); } } public <T> T evaluateJsonnet(String jsonnetCode, Class<T> targetType) { try { String jsonOutput = evaluateJsonnet(jsonnetCode); return jsonMapper.readValue(jsonOutput, targetType); } catch (Exception e) { throw new JsonnetEvaluationException( "Failed to evaluate Jsonnet to type: " + targetType.getSimpleName(), e); } } public String evaluateFile(String filePath) { try { return jsonnet.evaluateFile(filePath); } catch (JsonnetException e) { throw new JsonnetEvaluationException( "Failed to evaluate Jsonnet file: " + filePath, e); } } public <T> T evaluateFile(String filePath, Class<T> targetType) { try { String jsonOutput = evaluateFile(filePath); return jsonMapper.readValue(jsonOutput, targetType); } catch (Exception e) { throw new JsonnetEvaluationException( "Failed to evaluate Jsonnet file to type: " + targetType.getSimpleName(), e); } } public String evaluateToYaml(String jsonnetCode) { try { String jsonOutput = evaluateJsonnet(jsonnetCode); Object jsonObject = jsonMapper.readValue(jsonOutput, Object.class); return yamlMapper.writeValueAsString(jsonObject); } catch (Exception e) { throw new JsonnetEvaluationException("Failed to convert Jsonnet to YAML", e); } } }

2. Configuration Models

@Data @Builder @NoArgsConstructor @AllArgsConstructor public class ApplicationConfig { private String name; private String version; private DatabaseConfig database; private ServerConfig server; private List<FeatureFlag> features; private Map<String, String> environment; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DatabaseConfig { private String url; private String username; private String password; private int maxPoolSize; private int connectionTimeout; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ServerConfig { private int port; private String host; private SslConfig ssl; private CompressionConfig compression; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class FeatureFlag { private String name; private boolean enabled; private String description; private Map<String, Object> parameters; }

Jsonnet Configuration Examples

1. Basic Application Configuration

// config/app.jsonnet local env = std.extVar("ENVIRONMENT"); local version = std.extVar("APP_VERSION"); { application: { name: "my-spring-app", version: version, environment: env, // Conditional configuration based on environment debug: if env == "development" then true else false, server: { port: if env == "production" then 8443 else 8080, host: "0.0.0.0", ssl: { enabled: env == "production", keyStore: if env == "production" then "/etc/ssl/keystore.p12" else null, keyStorePassword: if env == "production" then std.extVar("SSL_PASSWORD") else null, }, compression: { enabled: true, mimeTypes: ["application/json", "text/html", "application/javascript"], minSize: 1024, }, }, database: { url: if env == "production" then "jdbc:postgresql://prod-db:5432/myapp" else if env == "staging" then "jdbc:postgresql://staging-db:5432/myapp" else "jdbc:postgresql://localhost:5432/myapp", username: std.extVar("DB_USERNAME"), password: std.extVar("DB_PASSWORD"), maxPoolSize: if env == "production" then 20 else 10, connectionTimeout: 30000, }, features: [ { name: "new-ui", enabled: env != "production", description: "New user interface", parameters: { theme: "dark", animations: true, }, }, { name: "payment-processing", enabled: true, description: "Payment processing system", parameters: { provider: "stripe", timeout: 30000, retryAttempts: 3, }, }, ], logging: { level: if env == "production" then "INFO" else "DEBUG", file: { enabled: env == "production", path: "/var/log/myapp/app.log", maxSize: "100MB", maxFiles: 10, }, }, }, }

2. Multi-Environment Configuration

// config/environments.jsonnet local baseConfig = { application: { name: "my-spring-app", version: "1.0.0", common: { server: { host: "0.0.0.0", compression: { enabled: true, mimeTypes: ["application/json", "text/html"], }, }, database: { maxPoolSize: 10, connectionTimeout: 30000, }, features: [ { name: "audit-logging", enabled: true, }, ], }, }, }; local development = baseConfig { application+: { environment: "development", debug: true, server+: { port: 8080, ssl: { enabled: false }, }, database+: { url: "jdbc:postgresql://localhost:5432/myapp_dev", }, features+: [ { name: "new-ui", enabled: true, }, { name: "debug-endpoints", enabled: true, }, ], logging: { level: "DEBUG", file: { enabled: false }, }, }, }; local staging = baseConfig { application+: { environment: "staging", debug: false, server+: { port: 8080, ssl: { enabled: true }, }, database+: { url: "jdbc:postgresql://staging-db:5432/myapp_staging", maxPoolSize: 15, }, features+: [ { name: "new-ui", enabled: true, }, ], logging: { level: "INFO", file: { enabled: true, path: "/var/log/myapp/staging.log" }, }, }, }; local production = baseConfig { application+: { environment: "production", debug: false, server+: { port: 8443, ssl: { enabled: true, keyStore: "/etc/ssl/keystore.p12", }, }, database+: { url: "jdbc:postgresql://prod-db:5432/myapp_prod", maxPoolSize: 20, }, features+: [ { name: "new-ui", enabled: false, }, ], logging: { level: "WARN", file: { enabled: true, path: "/var/log/myapp/production.log" }, }, }, }; // Export based on environment if std.extVar("ENVIRONMENT") == "production" then production else if std.extVar("ENVIRONMENT") == "staging" then staging else development

3. Advanced Jsonnet with Functions

// config/lib/kubernetes.jsonnet local namespace = "myapp"; local createDeployment(name, image, replicas, port, envVars={}) = { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: name, namespace: namespace, labels: { app: name, version: std.extVar("APP_VERSION"), }, }, spec: { replicas: replicas, selector: { matchLabels: { app: name, }, }, template: { metadata: { labels: { app: name, version: std.extVar("APP_VERSION"), }, }, spec: { containers: [ { name: name, image: image, ports: [ { containerPort: port, }, ], env: [ { name: "JAVA_OPTS", value: "-Xmx512m -Xms256m" }, { name: "SPRING_PROFILES_ACTIVE", value: std.extVar("ENVIRONMENT") }, ] + [ { name: k, value: v } for k, v in envVars ], resources: { requests: { memory: "256Mi", cpu: "100m", }, limits: { memory: "512Mi", cpu: "500m", }, }, livenessProbe: { httpGet: { path: "/actuator/health", port: port, }, initialDelaySeconds: 30, periodSeconds: 10, }, readinessProbe: { httpGet: { path: "/actuator/health", port: port, }, initialDelaySeconds: 5, periodSeconds: 5, }, }, ], }, }, }, }; local createService(name, port, targetPort) = { apiVersion: "v1", kind: "Service", metadata: { name: name, namespace: namespace, }, spec: { selector: { app: name, }, ports: [ { port: port, targetPort: targetPort, }, ], }, }; { apiVersion: "v1", kind: "Namespace", metadata: { name: namespace, }, deployments: [ createDeployment( name="api-service", image=std.extVar("API_IMAGE"), replicas=if std.extVar("ENVIRONMENT") == "production" then 3 else 1, port=8080, envVars={ "DB_URL": std.extVar("DB_URL"), "DB_USERNAME": std.extVar("DB_USERNAME"), } ), createDeployment( name="worker-service", image=std.extVar("WORKER_IMAGE"), replicas=2, port=8081, envVars={ "QUEUE_URL": std.extVar("QUEUE_URL"), } ), ], services: [ createService("api-service", port=80, targetPort=8080), createService("worker-service", port=80, targetPort=8081), ], configMaps: [ { apiVersion: "v1", kind: "ConfigMap", metadata: { name: "app-config", namespace: namespace, }, data: { "application.yml": std.toString(import "app.jsonnet"), }, }, ], }

Spring Boot Integration

1. Jsonnet Configuration Loader

@Component @Slf4j public class JsonnetConfigurationLoader { private final JsonnetService jsonnetService; private final Environment environment; public JsonnetConfigurationLoader(JsonnetService jsonnetService, Environment environment) { this.jsonnetService = jsonnetService; this.environment = environment; } @Bean @Primary public ApplicationConfig applicationConfig() { String env = environment.getProperty("app.environment", "development"); String version = environment.getProperty("app.version", "1.0.0"); Map<String, String> extVars = Map.of( "ENVIRONMENT", env, "APP_VERSION", version, "DB_USERNAME", environment.getProperty("DB_USERNAME", ""), "DB_PASSWORD", environment.getProperty("DB_PASSWORD", ""), "SSL_PASSWORD", environment.getProperty("SSL_PASSWORD", "") ); return loadConfiguration("config/app.jsonnet", extVars, ApplicationConfig.class); } public <T> T loadConfiguration(String configPath, Map<String, String> externalVars, Class<T> configType) { try { // Set external variables for Jsonnet evaluation Jsonnet jsonnet = new Jsonnet(); externalVars.forEach(jsonnet::addExtVar); String jsonOutput = jsonnet.evaluateFile(configPath); ObjectMapper mapper = new ObjectMapper(); return mapper.readValue(jsonOutput, configType); } catch (Exception e) { log.error("Failed to load configuration from: {}", configPath, e); throw new ConfigurationLoadException( "Failed to load configuration: " + configPath, e); } } public Map<String, Object> loadConfigurationAsMap(String configPath, Map<String, String> externalVars) { try { Jsonnet jsonnet = new Jsonnet(); externalVars.forEach(jsonnet::addExtVar); String jsonOutput = jsonnet.evaluateFile(configPath); ObjectMapper mapper = new ObjectMapper(); return mapper.readValue(jsonOutput, new TypeReference<Map<String, Object>>() {}); } catch (Exception e) { log.error("Failed to load configuration as map from: {}", configPath, e); throw new ConfigurationLoadException( "Failed to load configuration as map: " + configPath, e); } } }

2. Spring Configuration Properties

@Configuration @EnableConfigurationProperties @ConfigurationProperties(prefix = "app") @Data public class JsonnetApplicationProperties { private String environment; private String version; private String configPath; private Map<String, String> externalVars = new HashMap<>(); @PostConstruct public void init() { // Set default values if (environment == null) { environment = "development"; } if (version == null) { version = "1.0.0"; } if (configPath == null) { configPath = "config/app.jsonnet"; } } }

3. Configuration Refresh Mechanism

@Service @Slf4j public class DynamicConfigurationService { private final JsonnetService jsonnetService; private final JsonnetApplicationProperties properties; private final Map<String, Object> configCache = new ConcurrentHashMap<>(); private final AtomicLong lastReloadTime = new AtomicLong(); @Scheduled(fixedRate = 30000) // Reload every 30 seconds public void reloadConfiguration() { try { Map<String, String> extVars = createExternalVars(); Map<String, Object> newConfig = jsonnetService.evaluateFile( properties.getConfigPath(), new TypeReference<Map<String, Object>>() {}); configCache.clear(); configCache.putAll(newConfig); lastReloadTime.set(System.currentTimeMillis()); log.info("Configuration reloaded successfully"); } catch (Exception e) { log.warn("Failed to reload configuration", e); } } public <T> T getConfig(String path, Class<T> type) { Object value = getNestedValue(configCache, path); if (value == null) { return null; } ObjectMapper mapper = new ObjectMapper(); return mapper.convertValue(value, type); } public String getConfigAsString(String path) { Object value = getNestedValue(configCache, path); return value != null ? value.toString() : null; } public boolean getConfigAsBoolean(String path, boolean defaultValue) { Object value = getNestedValue(configCache, path); return value != null ? Boolean.parseBoolean(value.toString()) : defaultValue; } private Object getNestedValue(Map<String, Object> map, String path) { String[] keys = path.split("\\."); Object current = map; for (String key : keys) { if (current instanceof Map) { current = ((Map<?, ?>) current).get(key); } else { return null; } } return current; } private Map<String, String> createExternalVars() { Map<String, String> extVars = new HashMap<>(properties.getExternalVars()); extVars.put("ENVIRONMENT", properties.getEnvironment()); extVars.put("APP_VERSION", properties.getVersion()); return extVars; } }

Advanced Features

1. Template Inheritance

// config/templates/base.jsonnet local BaseTemplate(environment, appName, version) = { metadata: { name: appName, namespace: "default", labels: { app: appName, environment: environment, version: version, }, annotations: { "config.generated-by": "jsonnet", "config.timestamp": std.toString(std.native("timestamp")()), }, }, spec: { replicas: 1, strategy: { type: "RollingUpdate", rollingUpdate: { maxSurge: 1, maxUnavailable: 0, }, }, template: { metadata: { labels: { app: appName, environment: environment, version: version, }, }, }, }, }; // Export the template function BaseTemplate
// config/templates/deployment.jsonnet local BaseTemplate = import "base.jsonnet"; local DeploymentTemplate(environment, appName, version, image, port, resources={}) = BaseTemplate(environment, appName, version) { apiVersion: "apps/v1", kind: "Deployment", spec+: { selector: { matchLabels: { app: appName, }, }, template+: { spec+: { containers: [ { name: appName, image: image, ports: [ { containerPort: port, }, ], resources: if std.length(std.objectFields(resources)) > 0 then resources else { requests: { cpu: "100m", memory: "128Mi", }, limits: { cpu: "500m", memory: "512Mi", }, }, env: [ { name: "JAVA_OPTS", value: "-Xmx256m -Xms128m", }, ], }, ], }, }, }, }; DeploymentTemplate

2. Configuration Validation

@Service @Slf4j public class JsonnetConfigurationValidator { private final JsonnetService jsonnetService; public ValidationResult validateConfiguration(String jsonnetCode) { List<String> errors = new ArrayList<>(); List<String> warnings = new ArrayList<>(); try { String jsonOutput = jsonnetService.evaluateJsonnet(jsonnetCode); ObjectMapper mapper = new ObjectMapper(); Map<String, Object> config = mapper.readValue(jsonOutput, new TypeReference<Map<String, Object>>() {}); // Validate required fields validateRequiredFields(config, errors); // Validate types and constraints validateTypesAndConstraints(config, errors, warnings); // Validate environment-specific rules validateEnvironmentRules(config, warnings); } catch (Exception e) { errors.add("Failed to evaluate Jsonnet: " + e.getMessage()); } return new ValidationResult(errors, warnings); } public ValidationResult validateConfigurationFile(String filePath) { try { String jsonnetCode = Files.readString(Paths.get(filePath)); return validateConfiguration(jsonnetCode); } catch (Exception e) { return new ValidationResult( List.of("Failed to read file: " + e.getMessage()), List.of()); } } private void validateRequiredFields(Map<String, Object> config, List<String> errors) { if (!config.containsKey("application")) { errors.add("Missing required field: application"); return; } @SuppressWarnings("unchecked") Map<String, Object> app = (Map<String, Object>) config.get("application"); if (!app.containsKey("name")) { errors.add("Missing required field: application.name"); } if (!app.containsKey("version")) { errors.add("Missing required field: application.version"); } } private void validateTypesAndConstraints(Map<String, Object> config, List<String> errors, List<String> warnings) { // Implement type checking and constraint validation // Example: Validate port numbers, memory limits, etc. } private void validateEnvironmentRules(Map<String, Object> config, List<String> warnings) { @SuppressWarnings("unchecked") Map<String, Object> app = (Map<String, Object>) config.get("application"); String environment = (String) app.get("environment"); if ("production".equals(environment)) { // Production-specific validations validateProductionConfig(app, warnings); } } @Data @Builder public static class ValidationResult { private final List<String> errors; private final List<String> warnings; private final boolean valid; public ValidationResult(List<String> errors, List<String> warnings) { this.errors = errors != null ? errors : List.of(); this.warnings = warnings != null ? warnings : List.of(); this.valid = this.errors.isEmpty(); } } }

Testing Jsonnet Configurations

1. Unit Tests for Jsonnet Evaluation

@ExtendWith(MockitoExtension.class) class JsonnetServiceTest { @InjectMocks private JsonnetService jsonnetService; @Test void shouldEvaluateSimpleJsonnet() { // Given String jsonnetCode = """ { app: { name: "test-app", version: "1.0.0", port: 8080 } } """; // When String result = jsonnetService.evaluateJsonnet(jsonnetCode); // Then assertNotNull(result); assertTrue(result.contains("test-app")); assertTrue(result.contains("8080")); } @Test void shouldEvaluateWithExternalVariables() { // Given String jsonnetCode = """ { app: { name: "test-app", environment: std.extVar("ENV"), debug: if std.extVar("ENV") == "dev" then true else false } } """; // When Jsonnet jsonnet = new Jsonnet(); jsonnet.addExtVar("ENV", "dev"); String result = jsonnet.evaluateSnippet("test", jsonnetCode); // Then assertTrue(result.contains("dev")); assertTrue(result.contains("true")); } @Test void shouldConvertToJavaObject() { // Given String jsonnetCode = """ { name: "test-app", version: "1.0.0", features: [ { name: "feature1", enabled: true }, { name: "feature2", enabled: false } ] } """; // When ApplicationConfig config = jsonnetService.evaluateJsonnet( jsonnetCode, ApplicationConfig.class); // Then assertNotNull(config); assertEquals("test-app", config.getName()); assertEquals(2, config.getFeatures().size()); } }

2. Integration Tests

@SpringBootTest @TestPropertySource(properties = { "app.environment=test", "app.version=1.0.0", "app.config-path=classpath:config/test-app.jsonnet" }) class JsonnetConfigurationIntegrationTest { @Autowired private ApplicationConfig applicationConfig; @Autowired private DynamicConfigurationService dynamicConfigService; @Test void shouldLoadConfigurationOnStartup() { assertNotNull(applicationConfig); assertEquals("test-app", applicationConfig.getName()); assertEquals("test", applicationConfig.getEnvironment()); } @Test void shouldAccessDynamicConfiguration() { String appName = dynamicConfigService.getConfigAsString("application.name"); Integer port = dynamicConfigService.getConfig("application.server.port", Integer.class); assertEquals("test-app", appName); assertNotNull(port); } }

Best Practices

1. Configuration Organization

// config/main.jsonnet local environments = import "environments.jsonnet"; local kubernetes = import "kubernetes.jsonnet"; local monitoring = import "monitoring.jsonnet"; { application: environments, infrastructure: { kubernetes: kubernetes, monitoring: monitoring, }, // Merge all configurations metadata: { generatedAt: std.toString(std.native("timestamp")()), environment: std.extVar("ENVIRONMENT"), version: std.extVar("APP_VERSION"), }, }

2. Error Handling and Defaults

@ControllerAdvice @Slf4j public class JsonnetConfigurationExceptionHandler { @ExceptionHandler(JsonnetEvaluationException.class) public ResponseEntity<ErrorResponse> handleJsonnetEvaluationException( JsonnetEvaluationException e) { log.error("Jsonnet evaluation failed", e); ErrorResponse error = ErrorResponse.builder() .code("CONFIG_EVALUATION_ERROR") .message("Configuration evaluation failed") .details(e.getMessage()) .timestamp(Instant.now()) .build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); } @ExceptionHandler(ConfigurationLoadException.class) public ResponseEntity<ErrorResponse> handleConfigurationLoadException( ConfigurationLoadException e) { log.error("Configuration loading failed", e); ErrorResponse error = ErrorResponse.builder() .code("CONFIG_LOAD_ERROR") .message("Failed to load configuration") .details(e.getMessage()) .timestamp(Instant.now()) .build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); } }

Conclusion

Jsonnet provides a powerful way to manage complex configurations in Java applications through:

  1. Template-driven configuration with variables, functions, and inheritance
  2. Multi-environment support with conditional logic
  3. Type-safe integration with Java objects
  4. Dynamic configuration reloading capabilities
  5. Validation and testing support

By integrating Jsonnet with Spring Boot, teams can create maintainable, reusable configuration templates that adapt to different environments while maintaining type safety and validation. This approach significantly reduces configuration duplication and improves deployment consistency across environments.

Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/

OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/

OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/

Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.

https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics

Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2

Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide

Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2

Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide

Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.

https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server

Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.

Leave a Reply

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


Macro Nepal Helper