https://www.pexels.com/photo/colorful-toothed-wheels-171198/

Mapy właściwości w Javie (Properties)

Cześć! Minął prawie tydzień od ostatniego wpisu, najwyższy czas na coś nowego, żeby nie było, że się nie wywiązuję z postanowienia pisania minimum jednego posta w tygodniu! Tym razem temat jest bardzo lekki, w zasadzie będzie to trochę wolne tłumaczenie dokumentacji. Jednak tak jak kiedyś wspominałem, tego typu wpisy również się będą pojawiać, bo chciałbym aby jedną z funkcji tego bloga była agregacja moich notatek na różne tematy związane z programowaniem.

Przechodząc do sedna sprawy, dziś będzie o zapisywaniu konfiguracji programów, czyli o plikach z mapami właściwości. Do plików właściwości mogą być zapisywane pary klucz-wartość, przy czym obie części są łańcuchami znaków. Przykładowo jeśli chcielibyśmy zapisać pozycję startową postaci w jakiejś pisanej grze, reprezentowaną punktem na ekranie, moglibyśmy utworzyć plik z poniższą zawartością.

startPositionX=10
startPositionY=30

Choć nie nie jestem dinozaurem programowania, z plikami właściwości spotkałem się już wielokrotnie, choćby w przypadku konfiguracji log4j. Tworzyłem również plik messages.properties, w którym zapisywałem etykiety znajdujące się przy elementach formularzy w projekcie wykorzystującym Spring. Zaletą był brak konieczności przebudowywania aplikacji w przypadku zmiany etykiety jakiegoś pola tekstowego i bardzo prosty sposób odwołania się do zapisanej wartości.

<label class="control-label col-lg-4" for="password">
     <spring:message code="userProfile.form.oldPassword"/>
</label>

Z pewnością można znaleźć wiele innych, ciekawszych przykładów, jednak nie jest to cel mojego wpisu. W dalszej jego części opiszę jak tworzyć własne pliki z właściwościami, wykorzystując obiekt klasy Properties. Klasa ta dziedziczy po Hashtable<Object,Object>, a idąc dalej w łańcuchu przodków dojdziemy do interfejsu Map<K,V>, skąd wynika właśnie łączenie kluczy i wartości w pary.

Omówienie przykładu rozpocznę od utworzenia mapy właściwości z przykładowymi danymi. Klasa Properties dostarcza dwa rodzaje konstruktorów, tworzący pustą mapę jak poniżej oraz Properties(Properties defaults), tworzący mapę z domyślnymi właściwościami.

private static Properties prepareDefaultProperties() {
        Properties properties = new Properties();
        properties.setProperty("sys", "Linux");
        properties.setProperty("version", "1.0");
        properties.setProperty("backgroundColor", "blue");
        return properties;
    }

Dwie podstawowe metody klasy Properties to setProperty(String key, String defaultValue) i getProperty(String key), odpowiednio ustawiająca i pobierająca właściwość o zadanym kluczu. Metoda wczytująca przeszukuje mapę, jeśli nie znajdzie danego klucza szuka dalej w wartościach domyślnych. W przypadku dalszego niepowodzenia poszukiwania zadanego klucza, metoda na wyjściu daje null.

Utworzyłem prosty obiekt z mapą właściwości, w dalszym kroku zapiszę go do pliku. Klasa Properties umożliwia utrwalenie obiektu w formie pliku tekstowego (zwyczajowo *.properties) lub jako strukturę XML. Te dwa sposoby wymagają stosowania odmiennej metody, odpowiednio store() oraz storeToXML(). Żeby było ciekawiej, operacja zapisu może odbyć się z wykorzystaniem dekoratorów klasy Writer oraz OutputStream, przy czym w tym pierwszym przypadku możliwy jest zapis jedynie w formie czystego tekstu. Natomiast korzystając ze strumieni, jest możliwość utrwalenia właściwości w obu formach.

private static void savePropertiesFileWriter(Properties properties, String filename) {
        try (Writer writer = new FileWriter(filename)) {
            properties.store(writer, null);
        } catch (IOException ex) {
            Logger.getLogger(PropertiesRunner.class.getSimpleName()).log(Level.SEVERE, null, ex);
        }
    }

Powyższa metoda obrazuje pierwszy opisany przypadek. Za pomocą klasy FileWriter, utrwalany jest obiekt klasy Properties w pliku o nazwie filename. Do metody store() podałem parametr null, gdyż nie chcę wpisywać żadnego komentarza (drugi parametr metody umożliwia dodanie komentarza do pliku, w precyzyjniej mówiąc – nagłówka). Natomiast poniższy fragment kodu przedstawia zapis z wykorzystaniem plikowego strumienia wyjściowego. Parametr xmlFormat określa format pliku wynikowego i decyduje o użytej metodzie utrwalającej (store() lub storeToXML()).

private static void savePropertiesFileOutputStream(Properties properties, String filename, boolean xmlFormat) {
        filename = xmlFormat ? filename + ".xml" : filename;
        try (OutputStream os = new FileOutputStream(filename)) {
            if (xmlFormat) {
                properties.storeToXML(os, null, "UTF-8");
            } else {
                properties.store(os, null);
            }
 
        } catch (IOException ex) {
            Logger.getLogger(PropertiesRunner.class.getSimpleName()).log(Level.SEVERE, null, ex);
        }
    }

