google-site-verification: google61fe8ba583a51912.html
Dynamic Configuration: Mastering @RefreshScope for Live Configuration Updates in Java

Article

In modern microservices architectures, the ability to update application configuration without restarting services is crucial for maintaining high availability and rapid iteration. Spring Cloud's @RefreshScope provides a powerful mechanism to reload configuration properties at runtime, enabling dynamic updates from various configuration sources.

In this guide, we'll explore how to implement and leverage @RefreshScope in Java applications for seamless configuration management.

Why @RefreshScope Matters

  • Zero Downtime Updates: Change configuration without service restarts
  • Runtime Flexibility: Adapt to changing environments dynamically
  • Centralized Configuration: Integrate with Spring Cloud Config, Consul, etc.
  • Feature Flagging: Enable/disable features without redeployment
  • Emergency Controls: Quickly modify behavior in production incidents

Part 1: Fundamentals and Setup

1.1 Dependencies

<!-- pom.xml --> <properties> <spring-boot.version>3.1.0</spring-boot.version> <spring-cloud.version>2022.0.3</spring-cloud.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Cloud Context (for @RefreshScope) --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-context</artifactId> <version>4.0.2</version> </dependency> <!-- Spring Cloud Config Client (optional) --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> <version>4.0.2</version> </dependency> <!-- Observability --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>1.11.5</version> </dependency> </dependencies>

1.2 Project Structure

config-refresh-app/ ├── src/ │ ├── main/java/com/example/config/ │ │ ├── properties/ │ │ ├── refresh/ │ │ └── health/ │ └── resources/ │ └── application.yaml ├── config/ │ ├── application-dev.yaml │ └── application-prod.yaml └── scripts/ └── refresh-config.sh

Part 2: Basic @RefreshScope Implementation

2.1 Application Configuration

# src/main/resources/application.yaml spring: application: name: config-refresh-app config: import: optional:configserver:http://localhost:8888 # Optional Config Server app: config: refresh: enabled: true timeout-ms: 5000 features: new-payment-service: false dark-mode: true experimental-api: false service: name: user-service version: 1.0.0 endpoints: timeout: 5000 retry-attempts: 3 circuit-breaker: enabled: true failure-threshold: 50 database: pool: max-size: 20 min-size: 5 timeout: 30000 connection: max-lifetime: 1800000 cache: redis: ttl: 3600 max-entries: 10000 local: enabled: true size: 1000 management: endpoints: web: exposure: include: health,info,metrics,refresh,configprops,env enabled-by-default: true endpoint: refresh: enabled: true health: show-details: always show-components: always info: env: enabled: true logging: level: com.example.config: DEBUG org.springframework.cloud: INFO

2.2 Refresh-Scoped Configuration Properties

