Aplicând un borcan pentru a simplifica răspândirea aplicației

Cineva a spus că istoria se repetă de două ori: prima dată ca o tragedie, a doua oară ca o farsă. Am întâlnit prima dată prima situație, când trebuia să instalez aplicația Java executabilă pentru client. Am făcut acest lucru de multe ori și fiecare instalare a fost dificilă. Erori pot exista peste tot: atunci când se construiesc toate fișierele JAR de aplicație, se scriu scripturi de pornire pentru DOS și Unix (și Cygwin), atunci când se verifică corectitudinea setării tuturor variabilelor de mediu pe computerul utilizatorului. Dacă totul merge bine, aplicația rulează și execută corect. Dacă se întâmplă ceva rău, care se întâmplă de obicei, atunci, ca rezultat, este nevoie de o mulțime de ore pentru a lucra direct de la client.







După ultimele negocieri cu clientul indignat despre apariția excepțiilor de la ClassNotFound, am decis că am avut destule. Vroiam să găsesc o modalitate de a-mi împacheta aplicația într-un singur fișier JAR și să-i ofer clientului meu un mecanism simplu (similar cu java -jar) pentru lansarea acestuia.

Rezultatul a fost One-JAR, o soluție software de ambalare foarte simplă, care utilizează un încărcător de clasă Java personalizat pentru a încărca dinamic toate clasele dintr-o arhivă, care păstrează simultan structura fișierelor JAR auxiliare. În acest articol, voi descrie procesul de dezvoltare a One-JAR și apoi vă voi spune cum să îl utilizați pentru a distribui aplicațiile dvs. ca un singur fișier.

Revizuirea programului One-JAR

Înainte de a descrie detaliile despre One-JAR, permiteți-mi să vorbesc mai întâi despre obiectivele pe care le-am stabilit pentru mine. Am decis că arhiva One-JAR ar trebui:

  • Fii executabil folosind mecanismul java -jar.
  • Fiți capabili să conțină toate fișierele necesare, adică clasele și resursele în forma lor originală (neambalate).
  • Au o structură internă simplă, care poate fi asamblată folosind doar programul de jar.
  • Fi invizibil pentru aplicația originală, adică aplicația originală trebuie să fie împachetată în arhiva One-JAR fără a fi necesară modificarea.

Probleme și soluții

Cea mai mare problemă cu care am confruntat în timpul dezvoltării One-JAR a fost problema încărcării fișierelor JAR conținute într-un alt fișier JAR. Încărcătorul de clasă Java sun.misc.Launcher $ AppClassLoader. care funcționează atunci când porniți java -jar. știe să facă doar două lucruri:

  • Încărcați clase și resurse la rădăcina fișierului JAR.
  • Încărcați clasele și resursele în baza de date codebase la care se face referire prin atributul META-INF / MANIFEST.MF Class-Path.

În plus, ignoră conștient orice setări de variabile de mediu pentru CLASSPATH sau argumentul -cp de linie de comandă. pe care le specificați. De asemenea, el nu știe cum să încarce clase și resurse dintr-un fișier JAR conținut într-un alt fișier JAR.

Este clar că trebuia să prezic toate acestea pentru a-mi atinge obiectivele în One-JAR.

Soluția 1: Distribuiți fișierele JAR auxiliare

Prima mea încercare de a crea un singur executabil JAR-fișier ar face evidente si auxiliare JAR-fișierele din interiorul livrabil JAR-dosar, pe care o vom numi main.jar. Dacă există o clasă numită aplicații com.main.Main și pe presupunerea că aceasta depinde de două clase com.aA (în interiorul a.jar) și com.bB (în interiorul b.jar), un fișier JAR-ar arata ca aceasta :

Informația pe care clasa A.class a fost inițial în a.jar, precum și locația originală a clasei B.class se pierde. Și, deși acest lucru pare a fi o mică problemă, pot apărea probleme serioase, pe care le voi descrie în curând.

One-JAR și FJEP

Un program lansat recent, denumit FJEP (FatJar Eclipse Plugin), sprijină construirea de fișiere JAR plate în interiorul Eclipse. One-JAR a fost integrat cu FatJar pentru a sprijini introducerea fișierelor JAR fără plasarea lor. Pentru mai multe informații, consultați secțiunea Resurse.

Despachetarea fișierelor JAR auxiliare în sistemul de fișiere pentru a crea o structură plană poate dura destul de mult timp. Acest lucru necesită, de asemenea, lucrul cu programe de construcție, cum ar fi Ant, pentru despachetarea și rearmarea claselor auxiliare.

