JavaRush /Курсы /Java Server /src/main/resources и...

src/main/resources и путь ресурсов

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

1. Ресурсы рядом с Java-кодом

В выводе classes мы уже видели рядом compileJava и processResources. И это не случайность: для запуска приложению нужен не только Java-код. Нужны ещё и файлы, которые должны так же честно попасть в результат сборки, как и .class.

Если вы привыкли к учебным консольным программам, легко попасть в ловушку: «всё важное — в .java-файлах». Но как только приложение начинает хоть немного походить на реальный проект, выясняется, что ему нужны данные: текстовые шаблоны, небольшие справочники, статические файлы, баннеры, настройки по умолчанию. И всё это — не код.

Представьте, что Java-код — это «мозг» приложения, а ресурсы — «памятки и документы», которые этот мозг читает, чтобы действовать правильно. Можно, конечно, захардкодить всё в строках и константах, но тогда код быстро превращается в полку с папками, где папки приклеены скотчем прямо к стене. Работает… пока не нужно поменять одну строчку текста без перекомпиляции, положить рядом файл с данными или просто перестать смотреть на огромный JSON внутри String.

В Gradle-проекте для ресурсов есть отдельное место, и сборка умеет работать с ним по правилам. Это и есть наша цель: понять, где хранить такие файлы, как Gradle их подхватывает и как Java их читает, не привязываясь к «пути на диске».

Папка src/main/resources

Когда вы видите структуру Gradle-проекта, легко подумать, что src/main/resources — это просто ещё одна соседняя папка. Но для Gradle она имеет специальный смысл: Java-плагин считает всё внутри неё ресурсами приложения и автоматически включает эти файлы в результат сборки.

Посмотрим на типичную минимальную структуру:

readlater-starter
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── readlater
        │               └── ReadLaterApplication.java
        └── resources
            └── banner.txt

В src/main/java лежит код, который компилируется в .class-файлы. В src/main/resources лежат файлы, которые не компилируются, а попадают в сборку «как есть»: их нужно просто положить в результат, чтобы приложение могло прочитать их на запуске.

Очень важный нюанс: путь внутри src/main/resources превращается в путь внутри classpath. То есть если вы положили файл так:

src/main/resources/banner.txt

то во время выполнения приложение будет видеть его как ресурс по пути:

/banner.txt

А если вы положили так:

src/main/resources/com/example/readlater/messages.txt

то путь ресурса будет:

/com/example/readlater/messages.txt

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

2. processResources: обработка ресурсов

На первый взгляд кажется, что ресурсы «просто лежат в проекте», и всё. Но Gradle не запускает приложение прямо из src/. Он собирает результат в отдельную папку build/, и за это отвечает отдельная задача: processResources.

Если сказать по-простому, processResources берёт всё из src/main/resources и копирует в «рабочее место» сборки:

build/resources/main

Это часть жизненного цикла, который мы обсуждали в лекции 1: задача classes включает в себя и компиляцию Java-кода, и обработку ресурсов. В выводе Gradle вы часто увидите примерно такую последовательность задач:

> Task :compileJava
> Task :processResources
> Task :classes

Можно даже запустить только обработку ресурсов:

./gradlew processResources # только копирование ресурсов в build/

А потом проверить, что получилось:

build/resources/main/banner.txt

И вот здесь появляется дисциплина, которая спасает кучу нервов. Папка src/main/resources — это «источник истины», её вы редактируете руками. Папка build/resources/main — результат сборки, туда руками лезть не нужно. Если вы отредактируете файл в build/, а потом снова запустите сборку, изменения исчезнут так же внезапно, как надежды на ранний отпуск.

3. Classpath и чтение ресурсов

Если раньше вы читали файлы через Files.readString(Path.of("...")), у вас в голове наверняка сидит простая модель: «есть путь на диске — по нему и читаем». В backend-проекте эта модель быстро ломается, потому что приложение часто запускается не из папки с исходниками, а из собранного артефакта (например, jar).

