Compilare per una versione precedente di Java con Animal Sniffer

Spesso è necessario compilare le proprie applicazioni Java in modo che siano compatibili con versioni precedenti del Java Runtime Environment (JRE).

Benché sia possibile utilizzare una vecchia versione del compilatore javac, tale soluzione è sconsigliabile in quanto si rinuncia a tutte le più recenti migliorie apportate al compilatore stesso.

javac supporta l’opzione -target che consente di specificare qual è la versione minima su cui si intende far girare la classe compilata. Tale opzione, tuttavia, è necessaria ma non sufficiente a garantire il corretto funzionamento della classe, in quanto se da un lato verifica che non siano state utilizzate caratteristiche del linguaggio non supportate dalla versione specificata, dall’altro non si accerta che non siano state utilizzate API, ovvero classi e metodi, non presenti in quella versione, a meno che non si prendano precise precauzioni.

A questo proposito, consideriamo un caso pratico: a partire dalla versione 9 di Java, l’interfaccia java.util.Set è stata arricchita di una serie di metodi statici denominati of(…), che consentono, con una sola istruzione, di istanziare degli insiemi contenenti zero o più elementi.

La seguente classe denominata, con molta fantasia, AnimalSnifferExample.java, fa uso proprio di uno di questi metodi:

public class AnimalSnifferExample {

   public static void main(String... args) {
      System.out.println(java.util.Set.of("1st item", "2nd item", "3rd item"));
   }

}

