JavaRush /Курси /Claude code /Карта типів міграцій

Карта типів міграцій

Claude code
Рівень 29 , Лекція 3
Відкрита

1. Слово «міграція» надто загальне

Коли ви вперше чуєте слово migration, дуже легко уявити щось однотипне: підняли версії, виправили кілька імпортів, прогнали тести, готово. Проблема в тому, що під одним словом ховаються зміни різної природи: одні ламають компіляцію, інші — поведінку в рантаймі, треті — лише збирання в CI. Тому перша доросла дія в migration execution — назвати тип міграції, а не оголосити: «ми оновлюємо проєкт».

До цього місця у нас був один pilot: migration/boot3-pilot на reports/* з вибраним scope, rollback і validation. Сьогодні я не розширюю його до всього сервісу, а розкладаю за типами змін: де ризик і які датчики на кожному рівні. Дані та конфігурацію сюди свідомо не включаю — там інший ризик і інший патерн безпеки.

На практиці корисно тримати перед очима ось таку карту:

Сімейство міграції Що насправді змінюється Де найчастіше ламається Перша корисна перевірка
single-library одна конкретна залежність, яку ви оновлюєте явно локальне збирання, інтеграційні тести точковий diff версії + прогін цільового набору тестів
dependency дерево залежностей цілком, включно з транзитивними конфлікти версій, несумісні плагіни, неочікувані класи в classpath порівняння lockfile або графа залежностей
framework правила платформи, API, автоконфігурація, домовленості компіляція, конфігурація, безпека, поведінка в рантаймі читання release notes + characterization tests
runtime версія мови та середовища виконання CI, контейнер, рефлексія, TLS, поведінка JVM matrix-прогін на старій і новій версії
build wrapper, build plugins, pipeline, образ збирання «локально працює, в CI падає» clean build у CI на цільовому середовищі

Ця таблиця здається майже нудною — а це добрий знак: інженерна карта й має виглядати трохи нудніше, ніж катастрофа. Нудна таблиця дешевша за бадьорий дзвінок о 23:40 із питанням «а чому після оновлення Java у нас відвалився звіт, хоча код ми не чіпали?».

Для CashFlow Dashboard це особливо корисно, тому що перший великий стрибок у курсі — не одна міграція, а гібрид: ми зачіпаємо framework, runtime і build одночасно, а поруч ворушаться залежності. Не розкладіть цю суміш на частини — і будь-який MIGRATION_PLAN.md перетвориться на літературний твір без шансів на виконання.

2. Один цикл для всіх типів міграцій

Добра новина в тому, що типів міграцій багато, а базовий цикл у них один і той самий: він застосовується до різних класів змін, кожного разу з уточненням, де ризик і яким evidence доводити коректність. Вигадувати п’ять процесів не потрібно.

flowchart LR
    A[Журнал змін / release notes] --> B[Pilot-зріз]
    B --> C[Тести й перевірки]
    C --> D[Малий diff]
    D --> E[Review і рішення]

Ключове тут — спочатку читати release notes, а не відразу редагувати build.gradle з фразою «ну зараз усе само підкаже». Claude Code у цій фазі корисний, але в дуже конкретній ролі: читати документацію, зіставляти breaking changes із файлами pilot-зрізу, підказувати, де допоможе компілятор, а де врятують тести або smoke checks. Хороший запит виглядає так:

Вивчи release notes Spring Boot 3.x і наш pilot-зріз `reports/*`.
Код не змінюй.
Поверни:
1) які breaking changes стосуються цього зрізу;
2) що зловить компілятор;
3) що зловлять лише тести або runtime;
4) з якого файла безпечніше почати pilot.

Зверніть увагу на тон запиту: не «мігруй модуль», а «прочитай, зістав, поясни, де ризик». Це verification-first: модель допомагає побачити карту, а не кидається перебудовувати місто.

3. Міграції single-library і dependency

Найобманливіший тип міграції — той, що виглядає маленьким. Здається, ви просто змінюєте версію однієї бібліотеки, але насправді вона рідко приїжджає одна: тягне транзитивні залежності, обмеження сумісності, новий набір класів. Пакет оновили один, а розмовляти почали п’ятеро.

Для початківця зручно розділити два близькі поняття. Single-library migration — ви явно змінюєте одну залежність: версію SDK, драйвера, клієнта, starter-а (не плутайте зі зміною package або namespace). Dependency migration — коли через це змінюється все дерево. У Java це помітно через BOM, плагіни та бібліотеки, пов’язані через classpath; у JavaScript аналог — peer dependencies.

Ось чому lockfile — не бюрократія, а доказ: показує, що насправді змінилося, а не що ви думали, що змінили.

./gradlew dependencies > build/deps-before.txt
./gradlew --write-locks
./gradlew clean test --tests 'reports.*'
git diff gradle.lockfile          # дивимося, хто насправді змінився
./gradlew dependencyInsight --dependency jackson-databind

Цей фрагмент здається сухим, але він дуже практичний, і це значно краще, ніж оновити п’ять пакетів разом, а потім обговорювати, чия версія вирішила зіпсувати вам ранок.

У CashFlow Dashboard такий підхід особливо важливий поруч із фінансовою логікою. Торкнетеся бібліотеки в payments або billing — ціна помилки висока, тому перший pilot-зріз не починається там. Беріть reports або іншу ділянку лише для читання: від складних зон ви не відмовляєтеся назавжди, просто не робите з них полігон для перших падінь.

І ще один важливий момент — у single-library/dependency migration дуже поширений антипатерн: «раз уже залізли, оновімо заодно все застаріле». Звучить господарно, але руйнує довідність. Оновили вісімнадцять залежностей і два дні шукаєте, чий сюрприз прийшов у збирання, — це не міграція, а археологія після вибуху.

4. Framework migration: змінюються правила платформи

Із framework migration усе хитріше: тут ви змінюєте не версію залежності, а правила гри всієї платформи — домовленості, автоконфігурацію, життєвий цикл компонентів, інтеграцію з безпекою та валідацією. Тому major-оновлення майже завжди болючіше, ніж виглядає в diff.

У нашому проєкті це видно наочно: Spring Boot 2.7 → Boot 3.x — класична framework migration. У ній є dependency- і runtime-шар, але головна проблема — змінилися базові контракти платформи. Найпомітніший і мемний приклад — перехід javax.* → jakarta.*.

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.validation.Valid;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.validation.Valid;

Компілятор таке ловить охоче, і це навіть приємно — частина роботи робиться голосно й чесно. Але framework migration не закінчується на імпортах. Куди неприємніше те, що не падає під час компіляції: правила Spring Security, actuator, спостережуваність, автоконфігурація. Корисно дивитися на framework migration так — хто що ловить:

Що змінюється Хто зазвичай ловить проблему
javaxjakarta в коді компілятор
зміни в security-конфігурації інтеграційні тести
actuator / observability / автоконфігурація runtime і CI
поведінка web-шару та серіалізації API tests і smoke checks

Саме тому release notes для framework migration не факультативні: їх не читають лише ті, хто любить дізнаватися про breaking changes із production-логів. Якщо у вас є characterization tests із попередніх тем, драматизму менше: ви не вірите, що «Boot 3 теж має працювати», а порівнюєте поведінку pilot-зрізу до і після. Це перетворює міграцію з ритуалу на інженерну процедуру.

У CashFlow Dashboard перший framework-pilot логічно починати не з billing, payments або auth, а зі спокійного звітного контролера, де ціна помилки нижча.

5. Runtime migration: той самий код, нова платформа

Runtime migration часто недооцінюють, тому що здається: якщо код компілюється на новій версії мови — начебто все добре. На жаль, середовище виконання значно підступніше. Ви змінюєте поведінку віртуальної машини, базові бібліотеки, TLS, рефлексію, контейнерний образ, іноді навіть те, як запускаються тести.

У курсовому проєкті Java 8 → Java 21 — не додаток до framework migration, а повноцінна runtime migration. Boot 3 вимагає новішу Java, тому лінії йдуть разом, але розділяти їх у голові корисно. Інакше будь-яка проблема виглядає як «Boot 3 щось зламав», хоча насправді CI просто збирає проєкт на старому JDK.

Найдешевший спосіб не сперечатися про середовище виконання — ганяти старий і новий runtime на одному наборі перевірок. Якщо код підтримує обидві версії, зручна matrix у CI; але для pilot-а Boot 2.7 / Java 8 → Boot 3.x / Java 21 коректніше порівнювати baseline branch на JDK 8 і migration branch на JDK 21 — Boot 3 не зобов’язаний запускатися на Java 8.

# Такий matrix-підхід підходить, коли один і той самий код підтримує обидві JDK.
strategy:
  matrix:
    java: [ '8', '21' ]
steps:
  - uses: actions/setup-java@v4
    with:
      java-version: ${{ matrix.java }}
  - run: ./gradlew test --tests 'reports.*'

Сенс цього фрагмента не в красі YAML, а в дисципліні: відтворювані перевірки на двох рантаймах покажуть, де поведінка розходиться. Якщо baseline стабільний, а migration branch сиплеться — копати в runtime-шарі, а не звинувачувати навмання весь framework upgrade.

У runtime migration є ще одна неприємна особливість: частина проблем не видна локально, коли середовище розробника відрізняється від CI. Звідси правило: не «на ноутбуці зелено», а «в пайплайні зелено». Лише це evidence — решта оптимізм.

6. Build migration: ламається спосіб збирання

Build migration — той тип змін, який найчастіше дратує сильніше за всі інші, тому що код може бути абсолютно нормальним, а проєкт усе одно не збирається. І найприкріше, що розробник у таких випадках любить говорити: «Але ж я бізнес-логіку не змінював». Саме в цьому й проблема: ламається не предметна область, а механіка доставки — wrapper, плагіни, pipeline, образ збирання, іноді кроки генерації коду.

Коли ви змінюєте build-шар, ви фактично відповідаєте на питання: «Чи зберемо ми тим самим способом проєкт завтра, у CI, на іншій машині?». Якщо відповідь залежить від фаз місяця — build migration ще не завершена.

Найпростіший build-pilot не має бути героїчним — його завдання довести відтворюваність:

- name: Перевірка середовища
  run: |
    java -version
    ./gradlew --version
- name: Повне збирання pilot-зрізу
  run: ./gradlew clean build

Цей шматок хороший тим, що відразу показує, яка Java і який Gradle wrapper реально в пайплайні. Якщо локально ви на Java 21, а в CI старий рантайм — краще побачити це в перших рядках логу, ніж після двадцяти хвилин падінь на незрозумілих плагінах.

Тут же ховається ще одна тонкість: build-пайплайн і згенерований конфіг — теж частина migration-картини. Оновили Boot і Java, а Docker-образ у CI тягне старе середовище; або wrapper новий, а plugin для генерації коду несумісний. «Працює локально, падає в CI» — build migration, яку просто не назвали своїм ім’ям.

Якщо сказати зовсім просто, build migration перевіряє не «чи вміє код жити», а «чи вміє команда знову і знову отримувати один артефакт із чистого стану». Для legacy це важливіше за рефакторинг: без відтворюваного збирання ви й відкат не зробите.

7. Карта типів міграцій для CashFlow Dashboard

Тепер зберемо все в одну картинку на нашому проєкті — це корисно не лише для розуміння, а й як майже готовий фрагмент MIGRATION_PLAN.md. Коли ви називаєте тип міграції за шарами, план одразу стає зрозумілішим: у кожного шару з’являється своя перевірка, свій pilot і свої винятки.

Шар у першому pilot CashFlow Dashboard Що змінюємо в reports-зрізі Чим доводимо коректність Що свідомо не чіпаємо
framework Spring Boot 2.7 → 3.x, javax → jakarta, частину web/autoconfig компіляція + інтеграційні тести звітів + smoke на endpoint billing, payments, auth
runtime Java 8 → 21 для того самого pilot-зрізу CI matrix, один і той самий набір тестів на двох JDK глобальні JVM-особливості всієї системи
build wrapper, версія Java у CI, кроки збирання clean build у CI та повторюваний лог середовища release pipeline цілком
dependency супутні бібліотеки, підтягнуті переходом lockfile diff + dependencyInsight + targeted tests великі оновлення зовнішніх SDK

Даних і конфігурації тут свідомо немає: щойно ви додаєте schema changes, backup і owner approval починають жити за іншими правилами, а pilot перестає бути тим самим дешевим Boot 3 зрізом.

Невеликий фрагмент плану в такому стилі може виглядати так:

## Карта pilot-зрізу
- framework: Spring Boot 2.7 → 3.x в `reports/*`
- runtime: Java 8 → 21 для того самого зрізу
- build: оновити wrapper і CI JDK
- виключено: `billing/*`, `payments/*`, schema changes

Це вже звучить як інженерний документ, а не бойовий клич. І в цьому, власне, весь сенс сьогоднішньої лекції. Перестаєте говорити «оновимо проєкт» і починаєте «робимо framework + runtime + build pilot на reports/* з окремим dependency review» — і міграція стає керованою. Не легкою, не безпомилковою — керованою. У legacy-коді це майже завжди найкраща новина дня.

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