JavaRush /Курси /Spring Core /MessageSource проти ...

MessageSource проти Resource

Spring Core
Рівень 16 , Лекція 0
Відкрита

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», життя стає помітно спокійнішим. Щойно ви починаєте читати «ключі» як шляхи до файлів або зберігати «файли» як значення ключів — ви самі собі ускладнюєте підтримку і наступному розробнику додаєте «квест на розуміння».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