Effettueremo ora delle prove di compilazione ed esecuzione con diverse versioni di javac e java, utilizzando talvolta alcune specifiche opzioni di compilazione:

  • Compilando con javac 11 (l’importante è che la versione sia successiva alla 9) ed eseguendo con java 11 si ha:

    $ /usr/lib/jvm/jdk-11-bellsoft-arm32-vfp-hflt/bin/javac -version AnimalSnifferExample.java
    javac 11.0.1-BellSoft
    
    $ /usr/lib/jvm/jdk-11-bellsoft-arm32-vfp-hflt/bin/java -showversion AnimalSnifferExample
    openjdk version "11.0.1-BellSoft" 2018-10-16
    OpenJDK Runtime Environment (build 11.0.1-BellSoft+0)
    OpenJDK Server VM (build 11.0.1-BellSoft+0, mixed mode)
    [1st item, 3rd item, 2nd item]
    

    Nessuna sorpresa: il codice compila e funziona correttamente (se vi state chiedendo come mai gli elementi non siano ordinati, potete consultare la documentazione di java.util.Set).

    Se a questo punto provassimo a rieseguire con java 8 la classe compilata con javac 11 otterremmo invece il seguente errore:

    $ /usr/lib/jvm/jdk1.8.0_191/jre/bin/java -showversion AnimalSnifferExample
    java version "1.8.0_191"
    Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
    Java HotSpot(TM) Client VM (build 25.191-b12, mixed mode)
    
    Error: A JNI error has occurred, please check your installation and try again
    Exception in thread "main" java.lang.UnsupportedClassVersionError: AnimalSnifferExample has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0
       at java.lang.ClassLoader.defineClass1(Native Method)
       at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
       at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
       at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
       at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
       at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
       at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
       at java.security.AccessController.doPrivileged(Native Method)
       at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
       at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
       at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
       at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
       at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
    

    Ancora una volta, nessuna sorpresa: è normale che una classe compilata con una certa versione di javac, senza aver specificato opportune opzioni, non risulti eseguibile con versioni precedenti di java.

  • Ora compiliamo sempre con javac 11 ma specificando l’opzione -target 1.8 (e conseguentemente anche l’opzione -source 1.8 richiesta dal compilatore); successivamente proviamo ad eseguire la classe con java 8:

    $ /usr/lib/jvm/jdk-11-bellsoft-arm32-vfp-hflt/bin/javac -target 1.8 -source 1.8 -version AnimalSnifferExample.java
    javac 11.0.1-BellSoft
    warning: [options] bootstrap class path not set in conjunction with -source 8
    1 warning
    
    $ /usr/lib/jvm/jdk1.8.0_191/jre/bin/java -showversion AnimalSnifferExample
    java version "1.8.0_191"
    Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
    Java HotSpot(TM) Client VM (build 25.191-b12, mixed mode)
    
    Exception in thread "main" java.lang.NoSuchMethodError: java.util.Set.of(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/Set;
       at AnimalSnifferExample.main(AnimalSnifferExample.java:4)
    

    L’avviso emesso da javac relativamente al bootstrap class path non andrebbe sottovalutato, tuttavia la compilazione termina con successo. L’esecuzione, invece, termina in errore, segnalando che il metodo java.util.Set.of(…) non esiste. Questa situazione è assolutamente indesiderabile perché si ha un codice apparentemente compatibile con una versione precedente di Java, ma che in realtà va in errore a tempo di esecuzione.

  • Se volessimo rendere compatibile il nostro codice anche con java 7 (-target 1.7), paradossalmente saremmo più fortunati:

    $ /usr/lib/jvm/jdk-11-bellsoft-arm32-vfp-hflt/bin/javac -target 1.7 -source 1.7 -version AnimalSnifferExample.java
    javac 11.0.1-BellSoft
    warning: [options] bootstrap class path not set in conjunction with -source 7
    AnimalSnifferExample.java:4: error: static interface method invocations are not supported in -source 7
          System.out.println(java.util.Set.of("1st item", "2nd item", "3rd item"));
                                          ^
      (use -source 8 or higher to enable static interface method invocations)
    1 error
    1 warning
    

    Come si vede, la compilazione fallisce perché il supporto ai metodi statici nelle interfacce è stato introdotto con Java 8, quindi quando si ha un’incompatibilità a livello di linguaggio, si riscontra un errore già a tempo di compilazione. In questa situazione, almeno, non c’è quella pericolosa parvenza di compatibilità sperimentata nel caso precedente.


Fortunatamente esistono diversi modi per far sì che la build notifichi eventuali problemi di compatibilità, evitando, così, spiacevoli problemi a runtime:

  • Se per compilare si utilizza JDK versione 9 o successiva e si intende compilare per JRE versione 6 o successiva, la cosa migliore è aggiungere la nuova opzione -release alla riga di comando del compilatore, seguita dal numero di versione del runtime target desiderato, ad esempio -release 6 per JRE 1.6; fatto questo, il problema si può considerare completamente risolto e non occorre fare altro. Se invece non fosse possibile utilizzare JDK versione 9 o successiva per la compilazione, vedere i punti successivi.

  • Aggiungere l’opzione --boot-class-path al compilatore, specificando il percorso della libreria di runtime Java di interesse, che nel nostro caso è costituita dal file rt.jar incluso nel JRE 1.8:

    $ /usr/lib/jvm/jdk-11-bellsoft-arm32-vfp-hflt/bin/javac -source 1.8 -target 1.8 --boot-class-path /usr/lib/jvm/jdk1.8.0_191/jre/lib/rt.jar -version AnimalSnifferExample.java
    javac 11.0.1-BellSoft
    AnimalSnifferExample.java:4: error: cannot find symbol
          System.out.println(java.util.Set.of("1st item", "2nd item", "3rd item"));
                                          ^
      symbol:   method of(String,String,String)
      location: interface Set
    1 error
    

    Prima di tutto si nota che non viene più emesso il warning relativo al bootstrap class path, dopo di che la compilazione fallisce perché il compilatore non trova il metodo of(...) all’interno della libreria di runtime specificata.
    Si tratta di una soluzione assolutamente valida; il problema, tuttavia, è che non sempre si ha a disposizione il runtime necessario, specialmente se si utilizza un ambiente di build (ad esempio di continuous integration) fuori dal proprio controllo.

  • Utilizzare Animal Sniffer dopo la compilazione. Animal Sniffer è uno strumento open source che fa parte del progetto MojoHaus. Se integrato nel processo di build, è in grado di esaminare i file compilati (.class) per verificare che siano effettivamente compatibili con versioni precedenti del runtime Java. Questo controllo è reso possibile da opportune firme (signature) generate a partire dalle stesse librerie di runtime e rese disponibili sempre da MojoHaus. Vi sono anche firme specifiche per versioni particolari del JRE, anche proprietarie (ad es. Apple e IBM).

Nulla vieta, infine, di mettere in atto entrambi questi ultimi due accorgimenti.


Utilizzo di Animal Sniffer con Maven

Utilizzare Animal Sniffer con Maven è estremamente semplice, è sufficiente aggiungere l’apposito plugin nel pom.xml e il gioco è fatto:

<plugin>
   <groupId>org.codehaus.mojo</groupId>
   <artifactId>animal-sniffer-maven-plugin</artifactId>
   <version>1.16</version>
   <executions>
      <execution>
         <phase>process-classes</phase>
         <goals>
            <goal>check</goal>
         </goals>
      </execution>
   </executions>
   <configuration>
      <signature>
         <groupId>org.codehaus.mojo.signature</groupId>
         <artifactId>java18</artifactId>
         <version>1.0</version>
      </signature>
   </configuration>
</plugin>

Occorre impostare artifactId e version della signature a seconda della compatibilità desiderata. In generale le ultime cifre dell’artifactId indicano la versione del runtime di riferimento (ad esempio 18 corrisponde al JRE 1.8).

Tutte le firme disponibili sono consultabili sul repository Maven Central; ove fossero presenti più versioni dello stesso artefatto, conviene scegliere la più recente.

Un’ultima considerazione riguarda la fase di esecuzione, qui impostata su process-classes; in alcuni esempi presenti in rete viene preferita la fase verify, ma poiché verify è successiva a package, la segnalazione di errore si avrebbe ad artefatto ormai prodotto (sebbene non installato), invece con process-classes o test, Animal Sniffer interviene subito dopo la compilazione.

Esempio completo con Maven Vedi su GitHub

Prepariamo la seguente alberatura di progetto:

├── src
│   └── main
│       └── java
│           └── AnimalSnifferExample.java
└── pom.xml

Il contenuto del file AnimalSnifferExample.java è stato già presentato; segue invece il listato completo del file pom.xml di Maven:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

   <modelVersion>4.0.0</modelVersion>
   <groupId>com.example</groupId>
   <artifactId>animal-sniffer-example-mvn</artifactId>
   <version>0.0.1</version>

   <properties>
      <maven.compiler.source>1.8</maven.compiler.source>
      <maven.compiler.target>1.8</maven.compiler.target>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   </properties>

   <build>
      <plugins>
         <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>animal-sniffer-maven-plugin</artifactId>
            <version>1.16</version>
            <executions>
               <execution>
                  <phase>process-classes</phase>
                  <goals>
                     <goal>check</goal>
                  </goals>
               </execution>
            </executions>
            <configuration>
               <signature>
                  <groupId>org.codehaus.mojo.signature</groupId>
                  <artifactId>java18</artifactId>
                  <version>1.0</version>
               </signature>
            </configuration>
         </plugin>
      </plugins>
   </build>

</project>

Notare le righe evidenziate in cui sono presenti i riferimenti alle firme del runtime Java 1.8.

A questo punto, eseguendo Maven con Java 9 o versioni successive (di norma Maven utilizza il JDK referenziato dalla variabile di ambiente JAVA_HOME) si ha:

$ mvn clean package -V
Apache Maven 3.6.0 (97c98ec64a1fdfee7767ce5ffb20918da4f719f3; 2018-10-24T20:41:47+02:00)
Maven home: /opt/maven
Java version: 11.0.1-BellSoft, vendor: BellSoft, runtime: /usr/lib/jvm/jdk-11-bellsoft-arm32-vfp-hflt
Default locale: en_GB, platform encoding: UTF-8
OS name: "linux", version: "4.14.79-v7+", arch: "arm", family: "unix"
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------< com.example:animal-sniffer-example-mvn >---------------
[INFO] Building animal-sniffer-example-mvn 0.0.1
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ animal-sniffer-example-mvn ---
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ animal-sniffer-example-mvn ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /tmp/animal-sniffer-example-mvn/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ animal-sniffer-example-mvn ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /tmp/animal-sniffer-example-mvn/target/classes
[INFO]
[INFO] --- animal-sniffer-maven-plugin:1.16:check (default) @ animal-sniffer-example-mvn ---
[INFO] Checking unresolved references to org.codehaus.mojo.signature:java18:1.0
[ERROR] /tmp/animal-sniffer-example-mvn/src/main/java/AnimalSnifferExample.java:4: Undefined reference: java.util.Set java.util.Set.of(Object, Object, Object)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  15.422 s
[INFO] Finished at: 2018-12-15T09:17:14+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.codehaus.mojo:animal-sniffer-maven-plugin:1.16:check (default) on project animal-sniffer-example-mvn: Signature errors found. Verify them and ignore them with the proper annotation if needed. -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

Come è evidente, la build fallisce, prevenendo eventuali errori a tempo di esecuzione. L’output segnala inoltre il riferimento non valido al metodo java.util.Set.of(Object, Object, Object) che infatti in Java 8 non esiste.

Ulteriori informazioni sono disponibili sul sito di MojoHaus, nella pagina dedicata all’Animal Sniffer Maven Plugin.


Utilizzo di Animal Sniffer con Ant

Usare Animal Sniffer con Ant è leggermente più complicato rispetto a Maven, ma comunque nulla che richieda più di qualche minuto di messa a punto:

  1. Scaricare il file JAR animal-sniffer-ant-tasks (link) e il file .signature del runtime Java di interesse (link) dal repository Maven Central.
  2. Posizionare i due file scaricati all’interno della directory del progetto, ad esempio in una sottodirectory dedicata: ant/animal-sniffer/
  3. Modificare il file build.xml aggiungendo all’elemento <project> l’attributo xmlns:as="antlib:org.codehaus.mojo.animal_sniffer", in modo da definire un namespace as.
  4. Aggiungere, sempre al build.xml, un target che effettui il controllo delle classi compilate, ad esempio:

    <target name="check-signature">
       <typedef uri="antlib:org.codehaus.mojo.animal_sniffer">
          <classpath path="ant/animal-sniffer/animal-sniffer-ant-tasks-1.16.jar" />
       </typedef>
       <as:check-signature signature="ant/animal-sniffer/java18-1.0.signature">
          <path path="build/classes" />
          <classpath>
             <fileset dir="lib" erroronmissingdir="false">
                <include name="*.jar" />
             </fileset>
          </classpath>
       </as:check-signature>
    </target>
    

    Il task as:check-signature deve essere configurato con lo stesso classpath utilizzato dal task javac (che tipicamente si trova nel target compile). Verificare anche la correttezza dei riferimenti al JAR animal-sniffer-ant-tasks e al file .signature.

Esempio completo con Ant Vedi su GitHub

Prepariamo un’alberatura come la seguente, scaricando manualmente animal-sniffer-ant-tasks-1.16.jar e java18-1.0.signature dal repository Maven Central:

├── ant
│   └── animal-sniffer
│       ├── animal-sniffer-ant-tasks-1.16.jar
│       └── java18-1.0.signature
├── src
│   └── AnimalSnifferExample.java
└── build.xml

La classe AnimalSnifferExample.java è sempre la stessa; segue invece il listato completo del file build.xml di Ant:

<?xml version="1.0" encoding="UTF-8"?>
<project name="animal-sniffer-example-ant" xmlns:as="antlib:org.codehaus.mojo.animal_sniffer">

   <target name="clean">
      <delete dir="build" />
   </target>

   <fileset id="classpath" dir="lib" erroronmissingdir="false">
      <include name="*.jar" />
   </fileset>

   <target name="compile">
      <mkdir dir="build/classes" />
      <javac srcdir="src" destdir="build/classes" source="1.8" target="1.8" includeantruntime="false" debug="true">
         <compilerarg value="-version" />
         <classpath>
            <fileset refid="classpath" />
         </classpath>
      </javac>
   </target>

   <target name="check-signature">
      <typedef uri="antlib:org.codehaus.mojo.animal_sniffer">
         <classpath path="ant/animal-sniffer/animal-sniffer-ant-tasks-1.16.jar" />
      </typedef>
      <as:check-signature signature="ant/animal-sniffer/java18-1.0.signature">
         <path path="build/classes" />
         <classpath>
            <fileset refid="classpath" />
         </classpath>
      </as:check-signature>
   </target>

</project>

Notare la riga evidenziata in cui è presente il riferimento alle firme del runtime Java 1.8.

Eseguendo Ant con Java 9 o versioni successive (anche Ant, come Maven, utilizza il JDK referenziato dalla variabile di ambiente JAVA_HOME) si ha:

$ ant clean compile check-signature
Buildfile: /tmp/animal-sniffer-example-ant/build.xml

clean:

compile:
    [mkdir] Created dir: /tmp/animal-sniffer-example-ant/build/classes
    [javac] Compiling 1 source file to /tmp/animal-sniffer-example-ant/build/classes
    [javac] javac 11.0.1-BellSoft
    [javac] warning: [options] bootstrap class path not set in conjunction with -source 8
    [javac] 1 warning

check-signature:
[as:check-signature] In createClasspath
[as:check-signature] Checking unresolved references to /tmp/animal-sniffer-example-ant/ant/animal-sniffer/java18-1.0.signature
[as:check-signature] Ignoring the signatures from file to be checked: /tmp/animal-sniffer-example-ant/build/classes
[as:check-signature] /tmp/animal-sniffer-example-ant/build/classes/AnimalSnifferExample.class:4: Undefined reference: java.util.Set java.util.Set.of(Object, Object, Object)

BUILD FAILED
/tmp/animal-sniffer-example-ant/build.xml:26: Signature errors found. Verify them and ignore them with the proper annotation if needed.

Come nel precedente esempio con Maven, anche in questo caso la build fallisce segnalando le classi che contengono riferimenti non validi, inclusi i numeri di riga, se è stato impostato a true l’attributo debug del task javac di Ant.

Ulteriori informazioni sono disponibili sul sito di MojoHaus, nella pagina dedicata agli Animal Sniffer ANT Tasks.

Lascia un commento

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.