JavaRush /Курсы /Spring Core /Безопасное чтение Resource...

Безопасное чтение Resource

Spring Core
15 уровень , 4 лекция
Открыта

1. Чтение Resource как часть надёжности приложения

Ресурсы уже разложены по папкам и вошли в wiring как нормальные зависимости. Теперь осталась самая приземлённая часть: читать их так, чтобы приложение либо получает нормальный текст, либо падает сразу и понятно.

Для обязательных шаблонов ContextFlow хороший сценарий один: ресурс найден, читается и даёт валидный текст. Вариант “не получилось — отправим пустую строку” здесь хуже любой громкой ошибки, потому что поломка уезжает глубоко в бизнес-сценарий.

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

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

// ВНИМАНИЕ: относительный путь завязан на текущую директорию запуска (working dir).
// "Работает" в IDE — и ломается после сборки в jar или при запуске из другого места.
String text = Files.readString(Path.of(
        "src/main/resources/templates/notifications/order-created.txt"
)); // "работает", пока запуск из корня проекта

Проблема не в Files.readString() как таковом. Проблема в том, что такой код привязан к структуре исходников и working directory. Поэтому дальше мы строим другой контракт: обязательный шаблон либо успешно загружается на старте контейнера, либо приложение честно останавливается с понятной причиной.

2. Fail-fast при загрузке шаблонов

Раз ресурсы уже стали зависимостями, логично решить и момент их чтения на уровне контейнера. В инфраструктурных штуках почти всегда выгоднее подход “fail-fast”: если критичный ресурс отсутствует — мы не притворяемся, что всё нормально, а честно останавливаем сборку контекста. Это звучит строго, но на практике экономит часы времени. И да, это очень по-спринговски: контейнер умеет валиться на старте именно потому, что это полезнее, чем вылавливать проблемы позже в рандомном месте.

В нашем проекте это естественно связывается с темой lifecycle, которую мы уже проходили. @PostConstruct — идеальная точка, чтобы проверить существование шаблонов и загрузить их в память. Тогда ошибка проявится в момент старта ApplicationContext, а не когда пользователь уже “создал заказ” и ждёт уведомление.

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

flowchart TD
    A[Spring создаёт bean NotificationTemplateCatalog] --> B["@PostConstruct loadTemplates()"]
    B --> C{"Resource exists()?"}
    C -- нет --> D["IllegalStateException с getDescription()"]
    C -- да --> E["getInputStream + readAllBytes"]
    E --> F[Шаблон сохранён в поле/кэш]
    F --> G[Сервис использует готовую строку без I/O]

И вот минимальный “скелет” идеи в коде (пока без деталей чтения):

import jakarta.annotation.PostConstruct;

@PostConstruct
void loadTemplates() {
    // Вызывается один раз при старте контекста — удобное место для fail-fast проверки.
    // 1) проверить, что ресурсы существуют
    // 2) прочитать в строки
    // 3) сохранить в поля (кэш)
}

Важный методический момент: мы не делаем чтение шаблона при каждом отправлении уведомления. Шаблон — это часть конфигурации приложения, а не “данные пользователя”, поэтому его логично загрузить один раз и использовать много раз. Это и быстрее, и предсказуемее: вы не зависите от I/O в момент выполнения бизнес-сценария.

3. Методы Resource для диагностики

Когда вы только начинаете, легко утонуть в API: у Resource много методов, а у каждой реализации ещё больше нюансов. Нам сейчас нужен небольшой “диагностический набор”, который помогает принимать решения и писать полезные сообщения об ошибках. В учебном проекте лучше освоить 4–5 методов, но использовать их стабильно, чем знать 20 методов и каждый раз выбирать наугад.

Ниже — компактная таблица того, что чаще всего пригодится именно для нашей задачи (шаблоны и заготовки небольших текстов):

Метод Resource Зачем нужен Как используем в ContextFlow
exists() Проверить, найден ли ресурс Перед чтением обязательного шаблона
isReadable() Бывает, что ресурс есть, но читать нельзя Дополнительная защита, особенно для file:
getInputStream() Универсальный способ читать содержимое Teaching default для чтения текста
getDescription() Человекочитаемое описание ресурса Всегда добавляем в текст ошибки
getFilename() Имя файла (если оно есть) Иногда полезно для логов/сообщений, но не опора логики