W przypadku korzystania z metody storeToXML() istnieje możliwość wybrania kodowania pliku wyjściowego. Domyślną wartością jest UTF-8, zatem ustawienie trzeciego parametru w metodzie storeToXML(os, null, „UTF-8”) jest w tym przypadku zbędne. Jeśli już mowa o kodowaniu, należy mieć na uwadze, że metoda store(OutputStream, String) oraz analogiczna służąca do odczytu (load(InputStream)) operują na strumieniach ustawiających kodowanie ISO 8859-1.

Wychodząc nieco w przód, wywołanie tych dwóch metod (druga generująca strukturę XML) spowoduje utworzenie w głównym katalogu projektu dwóch plików z właściwościami.

#Tue Jun 27 21:01:12 CEST 2017
version=1.0
backgroundColor=blue
sys=Linux
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="version">1.0</entry>
<entry key="newProperty">newValue</entry>
<entry key="backgroundColor">blue</entry>
<entry key="sys">Windows</entry>
</properties>

Jak widać, oba pliki są do siebie podobne. Struktura XML nie jest do końca wykorzystana, bo posiada płaską strukturę. Można sztucznie spróbować utworzyć coś na wzór drzewa, stosując nazwy bardziej złożone, np. app.version zamiast version. Niekiedy nawet jest to konieczne, bo klucze muszą być niepowtarzalne. Jednak pomimo takiego zabiegu, plik XML z utrwalonymi za pomocą obiektu klasy Properties właściwościami, nie będzie przypominał typowego drzewa.

Załadowanie właściwości z pliku do obiektu klasy Properties wykonuje się z wykorzystaniem analogicznych metod jak w zapisie – load(InputStream/Reader) oraz loadFromXML(InputStream in). Tak jak w przypadku utrwalania danych, należy obsłużyć wyjątek IOException.

private static Properties loadPropertiesFileReader(String filename) {
        Properties properties = new Properties();
        try (Reader reader = new FileReader(filename)) {
            properties.load(reader);
        } catch (IOException ex) {
            Logger.getLogger(PropertiesRunner.class.getSimpleName()).log(Level.SEVERE, null, ex);
        }
        return properties;
    }
 
private static Properties loadPropertiesXML(String filename) {
        Properties properties = new Properties();
        try (InputStream is = new FileInputStream(filename)) {
            properties.loadFromXML(is);
        } catch (IOException ex) {
            Logger.getLogger(PropertiesRunner.class.getSimpleName()).log(Level.SEVERE, null, ex);
        }
        return properties;
    }

Na koniec wpisu zostawiłem listing klasy PropertiesRunner z metodą main(), w której uruchamiane są opisane metody.

public class PropertiesRunner {
 
    private static final String filename = "config.properties";
 
    public static void main(String[] args) {
 
        System.out.println("Ustawienie wartości początkowych");
        Properties properties = prepareDefaultProperties();
 
        System.out.println("Zapis do pliku config.properties");
        savePropertiesFileWriter(properties, PropertiesRunner.filename);
 
        System.out.println("Zapis do pliku config.properties.xml");
        properties.setProperty("newProperty", "newValue");
        properties.setProperty("sys", "Windows");
        savePropertiesFileOutputStream(properties, PropertiesRunner.filename, true);
 
        properties.setProperty("version", "3.0");
 
        System.out.println("\nOdczyt z pliku config.properties:");
        properties = loadPropertiesFileReader(PropertiesRunner.filename);
        properties.forEach((key, value) -> System.out.println(key + ": " + value));
 
        System.out.println("\nOdczyt z pliku config.properties.xml");
        properties = loadPropertiesXML(PropertiesRunner.filename + ".xml");
        properties.forEach((key, value) -> System.out.println(key + ": " + value));
    }
    // pozostałe metody
}

Nie ma tu żadnych nowych elementów, nieopisanych powyżej. Tworzona jest domyślna mapa właściwości prepareDefaultProperties(), po czym utrwalana jest w pliku config.properties (savePropertiesFileWriter()) oraz config.properties.xml (savePropertiesFileOutputStream()). W następnym kroku edytuję wartość właściwości o kluczu version, aby przy pobraniu danych z plików mieć pewność, że zostały prawidłowo załadowane (loadPropertiesFileReader()oraz loadPropertiesXML()). Pary klucz-wartość wczytane z pliku są wypisane na konsoli w metodzie forEach() przyjmującej jako argument interfejs funkcyjny BiConsumer<T,U>.

Wynik działania programu:

Ustawienie wartości początkowych
Zapis do pliku config.properties
Zapis do pliku config.properties.xml
 
Odczyt z pliku config.properties:
sys: Linux
backgroundColor: blue
version: 1.0
 
Odczyt z pliku config.properties.xml
sys: Windows
backgroundColor: blue
newProperty: newValue
version: 1.0

Literatura obowiązkowa

Properties tutorial
Properties JavaDoc
Hashtable JavaDoc
stackoverflow.com

2 thoughts to “Mapy właściwości w Javie (Properties)”

  1. Dzięki za posta, fajnie i przejrzyście zaprezentowane 🙂 Ostatnio przechodzę ze świata .NET do Javy właśnie, więc pewnie będę tu częściej zaglądał 🙂 Co do klasy Properties, można dodać, że klasa System przechowuje obiekt klasy Properties, dzięki czemu mamy dostęp do przydatnych informacji o systemie. Np. poprzez metodę getProperty() z argumentem definiującym klucz właściwości. Przykładowo: System.getProperty(„os.version”).

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *