JavaRush /Курсы /Spring Core /Шаблоны и заголовки как ресурсы

Шаблоны и заголовки как ресурсы

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

1. Архитектура шаблонов и заголовков

Когда проект маленький, очень хочется относиться к шаблону как к чему-то второстепенному: «ну текстик и текстик, положу куда-нибудь и потом как-нибудь прочитаю». Это нормальная стадия, как “сначала сделаю всё в одном классе, а потом рефакторну” (и потом… никогда). Но в ContextFlow шаблоны уведомлений и заголовки отчётов — это часть поведения приложения, просто выраженная не в Java, а в текстовых файлах.

Ключевая мысль: ресурс должен иметь понятное место в проекте и понятное имя, иначе он перестаёт быть “ресурсом приложения” и превращается в “случайный файл рядом с кодом”. А ещё важно, чтобы путь к ресурсу не дублировался по всему коду. Путь — это тоже конфигурационное знание. Если оно размазано по 5 классам, то “переименовать файл” становится мини-квестом, где главный босс — ваш собственный Ctrl+Shift+F.

Способ получить Resource у нас уже есть. Теперь надо зафиксировать, какие именно файлы есть у ContextFlow, как их назвать и где им жить, чтобы проект не расползся на template1.txt и случайные строки путей.

Карта ресурсов ContextFlow

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

Удобно прямо сейчас договориться, что имена файлов отражают сценарий. Это намного понятнее, чем template1.txt, потому что “template1” говорит примерно ничего, а order-created уже несёт смысл.

Ниже — минимальная “карта ресурсов” (не как бюрократия, а как шпаргалка для мозга):

Сценарий в приложении Ресурс (файл) Для чего используется
Создание заказа templates/notifications/order-created.txt Текст уведомления клиенту/оператору
Отмена заказа templates/notifications/order-cancelled.txt Текст уведомления об отмене
Генерация отчёта templates/reports/daily-report-header.txt Заголовок/шапка отчёта

Пока что мы не трогаем локализацию и MessageSource (это отдельная история), поэтому делаем одну версию каждого ресурса. Когда добавляются новые механики, структура расширяется, но стартовать лучше с простого и стабильного.

2. Структура src/main/resources

Когда вы кладёте шаблон “куда попало”, вы создаёте проблему будущему себе. Правильное место для входных ресурсов — src/main/resources. Это означает, что они попадут в classpath и поедут вместе с вашим приложением (в jar).

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

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

src/main/resources
├─ contextflow.properties
└─ templates
   ├─ notifications
   │  ├─ order-created.txt
   │  └─ order-cancelled.txt
   └─ reports
      └─ daily-report-header.txt

Отдельно полезно держать в голове границу: src/main/resources — это то, что приложение читает как вход. А всё, что приложение генерирует (отчёты, файлы аудита), мы складываем в build/ согласно правилам курса. Не потому что “так хочет Gradle”, а потому что это дисциплина воспроизводимости: вы всегда можете удалить build/ и не потерять “исходные знания” приложения, только результаты его работы.

Если хочется немного самоиронии, можно представить так: resources — это “книга рецептов”, а build/ — “грязная кухня после готовки”. Не надо печатать рецепт на сковородке.

3. Шаблоны: имена и содержимое

Когда вы придумываете формат шаблона, очень легко случайно изобрести мини-язык программирования. И потом героически его поддерживать. На текущем уровне нам важно другое: чтобы текст был читаемым, а placeholders были очевидными. Даже если позже вы будете делать подстановку простым String#replace, ваш будущий код выиграет от предсказуемого формата.

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

Пример содержания order-created.txt может быть максимально простым:

ORDER CREATED: id=%s, customer=%s

А order-cancelled.txt:

ORDER CANCELLED: id=%s, reason=%s

И заголовок отчёта daily-report-header.txt:

DAILY REPORT — ContextFlow
=========================

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

4. Централизация путей к ресурсам

Самая частая практическая боль с ресурсами — не чтение, а банальное “где лежит этот файл?”. Если путь к order-created.txt прописан строкой в трёх местах, то вы гарантированно получите расхождение: один класс будет искать order-created.txt, второй — order_create.txt, третий — вообще в другой папке. И вот уже приложение “иногда работает”, что особенно приятно… на демо.

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

Минимальный вариант — утилитный класс-константник:

package com.example.contextflow.infrastructure.resources;

public final class TemplatePaths {

    // Путь к шаблону уведомления о создании заказа (classpath resource).
    public static final String ORDER_CREATED = "templates/notifications/order-created.txt";

    // Путь к шаблону уведомления об отмене заказа (classpath resource).
    public static final String ORDER_CANCELLED = "templates/notifications/order-cancelled.txt";

    // Путь к заголовку (шапке) ежедневного отчёта (classpath resource).
    public static final String DAILY_REPORT_HEADER = "templates/reports/daily-report-header.txt";

    // Запрещаем создание экземпляров: это контейнер для констант, а не "объект с состоянием".
    private TemplatePaths() {
    }
}

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

5. Ресурсы как зависимости Spring

Если мы относимся к шаблонам как к части приложения, логично сделать следующий шаг: пусть Spring-контейнер тоже участвует в этой истории. Здесь нам нужен аккуратный registry ресурсов: классы ниже пока отвечают только за grouping и выбор Resource, без I/O и без рендеринга текста.

Один удобный приём для новичка — “сгруппировать” ресурсы в маленький объект. Тогда нам не нужно плодить десять разных Resource-bean’ов и затем разруливать их через @Qualifier. Например, создадим immutable-холдер для notification-шаблонов:

package com.example.contextflow.infrastructure.resources;

import org.springframework.core.io.Resource;

// Группируем ресурсы уведомлений в один объект,
// чтобы не размазывать по коду множество отдельных Resource-зависимостей.
public record NotificationTemplates(Resource orderCreated, Resource orderCancelled) {
}

Теперь в конфигурации можно собрать этот объект из ClassPathResource. Важно: ClassPathResource принимает путь без префикса classpath: — потому что мы и так явно говорим “это classpath” самим типом ресурса.

import com.example.contextflow.infrastructure.resources.NotificationTemplates;
import com.example.contextflow.infrastructure.resources.TemplatePaths;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;

@Bean
NotificationTemplates notificationTemplates() {
    // Создаём ресурсы строго из classpath, чтобы шаблоны ехали вместе с приложением в jar.
    // Пути берём из одного места (TemplatePaths), чтобы не плодить "магические строки".
    return new NotificationTemplates(
            new ClassPathResource(TemplatePaths.ORDER_CREATED),
            new ClassPathResource(TemplatePaths.ORDER_CANCELLED));
}

Дальше любой инфраструктурный bean может получить эти шаблоны через конструктор. Например, наш NotificationTemplateRegistry пока просто хранит ссылки на ресурсы и выбирает нужный по сценарию:

package com.example.contextflow.infrastructure.resources;

import org.springframework.stereotype.Component;

@Component
public class NotificationTemplateRegistry {

    // Registry знает "какие шаблоны есть", но пока не делает I/O.
    private final NotificationTemplates templates;

    public NotificationTemplateRegistry(NotificationTemplates templates) {
        // DI: получаем готовый набор ресурсов от контейнера.
        this.templates = templates;
    }
}

Это именно registry ссылок на ресурсы: чтение содержимого здесь специально не делаем.

Если вам нужно выбрать один из ресурсов по сценарию, это тоже можно выразить через простой метод, не читая содержимое:

import com.example.contextflow.domain.model.OrderStatus;
import org.springframework.core.io.Resource;

public Resource templateFor(OrderStatus status) {
    // Выбираем шаблон по смыслу (статус заказа),
    // а не по пути и не по "где лежит файл".
    return status == OrderStatus.CANCELLED
            ? templates.orderCancelled()
            : templates.orderCreated();
}

Точно так же можно поступить с отчётами. Например, сделать отдельный ReportTemplates с заголовком:

package com.example.contextflow.infrastructure.resources;

import org.springframework.core.io.Resource;

// Отдельный набор ресурсов для отчётности: сейчас тут только заголовок.
public record ReportTemplates(Resource dailyReportHeader) {
}

И зарегистрировать его в конфигурации:

import com.example.contextflow.infrastructure.resources.ReportTemplates;
import com.example.contextflow.infrastructure.resources.TemplatePaths;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;

@Bean
ReportTemplates reportTemplates() {
    // Заголовок отчёта тоже считаем входным ресурсом (classpath).
    return new ReportTemplates(new ClassPathResource(TemplatePaths.DAILY_REPORT_HEADER));
}

Обратите внимание на важный педагогический момент: мы пока не читаем текст, не открываем потоки и не разбираем ошибки I/O. Сейчас задача лекции — чтобы шаблон перестал быть “строкой пути” и стал явной зависимостью.

Пути в properties и константы

Сам способ превратить location в Resource у нас уже есть — через @Bean, @Value или ResourceLoader. Здесь другой вопрос: где держать source of truth для самих location.

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

Практическое правило простое: если путь к шаблону — стабильная часть приложения, его нормально держать в classpath и фиксировать константой. Это как Java-класс: вы же не выносите путь к OrderPlacementService в properties. Он просто часть программы.

Для текущего ContextFlow самый простой и ожидаемый вариант такой: шаблоны лежат в classpath и едут вместе с приложением. file: остаётся полезной альтернативой, когда шаблон действительно нужно вынести наружу.

Но если вам действительно нужно менять шаблоны без пересборки (или по профилю), тогда да — путь должен стать настройкой. В нашем курсе это особенно логично, потому что у нас уже есть внешний конфиг и профили.

Например, в contextflow.properties можно добавить такие ключи:

# Настройки путей к шаблонам как Resource-строки (classpath:... / file:...).
# Идея: можно менять расположение шаблонов без пересборки приложения.
contextflow.templates.notifications.order-created=classpath:templates/notifications/order-created.txt
contextflow.templates.notifications.order-cancelled=classpath:templates/notifications/order-cancelled.txt
contextflow.templates.reports.daily-header=classpath:templates/reports/daily-report-header.txt

И тогда вы можете собрать NotificationTemplates не через ClassPathResource, а через @Value, позволяя контейнеру самому преобразовать строку location в Resource:

import com.example.contextflow.infrastructure.resources.NotificationTemplates;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;

@Bean
NotificationTemplates notificationTemplates(
        @Value("${contextflow.templates.notifications.order-created}") Resource created,
        @Value("${contextflow.templates.notifications.order-cancelled}") Resource cancelled) {

    // Spring сам сконвертирует строку вида classpath:.../file:... в Resource.
    return new NotificationTemplates(created, cancelled);
}

Красота этого подхода в том, что вы можете позже (без изменения Java-кода) подменить classpath: на file: и подложить другой файл. Но важно помнить: гибкость должна быть оправданной. Если вы добавили конфиг только потому, что “так в Spring принято”, вы просто увеличили количество мест, где можно ошибиться.

Схема зависимостей

Когда проект растёт, мозгу становится тяжело держать в голове “кто кого знает”. А с ресурсами новичок часто делает одну из двух ошибок: либо тащит ResourceLoader в бизнес-сервис, либо наоборот “прячет” пути глубоко в коде. Нам нужен баланс: бизнес-слой знает смысл (“мне нужен текст уведомления”), инфраструктура знает технику (“он лежит в classpath”).

Вот простая схема зависимостей (без I/O-деталей), которую удобно держать в голове:

flowchart TD
  A["NotificationDispatchService
(application/service)"] --> B["NotificationTemplateRegistry
(infrastructure/resources)"] B --> C["NotificationTemplates
(infrastructure/resources)"] C --> D["Resource
order-created.txt / order-cancelled.txt"] R["ReportingService
(application/reporting)"] --> H["ReportTemplates
(infrastructure/resources)"] H --> HR["Resource
daily-report-header.txt"]

Главная “проверка на здравый смысл” здесь такая: application-слой не должен собирать строки путей и не должен думать, classpath: это или file:. Он должен получать готовые зависимости, которые отражают смысл. Всё знание о расположении ресурса сосредоточено в инфраструктуре и конфигурации.

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

Ошибка №1: хранить шаблоны рядом с Java-классами в src/main/java.
Иногда шаблон кладут в тот же пакет, что и сервис, “чтобы было рядом”. Это быстро ломает понимание структуры: Java-код смешивается с данными. Плюс вы теряете стандартную механику classpath resources. Держите входные файлы в src/main/resources — там их ожидают и вы, и сборка, и любые инструменты вокруг.

Ошибка №2: давать файлам имена без сценарного смысла.
template1.txt и header2.txt выглядят невинно… пока их не станет 12. Потом вы будете открывать их по очереди и угадывать, где “тот самый”. Имена вроде order-created.txt и daily-report-header.txt экономят время, нервы и количество саркастических комментариев в коммитах.

Ошибка №3: дублировать пути к ресурсам строками в разных классах.
Когда один и тот же путь прописан в 3 местах, в одном из них обязательно будет опечатка или “устаревшая версия”. Хуже того, вы перестаёте доверять коду: непонятно, где источник правды. Константы (или properties) должны задавать путь централизованно.

Ошибка №4: смешивать входные ресурсы и выходные артефакты.
Иногда разработчик записывает сгенерированный отчёт в src/main/resources — “чтобы он был рядом с шаблоном”. Это методически вредно: ресурсы — вход, build/ — выход. Если смешать эти миры, через неделю вы не поймёте, что является частью приложения, а что — результатом прошлого запуска.

Ошибка №5: превращать конфигурацию в склад путей “на всякий случай”.
Вынести пути в properties — хорошая идея только тогда, когда вы реально планируете их менять. Если вы вынесли в конфиг всё подряд, вы получили больше гибкости… и больше мест, где можно ошибиться. Для стабильных шаблонов classpath-константа часто проще и надёжнее.

1
Задача
Spring Core, 15 уровень, 3 лекция
Недоступна
Группировка notification templates в одном bean
Группировка notification templates в одном bean
1
Задача
Spring Core, 15 уровень, 3 лекция
Недоступна
Именованные Resource-beans для уведомлений и отчёта
Именованные Resource-beans для уведомлений и отчёта
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