// File: src/main/java/com/example/config/properties/AppConfigProperties.java package com.example.config.properties; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Component @RefreshScope @ConfigurationProperties(prefix = "app") @Validated public class AppConfigProperties { private ServiceConfig service; private DatabaseConfig database; private CacheConfig cache; private FeatureFlags features; private RefreshConfig refresh; // Getters and Setters public ServiceConfig getService() { return service; } public void setService(ServiceConfig service) { this.service = service; } public DatabaseConfig getDatabase() { return database; } public void setDatabase(DatabaseConfig database) { this.database = database; } public CacheConfig getCache() { return cache; } public void setCache(CacheConfig cache) { this.cache = cache; } public FeatureFlags getFeatures() { return features; } public void setFeatures(FeatureFlags features) { this.features = features; } public RefreshConfig getRefresh() { return refresh; } public void setRefresh(RefreshConfig refresh) { this.refresh = refresh; } // Nested Configuration Classes @Validated public static class ServiceConfig { @NotBlank private String name; @NotBlank private String version; private Endpoints endpoints = new Endpoints(); // Getters and Setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public Endpoints getEndpoints() { return endpoints; } public void setEndpoints(Endpoints endpoints) { this.endpoints = endpoints; } @Validated public static class Endpoints { @Min(1000) @Max(30000) private int timeout = 5000; @Min(1) @Max(10) private int retryAttempts = 3; private CircuitBreaker circuitBreaker = new CircuitBreaker(); // Getters and Setters public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public int getRetryAttempts() { return retryAttempts; } public void setRetryAttempts(int retryAttempts) { this.retryAttempts = retryAttempts; } public CircuitBreaker getCircuitBreaker() { return circuitBreaker; } public void setCircuitBreaker(CircuitBreaker circuitBreaker) { this.circuitBreaker = circuitBreaker; } public static class CircuitBreaker { private boolean enabled = true; @Min(1) @Max(100) private int failureThreshold = 50; @Min(1000) @Max(60000) private int timeout = 10000; // Getters and Setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public int getFailureThreshold() { return failureThreshold; } public void setFailureThreshold(int failureThreshold) { this.failureThreshold = failureThreshold; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } } } } @Validated public static class DatabaseConfig { private PoolConfig pool = new PoolConfig(); private ConnectionConfig connection = new ConnectionConfig(); // Getters and Setters public PoolConfig getPool() { return pool; } public void setPool(PoolConfig pool) { this.pool = pool; } public ConnectionConfig getConnection() { return connection; } public void setConnection(ConnectionConfig connection) { this.connection = connection; } public static class PoolConfig { @Min(1) @Max(100) private int maxSize = 20; @Min(1) @Max(10) private int minSize = 5; @Min(1000) @Max(60000) private int timeout = 30000; // Getters and Setters public int getMaxSize() { return maxSize; } public void setMaxSize(int maxSize) { this.maxSize = maxSize; } public int getMinSize() { return minSize; } public void setMinSize(int minSize) { this.minSize = minSize; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } } public static class ConnectionConfig { @Min(60000) @Max(3600000) private int maxLifetime = 1800000; // Getters and Setters public int getMaxLifetime() { return maxLifetime; } public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } } } public static class CacheConfig { private RedisConfig redis = new RedisConfig(); private LocalConfig local = new LocalConfig(); // Getters and Setters public RedisConfig getRedis() { return redis; } public void setRedis(RedisConfig redis) { this.redis = redis; } public LocalConfig getLocal() { return local; } public void setLocal(LocalConfig local) { this.local = local; } public static class RedisConfig { private int ttl = 3600; private int maxEntries = 10000; // Getters and Setters public int getTtl() { return ttl; } public void setTtl(int ttl) { this.ttl = ttl; } public int getMaxEntries() { return maxEntries; } public void setMaxEntries(int maxEntries) { this.maxEntries = maxEntries; } } public static class LocalConfig { private boolean enabled = true; private int size = 1000; // Getters and Setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } } } public static class FeatureFlags { private boolean newPaymentService = false; private boolean darkMode = true; private boolean experimentalApi = false; private Map<String, Boolean> customFlags = new HashMap<>(); // Getters and Setters public boolean isNewPaymentService() { return newPaymentService; } public void setNewPaymentService(boolean newPaymentService) { this.newPaymentService = newPaymentService; } public boolean isDarkMode() { return darkMode; } public void setDarkMode(boolean darkMode) { this.darkMode = darkMode; } public boolean isExperimentalApi() { return experimentalApi; } public void setExperimentalApi(boolean experimentalApi) { this.experimentalApi = experimentalApi; } public Map<String, Boolean> getCustomFlags() { return customFlags; } public void setCustomFlags(Map<String, Boolean> customFlags) { this.customFlags = customFlags; } } public static class RefreshConfig { private boolean enabled = true; private long timeoutMs = 5000; private List<String> monitoredProperties = new ArrayList<>(); // Getters and Setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public long getTimeoutMs() { return timeoutMs; } public void setTimeoutMs(long timeoutMs) { this.timeoutMs = timeoutMs; } public List<String> getMonitoredProperties() { return monitoredProperties; } public void setMonitoredProperties(List<String> monitoredProperties) { this.monitoredProperties = monitoredProperties; } } }

Part 3: Refresh-Scoped Services and Components

3.1 Refresh-Scoped Service

// File: src/main/java/com/example/config/refresh/RefreshableService.java package com.example.config.refresh; import com.example.config.properties.AppConfigProperties; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Service; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @Service @RefreshScope public class RefreshableService { private static final Logger logger = LoggerFactory.getLogger(RefreshableService.class); private final AppConfigProperties appConfig; private final AtomicInteger refreshCount = new AtomicInteger(0); private final AtomicLong lastRefreshTime = new AtomicLong(System.currentTimeMillis()); // Runtime state that gets reset on refresh private volatile String currentConfigHash; private volatile boolean configurationValid = true; public RefreshableService(AppConfigProperties appConfig) { this.appConfig = appConfig; } @PostConstruct public void initialize() { computeConfigHash(); logger.info("RefreshableService initialized with config: {}", appConfig.getService().getName()); logger.info("Feature flags - New Payment: {}, Dark Mode: {}", appConfig.getFeatures().isNewPaymentService(), appConfig.getFeatures().isDarkMode()); } public ServiceConfig getCurrentConfig() { return new ServiceConfig( appConfig.getService().getName(), appConfig.getService().getVersion(), appConfig.getService().getEndpoints().getTimeout(), appConfig.getService().getEndpoints().getRetryAttempts(), appConfig.getFeatures().isNewPaymentService(), appConfig.getFeatures().isDarkMode(), refreshCount.get(), lastRefreshTime.get() ); } public FeatureStatus getFeatureStatus(String featureName) { boolean enabled = switch (featureName.toLowerCase()) { case "newpaymentservice" -> appConfig.getFeatures().isNewPaymentService(); case "darkmode" -> appConfig.getFeatures().isDarkMode(); case "experimentalapi" -> appConfig.getFeatures().isExperimentalApi(); default -> appConfig.getFeatures().getCustomFlags().getOrDefault(featureName, false); }; return new FeatureStatus(featureName, enabled, System.currentTimeMillis()); } public DatabaseConfig getDatabaseConfig() { return new DatabaseConfig( appConfig.getDatabase().getPool().getMaxSize(), appConfig.getDatabase().getPool().getMinSize(), appConfig.getDatabase().getPool().getTimeout(), appConfig.getDatabase().getConnection().getMaxLifetime() ); } public CacheConfig getCacheConfig() { return new CacheConfig( appConfig.getCache().getRedis().getTtl(), appConfig.getCache().getRedis().getMaxEntries(), appConfig.getCache().getLocal().isEnabled(), appConfig.getCache().getLocal().getSize() ); } public void onRefresh() { refreshCount.incrementAndGet(); lastRefreshTime.set(System.currentTimeMillis()); // Recompute configuration hash computeConfigHash(); // Validate new configuration validateConfiguration(); logger.info("Configuration refreshed (count: {}). New config hash: {}", refreshCount.get(), currentConfigHash); // Perform any necessary reinitialization reinitializeComponents(); } private void computeConfigHash() { String configString = String.format("%s-%s-%b-%b", appConfig.getService().getName(), appConfig.getService().getVersion(), appConfig.getFeatures().isNewPaymentService(), appConfig.getFeatures().isDarkMode()); this.currentConfigHash = Integer.toHexString(configString.hashCode()); } private void validateConfiguration() { try { // Validate critical configuration values if (appConfig.getService().getEndpoints().getTimeout() <= 0) { throw new IllegalArgumentException("Timeout must be positive"); } if (appConfig.getDatabase().getPool().getMaxSize() < appConfig.getDatabase().getPool().getMinSize()) { throw new IllegalArgumentException("Max pool size cannot be less than min pool size"); } configurationValid = true; logger.debug("Configuration validation passed"); } catch (Exception e) { configurationValid = false; logger.error("Configuration validation failed: {}", e.getMessage()); // In production, you might want to revert to previous configuration } } private void reinitializeComponents() { if (!configurationValid) { logger.warn("Skipping component reinitialization due to invalid configuration"); return; } try { // Reinitialize components that depend on configuration logger.info("Reinitializing components with new configuration"); // Example: Reconfigure connection pools, caches, etc. reconfigureDatabasePool(); reconfigureCache(); } catch (Exception e) { logger.error("Error reinitializing components: {}", e.getMessage()); } } private void reconfigureDatabasePool() { // In a real application, this would reconfigure your database connection pool logger.info("Reconfiguring database pool - Max: {}, Min: {}, Timeout: {}", appConfig.getDatabase().getPool().getMaxSize(), appConfig.getDatabase().getPool().getMinSize(), appConfig.getDatabase().getPool().getTimeout()); } private void reconfigureCache() { // In a real application, this would reconfigure your cache settings logger.info("Reconfiguring cache - Redis TTL: {}, Local enabled: {}", appConfig.getCache().getRedis().getTtl(), appConfig.getCache().getLocal().isEnabled()); } public RefreshStats getRefreshStats() { return new RefreshStats( refreshCount.get(), lastRefreshTime.get(), currentConfigHash, configurationValid ); } // Data classes public record ServiceConfig( String serviceName, String version, int timeout, int retryAttempts, boolean newPaymentEnabled, boolean darkModeEnabled, int refreshCount, long lastRefreshTime ) {} public record FeatureStatus(String featureName, boolean enabled, long checkTime) {} public record DatabaseConfig(int maxPoolSize, int minPoolSize, int timeout, int maxLifetime) {} public record CacheConfig(int redisTtl, int redisMaxEntries, boolean localEnabled, int localSize) {} public record RefreshStats(int totalRefreshes, long lastRefreshTime, String configHash, boolean configValid) {} }

3.2 Configuration Change Listener

// File: src/main/java/com/example/config/refresh/ConfigChangeListener.java package com.example.config.refresh; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; @Component public class ConfigChangeListener { private static final Logger logger = LoggerFactory.getLogger(ConfigChangeListener.class); private final ConcurrentMap<String, AtomicLong> propertyChangeCount = new ConcurrentHashMap<>(); private final AtomicLong totalRefreshEvents = new AtomicLong(0); private final RefreshableService refreshableService; public ConfigChangeListener(RefreshableService refreshableService) { this.refreshableService = refreshableService; } @EventListener public void onRefreshScopeRefreshed(RefreshScopeRefreshedEvent event) { totalRefreshEvents.incrementAndGet(); logger.info("RefreshScope refreshed event received. Source: {}", event.getSource()); // Notify the refreshable service refreshableService.onRefresh(); // Log refresh statistics logRefreshStatistics(); } @EventListener public void onEnvironmentChange(org.springframework.cloud.context.environment.EnvironmentChangeEvent event) { logger.info("Environment changed. Keys: {}", event.getKeys()); // Track which properties are changing for (String key : event.getKeys()) { propertyChangeCount.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet(); } logger.debug("Property change counts updated"); } public void recordPropertyAccess(String propertyName) { propertyChangeCount.putIfAbsent(propertyName, new AtomicLong(0)); } public ConfigChangeStats getConfigChangeStats() { return new ConfigChangeStats( totalRefreshEvents.get(), System.currentTimeMillis(), new ConcurrentHashMap<>(propertyChangeCount) ); } private void logRefreshStatistics() { logger.info("Configuration refresh statistics - Total refreshes: {}, Tracked properties: {}", totalRefreshEvents.get(), propertyChangeCount.size()); } public record ConfigChangeStats( long totalRefreshes, long timestamp, ConcurrentMap<String, AtomicLong> propertyChangeCounts ) {} }

Part 4: Controllers and Endpoints

4.1 Configuration Controller

// File: src/main/java/com/example/config/refresh/ConfigController.java package com.example.config.refresh; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/v1/config") public class ConfigController { private final RefreshableService refreshableService; private final ConfigChangeListener configChangeListener; public ConfigController(RefreshableService refreshableService, ConfigChangeListener configChangeListener) { this.refreshableService = refreshableService; this.configChangeListener = configChangeListener; } @GetMapping("/current") public ResponseEntity<RefreshableService.ServiceConfig> getCurrentConfig() { return ResponseEntity.ok(refreshableService.getCurrentConfig()); } @GetMapping("/features/{featureName}") public ResponseEntity<RefreshableService.FeatureStatus> getFeatureStatus( @PathVariable String featureName) { return ResponseEntity.ok(refreshableService.getFeatureStatus(featureName)); } @GetMapping("/database") public ResponseEntity<RefreshableService.DatabaseConfig> getDatabaseConfig() { return ResponseEntity.ok(refreshableService.getDatabaseConfig()); } @GetMapping("/cache") public ResponseEntity<RefreshableService.CacheConfig> getCacheConfig() { return ResponseEntity.ok(refreshableService.getCacheConfig()); } @GetMapping("/refresh/stats") public ResponseEntity<RefreshableService.RefreshStats> getRefreshStats() { return ResponseEntity.ok(refreshableService.getRefreshStats()); } @GetMapping("/change/stats") public ResponseEntity<ConfigChangeListener.ConfigChangeStats> getChangeStats() { return ResponseEntity.ok(configChangeListener.getConfigChangeStats()); } @PostMapping("/features/{featureName}/track") public ResponseEntity<Map<String, String>> trackFeature(@PathVariable String featureName) { configChangeListener.recordPropertyAccess("app.features." + featureName); return ResponseEntity.ok(Map.of( "status", "success", "message", "Now tracking feature: " + featureName )); } @GetMapping("/health") public ResponseEntity<Map<String, Object>> health() { RefreshableService.RefreshStats stats = refreshableService.getRefreshStats(); return ResponseEntity.ok(Map.of( "status", "healthy", "service", "config-refresh-app", "refreshEnabled", true, "totalRefreshes", stats.totalRefreshes(), "configValid", stats.configValid(), "lastRefresh", stats.lastRefreshTime() )); } }

4.2 Custom Refresh Endpoint

// File: src/main/java/com/example/config/refresh/CustomRefreshEndpoint.java package com.example.config.refresh; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; @Component @Endpoint(id = "customrefresh") public class CustomRefreshEndpoint { private final ContextRefresher contextRefresher; private final RefreshableService refreshableService; private final AtomicLong refreshRequests = new AtomicLong(0); public CustomRefreshEndpoint(ContextRefresher contextRefresher, RefreshableService refreshableService) { this.contextRefresher = contextRefresher; this.refreshableService = refreshableService; } @WriteOperation public Map<String, Object> refresh() { long requestId = refreshRequests.incrementAndGet(); try { Map<String, Object> result = new HashMap<>(); result.put("requestId", requestId); result.put("timestamp", System.currentTimeMillis()); result.put("status", "refresh_initiated"); // Perform the refresh var refreshResult = contextRefresher.refresh(); result.put("refreshedProperties", refreshResult); result.put("refreshCount", refreshableService.getRefreshStats().totalRefreshes()); result.put("message", "Configuration refresh completed successfully"); return result; } catch (Exception e) { Map<String, Object> errorResult = new HashMap<>(); errorResult.put("requestId", requestId); errorResult.put("timestamp", System.currentTimeMillis()); errorResult.put("status", "error"); errorResult.put("error", e.getMessage()); return errorResult; } } @WriteOperation public Map<String, Object> refreshWithValidation(boolean validateOnly) { long requestId = refreshRequests.incrementAndGet(); try { Map<String, Object> result = new HashMap<>(); result.put("requestId", requestId); result.put("timestamp", System.currentTimeMillis()); if (validateOnly) { // Simulate validation without actual refresh boolean configValid = refreshableService.getRefreshStats().configValid(); result.put("status", "validation_completed"); result.put("configValid", configValid); result.put("message", "Configuration validation completed"); } else { // Perform actual refresh var refreshResult = contextRefresher.refresh(); result.put("status", "refresh_completed"); result.put("refreshedProperties", refreshResult); result.put("configValid", refreshableService.getRefreshStats().configValid()); result.put("message", "Configuration refresh with validation completed"); } return result; } catch (Exception e) { Map<String, Object> errorResult = new HashMap<>(); errorResult.put("requestId", requestId); errorResult.put("timestamp", System.currentTimeMillis()); errorResult.put("status", "error"); errorResult.put("error", e.getMessage()); return errorResult; } } }

Part 5: Advanced Refresh Patterns

5.1 Conditional Refresh Configuration

