JavaRush /Курси /Kotlin SELF /Збирання JAR: артефакт, точка входу та запуск поза IDE

Збирання JAR: артефакт, точка входу та запуск поза IDE

Kotlin SELF
Рівень 17 , Лекція 4
Відкрита

1. Що таке артефакт і чому JAR — це «пакунок» із вашою програмою

Коли ви пишете код, легко думати: «програма — це мої файли .kt». Але щойно ви натискаєте Run або запускаєте збирання, відбувається інженерна магія: вихідні тексти перетворюються на результат збирання. Його можна передати іншій людині, серверу або самому собі — скажімо, за тиждень, коли ви вже забудете, де була кнопка Run.

Оцей результат збирання й називають артефактом. На JVM найзвичніший артефакт — JAR-файл. Його зручно уявляти як пакунок: усередині лежать скомпільовані .class файли (а інколи й ресурси), а також службові метадані. Ідея проста: ви не носите із собою вихідні тексти й компілятор — ви віддаєте готовий «запакований результат».

Щоб не плутатися, тримайте в голові просту таблицю:

Термін Простими словами Приклад
Вихідні тексти Те, що ви пишете
src/main/kotlin/app/Main.kt
Збирання Процес перетворення вихідних текстів на результат
./gradlew build
Артефакт Те, що вийшло після збирання
build/libs/app-1.0.0.jar
JAR Формат артефакту для JVM Один файл .jar

І важлива думка: IDE часто приховує деталі, а JAR змушує побачити світ таким, яким він є. Іноді це дуже корисно. Іноді — несподівано боляче, але теж корисно.

2. Як Kotlin перетворюється на те, що JVM уміє запускати

Зараз буде трохи «закулісся» — рівно стільки, щоб розуміти JAR і точку входу. Без занурення в темні хащі байткоду та JVM-шаманізму. Kotlin/JVM компілюється у Java-сумісні .class файли. І саме ці .class файли JVM насправді й запускає.

Особливість Kotlin, яку важливо знати саме сьогодні: функції верхнього рівня (тобто функції поза класами) усе одно мають десь «приземлитися» у світі JVM, де все крутиться навколо класів. Тому Kotlin складає такі функції в спеціальний клас, імʼя якого зазвичай утворюється з назви файла.

Наприклад, якщо у вас є файл Main.kt і в ньому написано:


package app

fun main() {
    println("Привіт із JAR") // Привіт із JAR
}

То на рівні JVM це виглядатиме так, ніби існує клас app.MainKt із методом main. З погляду Kotlin це майже непомітно — доки ви не починаєте налаштовувати збирання та запуск JAR. Там уже треба вказати: «а який саме main запускати?».

Запамʼятайте як робоче правило (без спроби вивчити все назавжди): якщо ваш main лежить у файлі Main.kt у пакеті app, то імʼя точки входу для JVM зазвичай буде app.MainKt.

3. Точка входу: як JAR розуміє, який main запускати

Коли ви запускаєте програму з IDE, вона зазвичай сама розуміє, який main ви вибрали (бо ви натиснули зелену кнопку саме поруч із ним). Але коли ви запускаєте JAR поза IDE, «зеленої кнопки» немає. Тож JVM потрібна інструкція: який клас вважати стартовим.

Є два найпростіші способи запуску:

  1. java -cp ... <MainClass> — ви явно кажете JVM, який клас стартовий.
  2. java -jar app.jar — ви кажете: «усередині JAR уже записано, що стартувати».

Другий спосіб працює лише тоді, коли JAR запускний (runnable). А запускним його робить наявність файла маніфеста із записом про головний клас. Усередині JAR є службовий файл:

META-INF/MANIFEST.MF

І в ньому може бути рядок (спрощено):

Main-Class: app.MainKt

Можна уявити це як наліпку на коробці: «Відкривати звідси».

Намалюємо просту схему, щоб не було відчуття, ніби це все «магія»:

flowchart TD
    A[Ваші .kt файли] --> B[Компіляція Kotlin/JVM]
    B --> C[.class файли]
    C --> D[Пакування JAR]
    D --> E["JAR + META-INF/MANIFEST.MF"]
    E --> F["java -jar ... запускає Main-Class"]

Міні-налаштування в Gradle: де вказують main class

У межах цієї лекції ми не заглиблюємося в те, як улаштований Gradle усередині. Але корисно побачити, як це виглядає в build.gradle.kts, щоб не лякатися.

Найзрозуміліший «маркер наміру» — підʼєднати плагін application і вказати mainClass. Приклад (фрагмент):

plugins {
    kotlin("jvm") version "2.3.0"
    application
}

application {
    mainClass.set("app.MainKt")
}

Це читається майже як людська мова: «це застосунок, а точка входу — така-то». Під капотом Gradle допоможе і із запуском, і з формуванням метаданих.

Якщо у вашому проєкті кілька main, то саме тут ви «призначаєте головного». І це одразу зменшує хаос: ви більше не вгадуєте, який main запускався вчора.

4. Де шукати JAR після збирання

Коли ви лише вчитеся, здається, що програма живе в src. Коли стаєте трохи досвідченішими, розумієте: програма живе в артефакті, а src — це рецепт. Gradle за замовчуванням складає результати збирання в каталог build, а JAR найчастіше лежить у:

build/libs/

Саме туди варто зазирати, якщо ви хочете побачити те, що справді запускається поза IDE.

Щоб не перетворювати цей розділ на збірник заклинань, тримайте одну таблицю: «що роблю — що отримую».

Дія Що робить Gradle Що ви очікуєте побачити
./gradlew build
Збирає проєкт повністю (разом із перевірками, якщо їх налаштовано) У каталозі
build/
зʼявляються результати
./gradlew jar
Збирає саме JAR-пакування У
build/libs/
зʼявляється
.jar
./gradlew run
(якщо ввімкнено
application
)
Запускає застосунок через Gradle Вивід у консолі — як під час звичайного запуску

На Windows замість ./gradlew зазвичай використовують gradlew.bat, але сенс той самий: ви просите Gradle виконати дії, які IDE раніше робила «потайки».

5. Запуск поза IDE: чому args, env і робочий каталог важливі

На цьому етапі в багатьох трапляється перша «доросла» зустріч із реальністю: «в IDE працює, а в терміналі — ні». І це не тому, що Kotlin образився. Просто IDE під час запуску підставляє купу налаштувань: робочий каталог, аргументи, змінні середовища, інколи навіть кодування консолі. Поза IDE ви отримуєте «чистий запуск», де все залежить від того, як саме ви виконали команду.

Щоб зробити це відчуття максимально конкретним, продовжимо наш навчальний консольний застосунок. Нехай він називається CliBox (коробка з командами — звучить як щось, що потім можна продати, якщо додати туди слово «AI»).

Діагностика оточення: друкуємо args, env і working directory

Ми зробимо маленьку функцію, яку можна викликати на початку main. Вона не робить нічого «розумного» — лише виводить факти. Але саме такі функції економлять години налагодження.

package app

fun printRunDiagnostics(args: Array<String>) {
    val dir = System.getProperty("user.dir")
    val mode = System.getenv("APP_MODE") ?: "не задано"

    println("Кількість аргументів = ${args.size}") // Кількість аргументів = ...
    println("APP_MODE = $mode")                    // APP_MODE = ...
    println("Робочий каталог = $dir")              // Робочий каталог = ...
}

Зверніть увагу: System.getenv("APP_MODE") повертає nullable, і ми одразу застосовуємо ?:. Це саме та навичка, яка вам уже знадобилася в темі про null-safety.

Тепер підʼєднаємо це в main:

package app

fun main(args: Array<String>) {
    printRunDiagnostics(args)
    println("CliBox запущено") // CliBox запущено
}

Аргументи командного рядка: не припускайте, що вони є

Дуже типова помилка під час запуску поза IDE — очікувати, що аргументи «якось зʼявляться». Але якщо ви їх не передали, то args.size == 0, і звернення args[0] зламається.

Зробімо команду hello, яка бере імʼя з args:

package app

fun main(args: Array<String>) {
    val name = if (args.size >= 2) args[1] else "незнайомець"
    val cmd = if (args.isNotEmpty()) args[0] else "help"

    if (cmd == "hello") println("Привіт, $name!") else println("Використання: hello <name>")
    // Привіт, Alice!  (приклад)
}

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

6. JAR у реальному житті: бібліотека, застосунок і «прогін вручну»

Важливо не потрапити в пастку: «раз JAR, значить запускається». JAR — це контейнер, і він може бути як бібліотекою, так і застосунком. У бібліотеки може не бути точки входу взагалі: її підʼєднують до іншого проєкту як залежність, а запускати «саму по собі» не передбачається.

У застосунку, навпаки, є сенс «запустити й отримати поведінку». Тоді потрібен main і зазвичай потрібен Main-Class у маніфесті (якщо ви хочете запускати через java -jar).

Іноді корисно знати запасний варіант запуску без runnable-маніфеста. Він виглядає так:

java -cp app.jar app.MainKt

Це читається так: «поклади app.jar у classpath і запусти клас app.MainKt». Але початківцю краще прагнути до зрозумілого runnable-сценарію, щоб запуск був коротким і передбачуваним.

Міні-прогін: від збирання до запуску

Зараз зберемо в голові повний маршрут. Уявіть, що ви хочете надіслати свій застосунок другові (або на сервер), а IDE там немає. Тоді зазвичай є дві частини: зібрати артефакт і запустити його.

Збирання в терміналі в корені проєкту (там, де лежить build.gradle.kts):

./gradlew build

Після цього перевіряєте каталог:

ls build/libs

І знаходите JAR (імʼя залежить від version і назви проєкту), наприклад:

