JavaRush /Курси /Spring Core /Префікси classpath:...

Префікси classpath:, file: та URL

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

1. Префікси в шляху ресурсу

Коли новачок бачить рядок на кшталт "templates/notifications/order-created.txt", мозок автоматично домальовує: «це шлях до файла». І це нормально — ми всі звикли до File, Path і думки «ну я ж бачу цей каталог у проєкті». Але Spring-ресурси живуть у ширшому світі: ресурс може бути всередині jar, на диску або в мережі, а Resource має однаково вміти до нього дістатися.

Із самим Resource ми вже розібралися: він потрібен саме тому, що застосунок живе не лише у світі файлової системи. Тепер нам потрібна більш приземлена навичка — уміти задавати location так, щоб контейнер не вгадував за нас.

Префікс у рядку шляху — це, по суті, підказка контейнеру: «Гей, шукай це в classpath», або «Гей, це файл на диску», або «Гей, ось URL». І це не просто естетика. Це спосіб зробити поведінку передбачуваною, особливо коли ви запускаєте застосунок не з IDE, а як зібраний артефакт.

Уявіть, що «resource path» — це адреса доставки. Якщо ви пишете «вул. Сібуя, дім Асакаура», але забуваєте місто і країну, кур’єр (у ролі ResourceLoader) почне робити припущення. Іноді вгадає, іноді привезе вам у сусідній під’їзд, а іноді — у сусідній регіон. Префікс — це як «країна/місто» в адресі: не завжди обов’язковий, але майже завжди рятує нерви.

Запам’ятайте базову ідею: у Spring ми зазвичай оперуємо не «шляхами до файлів», а «локаціями ресурсів». Локація — це рядок, який може починатися з префікса, і саме за цим рядком Spring визначає, як створювати Resource.

2. classpath: ресурси застосунку

classpath: — це ваш найкращий друг для всього, що є частиною застосунку: шаблонів сповіщень, заголовків звітів, різних текстових заготовок за замовчуванням. Тобто для контенту, який ви зберігаєте в src/main/resources і який має поїхати в зібраний jar «всередині».

Ми вже розбирали, чому src/main/resources — це не шлях середовища виконання. Для ресурсів, які їдуть разом із застосунком, тут досить одного практичного правила: після збирання вони живуть у classpath, а classpath: фіксує саме цей спосіб пошуку.

Ось мінімальний приклад, який можна вставити хоч у ваш main, хоч у невеликий діагностичний раннер. У нас це навчальний проєкт — тут можна дозволити собі трохи більше «подивитися очима».

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.Resource;

public class ResourcePrefixDemo {

    public static void main(String[] args) {
        // Контекст одночасно виступає і як ResourceLoader: через нього зручно завантажувати ресурси в демо
        try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {

            // Явно кажемо Spring: шукати ресурс потрібно в classpath (зокрема всередині jar)
            Resource template = context.getResource(
                    "classpath:templates/notifications/order-created.txt"
            );

            // Опис корисний для налагодження: видно, як саме Spring трактує цей ресурс
            System.out.println(template.getDescription());
            // ресурс classpath [templates/notifications/order-created.txt]
        }
    }
}

Зверніть увагу на дві речі. По-перше, ми використовуємо context.getResource(...): ApplicationContext вміє виступати як ResourceLoader, і для демо це зручно. По-друге, шлях після classpath: — це шлях усередині classpath, а не шлях у файловій системі.

Дуже типовий запит: «Чи потрібно починати зі /?» У більшості випадків — ні. Для classpath: ви зазвичай пишете шлях відносно кореня classpath: templates/..., legacy/..., messages.properties тощо. Якщо ви додасте початковий слеш, у деяких місцях це спрацює, у деяких — створить плутанину, а новачку точно додасть зайвих «чому воно то працює, то ні».

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

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.Resource;

public class ClasspathCheck {

    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {
            // Ресурс усередині classpath: в IDE він "як файл", а в jar — "як запис в архіві"
            Resource r = context.getResource("classpath:templates/reports/daily-report-header.txt");

            // exists() — швидка перевірка: найчастіше false означає "поклали не туди" або "назвали не так"
            System.out.println("існує = " + r.exists());        // існує = true
            System.out.println("імʼя файла = " + r.getFilename()); // імʼя файла = daily-report-header.txt
        }
    }
}

