$ sudo teach IT
МОДУЛЬ 11 · УРОК 5

Properties и конфигурация

Почти любое приложение нуждается в настройках: адрес сервера, порт, имя базы данных, уровень логирования, пути к файлам. Хранить всё это прямо в коде — плохая практика. В этом уроке мы разберём java.util.Properties — класс для хранения и чтения конфигураций, а также переменные окружения и системные свойства Java.

⏱ ~30 минут Для новичков Java

Зачем нужна конфигурация?

Представьте, что вы разрабатываете веб-приложение. На вашем компьютере оно подключается к базе данных на localhost:5432. Но на сервере.production база находится по адресу db.mycompany.com:5432. Если адрес базы зашит прямо в код, вам придётся каждый раз менять код перед деплоем — а это чревато ошибками.

Правильный подход — вынести настройки в отдельный файл. Программа при запуске читает этот файл и использует значения из него. Такой подход называется конфигурацией приложения.

💡 Аналогия: Конфигурация — это как пульт от телевизора. Вы можете переключать каналы, регулировать громкость, менять яркость — не открывая сам телевизор. Конфигурационный файл — это пульт для вашей программы: вы меняете настройки, а программа подстраивается.

В Java для конфигурации традиционно используется класс java.util.Properties. Он прост, проверен временем и встроен в стандартную библиотеку. Хотя сегодня существуют более современные форматы (JSON, YAML), Properties всё ещё широко используется — особенно в проектах на Spring, в Android-приложениях и в серверных конфигурациях.

Класс java.util.Properties

Properties — это класс из пакета java.util, который хранит набор пар «ключ-значение». Ключи и значения — всегда строки. По сути, Properties — это специализированная версия Hashtable, адаптированная для работы со строковыми конфигурациями.

Почему именно строки? Пототому что конфигурация — это текст. Адрес сервера, номер порта, имя пользователя — всё это текстовые значения. Даже если порт — число 8080, в конфигурации он хранится как строка "8080", а программа сама преобразует его в int при необходимости.

import java.util.Properties;

public class PropertiesBasic {
    public static void main(String[] args) {
        Properties props = new Properties();

        props.setProperty("db.host", "localhost");
        props.setProperty("db.port", "5432");
        props.setProperty("db.name", "myapp");
        props.setProperty("db.user", "admin");
        props.setProperty("db.password", "secret123");
        props.setProperty("app.name", "My Application");
        props.setProperty("app.version", "1.0.0");

        String host = props.getProperty("db.host");
        String port = props.getProperty("db.port");
        String name = props.getProperty("db.name");

        System.out.println("Подключение к " + host + ":" + port);
        System.out.println("База данных: " + name);
    }
}

Вывод программы:

Подключение к localhost:5432
База данных: myapp

✅ Ключевой момент: Метод setProperty() принимает и ключ, и значение — оба типа String. Если ключ уже существует, его значение будет заменено. Это как словарь: один ключ — одно значение.

Создание и инициализация Properties

Есть несколько способов создать объект Properties и заполнить его данными.

Способ 1: Пустой конструктор + setProperty()

Самый простой способ — создать пустой объект и добавить значения по одному:

import java.util.Properties;

public class EmptyProperties {
    public static void main(String[] args) {
        Properties props = new Properties();

        props.setProperty("server.host", "192.168.1.100");
        props.setProperty("server.port", "8080");
        props.setProperty("server.timeout", "30000");

        System.out.println("Хост: " + props.getProperty("server.host"));
        System.out.println("Порт: " + props.getProperty("server.port"));
    }
}

Способ 2: Конструктор с Properties по умолчанию

Properties может иметь «родительский» объект Properties. Если ключ не найден в текущем объекте, поиск продолжается в родительском. Это удобно для задания значений по умолчанию:

import java.util.Properties;

public class DefaultProperties {
    public static void main(String[] args) {
        Properties defaults = new Properties();
        defaults.setProperty("db.host", "localhost");
        defaults.setProperty("db.port", "5432");
        defaults.setProperty("db.name", "test_db");
        defaults.setProperty("log.level", "INFO");

        Properties props = new Properties(defaults);

        props.setProperty("db.host", "production-server.com");
        props.setProperty("db.name", "production_db");

        System.out.println("Хост: " + props.getProperty("db.host"));
        System.out.println("Порт: " + props.getProperty("db.port"));
        System.out.println("БД: " + props.getProperty("db.name"));
        System.out.println("Логирование: " + props.getProperty("log.level"));
    }
}

