Posts tagged: build

Il grosso grasso JAR….

jarSalve…non sono morto, è che sto lavorando abbastanza e quindi trovare il tempo per parlare di cose interessanti, anche se mi piace molto, è difficile !!! ma poi con chi sto parlando visto che siete sempre così pochi ?? [crisi esistenziale]. Ciò detto, volevo rendervi partecipe di questa mia piccola scoperta…che poi scoperta in realtà non è, ma dal momento che a lavorare ci si imbatte nei problemi, questa volta la soluzione volevo condividerla…sai mai che a qualcuno possa tornare utile. Dunque il problema è il seguente: creare un jar contenente al suo interno altri jar, ovvero le librerie, che possa funzionare senza dover specificare nulla nel classpath.
Sostanzialmente, dato il jar devme.jar voglio lanciarlo usando il comando:

1
 java -jar devme.jar

senza preoccuparmi di altro.
Cercando su gooogle ho trovato un articolo della IBM che mi ha illuminato sul fare alcuni esperimenti, e quindi sul risolvere il problema…vediamo assieme.
Il classloader di java, per gli amici sun.misc.Launcher$AppClassLoader, che viene richiamato al lancio del comando java -jar, è a conocenza di 2 cose: 

  • Carica classi/risorse che compaiono nella root del JAR.
  • Carica classi/risorse che compaione nell’attributo Class-Path del file MANIFEST.MF.

Inoltre, ignora qualunque valore della variabile d’ambiente CLASSPATH o argomento fornito da riga di comando -cp, usato per specificare il classpath. Dulcis in fundo, si fa per dire, non sa come caricare classi/risorse all’interno di JAR presenti all’interno del jar da eseguire. Per cominciare creaimo un singolo JAR, che sarà il nostro eseguibile e che quindi chiamiamo main.jar. Supponiamo di avere una classe entry-point it.devme.main.Main e assumiamo che dipenda da 2 classi: it.devme.a.A all’interno del jar a.jar e it.devme.b.B all’interno del jar b.jar.

main.jar | it/devme/main/Main.class | it/devme/a/A.class | it/devme/b/B.class

Questo approccio ha delle limitazioni tali da suggerire l’utilizzo di un altro metodo. una di queste è che l’informazioni sulla provenienza originale della classi A.class e B.class viene persa. Un altra più importante è la seguente:
se a.jar e b.jar contengono una risorsa con lo stesso nome, quale scelgo? Cambiamo strada. Un altro approccio è quello di modificare il MANIFEST.MF a manoni, cercando di comporre quello di main.jar in modo che avesse visibiltà degli altri jar. Ma l’unica cosa che si riesce a fare è quella di porli nel filesystem a fianco di main.jar che è esattamente la cosa che si voleva evitare.

Per tagliare la testa al toro, il suggerimento dato è quello di scrivere un class loader personalizzato, in modo da caricare le classi che servono dall’interno di un JAR. Si tenga presente che scrivere un class loader personalizzato non è un operazione da prendere alla leggera, dal momento che questa ha un impatto molto profondo con il resto dell’applicazione, dal momento che si preoccupa di caricare le classi e di rispondere agli errori quando questi si verificano. Il concetto di class loader va oltre lo scopo di questo post, per cui ulteriori dettagli non verranno trattati. Tenendo presente la struttura del nostro jar:

one-jar.jar | META-INF/MANIFEST.MF | main/main.jar | lib/a.jar | lib/b.jar

proviamo a scrivere il nostro class-loader. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
com/simontuffs/onejar/JarClassLoader.java
protected URL findResource(String $resource) {
    try {
        // resolve($resource) returns the name of a
        // resource in the
        // byteCode Map if it is known to this
        // classloader.
        String resource = resolve($resource);
        if (resource != null) {
            // We know how to handle it.
            return new URL(Handler.PROTOCOL + ":" + resource);
        }
        return null;
    } catch (MalformedURLException mux) {
        WARNING("unable to locate " + $resource);
    }
    return null;
}

Si noti subito che per il recupero di una classe si utilizza un URL con rispettivo protocollo che permette di identificare una risorsa. Il protocollo in questo caso è un protocollo custom, che comincia con il prefisso onejar:. Di seguito abbiamo l’handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
com/simontuffs/onejar/Handler.java
package com.simontuffs.onejar;
...
public class Handler extends URLStreamHandler {
/**
* This protocol name must match the
* name of the package in which this class
* lives.
*/
   public static String PROTOCOL = "onejar";
   protected int len = PROTOCOL.length()+1;
   protected URLConnection openConnection(URL u) throws IOException {
       final String resource = u.toString().substring(len);
       return new URLConnection(u) {
 
           public void connect() {}
 
           public InputStream getInputStream() {
               // Use the Boot classloader to get the resource.
               // is only one per one-jar.
               JarClassLoader cl = Boot.getClassLoader();
               return cl.getByteStream(resource);
           }
       };
   }
}

Il nostro class loader andrà inserito nel MANIFEST.MF/Main-Class attribute. Verrà creato un nuovo bootstrap della main class, com.simontuffs.onejar.Boot, la quale è specificata come Main-Class attribute. La nuova classe creerà una nuova istanza del JarClassLoader userà il nuovo loader, per caricare it.devme.main.Main.class usando la riflessione per invocare il metodo main(). Finito di leggere l’articolo IBM che è possibile trovare qui, o forse nel mentre della lettura non ricordo, ho notato l’indirizzo di questo meraviglioso, risolvi problemi, plugin per eclipse….che realizza né più né meno la tecnica sopra descritta per produrre un jar eseguibile con l’accesso a librerie al suo interno. Il plugin lo trovate qui…è ancora una pre-release alpha, ma fino ad ora non ho avuto alcun problema nell’utilizzo.

WordPress Themes