app-1.0.0.jar

Запуск:

java -jar build/libs/app-1.0.0.jar hello Alice

І очікуєте побачити щось на кшталт:

Привіт, Alice!

Якщо у вашому main ще ввімкнена діагностика, ви побачите і аргументи, і робочий каталог, і значення APP_MODE.

Змінні середовища: «підкрутити поведінку» без переписування коду

У реальності програми часто отримують налаштування через env-змінні, бо це зручно для запуску в різних оточеннях. Навіть для навчального CliBox можна додати логіку: якщо APP_MODE=dev, то друкувати додатковий текст.

package app

fun isDevMode(): Boolean {
    val mode = System.getenv("APP_MODE") ?: return false
    return mode.lowercase() == "dev"
}

І використовуємо:

package app

fun main(args: Array<String>) {
    if (isDevMode()) println("Режим dev увімкнено") // Режим dev увімкнено
    println("CliBox запущено")                      // CliBox запущено
}

Тоді під час запуску (приблизно; синтаксис залежить від оболонки):

APP_MODE=dev java -jar build/libs/app-1.0.0.jar

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

Чому «в IDE працює, а в JAR ні»: як думати, щоб не панікувати

Коли програма ламається поза IDE, мозок зазвичай пропонує два шляхи: «я все зламав» і «компʼютер зламався». Є спокійніший третій шлях: порівняти умови запуску.

В IDE у вас могли бути налаштовані аргументи, які ви забули передати в java -jar. Також міг бути виставлений робочий каталог на корінь проєкту, а в терміналі ви запустили JAR з іншого місця. Або ж у конфігурації Run могла бути задана змінна середовища — а в звичайній консолі її немає.

Тому корисна звичка: перед тим як лагодити код, виведіть факти. Саме для цього ми й писали printRunDiagnostics. Це маленький «ліхтарик»: він не виправляє проблему, зате допомагає не блукати в темряві з криком «де я?».

7. Типові помилки під час збирання та запуску JAR

Помилка №1: думати, що «будь-який JAR запускається командою java -jar».
Формат JAR один, але вміст може бути різним. Якщо JAR — бібліотека, у нього може не бути точки входу, і це нормально. Навіть якщо у вас є main у вихідних текстах, JAR може бути зібраний без потрібного Main-Class у маніфесті — і тоді java -jar не зрозуміє, що запускати. У таких випадках або налаштовують runnable JAR, або запускають через -cp із зазначенням main-класу.

Помилка №2: кілька main у проєкті й «вгадування», який запускається.
На ранніх етапах навчання легко створити другий main «для експерименту», потім третій «для тесту», а потім дивуватися, чому Gradle запускає не те. IDE тут зазвичай рятує: ви запускаєте конкретний main. А от збирання JAR потребує однозначності. Краще заздалегідь мати один головний main для застосунку й не перетворювати проєкт на парк атракціонів.

Помилка №3: звернення до args[0] без перевірки розміру.
В IDE ви могли завжди запускати програму з аргументами (бо один раз налаштували Run Configuration), а в терміналі забули їх передати. Підсумок — помилка виходу за межі масиву. Лікується простою дисципліною: спочатку перевіряємо args.isNotEmpty() або args.size, і лише потім читаємо за індексом.

Помилка №4: очікування, що env-змінна «точно задана».
System.getenv("X") повертає String?, і це чесний контракт: змінної може не бути. В IDE ви могли задати її в налаштуваннях запуску, а в терміналі вона не задана. Якщо код не готовий до null, ви отримаєте або неправильну поведінку, або аварійне завершення, або «дивні значення за замовчуванням». Elvis-оператор ?: і акуратні перевірки тут — ваші друзі.

Помилка №5: нерозуміння робочого каталогу й відносних шляхів (навіть якщо ви поки не працюєте з файлами).
Навіть якщо ви ще не читаєте файли, робочий каталог впливає на багато що: де ви запускаєте процес, які відносні шляхи будуть «рахуватися», звідки ви очікуєте ресурси. В IDE робочий каталог часто автоматично виставлений у корінь проєкту, а в терміналі ви можете бути в зовсім іншому місці. Тому вивід System.getProperty("user.dir") — несподівано корисна діагностика.

Помилка №6: намагатися «лагодити» Gradle, не розуміючи, що ви лагодите конфігурацію, а не застосунок.
Коли збирання не вдається, новачки інколи починають додавати рядки в build.gradle.kts майже як у чарівний сувій. Стратегія, що працює краще, — простіша: памʼятайте, що build.gradle.kts — це конфігурація збирання. Правте її маленькими кроками, стежте за дужками й лапками. А після змін оновлюйте модель проєкту (sync) — інакше IDE й Gradle можуть жити «в різних реальностях».

1
Опитування
Організація Kotlin‑проєкту, рівень 17, лекція 4
Недоступний
Організація Kotlin‑проєкту
Організація Kotlin‑проєкту
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