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-константа часто проще и надёжнее.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