Якщо ви побачили існує = false, то майже завжди проблема не в Spring, а в одній із трьох причин: ресурс лежить не там, ім’я не збіглося, включно з регістром, або ви не поклали його в src/main/resources (поклали поруч із Java-класом, і Gradle його просто не упакував).

Є ще один «підступний» нюанс, який частіше проявляється, коли ви працюєте на Windows, а розгортання потім відбувається на Linux. У Windows файлова система часто «прощає» регістр (Order-Created.txt і order-created.txt начебто одне й те саме), а всередині jar і на Linux це вже різні імена. Тому для ресурсів краще одразу тримати дисципліну: імена в нижньому регістрі, дефіси, без сюрпризів.

3. file: ресурси на диску

file: потрібен, коли ресурс знаходиться у файловій системі. Це може бути корисно, якщо ви хочете, щоб файл можна було підмінити без перескладання застосунку, або якщо файл узагалі генерується чи кладеться поруч із застосунком, або якщо це артефакт у build/.

І тут важливо не переплутати дві різні категорії:

1) вхідні «еталонні» шаблони застосунку (їхнє навчальне правило за замовчуванням — classpath:),
2) файли, які застосунок створює сам (їхній дім — build/, і це вже частіше про Path/Files, але іноді ви зберігатимете на них Resource-посилання для однаковості).

Давайте подивимося, як виглядає file: у коді. Важливо: краще починати з відносних шляхів (відносно поточної директорії запуску), щоб не прив’язуватися до машини.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.Resource;

public class FileResourceDemo {

    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {

            // Явно кажемо: це файл у ФС, а не ресурс classpath
            // ./ — це робоча директорія процесу (часто корінь проєкту в навчальних запусках)
            Resource report = context.getResource("file:./build/reports/daily-report.txt");

            System.out.println(report.getDescription());
            // URL [file:./build/reports/daily-report.txt]
        }
    }
}

Тут є невеликий «занудний», але корисний момент: коли ви використовуєте ResourceLoader (через контекст), file: часто перетворюється не на FileSystemResource, а на варіант UrlResource (наприклад, FileUrlResource). Це нормально: file: — це URL-схема, і Spring спокійно обгортає її як URL-ресурс. Семантика та сама: «це файл на диску».

Якщо вам хочеться трохи менше магії в голові, можна думати так: file: — це «файл як URL», а FileSystemResource — це «файл як об’єкт файлової системи». У повсякденній роботі вам зазвичай важлива не точна реалізація, а те, що ви вмієте відкрити InputStream() і прочитати або записати дані.

Тепер два практичні зауваження, які економлять години.

Перше: file: із ./ майже завжди очікуваний і зручний у навчальному проєкті. Але пам’ятайте, що ./ — це «поточна директорія запуску». Якщо ви запускаєте застосунок з кореня проєкту — це корінь проєкту. Якщо запускаєте інакше, наприклад з іншої папки або через IDE зі дивною робочою директорією, відносний шлях «переїде». Тому для файлових артефактів ContextFlow ми намагаємося писати в build/ і запускати з кореня — так простіше.

Друге: якщо ви хочете отримати коректний file:-URL без ручного склеювання рядків, можна зібрати шлях через Path і перетворити його на URI. Це особливо приємно, тому що воно одразу працює і на Windows, і на Linux.

import java.nio.file.Path;

public class FileLocationBuilder {

    public static void main(String[] args) {
        // Збираємо шлях незалежно від платформи (Windows/Linux)
        Path p = Path.of("build", "reports", "daily-report.txt");

        // toUri() дає коректний file:// URL, який можна прямо використовувати як локацію ресурсу
        String location = p.toUri().toString();

        System.out.println(location);
        // file:///.../build/reports/daily-report.txt (абсолютний file-URL)
    }
}

Так, шлях вийде абсолютним, із вашим конкретним диском і папкою. Але ви його не хардкодите, а обчислюєте з відносного build/.... Це великий плюс порівняно з «C:\Users\Bob\Desktop\myreport.txt» у коді.

4. URL-ресурси http(s)://

Іноді корисно пам’ятати, що Resource — це не лише «classpath або диск». Якщо рядок виглядає як URL, наприклад починається з https://, Spring може дати вам Resource, який читає дані по мережі. І ваш код читання при цьому може бути майже таким самим: ви так само працюєте через getInputStream().