Вывод:

Хост: production-server.com
Порт: 5432
БД: production_db
Логирование: INFO

💡 Аналогия: Это как настройки телефона. У телефона есть значения по умолчанию (яркость 50%, звук 70%). Вы можете изменить только то, что вам нужно (поставить яркость 80%), а остальное останется по умолчанию. Аналогично: Properties с defaults хранит базовые настройки, а вы переопределяете только необходимые.

Чтение свойств: getProperty()

Метод getProperty() — основной способ получить значение по ключу. Он имеет две формы:

import java.util.Properties;

public class GetPropertyExample {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.setProperty("app.name", "MyApp");
        props.setProperty("app.version", "2.0");

        String name = props.getProperty("app.name");
        System.out.println("Имя: " + name);

        String missing = props.getProperty("app.description");
        System.out.println("Описание: " + missing);

        String withDefault = props.getProperty("app.description", "Нет описания");
        System.out.println("Описание: " + withDefault);
    }
}

Вывод:

Имя: MyApp
Описание: null
Описание: Нет описания

Заметьте: getProperty() без второго аргумента возвращает null для несуществующего ключа. Вторая форма с defaultValue вернёт указанное значение вместо null.

💡 Разница между getProperty() и get(): Метод get() наследуется от Hashtable и возвращает объект типа Object. Метод getProperty() специализирован для Properties и возвращает String. Используйте getProperty() — он удобнее и не требует приведения типа.

import java.util.Properties;

public class GetVsGetProperty {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.setProperty("port", "8080");

        Object obj = props.get("port");
        String str = props.getProperty("port");

        System.out.println("get(): " + obj.getClass().getSimpleName() + " = " + obj);
        System.out.println("getProperty(): " + str.getClass().getSimpleName() + " = " + str);
    }
}

Файл .properties: формат и структура

Файл .properties — это простой текстовый файл. Формат настолько прост, что его можно редактировать в любом текстовом редакторе. Вот его структура:

# Это комментарий
! Это тоже комментарий

# Настройки базы данных
db.host=localhost
db.port=5432
db.name=myapp
db.user=admin
db.password=secret123

# Настройки сервера
server.host=0.0.0.0
server.port=8080
server.timeout=30000

# Настройки логирования
log.level=INFO
log.file=app.log

Правила формата:

  • Каждая строка — это пара ключ=значение или ключ:значение
  • Строки, начинающиеся с # или ! — комментарии
  • Пустые строки игнорируются
  • Пробелы вокруг = или : не значимы (но если пробелы часть значения — они сохраняются)
  • Значения могут быть пустыми строками: key=
  • Для многострочных значений используйте обратный слэш в конце строки: key=first line \
    second line
  • Юникод-символы пишутся как \uXXXX: greeting=\u041f\u0440\u0438\u0432\u0435\u0442

⚠️ Важно: Файл .properties по умолчанию использует кодировку ISO-8859-1 (Latin-1). Если вам нужны русские символы, либо используйте Unicode-экранирование (\u041f\u0440\u0438\u0432\u0435\u0442), либо сохраняйте файл в UTF-8 и при чтении указывайте кодировку (см. раздел о загрузке).

Загрузка Properties из файла

Теперь самое важное — загрузка настроек из файла. Метод load() принимает InputStream и читает из него пары ключ-значение.

Загрузка через FileInputStream

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class LoadFromFile {
    public static void main(String[] args) {
        Properties props = new Properties();

        try (FileInputStream fis = new FileInputStream("config.properties")) {
            props.load(fis);
            System.out.println("Настройки загружены!");
        } catch (IOException e) {
            System.err.println("Ошибка чтения файла: " + e.getMessage());
        }

        String host = props.getProperty("db.host", "localhost");
        String port = props.getProperty("db.port", "3306");
        String db = props.getProperty("db.name", "test");

        System.out.println("Хост: " + host);
        System.out.println("Порт: " + port);
        System.out.println("БД: " + db);
    }
}

Загрузка через java.nio.file

Более современный способ — использовать Files.newInputStream():

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

