Apollo Config Server in Java: Dynamic Configuration Management

Apollo is a reliable configuration management system developed by Ctrip that enables applications to dynamically manage configurations across environments. For Java applications, Apollo provides robust client support with features like real-time configuration updates, namespace isolation, and audit trails.

What is Apollo Config Server?

Apollo Config Server provides centralized configuration management with:

  • Real-time configuration updates without application restarts
  • Namespace isolation for different environments and applications
  • Configuration grayscale release for controlled rollouts
  • Version management and rollback capabilities
  • Access control and audit logging

Java Client Setup and Dependencies

Maven Configuration:

<dependencies>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-core</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Spring Boot Integration -->
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>

Gradle Configuration:

dependencies {
implementation 'com.ctrip.framework.apollo:apollo-client:2.1.0'
implementation 'com.ctrip.framework.apollo:apollo-core:2.1.0'
implementation 'com.ctrip.framework.apollo:apollo-spring-boot-starter:2.1.0'
}

Basic Configuration Setup

application.properties:

# Apollo Meta Server
app.id=order-service
apollo.meta=http://apollo-config-service:8080
apollo.bootstrap.enabled=true
apollo.bootstrap.namespaces=application,redis,database
apollo.cacheDir=/opt/data/apollo-config
apollo.cluster=default
# Emergency fallback
apollo.autoUpdateInjectedSpringProperties=true
apollo.bootstrap.eagerLoad.enabled=true

bootstrap.yml:

app:
id: order-service
apollo:
bootstrap:
enabled: true
eagerLoad: true
namespaces: 
- application
- redis.properties
- database.yml
- FEATURE_TOGGLES
meta: http://apollo-config-service:8080
cacheDir: /opt/data/apollo-config
cluster: default
autoUpdateInjectedSpringProperties: true

Spring Boot Integration

ApolloConfigApplication.java:

@SpringBootApplication
@EnableApolloConfig
public class ApolloConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ApolloConfigApplication.class, args);
}
}

ApolloConfiguration.java:

@Configuration
public class ApolloConfiguration {
@Bean
public Config config() {
return ConfigService.getAppConfig();
}
@Bean
@ConditionalOnMissingBean
public ApolloConfigChangeListener apolloConfigChangeListener() {
return new ApolloConfigChangeListener();
}
}

Configuration Management

DynamicConfigService.java:

@Service
@Slf4j
public class DynamicConfigService {
private final Config applicationConfig;
private final Config redisConfig;
private final Config featureConfig;
public DynamicConfigService() {
this.applicationConfig = ConfigService.getAppConfig();
this.redisConfig = ConfigService.getConfig("redis.properties");
this.featureConfig = ConfigService.getConfig("FEATURE_TOGGLES");
}
// Basic configuration retrieval
public String getString(String key, String defaultValue) {
return applicationConfig.getProperty(key, defaultValue);
}
public Integer getInt(String key, Integer defaultValue) {
return applicationConfig.getIntProperty(key, defaultValue);
}
public Boolean getBoolean(String key, Boolean defaultValue) {
return applicationConfig.getBooleanProperty(key, defaultValue);
}
public Long getLong(String key, Long defaultValue) {
return applicationConfig.getLongProperty(key, defaultValue);
}
// Namespace-specific configuration
public String getRedisConfig(String key) {
return redisConfig.getProperty(key, null);
}
public boolean isFeatureEnabled(String featureName) {
return featureConfig.getBooleanProperty(featureName, false);
}
// Get all properties from a namespace
public Set<String> getPropertyNames(String namespace) {
Config config = ConfigService.getConfig(namespace);
return config.getPropertyNames();
}
// Get configuration with JSON parsing
public <T> T getJsonConfig(String key, Class<T> clazz) {
try {
String json = applicationConfig.getProperty(key, null);
if (json != null) {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, clazz);
}
} catch (Exception e) {
log.error("Failed to parse JSON config for key: {}", key, e);
}
return null;
}
}

Real-time Configuration Updates

ApolloConfigChangeListener.java:

@Component
@Slf4j
public class ApolloConfigChangeListener {
private final Map<String, List<ConfigChangeCallback>> changeCallbacks;
public ApolloConfigChangeListener() {
this.changeCallbacks = new ConcurrentHashMap<>();
initializeConfigChangeListener();
}
private void initializeConfigChangeListener() {
Config config = ConfigService.getAppConfig();
config.addChangeListener(this::handleConfigChange);
// Listen to additional namespaces
Config redisConfig = ConfigService.getConfig("redis.properties");
redisConfig.addChangeListener(this::handleRedisConfigChange);
Config featureConfig = ConfigService.getConfig("FEATURE_TOGGLES");
featureConfig.addChangeListener(this::handleFeatureToggleChange);
}
private void handleConfigChange(ConfigChangeEvent changeEvent) {
log.info("Configuration changed for namespace: {}", changeEvent.getNamespace());
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
log.info("Key: {} - OldValue: {}, NewValue: {}, ChangeType: {}",
key, change.getOldValue(), change.getNewValue(), change.getChangeType());
// Notify registered callbacks
notifyCallbacks(key, change);
// Handle specific configuration changes
handleSpecificConfigChanges(key, change);
}
}
private void handleRedisConfigChange(ConfigChangeEvent changeEvent) {
log.info("Redis configuration changed");
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
if ("redis.host".equals(key) || "redis.port".equals(key)) {
log.warn("Redis connection details changed, consider reinitializing connection");
// Trigger Redis connection refresh
}
}
}
private void handleFeatureToggleChange(ConfigChangeEvent changeEvent) {
log.info("Feature toggles changed");
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
log.info("Feature toggle updated: {} = {}", key, change.getNewValue());
// Handle feature toggle changes
if ("feature.new_payment_gateway".equals(key)) {
handlePaymentGatewayToggle(Boolean.parseBoolean(change.getNewValue()));
}
}
}
private void handleSpecificConfigChanges(String key, ConfigChange change) {
switch (key) {
case "server.port":
log.warn("Server port changed, application restart required");
break;
case "logging.level":
// Dynamic logging level changes
updateLoggingLevel(change.getNewValue());
break;
case "thread.pool.size":
// Update thread pool dynamically
updateThreadPoolSize(Integer.parseInt(change.getNewValue()));
break;
}
}
public void registerCallback(String key, ConfigChangeCallback callback) {
changeCallbacks.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>())
.add(callback);
}
private void notifyCallbacks(String key, ConfigChange change) {
List<ConfigChangeCallback> callbacks = changeCallbacks.get(key);
if (callbacks != null) {
callbacks.forEach(callback -> {
try {
callback.onConfigChange(key, change);
} catch (Exception e) {
log.error("Callback execution failed for key: {}", key, e);
}
});
}
}
@FunctionalInterface
public interface ConfigChangeCallback {
void onConfigChange(String key, ConfigChange change);
}
}

Dynamic Bean Configuration

RefreshableBeanConfiguration.java:

@Configuration
@Slf4j
public class RefreshableBeanConfiguration {
private final DynamicConfigService configService;
private final ApolloConfigChangeListener changeListener;
private volatile RestTemplate restTemplate;
private volatile DataSource dataSource;
public RefreshableBeanConfiguration(DynamicConfigService configService, 
ApolloConfigChangeListener changeListener) {
this.configService = configService;
this.changeListener = changeListener;
registerConfigChangeHandlers();
}
@Bean
@RefreshScope
public RestTemplate restTemplate() {
if (restTemplate == null) {
synchronized (this) {
if (restTemplate == null) {
restTemplate = createRestTemplate();
}
}
}
return restTemplate;
}
@Bean
@RefreshScope
public DataSource dataSource() {
if (dataSource == null) {
synchronized (this) {
if (dataSource == null) {
dataSource = createDataSource();
}
}
}
return dataSource;
}
private void registerConfigChangeHandlers() {
// Register for connection timeout changes
changeListener.registerCallback("http.client.timeout", (key, change) -> {
log.info("HTTP timeout changed, refreshing RestTemplate");
refreshRestTemplate();
});
// Register for database configuration changes
changeListener.registerCallback("spring.datasource.url", (key, change) -> {
log.warn("Database URL changed, consider reinitializing DataSource");
// In production, you might want more sophisticated handling
});
}
private RestTemplate createRestTemplate() {
int timeout = configService.getInt("http.client.timeout", 5000);
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(timeout, TimeUnit.MILLISECONDS)
.readTimeout(timeout, TimeUnit.MILLISECONDS)
.build();
return new RestTemplate(new OkHttp3ClientHttpRequestFactory(client));
}
private DataSource createDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(configService.getString("spring.datasource.url", ""));
config.setUsername(configService.getString("spring.datasource.username", ""));
config.setPassword(configService.getString("spring.datasource.password", ""));
config.setMaximumPoolSize(configService.getInt("spring.datasource.max-pool-size", 10));
return new HikariDataSource(config);
}
private void refreshRestTemplate() {
synchronized (this) {
this.restTemplate = createRestTemplate();
}
}
}