Це виглядає красиво, майже як «універсальний пульт керування всім». Але в нашому курсі та в нашому ContextFlow це радше частина загальної картини, ніж робоча практика. Адже мережеві ресурси одразу тягнуть за собою питання latency, доступності, таймаутів і повторів. А ми зараз навчаємося контейнерному мисленню, а не створюємо маленький браузер.

Проте приклад корисний, щоб ви побачили: префікси — це частина великого світу.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.Resource;

public class UrlResourceDemo {

    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {

            // Якщо рядок виглядає як URL, Spring створить URL-ресурс (для читання по мережі)
            Resource remote = context.getResource("https://example.com/templates/order-created.txt");

            // Тип ресурсу корисно дивитися в демо: це знімає частину "магії"
            System.out.println(remote.getClass().getSimpleName()); // UrlResource
            System.out.println(remote.getDescription());           // URL [https://example.com/templates/order-created.txt]
        }
    }
}

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

5. Без префікса: неявні правила

Дуже спокусливо писати просто "templates/notifications/order-created.txt" і сподіватися, що Spring «сам зрозуміє». І іноді він справді розуміє. Але в цей момент ви добровільно підписуєтеся на гру «вгадай правила».

Базова логіка ResourceLoader часто така: якщо рядок починається з classpath: — це classpath. Якщо рядок схожий на URL (file:, http:, https:) — це URL-ресурс. Якщо нічого з цього немає, Spring намагається трактувати рядок як classpath-ресурс або як ресурс відносно конкретного контексту. У нашому навчальному застосунку (через AnnotationConfigApplicationContext) «без префікса» зазвичай означає «classpath».

Ось невеликий експеримент, який допомагає це відчути.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.Resource;

public class NoPrefixSurprise {

    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {

            // Без префікса: вмикаються неявні правила ResourceLoader (залежить від контексту)
            Resource a = context.getResource("templates/notifications/order-created.txt");

            // Із префіксом: поведінка зафіксована і читається очима
            Resource b = context.getResource("classpath:templates/notifications/order-created.txt");

            System.out.println(a.getDescription());
            System.out.println(b.getDescription());
            // Обидва можуть виглядати подібно і обидва можуть бути classpath resources
        }
    }
}

І ось де починається пастка. Раз «і так працює», значить можна продовжувати без префікса. А потім хтось у команді, або ви самі через пів року, змінює спосіб запуску контексту, або підключає інший ResourceLoader, або починає читати ресурси з іншого місця — і раптом рядок без префікса починає означати інше. У навчальному проєкті це рідкість, а в реальних проєктах таке трапляється.

Ще більш показовий приклад: ви думаєте, що «без префікса» можна послатися на файл у build/. А Spring у нашому контексті сприймає це як classpath.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.Resource;

public class WrongAssumption {

    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {

            // Здається, що це "шлях до файла", але для Spring без префікса це може бути classpath-локація
            Resource r = context.getResource("./build/reports/daily-report.txt");

            System.out.println(r.getDescription());
            System.out.println("існує = " + r.exists());
            // існує = false (бо це не file:, а спроба знайти в classpath)
        }
    }
}

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

6. Ресурси в ContextFlow: читання і виведення

Коли семантика префіксів зрозуміла, для ContextFlow зручно вивести дуже просте правило. Перший світ — це шаблони і заготовки, які йдуть разом із застосунком. Це наші order-created.txt, order-cancelled.txt, daily-report-header.txt. Вони мають бути в src/main/resources і читатися як classpath:-ресурси. Другий світ — це файли, які застосунок виробляє сам, наприклад звіти або аудит у demo-режимі, і їхнє місце — у build/.

З цього легко вивести просте правило, яке сильно зменшує хаос: вхідні ресурси завжди описуються через classpath: (бо це «контент застосунку»), а шляхи до вихідних файлів ми тримаємо як шляхи файлової системи, і якщо потрібен саме Resource, то це буде file:.

На практиці це також добре лягає на вашу вже знайому модель зовнішньої конфігурації. Наприклад, у contextflow.properties або в профільному файлі можна зберігати локації ресурсів, включно з префіксами. І це дуже зручно: ви можете змінити «де лежить шаблон» без переписування коду.

Приклад того, як це може виглядати в properties (зверніть увагу: префікс — частина значення):

# Вхідні шаблони: частина застосунку => classpath
contextflow.templates.order-created=classpath:templates/notifications/order-created.txt
contextflow.templates.order-cancelled=classpath:templates/notifications/order-cancelled.txt
contextflow.reports.daily-header=classpath:templates/reports/daily-report-header.txt

