Umgebungsabhängige Konfiguration

Bei bisher jedem meiner Projekte wurde die Anwendung in verschiedenen Umgebungen installiert. Daher gab es stets die Anforderung eine Konfigurationsmöglichkeit für die umgebungsspezifischen  Parameter in einer Propertydatei außerhalb der Anwendung zu haben. Zusätzlich sollte es möglich sein einzelne Werte bei Bedarf zu verschlüsseln. Die Parameter selbst sollten zentral als Konstanten konfiguriert werden, damit zukünftige Änderungen nur an einer Stelle im Quellcode vorgenommen werden müssen. Da ich ein Freund davon bin, das Rad nicht immer neu zu erfinden, basiert die Konfiguration auf Apache Commons Configuration.

Zunächst einmal habe ich zwei Interfaces erstellt, um unabhängig von der Implementierung zu sein. Das Interface ConfigurationKey stellt die drei Methoden getDefaultValue, getKey und isCiphered zur Verfügung.  Diese werden dann von der Enum, welche die Konfigurationsparameter verwaltet, implementiert.

package de.volkerfaas.configuration;

public interface ConfigurationKey {

    String getDefaultValue();
    String getKey();
    boolean isCiphered();

}

Das Interface Configuration stellt seinerseits Methoden zur Verfügung um in der Anwendung auf die jeweiligen Konfigurationsparameter zuzugreifen.

package de.volkerfaas.configuration;

public interface Configuration {

    boolean getBoolean(ConfigurationKey key);
    int getInt(ConfigurationKey key);
    String getString(ConfigurationKey key);
    String[] getStringArray(ConfigurationKey key);

}

Nun zu den Implementierungen der Interfaces. Wie bereits erwähnt war das Ziel die Parameter als Konstanten zu verwalten. Daher erfolgt die Implementierung des Interfaces ConfigurationKey als Enum. Als Beispiel dient hier die Konfiguration einer Datenquelle. Die Enum ist um beliebige weitere Parameter erweiterbar.

package de.volkerfaas.configuration.impl;

import de.volkerfaas.configuration.ConfigurationKey;

public enum ConfigurationKeyImpl implements ConfigurationKey {

    CONFIGURATION_PARAM_DATASOURCE_DRIVERCLASSNAME("dataSource.driverClassName"),
    CONFIGURATION_PARAM_DATASOURCE_PASSWORD("dataSource.password", true),
    CONFIGURATION_PARAM_DATASOURCE_URL("dataSource.url"),
    CONFIGURATION_PARAM_DATASOURCE_USERNAME("dataSource.username", true);

    private final boolean ciphered;
    private final String defaultValue;
    private final String key;

    private ConfigurationKeyImpl(final String key) {
        this(key, null, false);
    }

    private ConfigurationKeyImpl(final String key, boolean cipher) {
        this(key, null, cipher);
    }

    private ConfigurationKeyImpl(final String key, final String defaultValue) {
        this(key, defaultValue, false);
    }

    private ConfigurationKeyImpl(final String key, final String defaultValue, boolean ciphered) {
        this.key = key;
        this.defaultValue = defaultValue;
        this.ciphered = ciphered;
    }

    @Override
    public String getDefaultValue() {
        return defaultValue;
    }

    @Override
    public String getKey() {
        return key;
    }

    @Override
    public boolean isCiphered() {
        return ciphered;
    }

}

Die Implementierung des Interfaces Configuration erweitert zusätzlich noch die Klasse PropertiesConfiguration aus Apache Commons Configuration. Für die Verschlüsselung nutze ich eine entsprechende Implementierung meines Interfaces Crypt, beschrieben im Beitrag Verschlüsselung. Dies wird in einem weiteren Beitrag beschrieben.

package de.volkerfaas.configuration;

import de.volkerfaas.crypto.Crypt;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;

public class EnumPropertiesConfiguration extends PropertiesConfiguration implements Configuration {

    private Crypt crypt;

    public EnumPropertiesConfiguration(File file) throws ConfigurationException {
        super(file);
    }

    public void setCrypt(Crypt crypt) {
        this.crypt = crypt;
    }

    @Override
    public boolean getBoolean(ConfigurationKey key) {
        if (key.getDefaultValue() != null) {
            return getBoolean(key.getKey(), Boolean.valueOf(key.getDefaultValue()));
        }

        return getBoolean(key.getKey());
    }

    @Override
    public int getInt(ConfigurationKey key) {
        if (key.getDefaultValue() != null) {
            return getInt(key.getKey(), Integer.valueOf(key.getDefaultValue()));
        }

        return getInt(key.getKey());
    }

    @Override
    public String getString(ConfigurationKey key) {
        String value;
        if (key.getDefaultValue() != null) {
            value = getString(key.getKey(), key.getDefaultValue());
        } else {
            value = getString(key.getKey());
        }

        if (key.isCiphered()) {
            if (crypt == null) {
                throw new IllegalArgumentException("No crypt available!");
            }

            try {
                value = crypt.decrypt(value);
            } catch (InvalidKeyException e) {
                throw new IllegalArgumentException(e);
            } catch (IOException e) {
                throw new IllegalArgumentException(e);
            } catch (BadPaddingException e) {
                throw new IllegalArgumentException(e);
            } catch (IllegalBlockSizeException e) {
                throw new IllegalArgumentException(e);
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            }
        }

        return value;
    }

    @Override
    public String[] getStringArray(ConfigurationKey key) {
        return getStringArray(key.getKey());
    }

}

Eine mögliche Anwendung der Konfiguration beschreibe ich Anhand von Spring. Hier sei zusätzlich erwähnt, dass das Verzeichnis, in welchem sich die Konfigurationsdatei befindet, über ein System-Property gesetzt wird. Die Datei selbst heißt application.properties. Außerdem wird die Datei alle 60 Sekunden auf Änderungen geprüft. Die Konfiguration des entsprechenden Spring Beans sieht dann wie folgt aus:

@Bean
public EnumPropertiesConfiguration configuration() throws Exception {
    final String configdir = System.getProperty("de.volkerfaas.configdir");
    if (configdir == null || configdir.isEmpty()) {
        throw new IllegalStateException("'de.volkerfaas.configdir' must not be empty!");
    }

    File file = new File(configdir, "application.properties");
    Crypt crypt = new DesCrypt();
    FileChangedReloadingStrategy reloadingStrategy = new FileChangedReloadingStrategy();
    reloadingStrategy.setRefreshDelay(60000L);

    EnumPropertiesConfiguration configuration = new EnumPropertiesConfiguration(file);
    configuration.setCrypt(crypt);
    configuration.setReloadingStrategy(reloadingStrategy);

    return configuration;
}

Abschließend noch ein Beispiel anhand eines Unit Tests wie auf die Konfiguration zugegriffen wird. Durch die Spring Dependency Injection des Configuration Interfaces ist die aufrufende Klasse komplett unabhängig von dessen Implementierung.

package de.volkerfaas.business.service;

import de.volkerfaas.configuration.Configuration;
import de.volkerfaas.TestLogicRootConfig;
import de.volkerfaas.configuration.impl.ConfigurationKeyImpl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.springframework.test.context.transaction.TransactionConfiguration;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestLogicRootConfig.class, loader = AnnotationConfigContextLoader.class)
public class ServiceTest {

    @Autowired
    private Configuration configuration;

    @Test
    public void test() throws Exception {
        String driverClassName = configuration.getString(ConfigurationKeyImpl.CONFIGURATION_PARAM_DATASOURCE_DRIVERCLASSNAME);
        assertNotNull("'driverClassName' must not be null", driverClassName);
    }
}

Schreibe einen Kommentar