Feature Toggle Management

FeatureToggleService.java:

@Service
@Slf4j
public class FeatureToggleService {
private final DynamicConfigService configService;
private final ApolloConfigChangeListener changeListener;
private final Map<String, Boolean> featureCache = new ConcurrentHashMap<>();
private final List<FeatureToggleListener> listeners = new CopyOnWriteArrayList<>();
public FeatureToggleService(DynamicConfigService configService,
ApolloConfigChangeListener changeListener) {
this.configService = configService;
this.changeListener = changeListener;
initializeFeatureToggles();
registerFeatureChangeListener();
}
public boolean isEnabled(String featureName) {
return featureCache.computeIfAbsent(featureName, 
key -> configService.isFeatureEnabled(key));
}
public boolean isEnabled(String featureName, boolean defaultValue) {
return featureCache.getOrDefault(featureName, defaultValue);
}
public Map<String, Boolean> getAllFeatures() {
return new HashMap<>(featureCache);
}
public void registerListener(FeatureToggleListener listener) {
listeners.add(listener);
}
private void initializeFeatureToggles() {
// Preload common feature toggles
String[] commonFeatures = {
"feature.new_payment_gateway",
"feature.advanced_search",
"feature.recommendation_engine",
"feature.analytics_dashboard"
};
for (String feature : commonFeatures) {
featureCache.put(feature, configService.isFeatureEnabled(feature));
}
}
private void registerFeatureChangeListener() {
changeListener.registerCallback("FEATURE_TOGGLES", (key, change) -> {
String featureName = key;
boolean newValue = Boolean.parseBoolean(change.getNewValue());
boolean oldValue = Boolean.parseBoolean(change.getOldValue());
if (oldValue != newValue) {
featureCache.put(featureName, newValue);
notifyFeatureListeners(featureName, newValue);
log.info("Feature {} changed to: {}", featureName, newValue);
}
});
}
private void notifyFeatureListeners(String featureName, boolean enabled) {
listeners.forEach(listener -> {
try {
listener.onFeatureToggleChange(featureName, enabled);
} catch (Exception e) {
log.error("Feature toggle listener failed for: {}", featureName, e);
}
});
}
@FunctionalInterface
public interface FeatureToggleListener {
void onFeatureToggleChange(String featureName, boolean enabled);
}
}

Configuration Validation

ConfigValidationService.java:

@Service
@Slf4j
public class ConfigValidationService {
private final DynamicConfigService configService;
private final ApolloConfigChangeListener changeListener;
public ConfigValidationService(DynamicConfigService configService,
ApolloConfigChangeListener changeListener) {
this.configService = configService;
this.changeListener = changeListener;
registerValidationHandlers();
}
public boolean validateConfiguration() {
List<String> errors = new ArrayList<>();
// Validate required configurations
validateRequiredConfig("spring.datasource.url", errors);
validateRequiredConfig("server.port", errors);
validateNumericConfig("server.port", 1024, 65535, errors);
validateNumericConfig("thread.pool.size", 1, 100, errors);
if (!errors.isEmpty()) {
log.error("Configuration validation failed: {}", errors);
return false;
}
return true;
}
private void validateRequiredConfig(String key, List<String> errors) {
String value = configService.getString(key, null);
if (value == null || value.trim().isEmpty()) {
errors.add("Required configuration missing: " + key);
}
}
private void validateNumericConfig(String key, int min, int max, List<String> errors) {
Integer value = configService.getInt(key, null);
if (value != null && (value < min || value > max)) {
errors.add(String.format("Configuration %s=%d is out of range [%d, %d]", 
key, value, min, max));
}
}
private void registerValidationHandlers() {
changeListener.registerCallback("server.port", (key, change) -> {
try {
int port = Integer.parseInt(change.getNewValue());
if (port < 1024 || port > 65535) {
log.error("Invalid server port configuration: {}", port);
// You might want to revert the change or take other actions
}
} catch (NumberFormatException e) {
log.error("Invalid server port format: {}", change.getNewValue());
}
});
}
}

Spring Configuration Properties Integration

ApolloConfigProperties.java:

@Configuration
@ConfigurationProperties(prefix = "app")
@RefreshScope
@Data
public class ApolloConfigProperties {
private String name;
private String version;
private Database database = new Database();
private Redis redis = new Redis();
private Security security = new Security();
@Data
public static class Database {
private String url;
private String username;
private String password;
private int maxPoolSize = 10;
private int connectionTimeout = 30000;
}
@Data
public static class Redis {
private String host;
private int port = 6379;
private String password;
private int database = 0;
private int timeout = 2000;
}
@Data
public static class Security {
private String secretKey;
private int tokenExpiration = 3600;
private boolean enableCsrf = true;
}
}

Kubernetes Deployment

apollo-configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
name: apollo-client-config
data:
application.properties: |
# Apollo Meta Server
app.id=order-service
apollo.meta=http://apollo-config-service:8080
apollo.bootstrap.enabled=true
apollo.bootstrap.namespaces=application,redis.properties,database.yml
apollo.cluster=default
apollo.cacheDir=/opt/app/apollo-config
apollo.autoUpdateInjectedSpringProperties=true

deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: order-service:latest
ports:
- containerPort: 8080
env:
- name: APP_ID
value: "order-service"
- name: APOLLO_META
value: "http://apollo-config-service:8080"
- name: APOLLO_CLUSTER
value: "default"
- name: JAVA_OPTS
value: "-Dapollo.cacheDir=/opt/app/apollo-config"
volumeMounts:
- name: apollo-cache
mountPath: /opt/app/apollo-config
livenessProbe:
httpGet:
path: /health
port: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
volumes:
- name: apollo-cache
emptyDir: {}

Testing Apollo Configuration

TestApolloConfiguration.java:

@SpringBootTest
@TestPropertySource(properties = {
"app.id=test-order-service",
"apollo.meta=http://localhost:8080",
"apollo.bootstrap.enabled=false"
})
@MockBean(ConfigService.class)
public class TestApolloConfiguration {
@Autowired
private DynamicConfigService configService;
@MockBean
private Config mockConfig;
@Test
public void testConfigRetrieval() {
when(mockConfig.getProperty("server.port", "8080")).thenReturn("9090");
String port = configService.getString("server.port", "8080");
assertEquals("9090", port);
}
@Test
public void testConfigChangeListener() {
ApolloConfigChangeListener listener = new ApolloConfigChangeListener();
ConfigChangeEvent event = mock(ConfigChangeEvent.class);
when(event.getNamespace()).thenReturn("application");
when(event.changedKeys()).thenReturn(Set.of("server.port"));
ConfigChange change = new ConfigChange("application", "server.port", "8080", "9090", PropertyChangeType.MODIFIED);
when(event.getChange("server.port")).thenReturn(change);
// Test that listener handles changes without errors
assertDoesNotThrow(() -> listener.handleConfigChange(event));
}
}

Best Practices for Java Applications

  1. Use appropriate namespaces for different configuration types
  2. Implement proper error handling for configuration retrieval
  3. Use configuration validation to catch issues early
  4. Implement circuit breakers for configuration updates
  5. Monitor configuration changes with proper logging
  6. Use feature toggles for controlled rollouts
  7. Implement configuration caching with appropriate TTL
  8. Secure sensitive configurations with proper access controls

Conclusion

Apollo Config Server provides a robust configuration management solution for Java applications with:

  • Real-time configuration updates without application restarts
  • Namespace-based configuration isolation
  • Feature toggle management for controlled releases
  • Comprehensive Spring Boot integration
  • Kubernetes-native deployment support

By implementing Apollo in your Java applications, you can achieve dynamic configuration management, reduce deployment friction, and enable feature flagging capabilities that support modern DevOps practices and continuous delivery workflows.

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