Classpath — это «список мест», откуда JVM умеет загружать классы и ресурсы. Эти «места» могут быть папками (в режиме разработки) или архивами jar (в упакованном виде). И классная часть в том, что для кода чтения ресурса это не важно: он спрашивает «дай мне ресурс /banner.txt», а JVM сама решает, где его искать — в папке или внутри архива.

В режиме ./gradlew run (когда Gradle запускает приложение) classpath обычно включает две ключевые части:

- build/classes/java/main — скомпилированные .class;
- build/resources/main — обработанные ресурсы.

Если нарисовать путь ресурса как цепочку, получится примерно так:

flowchart TD
    A["src/main/resources/banner.txt"] -->|processResources| B["build/resources/main/banner.txt"]
    B -->|run: classpath| C[Classpath JVM]
    C --> D["ReadLaterApplication.class.getResourceAsStream('banner.txt')"]
    D --> E[InputStream с содержимым]

Обратите внимание: на этапе запуска в этой схеме уже нет src/main/resources. Во время выполнения нас интересует то, что лежит в classpath, а не исходники проекта. Именно поэтому ресурсы правильно читать через classpath, а не через путь к исходной папке.

4. Первый ресурс: banner.txt

Чтобы ресурсы не остались абстракцией, сделаем простую и очень жизненную вещь: баннер приложения. Это может быть одна строка текста, которую приложение показывает при старте. Да, это маленькая деталь, но на ней сразу видно, что ресурс — это «данные рядом с кодом», а не строковая константа внутри main().

Создайте файл:

src/main/resources/banner.txt

Содержимое, например:

ReadLater Starter

Теперь важный момент: мы будем читать этот файл не из src/..., а из classpath. И это позволит приложению работать одинаково и при запуске из IDE, и при запуске через Gradle, и вообще где угодно, где есть собранный результат.

5. Чтение через getResourceAsStream()

Сейчас напишем код, который аккуратно читает banner.txt из classpath. Самый простой и понятный новичку путь — использовать getResourceAsStream(). Он возвращает InputStream, то есть поток байтов, который можно превратить в строку.

Мини-версия для ReadLaterApplication может выглядеть так:

package com.example.readlater;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

public class ReadLaterApplication {
    public static void main(String[] args) throws Exception {
        // 1) Ищем ресурс в classpath, а не на диске.
        // 2) Ведущий слеш означает "от корня classpath".
        // 3) requireNonNull даёт понятную ошибку, если файл не попал в сборку.
        try (InputStream in = Objects.requireNonNull(
                ReadLaterApplication.class.getResourceAsStream("/banner.txt"),
                "banner.txt not found")) {

            // Явно указываем UTF-8, чтобы не зависеть от кодировки по умолчанию на машине.
            System.out.println(new String(in.readAllBytes(), StandardCharsets.UTF_8)); // ReadLater Starter
        }
    }
}

Здесь стоит запомнить три вещи — как маленькое правило выживания:

Во-первых, путь "/banner.txt" начинается со слеша. Это означает: «искать от корня classpath». Если забыть слеш, Java будет искать относительно пакета класса, и вы будете долго смотреть на null, думая, что «Gradle опять что-то не так скачал».

Во-вторых, getResourceAsStream() может вернуть null, если ресурс не найден. Поэтому Objects.requireNonNull(...) — это маленькая страховка: если файл не попал в сборку, вы получите понятную ошибку с сообщением, а не загадочный NullPointerException где-то в глубине чтения байтов.

В-третьих, мы явно указываем StandardCharsets.UTF_8, чтобы не зависеть от «кодировки по умолчанию» на конкретной машине. На одной ОС это может быть UTF‑8, на другой — что-то ещё, и тогда ваш прекрасный текст превратится в набор символов, похожий на заклинание вызова древнего демона.

Этого уже достаточно, чтобы считать ReadLaterApplication рабочим состоянием проекта: баннер живёт в ресурсе, код читает его через classpath, а приложение не зависит от того, где лежат исходники на диске. Именно такую версию класса и стоит держать в проекте как рабочую.

6. Абсолютные и относительные пути

Пути ресурсов в Java — один из тех моментов, где новички чаще всего теряют время. Путаница не потому, что вы «не понимаете Java», а потому что здесь реально есть два режима адресации, и выглядят они почти одинаково.

Если путь начинается со слеша, он считается абсолютным относительно корня classpath:

// Абсолютный путь: ищем от корня classpath
ReadLaterApplication.class.getResourceAsStream("/banner.txt");

Если слеша нет, путь считается относительным к пакету класса. Например, если класс лежит в пакете com.example.readlater, то "banner.txt" будет интерпретировано как:

/com/example/readlater/banner.txt

То есть это сработает только в том случае, если вы положили ресурс именно туда:

src/main/resources/com/example/readlater/banner.txt

Мини-демонстрация (чисто для понимания, не обязательно так делать):

package com.example.readlater;

import java.io.InputStream;

public class ReadLaterApplication {
    public static void main(String[] args) {
        // Ищем ресурс в корне classpath (подходит, если banner.txt лежит прямо в src/main/resources)
        InputStream a = ReadLaterApplication.class.getResourceAsStream("/banner.txt");

        // Ищем ресурс относительно пакета класса: /com/example/readlater/banner.txt
        InputStream b = ReadLaterApplication.class.getResourceAsStream("banner.txt");

        // Выводим, нашли ли мы ресурс (null означает "не найден")
        System.out.println(a != null); // true/false в зависимости от расположения
        System.out.println(b != null); // true/false в зависимости от расположения
    }
}

В учебных проектах я обычно советую начинать с абсолютных путей (со слеша), потому что они проще для головы: ресурс лежит в корне resources — путь "/...". Ресурс лежит в подпапке — путь "/подпапка/...".

7. Чтение ресурсов без Files

Этот пункт — просто обязательный, потому что он объясняет, зачем ресурсы вообще существуют как механизм, а не просто как «ещё одна папка». Самая частая ошибка новичка — написать примерно так, потому что «ну файл же там лежит»:

package com.example.readlater;

import java.nio.file.Files;
import java.nio.file.Path;

public class ReadLaterApplication {
    public static void main(String[] args) throws Exception {
        // Это путь к исходникам в репозитории, а не к ресурсам в classpath во время запуска.
        // Иногда "повезёт" (IDE, правильная working directory), но это хрупко.
        String text = Files.readString(Path.of("src/main/resources/banner.txt"));

        System.out.println(text); // ReadLater Starter (если повезёт)
    }
}

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

Проблема в том, что путь src/main/resources/banner.txt — это путь внутри репозитория, то есть путь к исходникам. А приложение во время выполнения вообще не обязано иметь рядом исходники. Если вы передадите jar на другой компьютер, там будет jar, а папки src/ может не быть вообще. И это нормально.

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

8. Проверка ресурсов после сборки

Когда что-то не находится, новичок часто начинает хаотично менять код. На практике лучше сначала проверить: ресурс вообще попал в сборочный результат? Gradle довольно прозрачен: после processResources файл должен лежать в build/resources/main.

Можно сделать простую проверку глазами:

build
└── resources
    └── main
        └── banner.txt

А если хочется чуть больше уверенности, откройте файл и убедитесь, что это именно тот текст, который вы редактировали в src/main/resources.

Иногда бывает другая ситуация: файл есть в src/main/resources, но вы случайно положили его не туда — например, в src/main/java или в корень проекта рядом с README.md. Тогда Gradle не считает его ресурсом, processResources его не копирует, и при запуске вы получите banner.txt not found. Это не «ошибка Java», а просто дисциплина структуры проекта.

Чтобы мозгу было легче держать это в голове, вот компактная табличка «где что живёт»:

Где лежит Что это Кто кладёт Можно ли редактировать руками
src/main/resources исходные ресурсы вы да
build/resources/main обработанные ресурсы для запуска Gradle (processResources) нет, иначе потеряете изменения
classpath приложения «виртуальное пространство» ресурсов во время запуска Gradle/JVM руками туда не лезут

9. Типичные ошибки при работе с ресурсами

Работа с ресурсами выглядит простой ровно до первого null из getResourceAsStream(). Дальше начинается классический этап «почему оно не работает, я же всё правильно сделал». Это нормально: часть обучения как раз в том, чтобы понять путь ресурса и принять, что запускается не src/, а build/.

Ошибка №1: ресурс положили не в src/main/resources, а в src/main/java или в корень проекта.
Такое часто случается «на автомате»: вы видите дерево проекта и кидаете файл туда, куда у вас в этот момент открыт файловый менеджер. В итоге processResources его не копирует, в build/resources/main файла нет, и getResourceAsStream() честно возвращает null. Лечится просто: ресурсы живут в src/main/resources, потому что Gradle так договорился по умолчанию.

Ошибка №2: забыли ведущий слеш в пути getResourceAsStream("banner.txt").
Если вы кладёте banner.txt прямо в корень resources, а читаете без слеша, Java начинает искать в /com/example/readlater/banner.txt. Она не обязана вам это «объяснять» — она просто не находит ресурс и возвращает null. Если хотите искать от корня classpath, пишите "/banner.txt".

Ошибка №3: ресурс есть, но вы читаете его как файл из src/main/resources/... через Files.
Это работает ровно до первого реального запуска вне IDE. Чуть изменилась рабочая директория, чуть поменялся способ запуска — и всё, файл не найден. Путь src/... — это путь к исходникам, а не к результату сборки. Если ресурс должен жить вместе с приложением, его читают через classpath.

Ошибка №4: getResourceAsStream() вернул null, а код сразу делает in.readAllBytes() и падает с NullPointerException.
NullPointerException здесь не «злой рок», а вполне конкретный сигнал: ресурс не найден. Поэтому полезно либо проверить if (in == null), либо использовать Objects.requireNonNull(...) с понятным сообщением. Тогда вы экономите себе время: сразу видно, что проблема в пути или расположении файла.

Ошибка №5: проблемы с кодировкой: текст «кракозябрами».
Если в ресурсе есть не-ASCII-символы (например, русские буквы), и вы превращаете байты в строку без указания UTF-8, результат может зависеть от системы. На вашей машине всё красиво, на другой — «Ð Ð¸Ð´Ð»ÐµÑ‚ер». Лечится просто: всегда указывайте StandardCharsets.UTF_8.

1
Задача
Java Server, 4 уровень, 1 лекция
Недоступна
Ресурс в подпапке и абсолютный путь в classpath
Ресурс в подпапке и абсолютный путь в classpath
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
21 мая 2026
Gradle воспринимает classes как часть пути проекта, потому что ломается обработка аргументов %* // hier is correct version of gradlew.bat //---------------------------------------------------------------------- @echo off setlocal set "APP_HOME=%~dp0" set "APP_HOME=%APP_HOME:~0,-1%" set "SEARCH_DIR=%APP_HOME%" :find_wrapper if exist "%SEARCH_DIR%\gradlew.bat" if /I not "%SEARCH_DIR%"=="%APP_HOME%" ( call "%SEARCH_DIR%\gradlew.bat" -p "%APP_HOME%" %* exit /b %ERRORLEVEL% ) for %%I in ("%SEARCH_DIR%\..") do set "NEXT_DIR=%%~fI" if /I "%NEXT_DIR%"=="%SEARCH_DIR%" goto fallback set "SEARCH_DIR=%NEXT_DIR%" goto find_wrapper :fallback gradle -p "%APP_HOME%" %*