# Вихідний файл: артефакт на диску => file
contextflow.report.output-file=file:./build/reports/daily-report.txt

Так, у вас може виникнути запитання: «А чи можна output file тримати просто як ./build/... і не робити file:?» Можна, але ви щойно побачили, чим це закінчується: без префікса це не обов’язково буде ресурс файлової системи. Коли ви зберігаєте саме resource location, краще бути чесними і писати як ресурс.

Щоб тримати все це в голові трохи легше, ось невелика таблиця-шпаргалка. Її можна подумки роздрукувати й приклеїти поруч із монітором. Головне — не приклеюйте монітор до столу, потім незручно.

Локація ресурсу (рядок) Де шукати Типовий зміст у ContextFlow Сильна сторона Слабке місце
classpath:templates/ notifications/order-created.txt усередині classpath (зокрема всередині jar) вхідний шаблон сповіщення портативно, стабільно, «їде разом із застосунком» не можна «підмінити на диску» без перескладання (за замовчуванням)
file:./build/reports/daily-report.txt у файловій системі вихідний артефакт/файл звіту можна змінювати або переглядати зовні, зрозуміло, де лежить залежить від робочої директорії, якщо використовувати ./ без дисципліни
https://example.com/a.txt по мережі майже не використовуємо, але можливо єдиний API читання мережа, таймаути, нестабільність

І ще одна маленька схема, щоб «закріпилося очима», — що взагалі відбувається, коли ви пишете рядок локації.

flowchart TD
    A["Рядок локації
classpath:... / file:... / https://..."] --> B["ResourceLoader / ApplicationContext"] B --> C["Resource (ClassPathResource / UrlResource / File...Resource)"] C --> D["getInputStream()"] D --> E["Читання тексту / байтів
і робота застосунку"]

Головний висновок тут спокійний: ми не пишемо «десь у коді new File». Ми описуємо локацію ресурсу і дозволяємо Spring створити правильний Resource.

7. Типові помилки під час роботи з префіксами ресурсів

Префікси виглядають простими, і це якраз робить їх небезпечними: вони легко починають «працювати випадково», а потім ви раптом пів дня дивитеся в exists() і підозрюєте змову збирання, Gradle та фаз Місяця. Давайте проговоримо найчастіші граблі так, щоб ви наступили на них максимум один раз — і бажано в навчальному проєкті.

Помилка №1: посилатися на src/main/resources/... під час виконання.
Це, мабуть, чемпіон за популярністю. Під час виконання ваш застосунок не зобов’язаний знати, що колись був Gradle і вихідні файли. Якщо ви хочете читати ресурс, який постачається разом із застосунком, використовуйте classpath:templates/..., а не шлях до папки вихідних файлів. Папка src/main/resources — це для розробки, а не для запуску.

Помилка №2: плутати «ресурс у classpath» і «файл на диску» та вибирати неправильний префікс.
Якщо це шаблон сповіщення, який є частиною застосунку, — це майже завжди classpath:. Якщо це файл, який лежить поруч і може змінюватися без перескладання, — це file:. Коли ви ставите file: для «внутрішнього» шаблону, ви прив’язуєте запуск до наявності файла на конкретній машині.

Помилка №3: не писати префікс і сподіватися, що «воно саме».
Без префікса Spring застосовуватиме неявні правила. У нашому контексті «без префікса» часто означає classpath. Тому рядок "./build/reports/daily-report.txt" без file: майже напевно не працюватиме як файловий ресурс. Для новачка краще правило: «з префіксом завжди зрозуміліше».

Помилка №4: випадково використовувати абсолютні шляхи в file: і зробити застосунок непереносним.
Рядок на кшталт file:/Users/alice/Desktop/template.txt або file:C:\temp\template.txt може працювати в автора, але в команді перетворюється на «чому в мене не запускається». У навчальному проєкті тримайтеся file:./build/... або збирайте шлях через Path і обчислюйте його від відносного build/, щоб не хардкодити конкретну машину.

Помилка №5: думати, що classpath:-ресурс завжди можна перетворити на File.
Навіть якщо ви бачите файл в IDE, після збирання він може опинитися всередині jar. А всередині jar це вже не «звичайний файл» у файловій системі. Тому getFile() для classpath-ресурсів — не навчальне правило за замовчуванням. Якщо хочеться читати — відкривайте getInputStream(): це і є робоча точка опори.

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