Pe lângă aceste mici neînțelegeri, am întâlnit repede două probleme grave când am plasat fișiere auxiliare JAR în arhivă:

  • Dacă a.jar și b.jar conțin o resursă cu același nume de cale (de exemplu, log4j.properties), pe care o alegeți?
  • Ce veți face dacă licența pentru b.jar vă cere în mod clar să o distribuiți într-o formă nemodificată? Nu puteți să le distribuiți în această formă fără a încălca termenii licenței.

Am simțit că aceste restricții necesită o abordare diferită.

Soluția 2: Cale de clasă MANIFEST

Am decis să investighez mecanismul în încărcătorul de java. care încarcă clasele specificate într-un fișier special din arhiva numită META-INF / MANIFEST.MF. Specificând proprietatea Clasă-Căi. Speram să obțin posibilitatea de a adăuga alte arhive la încărcătorul de clasă original. Iată cum ar putea arăta fișierul One-JAR:

URLClassloader este clasa de bază a sun.misc.Launcher $ AppClassLoader. care acceptă o sintaxă de adrese URL destul de misterioasă care vă permite să accesați resursele dintr-un fișier JAR. Sintaxa arată astfel: jar: file: /fullpath/main.jar! /a.resource.

Teoretic, pentru a obține o înregistrare în fișierul JAR, ar trebui să utilizați ceva similar cu jar: file: /fullpath/main.jar! /lib/a.jar! /a.resource. dar, din păcate, acest lucru nu funcționează. Programa de procesare a fișierelor JAR ia în considerare numai ultimul caracter delimitator "! /" Ca pointer al fișierului JAR.

Dar această sintaxă conține cheia pentru decizia mea finală "One-JAR".

A funcționat? Oricum, mi sa părut atâta timp cât nu mișc fișierul principal.jar într-o altă locație și am încercat să o pornesc. Pentru a construi main.jar, am creat un subdirector numit lib și a pus fișierele a.jar și b.jar în el. Din păcate, încărcătorul de clasă de aplicații a ales pur și simplu fișiere JAR auxiliare din sistemul de fișiere. Nu a încărcat clase din fișiere JAR încorporate.

Apariția lui JarClassLoader

În această etapă, am fost dezamăgit. Cum aș putea obține aplicația pentru a încărca clasele din directorul lib în propriul fișier JAR? Am decis să creez un încărcător de clasă specială pentru a ridica astfel de greutăți. Scrierea unui încărcător de clasă nu este o sarcină care poate fi luată ușor. Deși nu sunt cu adevărat complicate, încărcătoarele de clasă au un impact atât de profund asupra aplicațiilor pe care le gestionează, ceea ce face dificilă diagnosticarea și interpretarea eșecurilor sistemelor. Deși o discuție detaliată a procesului de încărcare a claselor este dincolo de scopul acestui articol (vezi Resurse), voi vorbi despre conceptele de bază pentru a vă asigura că obțineți cantitatea maximă de informații din discuția ulterioară.







Încărcarea unei clase

Atunci când un JVM întâlnește un obiect a cărui clasă este necunoscută, el cheamă încărcătorul de clasă. încărcător clasa de lucru este de a găsi bytecode pentru clasa (bazată pe numele său), iar apoi aceste octeți în JVM, care le conectează la restul sistemului și pune la dispoziție o nouă clasă de cod care rulează. Clasa cheie din JDK este java.lang.Classloader și metoda loadClass. a cărui structură este dată mai jos:

Punctul principal de intrare în clasa ClassLoader este metoda loadClass (). Rețineți că ClassLoader este o clasă abstractă, dar nu declară nicio metodă abstractă. Acest lucru sugerează că loadClass () este principala metodă de luat în considerare. De fapt, el nu este: merge înapoi la vremurile bune de JDK 1.1 classloaders, loadClass () a fost singurul loc în care se poate extinde în mod eficient o ClassLoader, dar din moment ce JDK 1.2 este cel mai bine lăsat în pace și să nu interfereze cu a face ceva pentru care se intenționează, și anume:

  • Verifică dacă clasa este deja încărcată.
  • Verifică dacă încărcătorul clasa părinte poate încărca clasa.
  • Apelați FindClass (Nume șir) pentru a permite încărcătorului de clasă derivat să încarce clasa.

Implementarea ClassLoader.findClass () trebuie să genereze un nou ClassNotFoundException. fiind prima metodă de a opri atenția atunci când implementați un încărcător de clasă specializat.

Când este fișierul JAR nu un fișier JAR?