Давайте сразу “почувствуем” пользу getDescription(). Это тот случай, когда Spring прямо помогает писать нормальные ошибки:

import org.springframework.core.io.Resource;

String describe(Resource r) {
    // Хорошо подходит для исключений и логов: Spring сам формирует понятное описание.
    return r.getDescription(); // например: "class path resource [templates/.../order-created.txt]"
}

Если вы потом увидите исключение с этой строкой, вы почти мгновенно понимаете, что именно искали и где. Это гораздо лучше, чем “File not found” без контекста, где вы потом 20 минут вспоминаете: “а какой именно файл-то?”.

4. getInputStream вместо getFile

На этом месте многие новички (и, честно, некоторые взрослые разработчики после тяжёлой недели) делают одну и ту же ошибку: пытаются превратить Resource в File и дальше жить как раньше. Мысленно это выглядит так: “ну раз это ресурс, значит у него должен быть файл”. Но classpath-ресурс — это не обязательно файл, особенно когда приложение упаковано в jar.

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

import java.nio.file.Files;
import org.springframework.core.io.Resource;

String readViaFile(Resource resource) throws Exception {
    // Опасно: для classpath-ресурса внутри jar физического файла может не быть.
    return Files.readString(resource.getFile().toPath()); // может упасть для classpath внутри jar
}

Почему? Потому что у ресурса “внутри jar” физического файла в файловой системе может не существовать. Есть “кусочек архива”, а не путь на диске. Поэтому для универсального чтения Spring нам даёт гораздо более честный API: getInputStream().

Вот короткий и правильный вариант чтения текста (для маленьких шаблонов это отличный teaching default):

import java.nio.charset.StandardCharsets;
import org.springframework.core.io.Resource;

String readUtf8(Resource resource) throws Exception {
    // Универсально: работает и для classpath, и для file:, и для URL-ресурсов.
    try (var in = resource.getInputStream()) { // try-with-resources гарантирует закрытие потока
        // Для небольших шаблонов readAllBytes() читается просто и прозрачно.
        return new String(in.readAllBytes(), StandardCharsets.UTF_8);
    }
}

Здесь сразу два важных бонуса. Во‑первых, мы не привязаны к файловой системе: поток можно открыть хоть из classpath, хоть из URL. Во‑вторых, try-with-resources гарантирует закрытие потока. Поток — как кран с водой: если вы его открыли и не закрыли, “утечки” могут быть не видны сразу, но со временем начнут кусаться (особенно когда таких чтений много).

5. Helper ResourceTextReader

Если в двух-трёх классах вы начнёте вручную писать exists(), try-with-resources, new String(...), catch(IOException e) и сообщения об ошибках — очень быстро появится копипаст. А копипаст в инфраструктуре неприятен тем, что он размножает “разные стандарты поведения”: один класс падает, другой молчит, третий возвращает пустую строку, четвёртый логирует в System.out.println (да, это тоже грех, просто помягче).

Поэтому разумный шаг — вынести чтение в маленький helper, который станет единым способом работать с текстовыми ресурсами в ContextFlow. Он не должен быть “универсальным движком ресурсов”, мы не строим космический корабль. Нам нужен аккуратный, понятный и повторно используемый кирпичик.

Начнём с fail-fast проверки:

import org.springframework.core.io.Resource;

public void requireExists(Resource resource) {
    // Fail-fast: если обязательного ресурса нет — лучше упасть на старте с понятной причиной.
    if (!resource.exists()) {
        throw new IllegalStateException(
                "Resource not found: " + resource.getDescription()
        );
    }
}

Теперь добавим чтение и нормальную обёртку IOException в runtime-исключение, чтобы не тащить checked-исключения по всем сервисам:

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.springframework.core.io.Resource;

public String readRequiredUtf8(Resource resource) {
    requireExists(resource);

    try (var in = resource.getInputStream()) {
        // Явно фиксируем кодировку, чтобы поведение не зависело от платформы.
        return new String(in.readAllBytes(), StandardCharsets.UTF_8);
    } catch (IOException e) {
        // Важное место: не глотаем ошибку, а добавляем контекст и поднимаем как runtime-исключение.
        throw new IllegalStateException(
                "Cannot read resource: " + resource.getDescription(), e
        );
    }
}

