Úvod
Asi každý pisateľ kódu v Jave sa stretol s výnimkou ClassNotFoundException, ktorá indikuje chybový stav, keď Java Virtual Machine nebola schopná nájsť binárny kód pre danú triedu (teda súbor .class). Táto výnimka je veľmi „obľúbená" hlavne v prípade, keď nie je k dispozícii žiadne integrované vývojové prostredie (IDE) a sme obmedzení len na prácu s príkazovým riadkom.
Vyhľadávanie .class súborov môže spôsobiť veľký hlavybôľ, ale pri prečítaní správnej dokumentácie sa ukáže, že sa riadi pevne danými pravidlami. Problémom je ale niekoľko faktorov – predovšetkým adresárová štruktúra, štruktúra balíčkov, premenná CLASSPATH a parameter -cp pre java.exe, ktoré treba vhodne zladiť.
Príklady
Vo všetkých príkladoch budeme predpokladať, že máme pevný adresár pre „projekt", povedzme C:\Projects\javacp, v ktorom budeme uchovávať zdrojové kódy.
Jednoduchá trieda, žiadne balíčky
Predpokladajme, že máme jednoduchú triedu HelloWorld, ktorá sa nenachádza v žiadnom balíčku.
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Trieda HelloWorld sa podľa konvencií musí nachádzať v súbore HelloWorld.java a keďže neprislúcha žiadnemu balíčku, bude uložený priamo v projektovom adresári – teda v C:\Projects\javacp\HelloWorld.java.
Súbor môžeme jednoducho skompilovať
javac HelloWorld.java
čím vznikne C:\Projects\javacp\HelloWorld.class. Tento súbor spustíme zavolaním java.exe, teda
java HelloWorld
Program by mal podľa očakávaní vypísať
Hello World!
Z nejakého dôvodu sa však môže stať, že zažijeme typickú výnimku:
Exception in thread "main" java.lang.NoClassDefFoundError: HelloWorld
Zákernosťou môže byť to, že na našom počítači program môže ísť, ale v analogickom adresári na počítači suseda získame výnimku. Základným riešením ťažkého kalibru pre tento príklad je použitie „tajného parametra"
java -cp . HelloWorld
Ten prikáže virtuálnemu stroju, aby triedy pre program vyhľadával len a výhradne v aktuálnom adresári (teda v C:\Projects\javacp.)
Jednoduchá trieda v balíčku
Presťahujme teraz triedu HelloWorld do balíčka sk.java. To vyžaduje dva kroky:
- dodanie riadku
package sk.javado úvodu zdrojovej triedy
package sk.java;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
- založenie dodatočných adresárov zodpovedajúcich hierarchii balíčkov. Každému balíčku musí prislúchať jeden adresár v súborovom systéme. Vytvorme teda adresárovú štruktúru
C:\Projects\javacp\sk\java.
c:\projects\javacp>md sk\java
Pre istotu vymažme starý skompilovaný súbor (aby sme predišli zmäteniu):
rm HelloWorld.class
a presuňme do balíčka zdrojový súbor:
C:\projects\javacp>move HelloWorld.java C:\projects\javacp\sk\java\HelloWorld.java
Presuňme sa teraz na spodok hierarchie
C:\projects\javacp>cd C:\projects\javacp\sk\java
a spusťme kompilátor
javac HelloWorld.java
Po skompilovaní skúsme spustiť program HelloWorld tak, ako v predošlom prípade
java -cp . HelloWorld
Žiaľ, teraz sa skoro určite dožijeme výnimky:
C:\projects\javacp\sk\java>java -cp . HelloWorld
Exception in thread "main" java.lang.NoClassDefFoundError: HelloWorld (wrong name: sk/java/HelloWorld)
at java.lang.ClassLoader.defineClass1(Native Method)
V tomto prípade java oznamuje, že sa snažíme spustiť HelloWorld s nesprávnou špecifikáciou balíčka. Triedy, ktoré sa nachádzajú v balíčku sa musia volať úplným menom a navyše zo správneho adresára.
HelloWorld teda spustíme z adresára C:\Projects\javacp príkazom
C:\Projects\javacp>java -cp . sk.java.HelloWorld
Pri spustení sa začne vyhľadávať trieda počnúc aktuálnym adresárom. Keďže sa nachádza v balíčkoch, prejde sa adresárová štruktúra a trieda sa hľadá v súbore sk/java/HelloWorld.
Premenná prostredia CLASSPATH
Premenná prostredia CLASSPATH umožňuje nahradiť neustále zapisovanie parametra -cp. Ak nastavíme CLASSPATH na ., môžeme parameter -cp vynechať.
CLASSPATH nastavíme dočasne pomocou
SET CLASSPATH=.
Zjednodušené spúšťanie je potom cez
java sk.java.HelloWorld
Dosť často sa však stáva, že premenná CLASSPATH je už definovaná a nachádzajú sa v nej už nejaké adresáre, prípadne JAR súbory. Vypísať CLASSPATH môžeme jednoducho
SET CLASSPATH
Dodať ďalšie položky do existujúceho CLASSPATH môžeme pomocou
SET CLASSPATH=%CLASSPATH%;.;
Problém CLASSPATH a -cp
Značná väčšina problémov spočíva v nepochopení spolupráce CLASSPATH a parametra -cp.
Pravidlá pre vyhľadávanie tried
- Ak nie je definovaný ani
CLASSPATH, ani parameter-cp, trieda sa hľadá v aktuálnom adresári, prípadne v adresároch pod aktuálnym adresárom v prípade tried v balíčkoch - Ak je definovaný
CLASSPATH, predošlé pravidlo sa ignoruje. Triedy sa hľadajú v adresároch/JARoch špecifikovaných v tejto systémovej premennej. - Ak je špecifikovaný parameter
-cp, predošlé pravidlá sa ignorujú (premennáCLASSPATHsa teda ignoruje). Triedy sa hľadajú v adresároch/JARoch špecifikovaných v tomto parametri.
Špeciálne treba upozorniť na nasledovné chovanie:
- ak nie je definovaný
CLASSPATH, ani-cp, triedy sa hľadajú v aktuálnom adresári - ak je definovaný buď
CLASSPATHalebo-cpa chceme, aby sa triedy hľadali v aktuálnom adresári, musíme tento adresár vCLASSPATH/-cpexplicitne uviesť (pomocou bodky).
Ďalšie príklady
JAR súbory
Na JAR súbory sa možno pozerať ako na celú adresárovú/balíčkovú štruktúru zbalenú v jednom súbore s daným menom. JAR súbor možno pridať do CLASSPATH/-cp podobne ako akýkoľvek iný adresár.
Ak napr. máme JAR súbor C:\projects\javacp\log4j.jar a potrebujeme z neho využívať triedu org.apache.log4j.Logger, pridáme ho do CLASSPATH cez
SET CLASSPATH=%CLASSPATH%;C:\projects\javacp\log4j.jar
a prípadne do -cp cez
java -cp C:\projects\javacp\log4j.jar nazovABalicekTriedy
Ak by náš HelloWorld využíval knižnicu log4j a chceli by sme ho spustiť, príkaz by vyzeral:
C:\projects\javacp>java -cp .;log4j.jar sk.java.HelloWorld
Všimnite si, že aktuálny adresár je uvedený explicitne, z neho sa bude odvíjať vyhľadávanie triedy HelloWorld. JAR súbor log4j.jar sa bude tiež vyhľadávať v aktuálnom adresári.
Spustiteľné JAR súbory
JAR súbory je možné pripraviť na “priame” spustenie. Takéto priamo spustiteľné súbory je možné naštartovať cez
java -jar názovSúboru.jar
V tom prípade sa ignorujú všetky body v pravidlách pre vyhľadávanie tried a všetky triedy sa vyhľadávajú len v danom JAR súbore.
Príklad zo života
Majme triedu sk.java.MatrixSolver, ktorá využíva JAR súbor commons-math-1.1.jar. Adresárová štruktúra vyzerá:
|--bin
|--|--sk
|--|--|--java
|--|--|--|--MatrixSolver.class
|--src
|--|--sk
|--|--|--java
|--|--|--|--MatrixSolver.java
|--commons-logging.jar (JAR pre výpočet matíc)
|--javacp.jar (triedy projektu zabalené do spustiteľného JARu)
Program môžeme spustiť napr.
java -cp bin;commons-math-1.1.jar sk.java.MatrixSolver
V javacp.jar sa nachádza automaticky spustiteľná verzia programu. Lenže tá je závislá na commons-math-1.1.jar. Bez závislostí by sme ju spustili klasicky cez -jar parameter:
java -jar javacp.jar
Ale ako bolo povedané vyššie, -jar parameter ignoruje všetky predošlé body vyhľadávania a teda aj commons-math-1.1.jar. Nepomôže ani explicitné určenie:
c:\Projects\javacp>java -cp commons-math-1.1.jar -jar javacp.jar
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/math/linear/RealMatrix
V tomto prípade je možné vyriešiť problém pridaním oboch JARov do CLASSPATH a spustením hlavnej triedy:
c:\Projects\javacp>java -cp commons-math-1.1.jar;javacp.jar sk.java.MatrixSolver
Sumár a morálne ponaučenie
- pokiaľ nepoužívate JARy a dodržiavate adresárovú štruktúru, použite parameter
-cpa v ňom uveďte všetky adresáre/JAR súbory, ktoré používate a nezabudnite na aktuálny adresár (bodku). - ak je to možné, uveďte do
CLASSPATHpoložku pre aktuálny adresár, pomôže to v prípade jednoduchých projektov - pri spúšťaní triedy musíte uviesť jej úplné meno a spúšťať ju treba z adresára, ktorý sa nachádza nad adresárovou štruktúrou, ktorá zodpovedá balíčkom. Spustenie triedy, ktorá je v balíčku z adresára, v ktorom sa nachádza
.classsúbor vo väčšine prípadov nepomôže. - platí pravidlo:
CLASSPATHignoruje aktuálny adresár, pokiaľ ho nemá špecifikovaný explicitne-cpignorujeCLASSPATHa aktuálny adresár, pokiaľ ho nemá špecifikovaný explicitne- parameter
-jarignoruje predošlé pravidl
Referencie
- Referenčná príručka o algoritme vyhľadávania tried v JDK (Sun)
- JAR file revealed - popis niektorých často opomínaných vlastností JAR súborov