JavaRush /Курсы /Java Server /application.properties

application.properties и classpath

Java Server
20 уровень , 1 лекция
Открыта

1. application.properties как ресурс приложения

Если воспринимать конфигурационный файл как “ну это где-то на диске лежит”, вы очень быстро попадёте в ловушку: у вас всё будет работать в IDE, но перестанет работать при запуске через jar. Поэтому полезно сразу принять странную, но правильную мысль: когда приложение собрано, ваши ресурсы становятся частью артефакта. Они не “рядом”, они “внутри”.

В мире ReadLater Starter application.properties — это базовый источник настроек по умолчанию. Он должен лежать в src/main/resources, потому что именно эту папку Gradle трактует как “ресурсы приложения” и кладёт на classpath. Мы не делаем пока сложную систему конфигурации. Мы просто учимся читать аккуратный файл и не зависеть от конкретного расположения репозитория на диске.

Посмотрим, как обычно выглядит нужное нам расположение файлов (примерно так оно уже у вас устроено):

readlater-starter
└─ src
   └─ main
      ├─ java/com/example/readlater/...
      └─ resources
         └─ application.properties

Если файл лежит здесь, он будет доступен приложению во время выполнения. Это и есть цель: чтобы конфиг читался одинаково при запуске из IDE, при ./gradlew run и даже при запуске собранного jar.

2. resources и classpath вместо пути к файлу

В какой-то момент новичку хочется сделать просто: “ну я же вижу файл, сейчас открою его по пути”. Проблема в том, что путь src/main/resources/application.properties существует только в исходниках. Когда вы собираете приложение, Gradle копирует ресурсы в build-директорию и дальше упаковывает их в jar. А внутри jar “путь на диске” уже не работает как вы ожидаете.

Представьте, что ваш проект — это чемодан. Пока чемодан открыт (исходники), вы действительно видите вещи “по папкам”. Но когда вы чемодан закрыли (собрали jar), вам нужно доставать вещи через “доступ к чемодану”, а не через “папку на полу”.

Небольшая схема того, что происходит с ресурсами при сборке:

flowchart TD
    A["src/main/resources/application.properties"] --> B["Gradle processResources"]
    B --> C["build/resources/main/application.properties"]
    C --> D["Упаковка в jar"]
    D --> E["classpath внутри приложения"]

И вот здесь появляется слово, которое поначалу звучит как заклинание: classpath. На человеческом языке это “набор мест, где JVM ищет классы и ресурсы”. Ресурс — это не только .class, это и ваш application.properties, и позже logback.xml, и sample JSON для mock-режима.

Поэтому главный вывод этого раздела простой: читать application.properties нужно через classpath, а не через FileInputStream по пути src/main/resources/....

Чтобы почувствовать разницу, посмотрим на антипример (не делайте так в реальном проекте, это учебный “плохой пример”):

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

// Антипример: читаем ресурс по файловому пути из исходников.
// Это "везёт" в IDE, но ломается после сборки в jar и/или при запуске из другой директории.
Properties props = new Properties();
try (FileInputStream input = new FileInputStream("src/main/resources/application.properties")) {
    // Загружаем пары ключ=значение
    props.load(input);
}

В IDE это иногда “везёт” и работает. Но стоит вам собрать jar и запустить его из другой директории — и всё, файл не найден. Ничего личного, просто вы больше не живёте внутри папки исходников.

3. Формат .properties: ключи и строки

Чтобы уверенно читать application.properties, полезно понимать, что это за формат. Это очень старый, но очень живучий формат Java-мира: пары ключ=значение, комментарии и… в общем-то всё. Его сила не в красоте, а в том, что он прост и есть “из коробки” в JDK.

Вот пример минимального application.properties для нашего проекта (мы ещё будем его расширять, но база такая):

app.name=readlater

catalog.api.base-url=https://openlibrary.org
catalog.api.mode=mock
catalog.api.request-timeout-ms=2000

server.host=localhost
server.port=8080

Обратите внимание на два практических нюанса.

Первый: ключи мы называем группами через точки. Это не магия и не стандарт RFC, это просто удобная договорённость: читать глазами легче, и логически они “кучкуются”. Это важно, потому что конфиг быстро растёт, а мозг у нас один.

Второй: Properties читает значения как строки. Даже если вы написали server.port=8080, при чтении это будет строка "8080". Превращать в число мы будем в следующей лекции. Сегодня наша задача — только корректно загрузить и получить строки.

Ещё два микро-правила, которые спасают нервы.

Комментарии обычно пишут через #. Можно и через !, но давайте не плодить редкие традиции.

Пробелы вокруг = лучше не делать “как попало”. Формат допускает разные варианты, но чем меньше “вольностей”, тем меньше вопросов потом.

4. Загрузка через getResourceAsStream()

Теперь самое важное: как из Java-кода прочитать этот файл так, чтобы это работало и в IDE, и в собранном приложении. В JDK для этого есть классический путь: получить InputStream из classpath и загрузить его в Properties.

Ключевой метод дня: ClassLoader#getResourceAsStream("application.properties"). Он возвращает поток, или null, если ресурс не найден. И вот этот null обязательно надо проверять — иначе вы получите красивый NullPointerException и потратите 20 минут на вопрос “почему я не люблю Java”, хотя проблема просто в том, что файл лежит не там.

Самый минимальный пример (его можно даже временно вставить в main(), чтобы “почувствовать” механику):

import java.io.InputStream;

// Достаём ресурс из classpath (то есть "изнутри" jar / из ресурсов приложения)
InputStream input = ReadLaterApplication.class
        .getClassLoader()
        .getResourceAsStream("application.properties");

// Если ресурс найден — будет true. Если нет — null, и это надо уметь диагностировать.
System.out.println(input != null); // true (если файл лежит в resources)

Конечно, реальный код должен закрывать поток. Поэтому нормальная форма — try-with-resources:

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

Properties props = new Properties();

try (InputStream input = ReadLaterApplication.class
        .getClassLoader()
        .getResourceAsStream("application.properties")) {

    // Важно: getResourceAsStream может вернуть null, это не "исключение", это контракт метода
    if (input == null) {
        // Останавливаем запуск: без конфигурации приложение дальше жить не должно
        throw new IllegalStateException("application.properties not found");
    }

    // Загружаем пары ключ=значение в Properties
    props.load(input);
}

Здесь уже есть важные признаки “взрослого кода”.

Мы читаем ресурс через classpath, а значит не зависим от текущей директории запуска.

Мы явно говорим: если файл не найден — старт приложения должен быть прерван. Это правильнее, чем “ну ладно, продолжим”, потому что приложение без конфигурации часто работает непредсказуемо.

Мы закрываем InputStream автоматически.

И да, Properties#load() читает пары ключ=значение. Как только файл загружен, props можно спрашивать про значения.

Маленькая ремарка, чтобы избежать путаницы: есть ещё метод SomeClass.getResourceAsStream(). Он работает чуть иначе (там есть нюансы с ведущим /). Чтобы не собирать коллекцию граблей, сегодня придерживаемся варианта через ClassLoader — он проще для начинающих и соответствует нашей задаче “прочитать ресурс из корня classpath”.

5. getProperty(): читаем значения и smoke-check

Когда Properties загружен, хочется сразу “увидеть, что оно реально работает”. И это нормальное желание. Backend-разработчик вообще часто живёт в мире: “сначала убедимся, что данные приходят, потом уже построим красивую архитектуру”. Главное — не перепутать smoke-check с конечным дизайном.

Чтение значения выглядит просто:

// Читаем значение по ключу. Если ключа нет — вернётся null.
String appName = props.getProperty("app.name");

// Быстрый smoke-check: увидели ожидаемое значение — значит ресурс загрузился
System.out.println(appName); // readlater

Если ключа нет, getProperty вернёт null. Это нормально. Это не “ошибка Java”, это сигнал: “в файле нет такого ключа”. Уже в следующей лекции мы решим, что делать с обязательными ключами и дефолтами. Сейчас просто запомним правило: null возможен.

Давайте сделаем небольшой “снимок реальности” на примере наших ключей:

// Все значения приходят строками — даже порт.
String baseUrl = props.getProperty("catalog.api.base-url");
String mode = props.getProperty("catalog.api.mode");
String port = props.getProperty("server.port");

// Печатаем как проверку: конфиг прочитан, ключи доступны
System.out.println(baseUrl); // https://openlibrary.org
System.out.println(mode);    // mock
System.out.println(port);    // 8080

Обратите внимание: server.port всё ещё строка. И это хорошо, потому что мы пока не договорились о правилах валидации. Вдруг там написали server.port=котики — приложению нужно будет красиво объяснить, что это плохо. Это мы тоже сделаем дальше.

Есть ещё удобная перегрузка: getProperty(key, defaultValue). Она позволяет дать “план Б”, если ключа нет:

// Если ключ отсутствует, берём указанное значение по умолчанию
String host = props.getProperty("server.host", "localhost");

System.out.println(host); // localhost

Это хороший механизм, но пользоваться им надо осторожно. Для некоторых ключей (например, catalog.api.base-url) “дефолт” может скрыть ошибку конфигурации. Мы позже научимся отличать обязательные ключи от необязательных.

6. PropertiesLoader в пакете config

Если оставить загрузку Properties прямо в ReadLaterApplication, вы быстро получите “толстый main()”, который знает про всё: про аргументы запуска, про конфигурацию, про wiring, про логику. Мы уже договорились, что так делать не хотим. Поэтому сделаем маленький, но важный шаг: вынесем загрузку файла в отдельный класс в пакете config.

Пусть у нас будет простой класс PropertiesLoader. Он ничего “умного” не делает: просто загружает файл и возвращает Properties. Это ровно уровень сегодняшней лекции.

package com.example.readlater.config;

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

public class PropertiesLoader {

    // Загружаем application.properties из classpath и возвращаем Properties
    public Properties load() {
        Properties props = new Properties();

        // try-with-resources гарантирует закрытие InputStream
        try (InputStream input = getClass().getClassLoader()
                .getResourceAsStream("application.properties")) {

            // Явно проверяем отсутствие ресурса, чтобы ошибка была понятной
            if (input == null) {
                throw new IllegalStateException("application.properties not found");
            }

            // Загружаем пары ключ=значение
            props.load(input);
            return props;

        } catch (Exception e) {
            // Оборачиваем в понятную ошибку старта приложения
            throw new IllegalStateException("Failed to load application.properties", e);
        }
    }
}

Код чуть длиннее 10 строк, но он цельный и важный: его стоит увидеть одним куском. Если хочется, можно потом микро-рефакторингом вынести try и catch, но сейчас лучше оставить читаемым.

Теперь в точке входа (ReadLaterApplication) мы можем сделать простой smoke-check: загрузили, прочитали пару значений, убедились, что всё живо.

package com.example.readlater.app;

import com.example.readlater.config.PropertiesLoader;

import java.util.Properties;

public class ReadLaterApplication {

    public static void main(String[] args) {
        // Загружаем конфигурацию на старте приложения
        Properties props = new PropertiesLoader().load();

        // Smoke-check: выводим пару значений, чтобы убедиться, что ресурсы читаются
        System.out.println(props.getProperty("app.name")); // readlater
        System.out.println(props.getProperty("catalog.api.base-url")); // https://openlibrary.org
    }
}

Да, это временный вывод в консоль. Да, завтра у нас будет полноценное логирование. Но сегодня нам важно увидеть: ресурс читается, значения доступны, и мы больше не хардкодим URL прямо в коде.

И обратите внимание: ReadLaterApplication теперь не знает, как именно читается файл. Он знает только, что есть загрузчик. Это маленький, но правильный шаг к чистой структуре.

7. Нюансы: кодировка, пробелы, ключи

На первый взгляд Properties выглядит как “просто строки”. Но в реальной жизни детали начинают кусаться. Не смертельно, но лучше заранее знать, где можно встретить неожиданность.

Первая неожиданность — кодировка. Исторически Properties#load(InputStream) читает файл в формате ISO-8859-1, а не UTF-8. Пока в вашем конфиге только URL, числа и короткие английские значения — вам всё равно. Но если вы решите написать app.name=Список чтения (и это красиво!), вы можете получить кракозябры.

Если хочется жить спокойно, можно читать через Reader в UTF-8. Это выглядит так:

import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Properties;

try (InputStream input = getClass().getClassLoader()
        .getResourceAsStream("application.properties")) {

    Properties props = new Properties();

    // Читаем файл как UTF-8 (актуально, если в значениях есть кириллица и другие символы)
    props.load(new InputStreamReader(input, StandardCharsets.UTF_8));
}

Вторая неожиданность — пробелы и пустые строки. Properties в целом терпеливый, но если вы начнёте писать значения вида server.host= localhost с хвостовыми пробелами, то эти пробелы могут остаться, и потом вы будете удивляться, почему Integer.parseInt(" 8080 ") внезапно ругается. В следующей лекции мы будем делать преобразования типов, и там мы обязательно добавим trim() или будем аккуратно работать с isBlank().

Третья неожиданность — отсутствие ключа. getProperty возвращает null, и это не “ошибка”, это “нет значения”. На этом этапе это нормально, потому что мы ещё не описали политику: какой ключ обязателен, какой нет. Сегодня мы фиксируем только механику: null возможен, и его нельзя игнорировать, если ключ важен.

По сути, сейчас мы делаем фундамент: научились доставать значения из файла. Следующим шагом мы научимся не просто доставать, а превращать их в “человеческую” конфигурационную модель и делать ошибки конфигурации понятными.

8. Типичные ошибки при загрузке application.properties

Ошибка №1: application.properties лежит не в src/main/resources.
Потом человек пишет правильный код с getResourceAsStream("application.properties"), получает null и начинает подозревать заговор Gradle, JDK и соседей по комнате. На практике это почти всегда просто неверная папка. Ресурс должен лежать именно в src/main/resources, потому что именно она попадает на classpath. Если файл лежит “где-то рядом”, JVM о нём не знает.

Ошибка №2: файл читается через путь src/main/resources/... с FileInputStream.
Это работает ровно до тех пор, пока вы запускаете приложение из корня репозитория и только из IDE. Как только меняется директория запуска или вы собираете jar, путь превращается в тыкву. Для ресурсов нужна модель “достань по имени из classpath”, а не “открой по файловому пути”. Это одна из тех вещей, которые в backend-жизни ломаются в самый неудобный момент.

Ошибка №3: не проверяется null после getResourceAsStream().
Метод честно говорит: “либо поток, либо null”. Если не проверить, вы получите NullPointerException внутри props.load(), и сообщение будет не про то, что файл не найден, а про то, что “что-то null”. В учебном проекте это особенно обидно: проблема простая, а диагностика получается мутная. Явная проверка и понятная ошибка экономят кучу времени.

Ошибка №4: ожидание, что Properties вернёт int, boolean и другие “нормальные типы”.
Properties — это словарь строк. Всегда. Даже если вы написали server.port=8080, это будет строка "8080". Если это принять, дальше всё становится логичным: сначала грузим строки, потом в конфигурационном слое преобразуем и валидируем. Если пытаться “пропустить шаг”, код расползётся: где-то Integer.parseInt(...), где-то default, где-то try/catch, и очень быстро вы потеряете единые правила.

Ошибка №5: хаотичные имена ключей без группировки.
Сегодня вы добавили baseUrl, завтра catalogUrl, послезавтра catalog.apiBaseUrl, а потом удивляетесь, почему никто не может найти нужную настройку. Схема catalog.api.*, server.*, app.* — не обязательная “по стандарту”, но очень полезная дисциплина. Она делает конфиг читаемым даже без IDE.

1
Задача
Java Server, 20 уровень, 1 лекция
Недоступна
Загрузка `application.properties` из `resources`
Загрузка `application.properties` из `resources`
1
Задача
Java Server, 20 уровень, 1 лекция
Недоступна
Дамп всех свойств из classpath-ресурса
Дамп всех свойств из classpath-ресурса
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