public class LoadWithNio {
    public static void main(String[] args) {
        Properties props = new Properties();
        Path configPath = Path.of("config.properties");

        try (InputStream is = Files.newInputStream(configPath)) {
            props.load(is);
            System.out.println("Настройки загружены!");
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }

        props.forEach((key, value) ->
            System.out.println(key + " = " + value)
        );
    }
}

✅ try-with-resources: Обратите внимание на конструкцию try (... fis = ...) { }. Это try-with-resources, который автоматически закрывает файл после завершения блока. Вы не забудете вызвать close() — Java сделает это за вас.

Загрузка из ресурсов класса

Если файл .properties находится внутри JAR-файла (в пакете resources), используйте getResourceAsStream():

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class LoadFromResource {
    public static void main(String[] args) {
        Properties props = new Properties();

        try (InputStream is = LoadFromResource.class
                .getClassLoader()
                .getResourceAsStream("config.properties")) {
            if (is == null) {
                System.err.println("Файл config.properties не найден!");
                return;
            }
            props.load(is);
            System.out.println("Настройки загружены из ресурсов!");
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }

        props.forEach((key, value) ->
            System.out.println(key + " = " + value)
        );
    }
}

💡 Аналогия: Представьте, что файл конфигурации — это книга рецептов. FileInputStream — это когда вы берёте книгу с полки. getResourceAsStream — это когда книга вшита в обложку вашего блокнота. Второй способ удобнее для распространения: вам не нужно отдавать файл отдельно — он уже внутри вашего приложения.

Сохранение Properties в файл

Иногда нужно не только читать, но и записывать свойства. Метод store() сохраняет текущее состояние Properties в файл.

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

public class StoreProperties {
    public static void main(String[] args) {
        Properties props = new Properties();

        props.setProperty("app.name", "MyApp");
        props.setProperty("app.version", "1.0");
        props.setProperty("app.debug", "false");

        try (FileOutputStream fos = new FileOutputStream("output.properties")) {
            props.store(fos, "Конфигурация приложения");
            System.out.println("Сохранено!");
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }
    }
}

Созданный файл output.properties будет выглядеть так:

#Конфигурация приложения
#Thu Jun 25 10:00:00 MSK 2026
app.name=MyApp
app.version=1.0
app.debug=false

Заметьте: Java автоматически добавила комментарий с датой и временем сохранения. Сам комментарий — это то, что вы передали вторым аргументом в store().

Сохранение в XML

Для сохранения в XML-формат используйте storeToXML():

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

public class StoreXml {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.setProperty("db.host", "localhost");
        props.setProperty("db.port", "5432");

        try (FileOutputStream fos = new FileOutputStream("config.xml")) {
            props.storeToXML(fos, "Database Configuration");
            System.out.println("Сохранено в XML!");
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }
    }
}

Перебор всех свойств

Иногда нужно вывести или обработать все свойства. У Properties есть несколько способов для этого:

import java.util.Properties;
import java.util.Enumeration;
import java.util.Set;

public class IterateProperties {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.setProperty("db.host", "localhost");
        props.setProperty("db.port", "5432");
        props.setProperty("server.port", "8080");

        System.out.println("=== Способ 1: forEach ===");
        props.forEach((key, value) ->
            System.out.println(key + " = " + value)
        );

        System.out.println("\n=== Способ 2: propertyNames() ===");
        Enumeration<?> keys = props.propertyNames();
        while (keys.hasMoreElements()) {
            String key = (String) keys.nextElement();
            System.out.println(key + " = " + props.getProperty(key));
        }

        System.out.println("\n=== Способ 3: stringPropertyNames() ===");
        Set<String> keySet = props.stringPropertyNames();
        for (String key : keySet) {
            System.out.println(key + " = " + props.getProperty(key));
        }

        System.out.println("\n=== Способ 4: list() ===");
        props.list(System.out);
    }
}

💡 Разница между propertyNames() и stringPropertyNames(): propertyNames() возвращает все ключи, включая унаследованные от родительских Properties. stringPropertyNames() возвращает только собственные ключи (без унаследованных). Если у вас нет родительского объекта — разницы нет.

System.getenv(): переменные окружения

Помимо файлов конфигурации, настройки можно получать из переменных окружения операционной системы. Это особый механизм, который позволяет передавать конфигурацию программе без файлов.

Переменные окружения задаются в командной строке перед запуском программы:

# Linux / macOS
export DB_HOST=production-server.com
export DB_PORT=5432
export DB_PASSWORD=secret123
java -jar myapp.jar

# Windows (cmd)
set DB_HOST=production-server.com
set DB_PORT=5432
java -jar myapp.jar

# Windows (PowerShell)
$env:DB_HOST="production-server.com"
java -jar myapp.jar

В Java чтение переменных окружения выглядит так:

public class EnvVariables {
    public static void main(String[] args) {
        String dbHost = System.getenv("DB_HOST");
        String dbPort = System.getenv("DB_PORT");
        String dbPassword = System.getenv("DB_PASSWORD");

        System.out.println("Хост БД: " + dbHost);
        System.out.println("Порт БД: " + dbPort);
        System.out.println("Пароль: " + dbPassword);

        String home = System.getenv("HOME");
        System.out.println("Домашняя директория: " + home);

        String path = System.getenv("PATH");
        System.out.println("PATH: " + path);
    }
}

⚠️ Важно: System.getenv() возвращает null, если переменная не задана. Всегда проверяйте на null или используйте Optional:

import java.util.Optional;

public class SafeEnvAccess {
    public static void main(String[] args) {
        String dbHost = System.getenv().getOrDefault("DB_HOST", "localhost");
        System.out.println("Хост: " + dbHost);

        Optional<String> port = Optional.ofNullable(System.getenv("DB_PORT"));
        int dbPort = port.map(Integer::parseInt).orElse(5432);
        System.out.println("Порт: " + dbPort);
    }
}

Почему переменные окружения лучше для секретов?

Пароли и API-ключи — конфиденциальные данные. Их нельзя хранить в файлах, которые попадают в Git (контроль версий). Переменные окружения — безопасный способ передать секреты:

❌
Плохо: захардкоженный пароль

props.setProperty("db.password", "secret123"); — пароль в коде, который попадёт в Git-репозиторий.

❌
Плохо: пароль в .properties файле

db.password=secret123 в config.properties — файл может попасть в Git.

✅
Хорошо: переменная окружения

export DB_PASSWORD=secret123 — пароль не в файле, а в ОС.

System.getProperty(): системные свойства Java

Java имеет свой собственный набор свойств — системные свойства JVM. Их можно читать через System.getProperty(). Эти свойства задаются при запуске программы через флаг -D:

public class SystemProperties {
    public static void main(String[] args) {
        System.out.println("Версия Java: " + System.getProperty("java.version"));
        System.out.println("Поставщик: " + System.getProperty("java.vendor"));
        System.out.println("ОС: " + System.getProperty("os.name"));
        System.out.println("Архитектура: " + System.getProperty("os.arch"));
        System.out.println("Разделитель пути: " + System.getProperty("file.separator"));
        System.out.println("Пользователь: " + System.getProperty("user.name"));
        System.out.println("Домашняя директория: " + System.getProperty("user.home"));
        System.out.println("Рабочая директория: " + System.getProperty("user.dir"));
        System.out.println("Временная директория: " + System.getProperty("java.io.tmpdir"));
    }
}

Задание пользовательских свойств через -D:

java -Dapp.mode=production -Dapp.debug=false -Dapp.log.level=INFO MyApp
public class CustomSystemProps {
    public static void main(String[] args) {
        String mode = System.getProperty("app.mode", "development");
        String debug = System.getProperty("app.debug", "false");
        String logLevel = System.getProperty("app.log.level", "WARN");

        System.out.println("Режим: " + mode);
        System.out.println("Отладка: " + debug);
        System.out.println("Уровень логирования: " + logLevel);
    }
}

💡 Сравнение: System.getenv() читает переменные ОС (задаются в терминале/системе). System.getProperty() читает свойства JVM (задаются через -D при запуске). Оба возвращают null для несуществующих ключей.

Практика: конфигурируемое приложение

Давайте создадим полноценный пример конфигурируемого приложения. Оно будет читать настройки из файла, поддерживать значения по умолчанию и позволять переопределять настройки через переменные окружения.

Файл конфигурации (config.properties)

# Database
db.host=localhost
db.port=5432
db.name=myapp
db.user=admin

# Server
server.host=0.0.0.0
server.port=8080
server.timeout=30000

# Logging
log.level=INFO
log.file=app.log

Класс конфигурации

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

public class AppConfig {
    private final Properties props;

    public AppConfig() {
        this.props = new Properties();
        loadDefaults();
        loadFromFile();
        applyEnvOverrides();
    }

    private void loadDefaults() {
        props.setProperty("db.host", "localhost");
        props.setProperty("db.port", "3306");
        props.setProperty("db.name", "test_db");
        props.setProperty("db.user", "root");
        props.setProperty("db.password", "");
        props.setProperty("server.host", "0.0.0.0");
        props.setProperty("server.port", "8080");
        props.setProperty("server.timeout", "30000");
        props.setProperty("log.level", "INFO");
        props.setProperty("log.file", "app.log");
    }

    private void loadFromFile() {
        Path configPath = Path.of("config.properties");
        if (Files.exists(configPath)) {
            try (InputStream is = Files.newInputStream(configPath)) {
                props.load(is);
                System.out.println("Конфигурация загружена из файла");
            } catch (IOException e) {
                System.err.println("Ошибка чтения конфигурации: " + e.getMessage());
            }
        } else {
            System.out.println("Файл конфигурации не найден, используются значения по умолчанию");
        }
    }

    private void applyEnvOverrides() {
        String envPrefix = "APP_";
        System.getenv().forEach((key, value) -> {
            if (key.startsWith(envPrefix)) {
                String propKey = key.substring(envPrefix.length()).toLowerCase();
                props.setProperty(propKey, value);
                System.out.println("Переопределено из env: " + propKey + " = " + value);
            }
        });
    }

    public String get(String key) {
        return props.getProperty(key);
    }

    public String get(String key, String defaultValue) {
        return props.getProperty(key, defaultValue);
    }

    public int getInt(String key, int defaultValue) {
        String value = props.getProperty(key);
        if (value == null) return defaultValue;
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    public boolean getBoolean(String key, boolean defaultValue) {
        String value = props.getProperty(key);
        if (value == null) return defaultValue;
        return Boolean.parseBoolean(value.trim());
    }

    public void printAll() {
        System.out.println("\n=== Текущая конфигурация ===");
        props.forEach((key, value) ->
            System.out.printf("  %-20s = %s%n", key, value)
        );
        System.out.println("============================\n");
    }
}

Использование конфигурации

public class MyApp {
    public static void main(String[] args) {
        AppConfig config = new AppConfig();
        config.printAll();

        String dbHost = config.get("db.host");
        int dbPort = config.getInt("db.port", 3306);
        String dbName = config.get("db.name");
        int timeout = config.getInt("server.timeout", 30000);
        boolean debug = config.getBoolean("log.debug", false);

        System.out.println("Подключение к БД: " + dbHost + ":" + dbPort + "/" + dbName);
        System.out.println("Таймаут: " + timeout + " мс");
        System.out.println("Режим отладки: " + debug);
    }
}

✅ Порядок приоритетов: Значения по умолчанию → значения из файла → значения из переменных окружения. Это классический паттерн конфигурации: сначала базовые настройки, потом файл переопределяет, потом env-переменные переопределяют поверх.

Практика: консольный калькулятор с настройками

Давайте создадим простой калькулятор, который читает настройки из .properties файла:

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import java.util.Scanner;

public class CalculatorWithConfig {
    private static Properties loadConfig() {
        Properties props = new Properties();

        props.setProperty("calc.precision", "2");
        props.setProperty("calc.showHistory", "true");
        props.setProperty("calc.decimalSeparator", ",");

        Path configPath = Path.of("calculator.properties");
        if (Files.exists(configPath)) {
            try (InputStream is = Files.newInputStream(configPath)) {
                props.load(is);
            } catch (IOException e) {
                System.out.println("Ошибка чтения конфигурации: " + e.getMessage());
            }
        }

        return props;
    }

    public static void main(String[] args) {
        Properties config = loadConfig();

        int precision = Integer.parseInt(config.getProperty("calc.precision", "2"));
        boolean showHistory = Boolean.parseBoolean(config.getProperty("calc.showHistory", "true"));
        String separator = config.getProperty("calc.decimalSeparator", ",");

        System.out.println("Калькулятор (точность: " + precision + " знаков)");
        System.out.println("Команды: +, -, *, /, history, exit");
        System.out.println();

        StringBuilder history = new StringBuilder();
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.print(">>> ");
            String input = scanner.nextLine().trim();

            if (input.equals("exit")) {
                break;
            }

            if (input.equals("history")) {
                if (showHistory && history.length() > 0) {
                    System.out.println(history);
                } else {
                    System.out.println("История пуста");
                }
                continue;
            }

            try {
                String[] parts = input.split("\\s+");
                if (parts.length != 3) {
                    System.out.println("Формат: число операция число");
                    continue;
                }

                double a = Double.parseDouble(parts[0].replace(separator, "."));
                String op = parts[1];
                double b = Double.parseDouble(parts[2].replace(separator, "."));
                double result;

                switch (op) {
                    case "+": result = a + b; break;
                    case "-": result = a - b; break;
                    case "*": result = a * b; break;
                    case "/":
                        if (b == 0) {
                            System.out.println("Ошибка: деление на ноль!");
                            continue;
                        }
                        result = a / b;
                        break;
                    default:
                        System.out.println("Неизвестная операция: " + op);
                        continue;
                }

                String formatted = String.format("%." + precision + "f", result)
                    .replace(".", separator);
                System.out.println("= " + formatted);

                if (showHistory) {
                    history.append(input).append(" = ").append(formatted).append("\n");
                }

            } catch (NumberFormatException e) {
                System.out.println("Ошибка: введите числа!");
            }
        }

        System.out.println("До свидания!");
        scanner.close();
    }
}

Файл конфигурации calculator.properties:

calc.precision=3
calc.showHistory=true
calc.decimalSeparator=.

Практика: логгер с настройками из файла

Ещё один полезный пример — простой логгер, который читает настройки из файла:

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;

public class ConfigurableLogger {
    private static Properties config;
    private static DateTimeFormatter timestampFmt;

    static {
        config = new Properties();
        config.setProperty("log.level", "INFO");
        config.setProperty("log.file", "app.log");
        config.setProperty("log.timestamp", "yyyy-MM-dd HH:mm:ss");
        config.setProperty("log.format", "[%s] %s: %s");

        try (InputStream is = Files.newInputStream(Path.of("logger.properties"))) {
            config.load(is);
        } catch (IOException e) {
            System.out.println("Файл конфигурации логгера не найден, используются настройки по умолчанию");
        }

        timestampFmt = DateTimeFormatter.ofPattern(
            config.getProperty("log.timestamp")
        );
    }

    private static int levelPriority(String level) {
        switch (level) {
            case "DEBUG": return 0;
            case "INFO":  return 1;
            case "WARN":  return 2;
            case "ERROR": return 3;
            default:      return 1;
        }
    }

    public static void log(String level, String message) {
        String minLevel = config.getProperty("log.level", "INFO");

        if (levelPriority(level) < levelPriority(minLevel)) {
            return;
        }

        String timestamp = LocalDateTime.now().format(timestampFmt);
        String format = config.getProperty("log.format", "[%s] %s: %s");
        String logLine = String.format(format, timestamp, level, message);

        System.out.println(logLine);

        try {
            Files.writeString(
                Path.of(config.getProperty("log.file")),
                logLine + "\n",
                StandardOpenOption.CREATE,
                StandardOpenOption.APPEND
            );
        } catch (IOException e) {
            System.err.println("Не удалось записать в файл лога: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        log("DEBUG", "Инициализация приложения");
        log("INFO", "Приложение запущено");
        log("INFO", "Загружено 42 конфигурации");
        log("WARN", "Использовано 80% памяти");
        log("ERROR", "Ошибка подключения к БД");
        log("INFO", "Приложение остановлено");
    }
}

Файл logger.properties:

log.level=WARN
log.file=app.log
log.timestamp=HH:mm:ss
log.format=%s | %-5s | %s

При уровне WARN в консоль и файл попадут только WARN и ERROR:

10:30:15 | WARN  | Использовано 80% памяти
10:30:15 | ERROR | Ошибка подключения к БД

Работа со всеми переменными окружения

Иногда полезно увидеть все доступные переменные окружения — например, при отладке конфигурации сервера:

import java.util.Map;

public class AllEnvVars {
    public static void main(String[] args) {
        Map<String, String> env = System.getenv();

        System.out.println("Всего переменных окружения: " + env.size());
        System.out.println();

        env.entrySet().stream()
            .sorted(Map.Entry.comparingByKey())
            .forEach(entry -> {
                String value = entry.getValue();
                if (value.length() > 50) {
                    value = value.substring(0, 47) + "...";
                }
                System.out.printf("%-30s = %s%n", entry.getKey(), value);
            });
    }
}

💡 Совет: На Linux/macOS вы можете посмотреть переменные окружения командой env или printenv. На Windows — set в cmd или Get-ChildItem env: в PowerShell.

Практика: приложение для вывода информации о системе

Давайте создадим утилиту, которая собирает информацию о системе и выводит её в форматированном виде:

import java.util.Properties;

public class SystemInfo {
    public static void main(String[] args) {
        Properties sysProps = System.getProperties();

        String[] keys = {
            "java.version",
            "java.vendor",
            "os.name",
            "os.version",
            "os.arch",
            "user.name",
            "user.home",
            "user.dir",
            "file.separator",
            "line.separator",
            "java.io.tmpdir"
        };

        String[] labels = {
            "Версия Java",
            "Поставщик Java",
            "Операционная система",
            "Версия ОС",
            "Архитектура",
            "Пользователь",
            "Домашняя директория",
            "Рабочая директория",
            "Разделитель пути",
            "Перенос строки",
            "Временная директория"
        };

        System.out.println("╔══════════════════════════════════════╗");
        System.out.println("║      Информация о системе           ║");
        System.out.println("╚══════════════════════════════════════╝");
        System.out.println();

        for (int i = 0; i < keys.length; i++) {
            String value = sysProps.getProperty(keys[i], "неизвестно");
            System.out.printf("  %-24s %s%n", labels[i] + ":", value);
        }

        System.out.println();
        System.out.println("Всего системных свойств: " + sysProps.size());
    }
}

Современные альтернативы Properties

Хотя Properties — это проверенный временем инструмент, сегодня существуют более современные форматы конфигурации:

.properties

Простой формат, встроенная поддержка Java. Минус: нет вложенности, нет типов, кодировка ISO-8859-1.

JSON

Популярен для API. Требует сторонних библиотек (Jackson, Gson) для чтения. Поддерживает вложенность.

YAML

Читаемый формат с вложенностью. Популярен в Docker и Kubernetes. Требует библиотеку (SnakeYAML).

.env файлы

Популярен в DevOps. Похож на .properties, но с поддержкой кавычек и комментариев. Часто используется с Docker Compose.

💡 Когда использовать Properties: Для простых конфигураций (10-20 ключей), для JVM-приложений, для совместимости со старым кодом, для Spring-конфигурации. Для сложных вложенных структур лучше подходят JSON или YAML.

Практика: конфигурация приложения с валидацией

В реальном приложении важно проверять конфигурацию. Вот пример конфигурации с валидацией обязательных полей:

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

public class ValidatedConfig {
    private final Properties props;
    private final List<String> errors = new ArrayList<>();

    public ValidatedConfig() {
        props = new Properties();

        props.setProperty("db.host", "localhost");
        props.setProperty("db.port", "3306");
        props.setProperty("db.name", "");
        props.setProperty("db.user", "");
        props.setProperty("db.password", "");
        props.setProperty("server.port", "8080");
        props.setProperty("log.level", "INFO");

        Path configPath = Path.of("config.properties");
        if (Files.exists(configPath)) {
            try (InputStream is = Files.newInputStream(configPath)) {
                props.load(is);
            } catch (IOException e) {
                errors.add("Ошибка чтения конфигурации: " + e.getMessage());
            }
        }

        applyEnvOverrides();
        validate();
    }

    private void applyEnvOverrides() {
        System.getenv().forEach((key, value) -> {
            if (key.startsWith("APP_")) {
                String propKey = key.substring(4).toLowerCase().replace("_", ".");
                props.setProperty(propKey, value);
            }
        });
    }

    private void validate() {
        String[] required = {"db.host", "db.port", "db.name", "db.user"};
        for (String key : required) {
            String value = props.getProperty(key);
            if (value == null || value.trim().isEmpty()) {
                errors.add("Обязательный параметр не задан: " + key);
            }
        }

        try {
            int port = Integer.parseInt(props.getProperty("db.port"));
            if (port < 1 || port > 65535) {
                errors.add("Неверный порт БД: " + port + " (допустимо 1-65535)");
            }
        } catch (NumberFormatException e) {
            errors.add("Порт БД должен быть числом: " + props.getProperty("db.port"));
        }
    }

    public boolean isValid() {
        return errors.isEmpty();
    }

    public List<String> getErrors() {
        return errors;
    }

    public String get(String key) {
        return props.getProperty(key);
    }

    public void printReport() {
        if (isValid()) {
            System.out.println("Конфигурация валидна!");
            System.out.println("  БД: " + get("db.host") + ":" + get("db.port") + "/" + get("db.name"));
            System.out.println("  Сервер: порт " + get("server.port"));
            System.out.println("  Логи: " + get("log.level"));
        } else {
            System.out.println("Ошибки конфигурации:");
            for (String error : errors) {
                System.out.println("  - " + error);
            }
        }
    }

    public static void main(String[] args) {
        ValidatedConfig config = new ValidatedConfig();
        config.printReport();
    }
}

Практика: шаблон конфигурации для CLI-инструмента

Давайте создадим конфигурацию для командной утилиты, которая сканирует файлы:

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
import java.util.stream.Stream;

public class FileScannerConfig {
    private final Properties props;

    public FileScannerConfig() {
        props = new Properties();

        props.setProperty("scan.directory", ".");
        props.setProperty("scan.recursive", "true");
        props.setProperty("scan.extensions", ".java,.txt,.md,.xml");
        props.setProperty("scan.maxDepth", "10");
        props.setProperty("scan.followSymlinks", "false");
        props.setProperty("output.format", "table");

        Path userConfig = Path.of(System.getProperty("user.home"), ".filescanner", "config.properties");
        if (Files.exists(userConfig)) {
            try (InputStream is = Files.newInputStream(userConfig)) {
                props.load(is);
                System.out.println("Загружена пользовательская конфигурация");
            } catch (IOException e) {
                System.err.println("Ошибка: " + e.getMessage());
            }
        }
    }

    public String getDirectory() {
        return props.getProperty("scan.directory");
    }

    public boolean isRecursive() {
        return Boolean.parseBoolean(props.getProperty("scan.recursive"));
    }

    public String[] getExtensions() {
        return props.getProperty("scan.extensions").split(",");
    }

    public int getMaxDepth() {
        return Integer.parseInt(props.getProperty("scan.maxDepth"));
    }

    public void scan() {
        Path dir = Paths.get(getDirectory());
        if (!Files.isDirectory(dir)) {
            System.err.println("Директория не найдена: " + dir);
            return;
        }

        String[] extensions = getExtensions();
        int depth = isRecursive() ? getMaxDepth() : 1;

        System.out.println("Сканирование: " + dir.toAbsolutePath());
        System.out.println("Расширения: " + String.join(", ", extensions));
        System.out.println();

        try (Stream<Path> files = Files.walk(dir, depth)) {
            files.filter(Files::isRegularFile)
                .filter(p -> {
                    String name = p.getFileName().toString();
                    for (String ext : extensions) {
                        if (name.endsWith(ext.trim())) return true;
                    }
                    return false;
                })
                .forEach(p -> {
                    try {
                        long size = Files.size(p);
                        System.out.printf("  %-60s %,d байт%n",
                            dir.relativize(p), size);
                    } catch (IOException e) {
                        System.out.println("  " + p + " (размер неизвестен)");
                    }
                });
        } catch (IOException e) {
            System.err.println("Ошибка сканирования: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        FileScannerConfig config = new FileScannerConfig();
        config.scan();
    }
}

Итоги урока

  • java.util.Properties — класс для хранения пар ключ-значение (строки). Наследует Hashtable, адаптирован для конфигураций
  • setProperty() — добавляет/заменяет свойство. getProperty() — получает значение (или null/defaultValue)
  • load(InputStream) — загружает свойства из .properties файла
  • store(OutputStream, comments) — сохраняет свойства в файл. storeToXML — в XML
  • Файл .properties — простой текстовый формат: ключ=значение, комментарии через # или !
  • Свойства по умолчанию — передайте Properties в конструктор: new Properties(defaults)
  • System.getenv() — чтение переменных окружения ОС. Лучший способ для секретов
  • System.getProperty() — чтение системных свойств JVM. Задаются через -D при запуске
  • Порядок приоритетов: defaults → файл → env-переменные
  • Безопасность: пароли и API-ключи храните в переменных окружения, а не в файлах

В следующем уроке разберём регулярные выражения — мощный инструмент для поиска и обработки текста по шаблонам. 🚀

Тест: Properties и конфигурация

10 вопросов

Properties конфиг

Premium