Caricare immagini e altre risorse da un JAR file


© Copyright 2001 Cristiano Sadun - Tutti i diritti riservati.

È esplicitamente vietata la riproduzione con ogni mezzo, il mirroring e l'uso per scopi commerciali di questa pubblicazione completa o in parte, senza l'esplicito consenso scritto dell'autore. La stampa e/o riproduzione e/o distribuzione su carta di tutto o parte di questo documento sono permesse a patto che tali copie non siano prodotte a scopo di profitto o ad uso commerciale, e che queste note siano interamente stampate e/o riprodotte e/o distribuite in modo chiaro e visibile insieme al testo.

Versione 1.1 21/03/2002

Il meccanismo con cui una JVM trova una risorsa (file, immagine, etc, in poche parole, tutto ciò che non è una classe) da un file jar e' molto simile a quello con cui trova le classi: il ClassLoader esamina il classpath e cerca la risorsa in ciascuna delle entries.

Facciamo un momento una digressione sui files jar: il relativo formato e' semplicemente un'estensione del formato zip (la cui specifica, per inciso, è a http://www.pkware.com/support/appnote.html); tale formato (e quindi anche il formato jar) fornisce alcuni servizi di file-system, specificatamente la possibilita' di specificare e usare directories, files e pathnames, vedendo quindi il file zip come un filesystem a sè, la cui root / denota semplicemente il file stesso. Nel seguito, uso filesystem virtuale per indicarlo.

Di conseguenza, se un'entry è un file jar, il ClassLoader considera la "root" del file jar come una directory e la aggiunge al classpath (una notazione è file.jar!/), e poi segue le solite regole per linkare le classi desiderate (cfr. [R1.15]).

Ad esempio, a fronte di un classpath indicato /lib;tools.jar, il corrispondente classpath "espanso" è /lib;tools.jar!/, dove lib è una directory su dico mentre tools.jar!/ è la root directory del filesystem virtuale all'interno del file tools.jar (in breve, "directory virtuale").

Per le classi, il meccanismo è piuttosto trasparente: ogni classe compilata ha un buffer interno (detto constant pool) che dice - tra le altre cose - da quali altre classi essa dipende; quindi la JVM sa in ogni momento quali classi deve caricare e le regole di classloading governano in modo trasparente come le directories vengano mappate in package names - il programmatore in sostanza non deve fare nulla se non mettere i jar adeguati in classpath.

Per altre risorse, invece, il programmatore deve fare un filo di lavoro in più:

1. Risorse implicitamente in classpath

1.1 Classi e risorse nel package di default

Facciamo un esempio semplicissimo: una classe T che nel metodo main si limita ad aprire e chiudere (non interpretare) un'immagine GIF contenuta in un file il cui nome, con notevole fantasia :), è "immagine.gif". Il codice è semplicissimo:

public class T {

    public static void main(String args[]) throws Exception {
        java.io.InputStream is =
            ClassLoader.getSystemClassLoader().getResourceAsStream("immagine.gif");
        if (is==null)
            System.out.println(
              "C'e' ancora qualcosa di sbagliato nella tua configurazione."
            );
        else {
            is.close();
            System.out.println("Bravo! Stavolta ha funzionato!");
        }
    }
}

Il codice in rosso scuro è quello che fa il lavoro. In blu, il nome della risorsa (in rosso chiaro il codice che è specificatamente dovuto al fatto che siamo in metodo statico (main) e quindi non c'è un'oggetto (this) a cui riferisi, e di conseguenza dobbiamo usare il SystemClassLoader. All'interno di un metodo nonstatico, useremmo getClass().getResourceAsStream("immagine.gif");), dopo aver letto le note nel paragrafo 3.

Una volta compilata la classe, otteniamo T.class, e abbiamo due files da impacchettare in un jar: T.class, appunto, e immagine.gif.

Una nota: ricordate che, a runtime, il ClassLoader si limiterà a cercare immagine.gif ovunque in classpath: non è a rigore necessario che il file sia nel jar (a meno di requisiti di sicurezza e codebase di cui non parlo qui). È, ovviamente, questione di comodità - se immagine.gif è nel jar, installare l'applicazione non richiederà un passo ulteriore di setup per copiare l'immagine da qualche parte ed aggiungere questo "qualche parte" al classpath.

Usando jar, impacchettiamo classe e risorsa:


       jar cf t.jar T.class immagine.gif

Eseguendo java -classpath t.jar T la risorsa viene caricata correttamente.

1.2 Classi e risorse in package specifici

Che succede se la nostra classe è in un package p? Modifichiamo il codice sopra con:
package p;

public class T {
...
}

e ricompiliamo, spostando il risultante T.class in una directory p.

Reimpacchettiamo il tutto con


       jar cf t.jar p/T.class immagine.gif
effettivamente copiando T.class in una directory virtuale p nel filesystem virtuale di t.jar, invece che sotto la sua "root".

Non cambia, in effetti, nulla: getResourceAsStream() considera il nome passatogli un pathname (sia pur nel file jar) e quindi, con le solite regole dei pathnames, immagine.gif equivale a ./immagine.gif, ovvero viene cercata direttamente sotto la "root" del file jar.

Se vogliamo che anch'essa sia nella directory virtuale p, è sufficiente

e voilà: ancora una volta, il ClassLoader cercherà p/immagine.gif in classpath, e siccome t.jar è in classpath, cerchrà anche nella "root" del filesystem virtuale in t.jar, trovando la directory virtuale p e il "file" immagine.gif al suo interno.

1.3 Classi e risorse in package/directories diverse

E se volessimo avere la nostra risorsa in una directory che non ha nulla a che vedere con il package p, ad esempio sotto immagini?

Nessun problema:

Il nuovo t.jar avrà sotto la "root" del filesystem virtuale una directory virtuale p (contenente T.class) e una directory virtuale immagini (contenente immagine.gif). Il codice ricompilato usa proprio immagini/immagine.gif come path della risorsa nel filesystem virtuale, quindi la risorsa verrà aperta correttamente.

Il punto è che qualunque directory virtuale sotto la "root" del filesystem virtuale sarà implicitamente in classpath, quindi il ClassLoader non avrà problemi a trovarla.

2. Risorse non implicitamente in classpath

Talvolta, però, può essere desiderabile mantenere risorse in directory virtuale del tutto diverse da quelle delle classi - e non necessariamente sotto la "root" del filesystem virtuale.

Tipicamente, vogliamo nel jar una directory virtuale risorse con una sottodirectory immagini, un'altra suoni, eccetera; ma vogliamo potervici riferire, nel codice, senza indicare "risorse", ad esempio con codice come

        getResourceAsStream("immagini/immagine.gif") 

Ora, col meccanismo fin qui illustrato, la cosa non si può fare, perchè il path immagini/immagine.gif equivale a ./immagini/immagine.gif, ma nel jar la directory virtuale immagini si trova sotto risorse, quindi il ClassLoader non può trovarla.

Per realizzare ciò, è necessario in qualche modo dire al ClassLoader che la directory virtuale risorse dev'essere anche lei in classpath; mentre in presenza di un file jar il meccanismo di default, come abbiamo visto all'inizio, "espande" il classpath solo con la root <file jar>!/ del relativo filesystem virtuale.

Il modo consiste nel'usare il meccanismo di classpath extension messo a disposizione da java dalla versione 1.2.

Molto semplicemente, questo vi permette di definire un'entry Class-Path: nel Manifest di un jar che, appunto, dice al classloader di aggiungere ulteriori directory virtuali oltre alla root quando incontra il jar stesso. Vediamo il tutto in pratica.

Di norma, quando impacchettiamo classi e risorse con il tool jar, quest'ultimo aggiunge un Manifest file automaticamente, ma ovviamente questo non sa nulla della nostra directory virtuale risorse, quindi dobbiamo fare un po' di lavoro in più (ma non troppo).

Per prima cosa, quindi, dobbiamo scrivere un Manifest contenente, nel formato corretto, un'entry Class-Path:. L'unica cosa da tenere presente è che le entries così specificato devono contenere paths prefissati con "./, che sta appunto per la root del filesystem virtuale nel jar.

Voilà - eseguendo il solito java -classpath t.jar p.T la risorsa sarà correttamente trovata.

3. Usare getResource()/getResourceAsStream() di Class invece di ClassLoader

Nell'esempio sopra abbiamo visto come usare i metodi getResource()/getResourceAsStream() di ClassLoader. In un contesto nonstatico, però, è possible usare i metodi con lo stesso nome forniti da Class, con codice tipo

  package p;

  class T1 {

   public void metodo() { // In un comune metodo, il contesto è nonstatico, cioè
                          // a runtime c'è un oggetto this che lo sta eseguendo
      java.io.InputStream is = getClass().getResourceAsStream("immagine.gif");
   }
  }

Nonostante i nomi e gli argomenti dei metodi siano identici, c'è una differenza nel modo in cui l'argomento viene interpretato (cfr. Class.getResource() e Class.getResourceAsStream()).

Ammenoche' non si usi uno slash iniziale, Class.getResource() e Class.getResourceAsStream() si aspettano il nome non qualificato, perche' assumono che la risorsa sia nello stesso package della classe.

Cioe', col codice sopra, immagine.gif deve risiedere nello stesso package p della classe T1, e conseguentemente nella stessa directory virtuale p nel file jar (il suo path virtuale sarà p/immagine.gif).

Tuttavia, come potete vedere, l'argomento del metodo non menziona p - appunto perchè quest'informazione è già nota all'oggetto Class a cui la richiesta viene fatta, il quale si limiterà a prefissare l'argomento col nome del package (mappato opportunamente su un directory path) e ad invocare il relativo metodo su ClassLoader.

Il vantaggio di questa tecnica è che, cambiando il package di una classe (e delle risorse associate) il codice che accede alle risorse non necessita modifiche. Lo svantaggio è che le risorse devono risiedere nello stesso package delle classi.


© Copyright 2001 Cristiano Sadun - Tutti i diritti riservati.

È esplicitamente vietata la riproduzione con ogni mezzo, il mirroring e l'uso per scopi commerciali di questa pubblicazione completa o in parte, senza l'esplicito consenso scritto dell'autore. La stampa e/o riproduzione e/o distribuzione su carta di tutto o parte di questo documento sono permesse a patto che tali copie non siano prodotte a scopo di profitto o ad uso commerciale, e che queste note siano interamente stampate e/o riprodotte e/o distribuite in modo chiaro e visibile insieme al testo.