Хтось говорив, що історія повторюється двічі: перший раз як трагедія, другий раз як фарс. Нещодавно я зіткнувся з першою ситуацією, коли повинен був встановити виконується Java-додаток клієнту. Я робив це багато разів і кожна установка була пов'язана з труднощами. Помилки можуть існувати всюди: при складанні всіх JAR-файлів програми, написанні сценаріїв запуску для DOS і Unix (і Cygwin), при перевірці коректності установки всіх змінних оточення на комп'ютері користувача. Якщо все проходить гладко, додаток запускається і виконується належним чином. Якщо щось йде не так, що зазвичай і відбувається, - то в результаті потрібно багатогодинна робота безпосередньо у замовника.
Після останніх переговорів з обуреним клієнтом з приводу виникнення виняткових ситуацій ClassNotFound я вирішив, що з мене вистачить. Я зібрався знайти спосіб упаковки мого програми в один JAR-файл і надати моїм клієнтам простий механізм (аналогічний java -jar) для його запуску.
Результатом став One-JAR - дуже просте програмне рішення для упаковки, яке використовує для користувача завантажувач класів Java для динамічного завантаження всіх класів з одного архіву, що зберігає одночасно структуру допоміжних JAR-файлів. У цій статті я опишу процес розробки One-JAR і потім розповім вам, як його використовувати для поширення ваших додатків у вигляді одного файлу.
Огляд One-JAR
Перед описом деталей One-JAR дозвольте мені спочатку розповісти про цілі, які я перед собою ставив. Я вирішив, що архів One-JAR повинен:
- Бути виконуваним з використанням механізму java -jar.
- Бути здатним містити всі необхідні додатком файли, тобто, і класи і ресурси в їх оригінальній формі (в НЕ розпакованому вигляді).
- Мати просту внутрішню структуру, яка може бути зібрана за допомогою тільки програми jar.
- Бути невидимим для оригінального програми, тобто, оригінальне додаток повинен упаковуватися в архів One-JAR без необхідності модифікації.
Проблеми і рішення
Найбільшою проблемою, з якою я зіткнувся в процесі розробки One-JAR, була проблема завантаження JAR-файлів, що містяться всередині іншого JAR-файлу. Завантажувач класів Java sun.misc.Launcher $ AppClassLoader. який працює при старті java -jar. вміє робити тільки дві речі:
- Завантажувати класи і ресурси, що знаходяться в корені JAR-файлу.
- Завантажувати класи і ресурси, що знаходяться в базах коду (codebase), на які посилається атрибут META-INF / MANIFEST.MF Class-Path.
Більш того, він свідомо ігнорує будь-які налаштування змінних оточення для CLASSPATH або аргумент командного рядка -cp. який ви вказуєте. Він також не знає, як завантажувати класи і ресурси з JAR-файлу, що міститься всередині іншого JAR-файлу.
Зрозуміло що я повинен був передбачити все це для досягнення моїх цілей в One-JAR.
Рішення 1: Поширення допоміжних JAR-файлів
Моєю першою спробою в створенні одного виконуваного JAR-файлу було зробити очевидне і розмістити допоміжні JAR-файли всередині призначеного для поширення JAR-файлу, який ми будемо називати main.jar. При наявності класу додатка з назвою com.main.Main і в припущенні, що він залежить від двох класів com.aA (всередині a.jar) і com.bB (всередині b.jar), файл One-JAR міг би виглядати наступним чином :
Інформація про те, що клас A.class спочатку знаходився в a.jar, а також про первісному місці розташування класу B.class втрачена. І хоча це здається дрібницею, можуть виникнути серйозні проблеми, про які я незабаром розповім.
One-JAR і FJEP
Нещодавно випущена програма під назвою FJEP (FatJar Eclipse Plugin) підтримує побудову плоских JAR-файлів безпосередньо всередині Eclipse. One-JAR був інтегрований з FatJar для підтримки впровадження JAR-файлів без їх розміщення. Більш детальну інформацію ви знайдете в розділі "Ресурси".
Розпакування допоміжних JAR-файлів в файлову систему для створення плоскої структури може займати досить багато часу. Це вимагає також роботи з програмами компонування, такими як Ant, для розпакування і повторної архівації допоміжних класів.
Крім цих маленьких непорозумінь я швидко зіткнувся з двома серйозними проблемами при розміщенні всередині архіву допоміжних JAR-файлів:
- Якщо a.jar і b.jar містять ресурс з одним і тим же pathname (скажімо, log4j.properties), який саме ви виберете?
- Що ви зробите, якщо ліцензія для b.jar чітко вимагає від вас поширення його в НЕ модифікованому вигляді? Ви не можете поширювати його в такому вигляді без порушення умов ліцензії.
Я відчув, що ці обмеження вимагають використання іншого підходу.
Рішення 2: MANIFEST Class-Path
Я вирішив дослідити механізм в засобі завантаження java -jar. який завантажує класи, зазначені всередині спеціального файлу в архіві під назвою META-INF / MANIFEST.MF. Вказуючи властивість Class-Path. я сподівався отримати здатність додавати інші архіви в оригінальний завантажувач класів. Ось як міг би виглядати такий файл One-JAR:
URLClassloader є базовим класом sun.misc.Launcher $ AppClassLoader. підтримує досить загадковий синтаксис URL, який дозволяє звертатися до ресурсів усередині JAR-файлу. Синтаксис виглядає приблизно так: jar: file: /fullpath/main.jar! /a.resource.
Теоретично, для отримання запису всередині JAR-файлу ви повинні використовувати щось схоже на jar: file: /fullpath/main.jar! /lib/a.jar! /a.resource. але, на жаль, це не працює. Оброблювач протоколу JAR-файлу розглядає тільки останній символ-роздільник "! /" В якості покажчика JAR-файлу.
Але цей синтаксис містить ключ до мого остаточного рішення "One-JAR".
Працювало це? У всякому разі, так здавалося до тих пір, поки я не перемістив файл main.jar в інше місце і не спробував його запустити. Для складання main.jar я створював підкаталог з назвою lib і поміщав у нього файли a.jar і b.jar. На жаль, завантажувач класів додатки просто вибирав допоміжні JAR-файли з файлової системи. Він не завантажував класи з впроваджених JAR-файлів.
поява JarClassLoader
На цьому етапі мене спіткало розчарування. Як я міг змусити додаток завантажувати свої класи з каталогу lib всередині свого власного JAR-файлу? Я вирішив, що повинен створити спеціальний завантажувач класів для підняття таких тягарів. Написання завантажувача класів - це не те завдання, яке можна сприймати несерйозно. Хоча вони не так вже й складні насправді, завантажувачі класів мають настільки глибокий вплив на керовані ними додатки, що стає важким діагностувати і інтерпретувати виникають відмови систем. Хоча докладний розгляд процесу завантаження класів виходить за рамки даної статті (див. Розділ "Ресурси"), я розповім про основні концепції, щоб бути впевненим в тому, що ви отримаєте максимальну кількість інформації з подальшого обговорення.
Завантаження класу
Коли JVM зустрічає об'єкт, чий клас невідомий, вона викликає завантажувач класів. Робота завантажувача класів полягає в пошуку байткод для класу (грунтуючись на його назві) і потім в наданні цих байткод в JVM, яка пов'язує їх з рештою системи і робить новий клас доступним виконувати коду. Ключовим класом в JDK є java.lang.Classloader і його метод loadClass. структура якого наводиться нижче:
Головною точкою входу в клас ClassLoader є метод loadClass (). Зауважте, що ClassLoader є абстрактним класом, але не оголошує жодного абстрактного методу. Це наштовхує на думку, що loadClass () є головним методом для розгляду. Насправді він таким не є: повертаючись до старих добрих днях загрузчиков класів JDK 1.1, loadClass () був єдиним місцем, де ви могли ефективно розширити завантажувач класів, але після JDK 1.2 його краще залишити в спокої і не заважати робити те, для чого він призначений, а саме:
- Перевірки того, завантажений чи вже клас.
- Перевірки того, чи може батьківський завантажувач класів завантажити клас.
- Виклику findClass (String name) для дозволу похідному завантажувачу класів завантажити клас.
Реалізація ClassLoader.findClass () повинна генерувати нову виняткову ситуацію ClassNotFoundException. будучи першим методом, на якому слід зупинити увагу при реалізації спеціалізованого завантажувача класів.
Коли JAR-файл не є JAR-файлом?
Наступним моїм дією була ітерація за всіма записами JAR-файлу мого програми і завантаження їх в пам'ять, як показано в лістингу 1:
Лістинг 1. Ітерація для пошуку упроваджених JAR-файлів
Зверніть увагу, що LIB_PREFIX встановлюється в рядок lib /. а MAIN_PREFIX - в рядок main /. Я хотів в циклі завантажувати в пам'ять для використання байткод всіх класів, що починаються з lib / або з main /. і ігнорувати будь-які інші елементи JAR-файлу.
Каталог main
Я говорив про роль підкаталогу lib /, але для чого потрібен цей каталог main /? Коротко: режим делегування функцій в завантажувач класів вимагає розміщення основного класу com.main.Main в своєму власному JAR-файлі, для того щоб він міг знайти бібліотечні класи (від яких залежить). Новий JAR-файл виглядав приблизно так:
У наведеному вище лістингу 1 метод loadByteCode () отримує потік з елемента JAR-файлу, назва елемента, завантажує байти елемента в пам'ять і надає їм до двох назв, залежно від того, представляє елемент клас або ресурс. Найкраще проілюструвати це на прикладі. Припустимо, що a.jar містить клас A.class і ресурс A.resource. Завантажувач класів One-JAR створює наступну структуру Map з назвою JarClassLoader.byteCode. що містить одну пару ключ-значення для класів і два ключа для ресурсу:
Малюнок 1. Внутрішня структура One-JAR
Якщо ви уважно подивитеся на малюнок 1, то побачите, що елементи класів позначаються в залежності від назв класу, а елементи ресурсів позначаються в залежності від пари імен: глобального імені і локального імені. Цей механізм використовується для вирішення конфлікту імен ресурсів: якщо два бібліотечних JAR-файлу визначають ресурс з однаковим глобальним ім'ям, будуть використовуватися локальні імена на основі фрейму стека викликає процедури. Для додаткової інформації дивіться розділ "Ресурси".
Пошук класів
Згадайте, що я залишив без уваги в огляді завантаження класів в методі findClass (). Метод findClass () отримує назву класу як тип String і повинен знайти і визначити байткод, які представляє ця назва. Оскільки loadByteCode люб'язно створює Map між назвою класу і його байткод, реалізувати це тепер дуже просто: потрібно знайти байткод, грунтуючись на назві класу, і викликати defineClass (). як показано в лістингу 2:
Лістинг 2. Фрагмент findClass ()
Завантаження ресурсів
Під час розробки One-JAR findClass був першою річчю, яка почала працювати як доказ вірності концепції. Але коли я почав поширювати більш складні додатки, я зрозумів, що повинен мати справу з завантаженням ресурсів, так само як і класів. Ось там робота почала пробуксовувати. Визначаючи в ClassLoader відповідний для перевизначення метод для задачі пошуку ресурсів, я вибрав один, з яким найбільше був знайомий. Подивіться лістинг 3:
Лістинг 3. Метод getResourceAsStream ()
Дзвіночок в цьому місці повинен був прозвенеть обов'язково: я просто не міг зрозуміти, чому для пошуку ресурсів використовувався URL. Отже, я проігнорував цю реалізацію і вставив свою власну, показану в лістингу 4:
Лістинг 4. Реалізація методу getResourceAsStream () в One-JAR
Остання перешкода
Моя нова реалізація методу getResourceAsStream (). здавалося, робила все як треба, до тих пір поки я не спробував в One-JAR додаток, який завантажує ресурс за допомогою URL url = object.getClass (). getClassLoader (). getResource (); тут додаток зазнало невдачі. Чому? Тому що повертається реалізацією за замовчуванням ClassLoader URL. дорівнював null, що ламало код викликає процедури.
З цього моменту все заплуталося. Я повинен був розгадати, який URL повинен використовуватися для посилання на ресурс всередині JAR-файлу в каталозі / lib. Чи повинен він бути, наприклад, jar: file: main.jar! Lib / a.jar! Com.a.A.resource?
Я спробував все комбінації, які тільки міг уявити, - жодна з них не працювала. Синтаксис jar: просто не підтримує вкладені JAR-файли, що поставило мене перед явно безвихідною ситуацією в моєму підході до One-JAR. Хоча більшість додатків, мабуть, не використовують ClassLoader.getResource. деякі виразно це роблять, і я зовсім не був ощасливлений винятком, що говорять "Якщо ваш додаток використовує ClassLoader.getResource () ви не можете застосовувати One-JAR".
І, нарешті, рішення.
Намагаючись розгадати синтаксис jar. я спіткнувся об механізм, який використовує Java Runtime Environment для відображення URL-префіксів в обробники. Це був ключ, в якому я потребував для вирішення проблеми в findResource. я просто винайшов свій власний префікс протоколу з назвою onejar. Потім я зміг би відобразити новий префікс в обробник протоколу, який повернув би потік байтів для ресурсу, як показано в лістингу 5. Зверніть увагу на те, що лістинг 5 представляє код з двох файлів, JarClassLoader і нового файлу з назвою com / simontuffs / onejar /Handler.java.
Лістинг 5. findResource і протокол onejar:
Самозавантаження JarClassLoader
В даний момент у вас, напевно, залишився єдиний питання: як вставити JarClassLoader в послідовність завантаження так, щоб він міг почати завантажувати класи з файлу One-JAR першим? Детальне пояснення виходить за рамки даної статті; але, суть в наступному. Замість використання основного класу com.main.Main як атрибут META-INF / MANIFEST.MF / Main-Class я створив новий клас початкової завантаження com.simontuffs.onejar.Boot. який вказується як атрибут Main-Class. Новий клас робить наступне:
- Створює новий JarClassLoader.
- Використовує новий завантажувач для завантаження com.main.Main з main / main.jar (грунтуючись на елементі META-INF / MANIFEST.MF Main-Class в main.jar).
- Викликає com.main.Main.main (String []) (або з іншою назвою Main-Class. Зазначеним у файлі main.jar / MANIFEST.MF), завантажуючи клас і використовуючи відображення для виклику main (). Зазначені в командному рядку One-JAR аргументи передаються методу main додатки без змін.
На закінчення
Якщо у вас від всього цього закрутилася голова, не переживайте: використовувати One-JAR набагато легше, ніж намагатися зрозуміти, як він працює. З появою модуля FatJar Eclipse Plugin (див. FJEP в розділі "Ресурси"), користувачі Eclipse можуть створювати One-JAR-додаток, зазначивши прапорець в майстра. Зовсім бібліотеки поміщаються в каталог / lib, основна програма і класи поміщаються в main / main.jar, а файл META-INF / MANIFEST.MF записується автоматично. Якщо ви використовуєте JarPlug (знову дивіться розділ "Ресурси") ви можете подивитися всередину створеного JAR-файлу і запустити його з IDE.
Загалом, One-JAR являє собою просте, але потужне рішення проблеми пакетування програм для поширення. Однак він не підходить для всіх можливих сценаріїв. Наприклад, якщо ваше додаток використовує завантажувач класів старого стилю JDK 1.1, яка не делегує управління своєму батькові, такий завантажувач класів зазнає невдачі при пошуку класів у вкладеному JAR-файлі. Ви можете подолати цю перешкоду, створивши і розгорнувши "обгортковий" (wrapping) завантажувач класів для зміни поведінки такого упорствующего завантажувача, хоча це спричинить за собою використання техніки маніпуляції байткод за допомогою таких програмних засобів як Javassist або Byte Code Engineering Library (BCEL).
Ви можете також зіткнутися з проблемами при застосуванні деяких спеціалізованих типів загрузчиков класів, використовуваних вбудовуваними додатками і Web-серверами. Зокрема, ви можете мати проблеми з завантажувачами класів, не відразу звертаються до батьківського завантажувачу, або тими, які шукають бази коду в файлової системі. У цьому випадку повинен допомогти включений в One-JAR механізм, при використанні якого можна розширити елементи JAR-файлу на файлову систему. Цей механізм управляється атрибутом One-JAR-Expand файлу META-INF / MANIFEST.MF. В якості альтернативи ви могли б спробувати використовувати маніпуляцію байткод для модифікації завантажувача класів на льоту, під час роботи, без порушення цілісності допоміжних JAR-файлів. Якщо ви йдете цим шляхом, для кожного індивідуального випадку, можливо, буде потрібно спеціалізований "обгортковий" завантажувач класів.
У розділі "Ресурси" наведені посилання для завантаження модулів FatJar Eclipse Plugin і JarPlug, а також додаткову інформацію про One-JAR.