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}, ...
// "Замовлення створено" — резервний текст на випадок відсутності ключа.
// locale — вибір мови або локалі.
return messages.getMessage(
"order.created",
new Object[]{"ORD-1"},
"Замовлення створено",
locale
);
}
}
Тут важливо вловити ідею, а не запам’ятати параметри напам’ять. Ми передаємо «код повідомлення» (order.created), параметри для підстановки (ORD-1), запасний текст на випадок, якщо ключ не знайдено ("Замовлення створено"), і 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},
"Замовлення створено",
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},
"Замовлення створено",
locale
);
}
}
Зараз ми не обговорюємо, де лежить текст order.created.for-customer і як улаштовані файли бандлів — це буде наступна лекція. Нам важлива ідея: код передає значення, а текст вирішує, як їх вставити.
Такий підхід не лише про локалізацію. Він ще й про підтримку. Текст можна змінити без зміни Java-класу. І, що особливо приємно, можна зробити різні формулювання для різних мов без «розкладання речення по шматках» у коді.
6. Де це потрібно в ContextFlow
Наразі наш проєкт — не вебзастосунок, а застосунок, який запускають сценарії, і він пише щось у консоль, у файли та в аудит. І в новачка часто виникає думка: «Локалізація — це ж для веба. У нас немає 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 не повинен знати про ресурси і *.properties — лише про MessageSource.
this.messages = messages;
}
public void printCreated(String orderId, Locale locale) {
// Беремо текст за ключем + аргументи + локаль.
String text = messages.getMessage(
"order.created",
new Object[]{orderId},
"Замовлення створено",
locale
);
// Далі використовуємо як звичайний рядок: консоль, лог, файл тощо.
System.out.println(text); // наприклад: "Замовлення ORD-1 створено"
}
}
Зверніть увагу на важливий момент: цей клас не читає файли. Він не знає про messages_uk.properties. Він не знає про classpath:. Він знає лише: «у мене є MessageSource — і я прошу в нього повідомлення».
Це саме те, до чого ми прагнемо: деталі зберігання — в інфраструктурі, використання — у сервісі.
7. Що буде далі
Дуже хочеться одразу запитати: «Гаразд, а де саме лежать ці тексти? Як Spring їх знаходить?» Це правильне запитання, але сьогодні ми тримаємо фокус на мотивації та межі між Resource і MessageSource. У наступних лекціях ми акуратно зберемо повну картину: які файли створюємо, як улаштований Locale, що таке fallback і як зареєструвати MessageSource в контексті так, щоб він став звичайною залежністю, як OrderStore або DiscountPolicy.
Поки достатньо запам’ятати просту схему, як житиме «словник повідомлень» у нашому застосунку:
flowchart LR
%% Service / Presenter запитує повідомлення за ключем, а MessageSource повертає готову фразу.
A["Сервіс / Presenter"] -->|просить key + args + locale| B["MessageSource"]
B --> C["Пакети повідомлень у 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», життя стає помітно спокійнішим. Щойно ви починаєте читати «ключі» як шляхи до файлів або зберігати «файли» як значення ключів — ви самі собі ускладнюєте підтримку і наступному розробнику додаєте «квест на розуміння».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