Următorul pas a fost să repet prin toate intrările din fișierul JAR al aplicației mele și să le introduc în memorie, după cum se arată în listare 1:

Listă 1. Iterare pentru căutarea fișierelor JAR încorporate

Rețineți că LIB_PREFIX este instalat în linia lib /. și MAIN_PREFIX - în linia principală /. Am vrut să încărc în memorie să utilizez bytecodes din toate clasele începând cu lib / sau cu main /. și ignorați orice alte elemente ale fișierului JAR.

Catalog principal

Vorbeam despre rolul lib / subdirector, dar de ce avem nevoie de acest director principal /? Pe scurt: modul de delegare a funcțiilor în încărcătoare de clasă necesită plasarea com.main.Main din clasa principală în propriul fișier JAR, astfel încât să găsească clasele de bibliotecă (de care depinde). Noul fișier JAR a arătat ceva de genul:

În Listarea 1 de mai sus loadByteCode () metoda primește fluxul de numele elementului JAR-fișier, elementul încarcă octeți în memorie și le atribuie două nume, în funcție de faptul dacă elementul este o clasă sau o resursă. Cel mai bun mod de a ilustra acest lucru este cu un exemplu. Să presupunem că a.jar conține clasa A.class și resursa A.resource. Încărcătorul de clasă One-JAR creează următoarea structură a hărții cu numele JarClassLoader.byteCode. care conține o pereche cheie-valoare pentru clase și două chei pentru resursă:

Figura 1. Structura internă a One-JAR

Dacă te uiți atent la figura 1, vedem că intrările de clasă sunt fixate în funcție de numele clasei, iar elementele de resurse sunt identificate în funcție de perechea de nume: nume globale și nume locale. Acest mecanism este folosit pentru a rezolva conflictul în nume de resurse: dacă două bibliotecă JAR-fișier definesc o resursă cu același nume global va fi folosit de nume locale, pe baza cadrului stiva apelantului. Pentru mai multe informații, consultați secțiunea Resurse.

Căutați clase

Amintiți-vă că l-am lăsat din clasa de încărcare a clasei în metoda findClass (). Metoda findClass () primește numele clasei ca tip de String și trebuie să găsească și să determine bauturile care reprezintă acest nume. Deoarece loadByteCode creează cu bunătate o hartă între numele clasei și octetul său, acum este foarte ușor de implementat: trebuie să găsiți octeții bazați pe numele clasei și să apelați defineClass (). după cum se arată în Lista 2:

Listing 2. FindClass ()

Încărcarea resurselor

În timpul dezvoltării One-JAR, findClass a fost primul lucru care a început să funcționeze ca dovadă a fidelității conceptului. Dar când am început să distribuim aplicații mai complexe, mi-am dat seama că trebuie să mă descurc cu descărcarea de resurse, precum și cu orele. Acolo lucrarea a început să alunece. Definind în ClassLoader o metodă potrivită pentru suprimarea sarcinii de căutare a resurselor, am ales pe cea cu care eram cel mai familiar. Vedeți listele 3:

Listing 3. metoda getResourceAsStream ()

Apelul în acest loc trebuie în mod necesar să sune: nu am putut înțelege de ce URL-ul a fost folosit pentru a găsi resurse. Deci, am ignorat această implementare și mi-am introdus propriile, prezentate în Lista 4:

Listing 4. Implementarea metodei getResourceAsStream () în One-JAR

Ultimul obstacol

Noua mea implementare a metodei getResourceAsStream (). Se părea să facă truc, până când am încercat să nu-One JAR o aplicație care încărcate de resurse utilizând URL-ul URL = object.getClass () getClassLoader () getResource () ..; aici aplicația a eșuat. De ce? Deoarece implementarea implicită returnează o adresă URL ClassLoader. a fost nulă, care a rupt codul procedurii de apelare.

Din acest punct, totul a devenit încurcat. A trebuit să ghicesc ce adresa URL ar trebui utilizată pentru a se referi la resursa din fișierul JAR din directorul / lib. Ar trebui să fie, de exemplu, jar: file: main.jar! Lib / a.jar! Com.a.A.resource?

Am încercat toate combinațiile pe care le puteam imagina - nici unul nu a funcționat. Borcanul: Sintaxa pur și simplu nu acceptă imbricate JAR-files, care ma pus în fața situației, evident, fără speranță în abordarea mea One-JAR. Deși majoritatea aplicațiilor nu par să utilizeze ClassLoader.getResource. unii cu siguranta fac, și nu am fost fericit excepție, spunând că „Dacă aplicația utilizează ClassLoader.getResource () nu puteți utiliza One-JAR“.

