JavaRush /Курси /Spring Core /Resource замість

Resource замість File-підходу

Spring Core
Рівень 15 , Лекція 0
Відкрита

1. Зовнішні шаблони в ContextFlow

Коли проєкт невеликий, дуже легко почати вставляти текст просто в Java: "... замовлення створено ..." тут, "... замовлення скасовано ..." там. Поки рядків два — усе терпимо. Але щойно вам потрібен нормальний текст сповіщення, шапка звіту й кілька варіантів формулювань, код перетворюється на літературний гурток усередині @Service‑класу. А підтримувати це стає так само «приємно», як розвʼязувати конфлікт злиття у файлі на 2 000 рядків.

У ContextFlow уже є логіка сповіщень і звітності. Це означає, що в нас зʼявляються дані, які живуть поруч із застосунком, але не обовʼязково мають бути частиною Java‑коду. Найпростіший приклад — тексти «замовлення створено» і «замовлення скасовано». Ми хочемо зберігати їх у src/main/resources, щоб вони постачалися разом із застосунком, були версіонованими в Git і не потребували перекомпіляції, якщо зміниться формулювання.

Ми вже виносили назовні налаштування й режими збирання. Із текстовими артефактами логіка та сама: це частина застосунку, але не частина бізнес-коду. Тому спершу відокремимо сам ресурс від звичайного шляху до файла, а вже потім вирішуватимемо, як такий ресурс вписати у впровадження залежностей.

І тут важливо вловити головний смисловий зсув дня. Ми хочемо говорити не «ось шлях до файла», а «ось ресурс застосунку: шаблон сповіщення про створення замовлення». Тобто думати не про те, де лежить файл, а про те, яке місце цей текст займає в системі. Це звучить майже філософськи, але на практиці рятує від купи безглуздих багів.

2. Крихкість підходу з File

Якщо ви вже писали консольні застосунки, рука автоматично тягнеться до чогось на кшталт new File("..."). І це нормально: File здається простим і чесним. Але в ньому є важливе приховане припущення: «ресурс — це файл у файловій системі». А в застосунку на Spring, та й узагалі у Java‑світі, це припущення доволі часто хибне.

Давайте подивимося на типовий «нешкідливий» код:

import java.io.File;

// ВАЖЛИВО: шлях відносний — він обчислюється від поточного робочого каталогу (user.dir),
// а не “від кореня проєкту” і не “від src/main/resources”.
File template = new File("src/main/resources/templates/notifications/order-created.txt");

System.out.println(template.exists()); // true (інколи), false (в іншому всесвіті)
System.out.println(template.getAbsolutePath()); // допомагає зрозуміти, де JVM насправді “шукає” файл

На вашому компʼютері з IDE це може «випадково» працювати. Зазвичай тому, що поточний робочий каталог (user.dir) збігається з коренем проєкту, і шлях src/main/resources/... справді існує. Але це не робить код правильним — це робить його залежним від того, як саме ви запустили застосунок.

Щоб відчути крихкість, достатньо додати один рядок:

// Діагностика: дивимося, від чого насправді обчислюються відносні шляхи.
System.out.println(System.getProperty("user.dir")); // наприклад: /Users/you/projects/spring-core-contextflow

Сьогодні user.dir — корінь проєкту. Завтра ви запускаєте застосунок через Gradle з іншого каталогу, або IDE вирішує стартувати з папки модуля, або тести запускаються в CI, де структура інша. І ваш «шлях до шаблону» перетворюється на гарбуз.

Друга, і ще важливіша, проблема: src/main/resources — це структура вихідних файлів, а не «папка, у якій застосунок шукає ресурси під час виконання». У зібраному застосунку src/main/resources може взагалі не існувати. Коли ви робите jar, ваші ресурси потрапляють усередину jar‑файла, а jar — це не папка у файловій системі в звичайному сенсі. Це архів. І там уже не можна чесно сказати: «дай мені File». Можна сказати лише: «дай мені потік байтів».

І ось тут File починає поводитися як друг, який обіцяв допомогти з переїздом, але раптом каже: «Ой, я у відпустці». Формально він не винен: File просто не про це.

3. Resource у Spring: «ручка» до вмісту

Щоб не будувати застосунок навколо припущення «усе — файл», Spring пропонує абстракцію Resource. Ідея проста: ресурс — це не обовʼязково файл, але це те, звідки можна отримати вміст. Іноді він лежить у classpath, усередині jar, іноді — у файловій системі, іноді — за URL. Наш код при цьому не повинен переписуватися щоразу, коли змінюється спосіб доставки.

Технічно це інтерфейс org.springframework.core.io.Resource. Він не «читає текст за вас» і не перетворює світ на веселку, але дає спільний контракт: «я можу існувати», «у мене є опис», «я можу дати InputStream».

Наймінімальніший приклад із classpath‑ресурсом виглядає так:

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

// Шлях задаємо ВСЕРЕДИНІ classpath (тобто відносно src/main/resources),
// без префікса src/main/resources.
Resource template = new ClassPathResource("templates/notifications/order-created.txt");

System.out.println(template.exists());         // true (якщо ресурс справді є)
System.out.println(template.getDescription()); // зручно для налагодження: звідки взяли ресурс

Зверніть увагу: ми не говоримо src/main/resources/.... Ми говоримо про шлях усередині classpath. Це дуже важлива різниця.

А ось ще більш прикладний мініприклад, який показує головне: Resource — це не текст, а вхід до читання:

import java.nio.charset.StandardCharsets;

import org.springframework.core.io.ClassPathResource;

var resource = new ClassPathResource("templates/notifications/order-created.txt");

// ВАЖЛИВО: читаємо через потік — так це працюватиме і для jar, і для файлової системи.
try (var in = resource.getInputStream()) { // try-with-resources гарантує закриття потоку
    // ВАЖЛИВО: явно задаємо кодування, щоб не залежати від платформи.
    String text = new String(in.readAllBytes(), StandardCharsets.UTF_8);
    System.out.println(text); // виведе вміст шаблону (якщо він є)
}

Так, тут усе ще є I/O. І так, потік потрібно закривати. Але ключовий виграш в іншому: цей код працюватиме, навіть якщо ресурс опиниться всередині jar, тобто «не як файл». Бо «всередині jar» можна відкрити потік, а «всередині jar» не можна гарантувати File.

Щоб закріпити в голові відмінність, корисно порівняти File і Resource трохи системніше:

Модель Що це означає Головний ризик Коли доречно
java.io.File «Шлях до файла на диску» Залежність від робочого каталогу і від того, що ресурс справді є файлом Коли ви точно працюєте з файловою системою, наприклад пишете звіт у build/
Resource «Доступ до вмісту ресурсу» Потрібно явно читати через InputStream і памʼятати про обробку помилок Коли ресурс — частина застосунку, наприклад шаблони, конфіги чи текстові заготовки, особливо якщо він у classpath

Можна сказати так: File — це «адреса будинку», а Resource — це «контакт, за яким можна отримати посилку». Посилка може приїхати курʼєром, поштою або телепортом. Вам важлива саме посилка.

4. Classpath і ресурси в рантаймі

Слово classpath звучить як заклинання з Java‑Гоґвортсу, але на рівні здорового глузду все доволі приземлено. Classpath — це набір місць, де JVM шукає класи й ресурси. Коли ви кладете файли в src/main/resources, Gradle та IDE роблять так, щоб ці файли потрапили в classpath під час запуску. Але у рантаймі застосунок не знає і не повинен знати про src/main/resources як про папку вихідних файлів.

Корисно тримати в голові шлях ресурсу через збирання:

flowchart TD
  A["src/main/resources — вихідні файли"] --> B["build/resources/main — результат збирання"]
  B --> C["jar-файл — пакування"]
  C --> D["classpath у середовищі виконання, де JVM шукає ресурси"]

Коли ви запускаєте застосунок з IDE, ресурси часто лежать як звичайні файли в build/resources/main. Тому здається, що «це ж просто файл». Але під час запуску з jar це вже не «просто файл», а запис усередині архіву. Саме тому Resource і потокова модель читання — базові.

Якщо хочеться побачити це наочно, можна зробити так, без Spring:

var cl = Thread.currentThread().getContextClassLoader();
var url = cl.getResource("templates/notifications/order-created.txt");

// Важливий момент: протокол може бути file: (ресурс на диску) або jar: (ресурс усередині jar).
System.out.println(url); // file:/.../build/resources/main/... або jar:file:/.../app.jar!/...

І ось тут зазвичай настає момент осяяння. URL може бути file: (коли ресурс справді як файл) або jar: (коли ресурс усередині jar). Обидва варіанти нормальні. Ненормально — якщо ваш код працює лише з одним варіантом, а другий ламає застосунок.

5. Шаблони як залежність Resource

У нашому проєкті правильний напрям — не розкидати по сервісах рядки шляхів і new File(...), а хоча б на рівні впровадження залежностей описати ресурс як окрему залежність. Поки без читання й кешу: тут нам важливо одне — клас має просити у контейнера «шаблон створення замовлення», а не самостійно лізти у файлову систему.

Почнемо з того, що зареєструємо Resource як звичайний bean у конфігурації. Так, Resource — це теж обʼєкт, і його можна впроваджувати через конструктор, як будь-яку іншу залежність.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

@Configuration
public class TemplatesConfig {

    @Bean
    Resource orderCreatedTemplate() {
        // Такий Resource-bean — нормальна практика: це інфраструктурна залежність, а не бізнес-логіка.
        return new ClassPathResource("templates/notifications/order-created.txt");
    }
}

Тепер зробимо невеликий клас-обгортку, який отримує ресурс через конструктор. Коли таких ресурсів стане більше одного, @Qualifier — це нормальний спосіб явно показати, який саме вам потрібен.

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

@Component
public class OrderCreatedTemplateRef {

    // Зберігаємо Resource як залежність: це “ручка” до вмісту, а не шлях до файла.
    private final Resource orderCreatedTemplate;

    public OrderCreatedTemplateRef(
            // Qualifier фіксує, який саме Resource-bean нам потрібен.
            @Qualifier("orderCreatedTemplate") Resource orderCreatedTemplate) {
        this.orderCreatedTemplate = orderCreatedTemplate;
    }
}

Поки ми нічого не читаємо. Це важливо: ми просто зробили модель залежності правильною. Тепер клас не каже «я полізу у файлову систему туди-то», він каже «мені потрібен ресурс “шаблон створення замовлення”». Це саме той рівень абстракції, який робить код переносним.

Якщо вам дуже хочеться перевірити, що все справді працює, можна тимчасово, для діагностики, вивести опис ресурсу, не читаючи його вміст:

import org.springframework.stereotype.Component;

@Component
public class TemplateDiagnostics {

    public TemplateDiagnostics(OrderCreatedTemplateRef templateRef) {
        // Діагностичний “якір”: дає змогу переконатися, що контекст піднявся і залежності впроваджено.
        System.out.println("Шаблони успішно впроваджено."); // Шаблони успішно впроваджено.
    }
}

Так, це навчальна заглушка. Але сенс у тому, що wiring уже виглядає правильно: Resource живе в інфраструктурі й впроваджується контейнером, а не створюється через new File(...) десь глибоко в бізнес-методі. Читання тексту — це вже окремий обовʼязок.

Найприємніше, що від цього моменту ви можете змінити спосіб зберігання шаблону, не переписуючи сервіси. Сьогодні це classpath, завтра — інше джерело. Ми не будемо зараз ускладнювати; головне в тому, що ви вже не замкнули себе у File‑світі.

6. Типові помилки під час переходу від File до Resource

Коли ви вперше починаєте працювати з Resource, мозок по інерції продовжує думати в категоріях «папка / файл / шлях», і це нормально. Але є кілька помилок, які зʼявляються майже в усіх. Краще спіймати їх зараз, поки вони не перетворилися на «традиції проєкту».

Помилка №1: використовувати шлях src/main/resources/... як «шлях до ресурсу».
Це шлях до вихідних файлів у репозиторії, а не до ресурсу в рантаймі. У рантаймі потрібно мислити «шлях усередині classpath». Якщо ви бачите рядок src/main/resources усередині застосунку — це майже завжди сигнал, що ви випадково привʼязалися до структури проєкту, а не до моделі постачання ресурсу.

Помилка №2: очікувати, що Resource — це обовʼязково File, і намагатися мислити диском.
Психологічно хочеться сказати: «Ну раз ресурс — це файл, я візьму getFile() і житиму спокійно». Проблема в тому, що classpath‑ресурс не зобовʼязаний бути дисковим файлом. Він може бути всередині jar. Тому ваш безпечний універсальний шлях — читання через getInputStream().

Помилка №3: тримати шляхи до шаблонів прямо в бізнес-сервісах.
Якщо OrderPlacementService знає, що шаблон лежить у templates/notifications/..., то бізнес-сервіс раптово починає залежати від структури ресурсів. Це майже те саме, що змусити доменну модель знати, де лежить application.properties. Краще нехай бізнес-шар залежить від «каталогу шаблонів», а не від рядків.

Помилка №4: вважати, що Resource «вирішив усе» і тепер можна не думати про I/O.
Resource — це чудовий вхід до читання, але він не скасовує реальність: потік потрібно закривати, кодування потрібно обирати, помилки потрібно обробляти. Якщо зробити вигляд, що читання «не може впасти», ви отримаєте або незрозумілі NPE, або тихі порожні шаблони, які потім спливуть у найнеприємнішому місці, зазвичай під час демо перед керівником.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