Обратите внимание: мы не “глотаем” ошибку. Мы её поднимаем, добавляя контекст. Это и есть нормальная диагностика: ошибка не скрывается, но становится понятнее.

В реальном проекте такой helper удобно сделать Spring-bean’ом, чтобы его можно было внедрять в каталоги шаблонов и другие infrastructure-компоненты:

import org.springframework.stereotype.Component;

@Component
public class ResourceTextReader {
    // методы requireExists(...) и readRequiredUtf8(...)
}

6. Шаблоны уведомлений в ContextFlow

Теперь соберём рабочий вариант ContextFlow, где каталог шаблонов делает сразу три вещи: держит ссылки на ресурсы, читает их fail-fast на старте и отдаёт бизнес-сервисам готовые методы render...().

Пусть в src/main/resources лежат такие файлы:

src/main/resources/templates/notifications
├─ order-created.txt
└─ order-cancelled.txt

Чтобы не вводить отдельный template-language, держим шаблоны максимально простыми: обычный текст плюс %s, который потом подставляем через String.formatted(...).

# order-created.txt
ORDER CREATED: id=%s, customer=%s
# order-cancelled.txt
ORDER CANCELLED: id=%s, reason=%s

Теперь сделаем каталог, который внедряет два Resource и читает их в @PostConstruct. Чтобы не усложнять wiring, можно внедрить ресурсы через @Value прямо с classpath:. Это не “плохой хардкод”, если вы воспринимаете их как часть поставки приложения.

Фрагмент конструктора:

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

@Component
public class NotificationTemplateCatalog {

    public NotificationTemplateCatalog(
            // Явно говорим Spring, что это classpath-ресурс (а не "файл на диске").
            @Value("classpath:templates/notifications/order-created.txt") Resource created,
            @Value("classpath:templates/notifications/order-cancelled.txt") Resource cancelled) {
        // ...
    }
}

Теперь — ключевой кусок: @PostConstruct и чтение через наш helper. Разобьём на маленькие куски, чтобы код не выглядел как простыня.

Поля:

import org.springframework.core.io.Resource;

private final Resource createdTemplate;
private final Resource cancelledTemplate;
private final ResourceTextReader reader;

private String createdText;
private String cancelledText;

@PostConstruct загрузка:

import jakarta.annotation.PostConstruct;

@PostConstruct
void load() {
    createdText = reader.readRequiredUtf8(createdTemplate);
    cancelledText = reader.readRequiredUtf8(cancelledTemplate);
}

И методы, которые возвращают уже подготовленный текст. Мы можем сделать самый простой “template engine” на уровне String.formatted() — без библиотек и без магии:

public String renderCreated(String orderId, String customerName) {
    return createdText.formatted(orderId, customerName);
}

public String renderCancelled(String orderId, String reason) {
    return cancelledText.formatted(orderId, reason);
}

Именно такая форма каталога здесь полезна: класс честно зависит от ресурсов, читает их один раз и дальше отдаёт сервисам простой API “сформируй текст уведомления”. Бизнес-сервисы больше не знают, где лежат файлы и как они читаются. Это отличный пример здоровой изоляции.

Диагностика ошибок и “роль” ресурса

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

Сравним два сообщения:

Плохое (формально “правда”, но бесполезно):

IllegalStateException: Cannot read template

Хорошее (в нём уже есть конкретика):

IllegalStateException: Cannot read resource: class path resource [templates/notifications/order-created.txt]

Чтобы улучшить диагностику ещё на шаг, иногда полезно добавить “роль” ресурса. Не просто “не прочитали ресурс”, а “не прочитали шаблон создания заказа”. Тогда у вас в голове сразу связывается проблема с бизнес-сценарием.

Вот минимальный вариант метода, который принимает роль и добавляет её в исключение:

import org.springframework.core.io.Resource;

public String readRequiredUtf8(Resource resource, String role) {
    if (!resource.exists()) {
        throw new IllegalStateException(
                "Missing resource (" + role + "): " + resource.getDescription()
        );
    }
    return readRequiredUtf8(resource); // используем базовый метод
}

А вызов станет самодокументируемым:

createdText = reader.readRequiredUtf8(createdTemplate, "order-created template");

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

NotificationDispatchService использует каталог

