Properties и конфигурация
Почти любое приложение нуждается в настройках: адрес сервера, порт, имя базы данных, уровень логирования, пути к файлам. Хранить всё это прямо в коде — плохая практика. В этом уроке мы разберём java.util.Properties — класс для хранения и чтения конфигураций, а также переменные окружения и системные свойства 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-репозиторий.
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 — это проверенный временем инструмент, сегодня существуют более современные форматы конфигурации:
Простой формат, встроенная поддержка Java. Минус: нет вложенности, нет типов, кодировка ISO-8859-1.
Популярен для API. Требует сторонних библиотек (Jackson, Gson) для чтения. Поддерживает вложенность.
Читаемый формат с вложенностью. Популярен в Docker и Kubernetes. Требует библиотеку (SnakeYAML).
Популярен в 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 вопросов