// File: src/main/java/com/example/config/refresh/ConditionalRefreshService.java package com.example.config.refresh; import com.example.config.properties.AppConfigProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @Service @RefreshScope public class ConditionalRefreshService { private static final Logger logger = LoggerFactory.getLogger(ConditionalRefreshService.class); private final AppConfigProperties appConfig; private final ContextRefresher contextRefresher; private final ConcurrentMap<String, Long> lastRefreshAttempts = new ConcurrentHashMap<>(); public ConditionalRefreshService(AppConfigProperties appConfig, ContextRefresher contextRefresher) { this.appConfig = appConfig; this.contextRefresher = contextRefresher; } public boolean shouldRefreshBasedOnConditions() { if (!appConfig.getRefresh().isEnabled()) { logger.debug("Refresh is disabled in configuration"); return false; } // Check if we're in a maintenance window or have other business logic if (isInMaintenanceWindow()) { logger.debug("In maintenance window, skipping refresh"); return false; } // Check rate limiting if (isRateLimited("conditional_refresh")) { logger.debug("Rate limited, skipping refresh"); return false; } return true; } @Scheduled(fixedRateString = "${app.config.refresh.check-interval:300000}") // 5 minutes public void scheduledConfigCheck() { if (shouldRefreshBasedOnConditions()) { logger.info("Scheduled config check passed conditions, initiating refresh"); performConditionalRefresh(); } } public RefreshResult performConditionalRefresh() { try { if (!shouldRefreshBasedOnConditions()) { return new RefreshResult(false, "Refresh conditions not met", null); } logger.info("Performing conditional configuration refresh"); var refreshedProperties = contextRefresher.refresh(); recordRefreshAttempt("conditional_refresh", true); return new RefreshResult(true, "Refresh completed successfully", refreshedProperties); } catch (Exception e) { logger.error("Conditional refresh failed: {}", e.getMessage()); recordRefreshAttempt("conditional_refresh", false); return new RefreshResult(false, "Refresh failed: " + e.getMessage(), null); } } private boolean isInMaintenanceWindow() { // Implement your maintenance window logic // For example, check current time against configured maintenance windows return false; } private boolean isRateLimited(String operation) { Long lastAttempt = lastRefreshAttempts.get(operation); if (lastAttempt == null) { return false; } long timeSinceLastAttempt = System.currentTimeMillis() - lastAttempt; long minInterval = appConfig.getRefresh().getTimeoutMs(); return timeSinceLastAttempt < minInterval; } private void recordRefreshAttempt(String operation, boolean success) { lastRefreshAttempts.put(operation, System.currentTimeMillis()); logger.debug("Recorded {} refresh attempt. Success: {}", operation, success); } public record RefreshResult(boolean success, String message, Object refreshedProperties) {} }

5.2 Configuration Versioning and Rollback

// File: src/main/java/com/example/config/refresh/ConfigVersioningService.java package com.example.config.refresh; import com.example.config.properties.AppConfigProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @Service @RefreshScope public class ConfigVersioningService { private static final Logger logger = LoggerFactory.getLogger(ConfigVersioningService.class); private final AppConfigProperties appConfig; private final List<ConfigVersion> configHistory = new CopyOnWriteArrayList<>(); private final Map<String, ConfigVersion> versionSnapshots = new ConcurrentHashMap<>(); private final int maxHistorySize = 10; public ConfigVersioningService(AppConfigProperties appConfig) { this.appConfig = appConfig; // Record initial configuration recordConfigVersion("initial", "Initial configuration"); } public ConfigVersion recordConfigVersion(String versionId, String description) { ConfigVersion version = new ConfigVersion( versionId, System.currentTimeMillis(), description, snapshotCurrentConfig() ); configHistory.add(version); versionSnapshots.put(versionId, version); // Maintain history size limit if (configHistory.size() > maxHistorySize) { ConfigVersion removed = configHistory.remove(0); versionSnapshots.remove(removed.versionId()); logger.debug("Removed old config version from history: {}", removed.versionId()); } logger.info("Recorded configuration version: {} - {}", versionId, description); return version; } public ConfigVersion getCurrentVersion() { if (configHistory.isEmpty()) { return null; } return configHistory.get(configHistory.size() - 1); } public List<ConfigVersion> getConfigHistory() { return new ArrayList<>(configHistory); } public Optional<ConfigVersion> getVersion(String versionId) { return Optional.ofNullable(versionSnapshots.get(versionId)); } public ConfigVersion createSnapshot(String description) { String versionId = "snapshot-" + System.currentTimeMillis(); return recordConfigVersion(versionId, description); } public ConfigComparison compareWithVersion(String versionId) { Optional<ConfigVersion> targetVersion = getVersion(versionId); if (targetVersion.isEmpty()) { throw new IllegalArgumentException("Version not found: " + versionId); } ConfigVersion currentVersion = getCurrentVersion(); Map<String, Object> differences = findConfigDifferences( targetVersion.get().configSnapshot(), currentVersion.configSnapshot() ); return new ConfigComparison( targetVersion.get(), currentVersion, differences ); } private Map<String, Object> snapshotCurrentConfig() { Map<String, Object> snapshot = new HashMap<>(); snapshot.put("service.name", appConfig.getService().getName()); snapshot.put("service.version", appConfig.getService().getVersion()); snapshot.put("service.endpoints.timeout", appConfig.getService().getEndpoints().getTimeout()); snapshot.put("features.newPaymentService", appConfig.getFeatures().isNewPaymentService()); snapshot.put("features.darkMode", appConfig.getFeatures().isDarkMode()); snapshot.put("database.pool.maxSize", appConfig.getDatabase().getPool().getMaxSize()); snapshot.put("cache.redis.ttl", appConfig.getCache().getRedis().getTtl()); return snapshot; } private Map<String, Object> findConfigDifferences(Map<String, Object> oldConfig, Map<String, Object> newConfig) { Map<String, Object> differences = new HashMap<>(); // Find keys in old config but not in new config oldConfig.forEach((key, oldValue) -> { Object newValue = newConfig.get(key); if (!Objects.equals(oldValue, newValue)) { differences.put(key, Map.of("old", oldValue, "new", newValue)); } }); // Find keys in new config but not in old config newConfig.forEach((key, newValue) -> { if (!oldConfig.containsKey(key)) { differences.put(key, Map.of("old", null, "new", newValue)); } }); return differences; } public record ConfigVersion( String versionId, long timestamp, String description, Map<String, Object> configSnapshot ) {} public record ConfigComparison( ConfigVersion baseVersion, ConfigVersion currentVersion, Map<String, Object> differences ) {} }

Part 6: Testing Configuration Refresh

6.1 Test Configuration

// File: src/test/java/com/example/config/refresh/ConfigRefreshTest.java package com.example.config.refresh; import com.example.config.properties.AppConfigProperties; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.test.context.TestPropertySource; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @TestPropertySource(properties = { "app.service.name=test-service", "app.service.version=1.0.0", "app.features.new-payment-service=false", "app.features.dark-mode=true" }) class ConfigRefreshTest { @Autowired private RefreshableService refreshableService; @Autowired private AppConfigProperties appConfigProperties; @Test void testConfigurationPropertiesLoaded() { assertNotNull(appConfigProperties); assertEquals("test-service", appConfigProperties.getService().getName()); assertEquals("1.0.0", appConfigProperties.getService().getVersion()); assertFalse(appConfigProperties.getFeatures().isNewPaymentService()); assertTrue(appConfigProperties.getFeatures().isDarkMode()); } @Test void testRefreshableServiceInitialization() { var config = refreshableService.getCurrentConfig(); assertNotNull(config); assertEquals("test-service", config.serviceName()); assertEquals("1.0.0", config.version()); assertFalse(config.newPaymentEnabled()); assertTrue(config.darkModeEnabled()); } @Test void testFeatureStatus() { var featureStatus = refreshableService.getFeatureStatus("darkMode"); assertNotNull(featureStatus); assertEquals("darkMode", featureStatus.featureName()); assertTrue(featureStatus.enabled()); } @Test void testRefreshStats() { var stats = refreshableService.getRefreshStats(); assertNotNull(stats); assertTrue(stats.totalRefreshes() >= 0); assertNotNull(stats.configHash()); } }

6.2 Integration Test

// File: src/test/java/com/example/config/refresh/ConfigRefreshIntegrationTest.java package com.example.config.refresh; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.ResponseEntity; import org.springframework.test.context.TestPropertySource; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = { "management.endpoints.web.exposure.include=*" }) class ConfigRefreshIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void testConfigEndpoints() { String baseUrl = "http://localhost:" + port; // Test current config endpoint ResponseEntity<Map> configResponse = restTemplate.getForEntity( baseUrl + "/api/v1/config/current", Map.class); assertTrue(configResponse.getStatusCode().is2xxSuccessful()); assertNotNull(configResponse.getBody()); // Test health endpoint ResponseEntity<Map> healthResponse = restTemplate.getForEntity( baseUrl + "/api/v1/config/health", Map.class); assertTrue(healthResponse.getStatusCode().is2xxSuccessful()); assertEquals("healthy", healthResponse.getBody().get("status")); // Test refresh stats endpoint ResponseEntity<Map> statsResponse = restTemplate.getForEntity( baseUrl + "/api/v1/config/refresh/stats", Map.class); assertTrue(statsResponse.getStatusCode().is2xxSuccessful()); assertNotNull(statsResponse.getBody()); } }

Best Practices for @RefreshScope

  1. Use Judiciously: Only apply @RefreshScope to components that truly need dynamic updates
  2. Validate Changes: Implement validation logic to ensure new configurations are valid
  3. Monitor Impact: Track configuration changes and their effects on application behavior
  4. Implement Rollback: Have mechanisms to revert problematic configuration changes
  5. Use Feature Flags: Leverage configuration refresh for feature toggling
  6. Secure Endpoints: Protect refresh endpoints in production environments
  7. Test Thoroughly: Ensure configuration changes don't break application functionality
  8. Document Changes: Maintain audit trails of configuration modifications

Conclusion

Implementing @RefreshScope in Java applications provides powerful capabilities for dynamic configuration management. By following the patterns and best practices outlined in this guide, you can:

  • Achieve zero-downtime configuration updates without service restarts
  • Implement feature flagging for controlled feature rollouts
  • Create self-healing applications that can adapt to configuration changes
  • Maintain comprehensive audit trails of configuration modifications
  • Build resilient systems with proper validation and rollback mechanisms

The combination of @RefreshScope with proper architecture patterns enables truly dynamic applications that can adapt to changing requirements while maintaining stability and observability.

Java Logistics, Shipping Integration & Enterprise Inventory Automation (Tracking, ERP, RFID & Billing Systems)

https://macronepal.com/blog/aftership-tracking-in-java-enterprise-package-visibility/
Explains how to integrate AfterShip tracking services into Java applications to provide real-time shipment visibility, delivery status updates, and centralized tracking across multiple courier services.

https://macronepal.com/blog/shipping-integration-using-fedex-api-with-java-for-logistics-automation/
Explains how to integrate the FedEx API into Java systems to automate shipping tasks such as creating shipments, calculating delivery costs, generating shipping labels, and tracking packages.

https://macronepal.com/blog/shipping-and-logistics-integrating-ups-apis-with-java-applications/
Explains UPS API integration in Java to enable automated shipping operations including rate calculation, shipment scheduling, tracking, and delivery confirmation management.

https://macronepal.com/blog/generating-and-reading-qr-codes-for-products-in-java/
Explains how Java applications generate and read QR codes for product identification, tracking, and authentication, supporting faster inventory handling and product verification processes.

https://macronepal.com/blog/designing-a-robust-pick-and-pack-workflow-in-java/
Explains how to design an efficient pick-and-pack workflow in Java warehouse systems, covering order processing, item selection, packaging steps, and logistics preparation to improve fulfillment efficiency.

https://macronepal.com/blog/rfid-inventory-management-system-in-java-a-complete-guide/
Explains how RFID technology integrates with Java applications to automate inventory tracking, reduce manual errors, and enable real-time stock monitoring in warehouses and retail environments.

https://macronepal.com/blog/erp-integration-with-odoo-in-java/
Explains how Java applications connect with Odoo ERP systems to synchronize inventory, orders, customer records, and financial data across enterprise systems.

https://macronepal.com/blog/automated-invoice-generation-creating-professional-excel-invoices-with-apache-poi-in-java/
Explains how to automatically generate professional Excel invoices in Java using Apache POI, enabling structured billing documents and automated financial record creation.

https://macronepal.com/blog/enterprise-financial-integration-using-quickbooks-api-in-java-applications/
Explains QuickBooks API integration in Java to automate financial workflows such as invoice management, payment tracking, accounting synchronization, and financial reporting.

Leave a Reply

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


Macro Nepal Helper