Чтобы закрепить, что мы реально улучшили архитектуру, давайте посмотрим на то, как это выглядит в сервисе отправки уведомлений. Раньше там легко мог появиться соблазн “а давайте прямо тут прочитаем файл”. Теперь он просто просит каталог сгенерировать текст.

Мини-версия сервиса:

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {

    private final NotificationTemplateCatalog templates;
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationTemplateCatalog templates,
                                       NotificationSender sender) {
        this.templates = templates;
        this.sender = sender;
    }
}

И метод, который отправляет уведомление о создании заказа:

public void notifyOrderCreated(String orderId, String customerName) {
    String text = templates.renderCreated(orderId, customerName);
    sender.send(text); // детали отправки — в реализации NotificationSender
}

Если вы где-то в сценарии (например, в ScenarioRunner) вызовете это, вы получите понятный результат, а чтение ресурсов произойдёт ровно один раз на старте. В хорошем варианте сценарный код вообще не должен иметь I/O, связанного с шаблонами: он занимается бизнес-действиями, а инфраструктура подготовлена заранее.

Если хочется увидеть глазами, можно временно использовать ConsoleNotificationSender и увидеть что-то вроде:

System.out.println(text); // ORDER CREATED: id=ORD-1, customer=Alice

И приятный факт: если вы случайно переименовали order-created.txt или положили его не в ту папку, вы узнаете об этом при старте контекста, а не “внутри” notifyOrderCreated().

Здесь мы работаем с текстом как с конкретным физическим ресурсом: нашли файл, прочитали его, подставили значения. Когда текстов становится много и их уже нужно искать не по пути, а по ключу, locale и fallback-правилам, поверх этой же ресурсной основы появляется MessageSource.

7. Типичные ошибки при чтении Resource

Ошибка №1: сразу вызывать getFile у classpath-ресурса.
Эта ошибка появляется из старой привычки: “если что-то похоже на файл, значит это файл”. Но classpath-ресурс может жить внутри jar, и физического файла может не существовать. Поэтому getFile иногда работает в IDE и внезапно падает после сборки. Teaching default для чтения — getInputStream().

Ошибка №2: забывать закрывать поток.
resource.getInputStream() отдаёт InputStream, а поток — это ресурс в буквальном смысле. Его надо закрывать. Самый простой и правильный способ — try-with-resources. Если читать ресурсы “вручную” и не закрывать, можно получить странные проблемы в долгоживущем приложении и очень неприятные симптомы, которые тяжело связываются с первопричиной.

Ошибка №3: глотать IOException и продолжать с пустым шаблоном.
Иногда хочется сделать “как в UI”: если что-то не загрузилось, покажем пустоту. Но шаблон уведомления или заголовок отчёта — это часть конфигурации приложения, а не необязательный элемент. Если его нет, лучше упасть сразу и объяснить причину. Иначе вы получите “тихую поломку”: сценарии продолжают выполняться, но уведомления становятся бессмысленными, а отладка превращается в охоту на привидений.

Ошибка №4: писать сообщение об ошибке без getDescription().
Сообщение “Resource not found” без указания, какой именно ресурс искали, почти не помогает. getDescription() в Spring обычно уже содержит информацию о типе ресурса и его “адресе” (classpath/file/url). Это бесплатная диагностика — грех не пользоваться.

Ошибка №5: разносить чтение ресурсов по бизнес-сервисам.
Если OrderPlacementService или NotificationDispatchService начинают собирать строки путей и заниматься I/O, у вас возникает “скрытая инфраструктура” внутри бизнес-кода. Такой код трудно тестировать, трудно менять и трудно читать. Гораздо лучше держать ресурсную логику в одном infrastructure/helper bean (например, NotificationTemplateCatalog + ResourceTextReader) и отдавать бизнес-слою готовый API “дай текст”.

1
Задача
Spring Core, 15 уровень, 4 лекция
Недоступна
Helper для чтения обязательного ресурса
Helper для чтения обязательного ресурса
1
Задача
Spring Core, 15 уровень, 4 лекция
Недоступна
Fail-fast загрузка отсутствующего шаблона
Fail-fast загрузка отсутствующего шаблона
1
Опрос
Ресурсы Spring, 15 уровень, 4 лекция
Недоступен
Ресурсы Spring
Работа с ресурсами Spring
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