1. Проблема: тексты в коде
Когда мы пишем учебное приложение, хочется, чтобы оно разговаривало с нами по‑человечески: “заказ создан”, “заказ отменён”, “отчёт сформирован”, “аудит включён”. И в самом начале это обычно выглядит невинно: одна строка в println, пару строк в NotificationSender, и всё работает. Но как только проект перестаёт быть “одним классом на весь мир”, текст внутри кода превращается в странную смесь логики и формулировок, где менять одно неудобно, а проверять другое — ещё неудобнее.
Представьте, что у нас есть сервис, который формирует фразу для вывода в консоль (или для уведомления):
public String createdMessage(String orderId) {
// Плохо масштабируется: текст «зашит» в код и меняется только через релиз.
// Плюс легко ошибиться с пробелами/пунктуацией при конкатенации.
return "Заказ " + orderId + " создан";
}
Вроде бы всё нормально. Но теперь реальность начинает подкидывать “маленькие” требования, которые на самом деле не маленькие. Текст нужно поменять (“создан успешно”, “принят в обработку”, “зарегистрирован”). Нужно сделать английскую версию для демонстрации. Нужно убрать слово “заказ” и заменить на “order”, но не везде. Нужно сделать формат одинаковым во всех местах, чтобы отчёты и консоль выглядели единообразно.
И получается неприятный эффект: чтобы поменять текст, приходится менять Java-код. То есть текст становится частью релиза. Вы правите строку — пересобираете — выпускаете — обновляете. И если вы думаете “да ладно, это же просто текст”, то поздравляю: вы только что обнаружили, как в проектах рождаются “правки на 2 минуты”, которые почему-то стоят половину рабочего дня.
Вторая сторона проблемы — тестируемость. Когда текст склеен конкатенацией, его легко “сломать” пробелами, запятыми, порядком частей. Тесты начинают проверять строковые литералы, и через месяц вы уже не уверены: тест проверяет бизнес-смысл или пунктуацию.
2. MessageSource: словарь сообщений
После темы Resource очень легко сделать неверный вывод: “Ну раз это тексты, значит снова читаем файлы”. Но MessageSource — это другой уровень абстракции. Он не говорит “вот тебе файл”. Он говорит: “вот тебе сообщение по ключу”. То есть код просит не конкретный текст и не конкретный файл, а смысловую “карточку”: order.created, order.cancelled, report.daily.title. А уже где и как хранится текст — это забота инфраструктуры, а не бизнес-метода.
В Spring это оформлено через интерфейс MessageSource из пакета org.springframework.context. У него ключевой метод выглядит примерно так (в упрощённом виде):
import org.springframework.context.MessageSource;
import java.util.Locale;
public class MessageExample {
public String text(MessageSource messages, Locale locale) {
// "order.created" — ключ (контракт) между кодом и словарём сообщений
// new Object[]{"ORD-1"} — аргументы для подстановки в {0}, {1}, ...
// "Order created" — fallback на случай отсутствия ключа
// locale — выбор языка/локали
return messages.getMessage(
"order.created",
new Object[]{"ORD-1"},
"Order created",
locale
);
}
}
Здесь важно поймать идею, а не запомнить параметры наизусть. Мы передаём “код сообщения” (order.created), параметры для подстановки (ORD-1), запасной текст на случай, если ключ не найден ("Order created"), и Locale, который определяет язык.
Пока это выглядит как “ну окей, мы заменили конкатенацию на вызов метода”. Но концептуально разница огромная: текст перестаёт быть частью кода. Код становится “потребителем сообщений”, а не “сборщиком предложений из кусков”.
И ещё одна тонкость, которую приятно знать заранее: ApplicationContext в Spring — это не только контейнер бинов. Он, среди прочего, умеет быть MessageSource. То есть контекст — это место, где живёт инфраструктура сообщений. Но (и это важно для здорового DI-мышления) в бизнес-код мы не таскаем сам контекст; мы внедряем именно MessageSource как зависимость. Контекст — “входная дверь” приложения, а не глобальная переменная.
3. MessageSource vs Resource
После вчерашнего дня логично спросить: “А зачем вообще нужен MessageSource, если у нас уже есть Resource и мы умеем читать файлы?” Ответ: потому что Resource решает задачу “получи содержимое ресурса”, а MessageSource решает задачу “получи короткий текст по ключу”. Они пересекаются по теме “где-то лежит текст”, но они про разные масштабы и разные сценарии.
Давайте сравним это прямо в лоб — без философии, а как инженер, которому нужно выбрать инструмент.
| Характеристика | Resource | MessageSource |
|---|---|---|
| “Единица” текста | файл целиком (часто много строк) | одно сообщение (обычно 1 строка) |
| Как обращаемся | по пути (classpath:templates/...) | по ключу (order.created) |
| Типичный кейс | шаблон уведомления, заголовок отчёта, внешний текстовый артефакт | короткие фразы, подписи, заголовки, сообщения в консоль |
| Параметры | обычно подставляем вручную (или простым шаблонизатором) | встроенная поддержка аргументов {0}, {1} |
| Про локаль | можно хранить разные файлы, но выбор делаем сами | локаль — часть модели, MessageSource помогает выбирать |
В коде это ощущается так.
Resource по‑прежнему прекрасен для файлов:
import org.springframework.core.io.Resource;
public class NotificationTemplateCatalog {
// Resource — это «указатель» на внешний артефакт (файл/URL/classpath-ресурс).
private final Resource orderCreatedTemplate;
public NotificationTemplateCatalog(Resource orderCreatedTemplate) {
// Обычно такой Resource настраивается инфраструктурой (контекстом Spring).
this.orderCreatedTemplate = orderCreatedTemplate;
}
}
А MessageSource — для коротких сообщений:
import org.springframework.context.MessageSource;
import java.util.Locale;
public class OrderText {
public String created(MessageSource messages, String orderId, Locale locale) {
// Здесь мы не знаем, где лежат файлы сообщений.
// Мы просим «фразу по ключу» и передаём локаль.
return messages.getMessage(
"order.created",
new Object[]{orderId},
"Order created",
locale
);
}
}
Чтобы лучше зафиксировать различие, представьте простой поток данных:
flowchart TD
%% Выбор инструмента зависит от того, «файл» это или «короткое сообщение по ключу».
A["Java-код: просит ТЕКСТ"] --> B{Что это за текст?}
B -->|Целый шаблон/файл| C["Resource (classpath/file/url)"]
B -->|Короткая фраза по ключу| D["MessageSource (key + Locale)"]
C --> E["Читаем содержимое файла"]
D --> F["Ищем значение по key"]
E --> G["Дальше используем в приложении"]
F --> G
В этом смысле Resource — это “доступ к артефактам”, а MessageSource — “доступ к словарю фраз”.
4. Message key как контракт
Когда разработчики впервые видят MessageSource, они часто хотят сделать ключами сами фразы. Типа messages.getMessage("Заказ создан", ...). Мозг думает: “Ну ключ же строка, значит пусть будет текст”. Но это путь к хаосу, потому что ключ — это не “значение”, а “идентификатор”.
Ключ должен быть стабильным. Он переживает редактуру текста, перевод на другой язык, смену стиля и формулировки. Если ключ — это фраза, то при смене формулировки вы вынуждены менять ключ… а значит, менять код… то есть вы вернулись туда, откуда начинали, просто с лишним шагом.
Поэтому нормальная практика — делать ключи смысловыми и короткими. В ContextFlow это легко читается:
order.created
order.cancelled
report.daily.title
audit.mode.enabled
Такие ключи становятся маленьким контрактом между Java‑кодом и “миром текстов”. Код говорит: “Мне нужно order.created”. А “мир текстов” отвечает: “Окей, вот тебе это сообщение на русском / английском / базовом варианте”.
Отдельный приятный бонус: ключи можно группировать, искать, переиспользовать. И если вы когда-нибудь видели проект, где люди месяцами правят строки “в 17 местах”, то как раз потому, что не было слоя, который говорит: “Вот один ключ — и одно место, где лежит текст”.
5. Параметризованные сообщения
Есть два популярных способа собрать сообщение с динамическими кусками. Первый — конкатенация. Второй — параметризованное сообщение. Конкатенация кажется проще ровно до момента, когда в строке появляются параметры, условия, форматирование, пробелы, склонения и “а давайте добавим ещё имя клиента”.
Плохая версия (но честная — так пишет почти каждый в начале):
// Конкатенация: быстро, но потом больно поддерживать и локализовать.
String text = "Заказ " + orderId + " для клиента " + customerName + " создан";
System.out.println(text); // Заказ ORD-1 для клиента Alice создан
Проблема не в том, что это “некрасиво”. Проблема в том, что это плохо масштабируется. Поменяли порядок слов в английской версии — и конкатенация уже не выглядит очевидной. Добавили “сумма: …” — и строка стала длиннее, а логика перемешалась с формулировкой.
MessageSource предлагает более аккуратную модель: текст хранится с плейсхолдерами {0}, {1}, а код передаёт значения отдельно.
import org.springframework.context.MessageSource;
import java.util.Locale;
public class OrderText {
private final MessageSource messages;
public OrderText(MessageSource messages) {
// Внедряем словарь сообщений как зависимость (а не читаем файлы руками).
this.messages = messages;
}
public String created(String orderId, String customerName, Locale locale) {
// Параметры передаём отдельно — текст сам решит, куда их вставить для конкретной локали.
return messages.getMessage(
"order.created.for-customer",
new Object[]{orderId, customerName},
"Order created",
locale
);
}
}
Сейчас мы не обсуждаем, где лежит текст order.created.for-customer и как устроены файлы бандлов — это будет следующая лекция. Нам важна идея: код передаёт значения, а текст решает, как их вставить.
Такой подход не только про локализацию. Он ещё и про поддержку. Текст можно поменять без изменения Java-класса. И, что особенно приятно, можно сделать разные формулировки для разных языков без “раскладывания предложения по кусочкам” в коде.
6. Где это нужно в ContextFlow
Сейчас наш проект — non-web приложение, которое запускается сценариями и пишет что-то в консоль, в файлы, в аудит. И у новичка часто возникает мысль: “Локализация — это же для веба. У нас нет UI. Зачем?” Но давайте посмотрим честно: у нас всё равно есть текстовый слой, просто он не в браузере. Это могут быть сообщения в консоль, тексты уведомлений, заголовки отчётов, подписи к audit-записям.
В ContextFlow можно выделить два типичных класса текстов.
Первый класс — большие шаблоны. Например, “тело уведомления о создании заказа” на несколько строк. Это остаётся в мире Resource, потому что это файл, который удобно хранить как отдельный артефакт и читать целиком.
Второй класс — короткие фразы. Например, заголовок “Дневной отчёт”, подпись “Заказ отменён”, короткая строка “Аудит включён”. Это идеальные кандидаты для MessageSource: они маленькие, повторяются в разных местах и зависят от языка.
Пример того, как это может выглядеть на уровне сервиса (пока без деталей конфигурации):
import org.springframework.context.MessageSource;
import java.util.Locale;
public class ConsoleOrderPresenter {
private final MessageSource messages;
public ConsoleOrderPresenter(MessageSource messages) {
// Presenter не должен знать про resources и *.properties — только про MessageSource.
this.messages = messages;
}
public void printCreated(String orderId, Locale locale) {
// Берём текст по ключу + аргументы + локаль.
String text = messages.getMessage(
"order.created",
new Object[]{orderId},
"Order created",
locale
);
// Дальше используем как обычную строку (консоль, лог, файл и т.д.).
System.out.println(text); // например: "Заказ ORD-1 создан"
}
}
Обратите внимание на важный момент: этот класс не читает файлы. Он не знает про messages_ru.properties. Он не знает про classpath:. Он знает только “у меня есть MessageSource — и я прошу у него сообщение”.
Это ровно то, к чему мы стремимся: детали хранения — в инфраструктуре, использование — в сервисе.
7. Что будет дальше
Очень хочется сразу спросить: “Окей, а где именно лежат эти тексты? Как Spring их находит?” Это правильный вопрос, но сегодня мы держим фокус на мотивации и границе между Resource и MessageSource. В следующих лекциях дня мы аккуратно соберём полную картину: какие файлы создаём, как устроен Locale, что такое fallback, и как зарегистрировать MessageSource в контексте так, чтобы он стал обычной зависимостью, как OrderStore или DiscountPolicy.
Пока достаточно запомнить простую схему, как будет жить “словарь сообщений” в нашем приложении:
flowchart LR
%% Service/Presenter запрашивает сообщение по ключу, а MessageSource возвращает готовую фразу.
A["Service / Presenter"] -->|просит key + args + locale| B["MessageSource"]
B --> C["Message bundles в resources"]
C --> B
B -->|возвращает готовый текст| A
Именно эта схема превращает “текст в коде” в “текст рядом с кодом”. То есть код перестаёт быть редактором сообщений и становится пользователем сообщений. Звучит немного пафосно, но на практике это означает: вы меняете формулировку — и вам не нужно лезть в 12 классов.
8. Типичные ошибки при работе с MessageSource
Ошибка №1: хранить короткие пользовательские тексты прямо в сервисах “потому что так быстрее”.
На маленьком примере действительно быстрее написать строку прямо в методе. Но цена этого решения приходит позже, когда текст начинает повторяться, когда нужно поменять формулировку “везде”, когда появляется второй язык или хотя бы второй стиль. Если вы видите, что фраза повторяется или должна быть заменяемой — это уже кандидат в MessageSource.
Ошибка №2: пытаться использовать MessageSource как хранилище больших шаблонов.
Иногда хочется “всё текстовое сложить в одно место”, и это приводит к тому, что в messages.properties начинают жить многострочные письма, большие куски отчётов и вообще всё подряд. Это неудобно поддерживать, неудобно читать, и вы теряете преимущества Resource. Большие шаблоны остаются в Resource‑файлах, а MessageSource держим для коротких сообщений и подписей.
Ошибка №3: делать ключами полноценные фразы.
Ключ вида order created successfully выглядит “самодокументируемым”, пока вы не захотите изменить формулировку. После этого ключ перестаёт быть ключом и становится текстом, а значит ломает идею “меняем текст без изменения кода”. Лучше ключи в стиле order.created и нормальные значения в файлах сообщений.
Ошибка №4: собирать динамическую часть сообщения через конкатенацию, даже используя MessageSource.
Иногда встречается гибрид: “текст возьмём из MessageSource, но параметры склеим сами”. Тогда вы снова получаете проблемы пробелов, порядка слов и локализации. Если сообщение зависит от значений — используйте параметры {0}, {1} и передавайте аргументы массивом.
Ошибка №5: путать назначение MessageSource и Resource, пытаясь решить обе задачи одним инструментом.
Если держать в голове простое правило “файл — через Resource, фраза — через MessageSource”, жизнь становится заметно спокойнее. Как только вы начинаете читать “ключи” как пути к файлам или хранить “файлы” как значения ключей — вы сами себе усложняете поддержку и следующему разработчику добавляете “квест на понимание”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