Și, în final, soluția.

Încercarea de a rezolva jar-ul de sintaxă. Am dat peste un mecanism care folosește mediul Java Runtime Environment pentru a afișa prefixele de URL ale operatorilor. Aceasta a fost cheia de care aveam nevoie pentru a rezolva problema în findResource. Tocmai mi-am inventat propriul prefix de protocol cu ​​numele onejar. Am mapa atunci noul prefix la un tip de protocol, care ar reveni fluxul de octet pentru resursa, așa cum se arată în Listarea 5. Rețineți că înregistrarea 5 reprezintă codul în două fișiere, JarClassLoader și un nou fișier cu numele com / simontuffs / onejar /Handler.java.

Listing 5. findResource și protocolul onejar:

Bootable JarClassLoader

În acest moment, probabil că aveți doar o singură întrebare rămasă: cum să introduceți JarClassLoader în secvența de pornire, astfel încât să poată începe clase de încărcare din fișierul One-JAR mai întâi? O explicație detaliată depășește domeniul de aplicare al acestui articol; dar esența este după cum urmează. În loc de a folosi com.main.Main principal de clasă ca META-INF / MANIFEST.MF / Main-Clasa I a creat o nouă clasă de com.simontuffs.onejar.Boot de boot. care este specificat ca atributul Main-Class. Noua clasă face următoarele:

  • Creează un JarClassLoader nou.
  • Utilizează un încărcător nou pentru a încărca com.main.Main din main / main.jar (bazat pe elementul principal META-INF / MANIFEST.MF din main.jar).
  • Cauze com.main.Main.main (String []) (sau alt nume principal-Class. Indicat în main.jar fișier / MANIFEST.MF) sunt încărcate de clasă și folosind maparea principal apel (). Argumentele specificate în linia de comandă One-JAR sunt transmise metodei principale de aplicare fără modificări.

În concluzie

Dacă vă simțiți amețit de toate acestea, nu vă faceți griji: utilizarea One-JAR este mult mai ușoară decât încercarea de a înțelege cum funcționează. Odată cu apariția de plug-FatJar Eclipse Plugin (vezi. FJEP vezi "Resurse"), utilizatorii pot crea Eclipse One-JAR-aplicație prin verificarea expertul. Bibliotecile dependente sunt plasate în directorul / lib, programul principal și clasele sunt plasate în principal / main.jar, un META-INF fișier / MANIFEST.MF este scris în mod automat. Dacă utilizați JarPlug (vedeți din nou "Resurse"), puteți privi în interiorul fișierului JAR creat și îl puteți rula din IDE.

În general, One-JAR este o soluție simplă, dar puternică pentru problema distribuției de pachete de aplicații. Cu toate acestea, nu este potrivit pentru toate scenariile posibile. De exemplu, dacă aplicația utilizează clasa încărcător de stil vechi JDK 1.1, care nu delega mamă, această clasă încărcător eșuează atunci când caută clase într-un fișier JAR-imbricată. Puteți depăși acest obstacol prin crearea și implementarea unui „înveliș“ (ambalaj) un încărcător de clasă pentru a modifica încărcătorul recalcitrant, cu toate că acest lucru ar atrage după sine folosind tehnici bytecode de manipulare cu ajutorul unor instrumente cum ar fi Javassist sau Byte Code Engineering Biblioteca (BCEL).

De asemenea, puteți rula probleme când utilizați anumite tipuri de încărcătoare de clasă specializate utilizate de aplicațiile încorporate și serverele Web. În special, este posibil să aveți probleme cu încărcătoare de clasă care nu se referă imediat la bootloaderul părinte sau cele care caută baza de cod în sistemul de fișiere. În acest caz, mecanismul inclus în One-JAR ar trebui să ajute, prin utilizarea căruia puteți extinde elementele fișierului JAR la sistemul de fișiere. Acest mecanism este controlat de atributul One-JAR-Expand al fișierului META-INF / MANIFEST.MF. În mod alternativ, puteți încerca să utilizați manipularea bytecode pentru a modifica în timp ce încărcați încărcătorul de clasă, fără a afecta integritatea fișierelor JAR auxiliare. Dacă mergeți în acest fel, pentru fiecare caz în parte, este posibil să aveți nevoie de un încărcător de clasă specializat "înfășurare".

Secțiunea "Resurse" conține linkuri pentru descărcarea pluginurilor FatJar Eclipse Plugin și JarPlug, precum și informații suplimentare despre One-JAR.

Descărcați resurse

Subiecte conexe







Trimiteți-le prietenilor: