Condividi tramite


Transizione da Java 8 a Java 11

Non esiste una soluzione universale per eseguire la transizione del codice da Java 8 a Java 11. Per un'applicazione non semplice, il passaggio da Java 8 a Java 11 può essere una quantità significativa di lavoro. I potenziali problemi includono l'API rimossa, i pacchetti deprecati, l'uso dell'API interna, le modifiche ai caricatori di classi e le modifiche apportate a Garbage Collection.

In generale, gli approcci riguardano l'esecuzione in Java 11 senza ricompilazione o la compilazione con JDK 11. Se l'obiettivo è quello di rendere operativa un'applicazione il più rapidamente possibile, il semplice tentativo di esecuzione in Java 11 è spesso l'approccio migliore. Per una libreria, l'obiettivo sarà pubblicare un artefatto compilato e testato con JDK 11.

Il passaggio a Java 11 vale la pena. Sono state aggiunte nuove funzionalità e sono stati apportati miglioramenti a partire da Java 8. Queste funzionalità e miglioramenti migliorano l'avvio, le prestazioni, l'utilizzo della memoria e offrono una migliore integrazione con i contenitori. Sono inoltre disponibili aggiunte e modifiche all'API che migliorano la produttività degli sviluppatori.

Questo documento illustra gli strumenti per esaminare il codice. Vengono inoltre illustrati i problemi che è possibile riscontrare e le raccomandazioni per risolverli. È anche consigliabile consultare altre guide, ad esempio la Guida alla migrazione di Oracle JDK. Come rendere modulare il codice esistente non è trattato qui.

La cassetta degli attrezzi

Java 11 include due strumenti, jdeprscan e jdeps, utili per individuare potenziali problemi. Questi strumenti possono essere eseguiti su file jar o di classe esistenti. È possibile valutare lo sforzo di transizione senza dover ricompilare.

jdeprscan cerca l'uso dell'API deprecata o rimossa. L'uso dell'API deprecata non è un problema di blocco, ma è qualcosa da esaminare. Esiste un file JAR aggiornato? È necessario registrare un problema per risolvere l'uso dell'API deprecata? L'uso dell'API rimossa è un problema di blocco che deve essere risolto prima di provare a essere eseguito in Java 11.

jdeps, che è un analizzatore delle dipendenze della classe Java. Se usato con l'opzione --jdk-internals , jdeps indica quale classe dipende dall'API interna. È possibile continuare a usare l'API interna in Java 11, ma la sostituzione dell'utilizzo deve essere una priorità. La pagina wiki di OpenJDK Java Dependency Analysis Tool ha consigliato sostituzioni per alcune API interne JDK di uso comune.

Sono disponibili plug-in jdeps e jdeprscan per Gradle e Maven. È consigliabile aggiungere questi strumenti agli script di compilazione.

Il compilatore Java stesso, javac, è un altro strumento nella casella degli strumenti. Gli avvisi e gli errori generati da jdeprscan e jdeps usciranno dal compilatore. Il vantaggio dell'uso di jdeprscan e jdeps è che è possibile eseguire questi strumenti su file jar e classi esistenti, incluse le librerie di terze parti.

Ciò che jdeprscan e jdeps non possono fare è segnalare l'uso della riflessione per accedere all'API incapsulata. L'accesso riflettente viene verificato in fase di esecuzione. In definitiva, è necessario eseguire il codice in Java 11 per conoscere con certezza.

Uso di jdeprscan

Il modo più semplice per usare jdeprscan consiste nel assegnare un file jar da una build esistente. È anche possibile assegnargli una directory, ad esempio la directory di output del compilatore o un nome di classe individuale. Usare l'opzione --release 11 per ottenere l'elenco più completo dell'API deprecata. Se si vuole classificare in ordine di priorità l'API deprecata da eseguire dopo, riassegnare l'impostazione a --release 8. È probabile che l'API deprecata in Java 8 venga rimossa prima dell'API deprecata più di recente.

jdeprscan --release 11 my-application.jar

Lo strumento jdeprscan genera un messaggio di errore se si verificano problemi durante la risoluzione di una classe dipendente. Ad esempio: error: cannot find class org/apache/logging/log4j/Logger. È consigliabile aggiungere classi dipendenti a --class-path o usando il percorso della classe dell'applicazione, ma lo strumento continuerà l'analisi senza di essa. L'argomento è --class-path. Non funzioneranno altre varianti dell'argomento class-path.

jdeprscan --release 11 --class-path log4j-api-2.13.0.jar my-application.jar
error: cannot find class sun/misc/BASE64Encoder
class com/company/Util uses deprecated method java/lang/Double::<init>(D)V

Questo output ci dice che la classe com.company.Util sta chiamando un costruttore deprecato della classe java.lang.Double. Javadoc consiglia l'API da usare al posto dell'API deprecata. Nessuna quantità di lavoro risolverà il problema con error: cannot find class sun/misc/BASE64Encoder poiché si tratta di un'API che è stata rimossa. A partire da Java 8, java.util.Base64 deve essere usato.

Eseguire jdeprscan --release 11 --list per ottenere un'idea dell'API deprecata a partire da Java 8. Per ottenere un elenco di API rimosse, eseguire jdeprscan --release 11 --list --for-removal.

Uso di jdeps

Usare jdeps, con l'opzione --jdk-internals per trovare le dipendenze dall'API interna JDK. L'opzione --multi-release 11 della riga di comando è necessaria per questo esempio perché log4j-core-2.13.0.jar è un file con estensione jar a più versioni. Senza questa opzione, jdeps si lamenta se trova un file JAR multi-release. L'opzione specifica la versione dei file di classe da esaminare.

jdeps --jdk-internals --multi-release 11 --class-path log4j-core-2.13.0.jar my-application.jar
Util.class -> JDK removed internal API
Util.class -> jdk.base
Util.class -> jdk.unsupported
   com.company.Util        -> sun.misc.BASE64Encoder        JDK internal API (JDK removed internal API)
   com.company.Util        -> sun.misc.Unsafe               JDK internal API (jdk.unsupported)
   com.company.Util        -> sun.nio.ch.Util               JDK internal API (java.base)

Warning: JDK internal APIs are unsupported and private to JDK implementation that are
subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependence on any JDK internal APIs.
For the most recent update on JDK internal API replacements, please check:
https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool

JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.misc.BASE64Encoder                   Use java.util.Base64 @since 1.8
sun.misc.Unsafe                          See http://openjdk.java.net/jeps/260   

L'output offre alcuni consigli utili sull'eliminazione dell'uso dell'API interna JDK. Laddove possibile, viene suggerita l'API sostitutiva. Il nome del modulo in cui il pacchetto è incapsulato viene assegnato tra parentesi. Il nome del modulo può essere usato con --add-exports o --add-opens se è necessario interrompere in modo esplicito l'incapsulamento.

L'uso di sun.misc.BASE64Encoder o sun.misc.BASE64Decoder genererà java.lang.NoClassDefFoundError in Java 11. Il codice che usa queste API deve essere modificato per usare java.util.Base64.

Provare a eliminare l'uso di qualsiasi API proveniente dal modulo jdk.unsupported. L'API di questo modulo farà riferimento a JDK Enhancement Proposal (JEP) 260 come sostituzione suggerita. In breve, JEP 260 indica che l'uso dell'API interna sarà supportato fino a quando non sarà disponibile l'API sostitutiva. Anche se il codice può usare l'API interna JDK, continuerà a essere eseguito, almeno per un po'. Ti consiglio di dare un'occhiata a JEP 260 perché indica i sostituti per alcune API interne. gli handle di variabile possono essere usati al posto di alcune API sun.misc.Unsafe , ad esempio.

Jdeps può eseguire più operazioni di analisi per l'uso di elementi interni JDK. È uno strumento utile per l'analisi delle dipendenze e per la generazione di file di informazioni sui moduli. Per altre informazioni, vedere la documentazione .

Utilizzo di javac

La compilazione con JDK 11 richiederà aggiornamenti per compilare script, strumenti, framework di test e librerie incluse. Usare l'opzione -Xlint:unchecked per javac per ottenere i dettagli sull'uso dell'API interna JDK e di altri avvisi. Potrebbe anche essere necessario usare --add-opens o --add-reads esporre pacchetti incapsulati al compilatore (vedere JEP 261).

Le librerie possono prendere in considerazione la creazione di pacchetti come file con estensione jar a più versioni. I file con estensione jar a più versioni consentono di supportare runtime Java 8 e Java 11 dallo stesso file JAR. Aggiungono complessità alla compilazione. La compilazione di file JAR con più versioni esula dall'ambito di questo documento.

Esecuzione su Java 11

La maggior parte delle applicazioni deve essere eseguita in Java 11 senza modifiche. La prima cosa da provare è eseguire in Java 11 senza ricompilare il codice. Lo scopo del solo eseguire è vedere quali avvisi ed errori emergono dall'esecuzione. Questo approccio ottiene un
applicazione da eseguire in Java 11 più rapidamente concentrandosi sul minimo che deve essere fatto.

La maggior parte dei problemi che possono verificarsi può essere risolta senza dover ricompilare il codice. Se è necessario risolvere un problema nel codice, apportare la correzione ma continuare a compilare con JDK 8. Se possibile, fai in modo che l'applicazione venga eseguita con java la versione 11 prima di compilarla con JDK 11.

Controllare le opzioni della riga di comando

Prima di eseguire java 11, eseguire un'analisi rapida delle opzioni della riga di comando. Le opzioni che sono state rimosse causeranno la chiusura della macchina virtuale Java (JVM). Questo controllo è particolarmente importante se si usano le opzioni di registrazione GC perché sono cambiate drasticamente rispetto a Java 8. Lo strumento JaCoLine è un ottimo strumento da usare per rilevare i problemi con le opzioni della riga di comando.

Controllare le librerie di terze parti

Una potenziale fonte di problemi è costituita da librerie di terze parti che non si controllano. È possibile aggiornare in modo proattivo le librerie di terze parti alle versioni più recenti. In alternativa, è possibile osservare i risultati dell'esecuzione dell'applicazione e aggiornare solo le librerie necessarie. Il problema relativo all'aggiornamento di tutte le librerie a una versione recente è che rende più difficile trovare la causa radice se si verifica un errore nell'applicazione. L'errore si è verificato a causa di una libreria aggiornata? Oppure l'errore è stato causato da alcune modifiche nel runtime? Il problema relativo all'aggiornamento solo di ciò che è necessario è che potrebbero essere necessarie diverse iterazioni per risolvere.

È consigliabile apportare il minor numero possibile di modifiche e aggiornare le librerie di terze parti come sforzo separato. Se si aggiorna una libreria di terze parti, più spesso si vuole che la versione più recente e più grande compatibile con Java 11. A seconda della distanza dalla versione corrente, è consigliabile adottare un approccio più prudente e eseguire l'aggiornamento alla prima versione compatibile con Java 9+.

Oltre a esaminare le note sulla versione, è possibile usare jdeps e jdeprscan per valutare il file jar. Inoltre, il gruppo di qualità OpenJDK gestisce una pagina wiki di sensibilizzazione sulla qualità che elenca lo stato dei test di molti progetti FOSS (Free Open Source Software) rispetto alle versioni di OpenJDK.

Impostare esplicitamente la raccolta dei rifiuti

Parallel Garbage Collector (Parallel GC) è l'GC predefinito in Java 8. Se l'applicazione usa l'impostazione predefinita, il GC deve essere impostato in modo esplicito con l'opzione -XX:+UseParallelGCdella riga di comando . L'impostazione predefinita è stata modificata in Java 9 nel Garbage Collector Garbage First (G1GC). Per eseguire un confronto equo di un'applicazione in esecuzione in Java 8 rispetto a Java 11, le impostazioni GC devono essere uguali. L'esperimento con le impostazioni GC deve essere posticipato fino a quando l'applicazione non è stata convalidata in Java 11.

Impostare in modo esplicito le opzioni predefinite

Se è in esecuzione nella macchina virtuale HotSpot, l'impostazione dell'opzione -XX:+PrintCommandLineFlags della riga di comando eseguirà il dump dei valori delle opzioni impostate dalla macchina virtuale, in particolare le impostazioni predefinite impostate dal GC. Esegui con questo flag su Java 8 e usa le opzioni stampate quando esegui su Java 11. Per la maggior parte, le impostazioni predefinite sono uguali da 8 a 11. Tuttavia, l'uso delle impostazioni da 8 garantisce la parità.

È consigliabile impostare l'opzione --illegal-access=warn della riga di comando. In Java 11, l'uso della reflection per accedere all'API interna di JDK genererà un avviso di accesso riflettente non valido. Per impostazione predefinita, l'avviso viene generato solo per il primo accesso non valido. La configurazione --illegal-access=warn provocherà un avviso in caso di ogni accesso riflettente non valido. Troverai più casi di accesso illegale se l'opzione è impostata su avvisare. Ma si otterranno anche molti avvisi ridondanti.
Dopo l'esecuzione dell'applicazione in Java 11, impostare --illegal-access=deny per simulare il comportamento futuro del runtime Java. A partire da Java 16, il valore predefinito sarà --illegal-access=deny.

Avvertenze sul ClassLoader

In Java 8 è possibile effettuare un cast del caricatore della classe di sistema in un oggetto URLClassLoader. Questa operazione viene in genere eseguita da applicazioni e librerie che vogliono inserire classi nel classpath in fase di esecuzione. La gerarchia del caricatore di classi è cambiata in Java 11. Il caricatore di classi di sistema (noto anche come caricatore di classi dell'applicazione) è ora una classe interna. La trasformazione in un URLClassLoader genererà un'eccezione ClassCastException in fase di esecuzione. Java 11 non dispone dell'API per aumentare dinamicamente il classpath in fase di esecuzione, ma può essere eseguita tramite reflection, con le ovvie avvertenze sull'uso dell'API interna.

In Java 11 il caricatore della classe di avvio carica solo i moduli principali. Se si crea un caricatore di classi con un elemento padre Null, potrebbe non trovare tutte le classi della piattaforma. In Java 11 è necessario passare ClassLoader.getPlatformClassLoader() anziché null come caricatore di classe padre in questi casi.

Modifiche ai dati di localizzazione

L'origine predefinita per i dati delle impostazioni locali in Java 11 è stata modificata con JEP 252 in quello del Common Locale Data Repository del Consorzio Unicode. Ciò può avere un impatto sulla formattazione localizzata. Impostare la proprietà java.locale.providers=COMPAT,SPI di sistema per ripristinare il comportamento delle impostazioni locali Java 8, se necessario.

Potenziali problemi

Ecco alcuni dei problemi comuni che potresti riscontrare. Per altri dettagli su questi problemi, seguire i collegamenti.

Opzioni non riconosciute

Se è stata rimossa un'opzione della riga di comando, l'applicazione visualizzerà Unrecognized option: o Unrecognized VM option seguita dal nome dell'opzione incriminata. Un'opzione non riconosciuta causerà la chiusura della macchina virtuale. Le opzioni deprecate, ma non rimosse, genereranno un avviso di macchina virtuale.

In generale, le opzioni rimosse non sostituiscono e l'unico ricorso consiste nel rimuovere l'opzione dalla riga di comando. L'eccezione riguarda le opzioni per la registrazione della garbage collection. La registrazione GC è stata riimpressa in Java 9 per usare il framework di registrazione JVM unificato. Fare riferimento a "Tabella 2-2 Mappatura delle flag di registrazione della Garbage Collection legacy alla configurazione di Xlog" nella sezione Abilitare la registrazione con il framework di logging unificato della JVM del Java SE 11 Tools Reference.

Avvisi delle macchine virtuali

L'uso di opzioni deprecate genererà un avviso. Un'opzione è deprecata quando è stata sostituita o non è più utile. Come per le opzioni rimosse, queste opzioni devono essere rimosse dalla riga di comando. L'avviso VM Warning: Option <option> was deprecated indica che l'opzione è ancora supportata, ma il supporto potrebbe essere rimosso in futuro. Opzione che non è più supportata e genererà l'avviso VM Warning: Ignoring option. Le opzioni non più supportate non hanno alcun effetto sul runtime.

La pagina Web Esploratore opzioni VM fornisce un elenco completo delle opzioni che sono state aggiunte o rimosse da Java a partire da JDK 7.

Errore: Impossibile creare la macchina virtuale Java

Questo messaggio di errore viene stampato quando la JVM rileva un'opzione non riconosciuta.

AVVISO: si è verificata un'operazione di accesso riflettente non valida

Quando il codice Java usa la reflection per accedere all'API interna di JDK, il runtime genera un avviso di accesso riflettente non valido.

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by my.sample.Main (file:/C:/sample/) to method sun.nio.ch.Util.getTemporaryDirectBuffer(int)
WARNING: Please consider reporting this to the maintainers of com.company.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

Ciò significa che un modulo non ha esportato il pacchetto a cui si accede tramite reflection. Il pacchetto viene incapsulato nel modulo ed è, fondamentalmente, l'API interna. L'avviso può essere ignorato come primo tentativo di mettersi in funzione ed eseguire Java 11. Il runtime Java 11 consente l'accesso riflettente in modo che il codice legacy possa continuare a funzionare.

Per risolvere questo avviso, cercare il codice aggiornato che non usa l'API interna. Se il problema non può essere risolto con il codice aggiornato, è possibile usare o l'opzione --add-exports della --add-opens riga di comando per aprire l'accesso al pacchetto. Queste opzioni consentono l'accesso a tipi non esportati di un modulo da un altro modulo.

L'opzione --add-exports consente al modulo di destinazione di accedere ai tipi pubblici del pacchetto denominato del modulo di origine. A volte il codice verrà usato setAccessible(true) per accedere a membri e API non pubblici. Questa operazione è nota come riflessione profonda. In questo caso, usare --add-opens per concedere al codice l'accesso ai membri non pubblici di un pacchetto. Se non si è certi se usare --add-export o --add-opens, iniziare con --add-export.

Le --add-exports opzioni o --add-opens devono essere considerate come soluzione alternativa, non a lungo termine. L'uso di queste opzioni interrompe l'incapsulamento del sistema del modulo, che è progettato per impedire l'uso dell'API interna di JDK. Se l'API interna viene rimossa o modificata, l'applicazione avrà esito negativo. L'accesso riflettente verrà negato in Java 16, ad eccezione del caso in cui l'accesso sia abilitato dalle opzioni della riga di comando, come --add-opens ad esempio. Per simulare il comportamento futuro, impostare --illegal-access=deny sulla riga di comando.

L'avviso nell'esempio precedente viene generato perché il sun.nio.ch pacchetto non viene esportato dal java.base modulo. In altre parole, non c'è alcun exports sun.nio.ch; nel file module-info.java del modulo java.base. Questo problema può essere risolto con --add-exports=java.base/sun.nio.ch=ALL-UNNAMED. Le classi non definite in un modulo appartengono in modo implicito al modulo senza nome , denominate ALL-UNNAMEDletteralmente .

java.lang.reflect.InaccessibleObjectException

Questa eccezione indica che si sta tentando di chiamare setAccessible(true) su un campo o un metodo di una classe incapsulata. È anche possibile che venga visualizzato un avviso di accesso riflettente non valido. Usare l'opzione --add-opens per concedere al codice l'accesso ai membri non pubblici di un pacchetto. Il messaggio di eccezione ti dirà che il modulo "non apre" il pacchetto al modulo che sta tentando di chiamare setAccessible. Se il modulo è "modulo senza nome", usare UNNAMED-MODULE come target-module nell'opzione --add-opens .

java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.ArrayList jdk.internal.loader.URLClassPath.loaders accessible: 
module java.base does not "opens jdk.internal.loader" to unnamed module @6442b0a6

$ java --add-opens=java.base/jdk.internal.loader=UNNAMED-MODULE example.Main

java.lang.NoClassDefFoundError

NoClassDefFoundError è probabilmente causato da un pacchetto diviso o facendo riferimento a moduli rimossi.

NoClassDefFoundError causato da pacchetti divisi

Un pacchetto diviso è quando un pacchetto viene trovato in più di una libreria. Il sintomo di un problema di divisione del pacchetto è che una classe che si sa essere nel percorso di classi non viene trovata.

Questo problema si verifica solo quando si usa module-path. Il sistema di moduli Java ottimizza la ricerca della classe limitando un pacchetto a un modulo denominato . Il runtime dà la preferenza al percorso del modulo rispetto al percorso della classe durante l'esecuzione di una ricerca di classe. Se un pacchetto è suddiviso tra un modulo e il percorso della classe, viene usato solo il modulo per eseguire la ricerca della classe. Ciò può causare NoClassDefFound errori.

Un modo semplice per verificare la presenza di un pacchetto diviso consiste nel collegare il percorso del modulo e il percorso della classe in jdeps e usare il percorso dei file della classe dell'applicazione come <percorso>. Se è presente un pacchetto diviso, jdeps visualizzerà un avviso: Warning: split package: <package-name> <module-path> <split-path>.

Questo problema può essere risolto usando --patch-module <module-name>=<path>[,<path>] per aggiungere il pacchetto suddiviso nel modulo denominato.

NoClassDefFoundError causato dall'uso di moduli Java EE o CORBA

Se l'applicazione viene eseguita in Java 8 ma genera un java.lang.NoClassDefFoundError o un java.lang.ClassNotFoundException, è probabile che l'applicazione usi un pacchetto dai moduli Java EE o CORBA. Questi moduli sono stati deprecati in Java 9 e rimossi in Java 11.

Per risolvere il problema, aggiungere una dipendenza di runtime al progetto.

Modulo rimosso Pacchetto interessato Dipendenza suggerita
API Java per i servizi Web XML (JAX-WS) java.xml.ws Runtime JAX WS RI
Architettura Java per il binding XML (JAXB) java.xml.bind JAXB Runtime
JavaBeans Activation Framework (JAV) java.activation Framework di attivazione JavaBeans (TM)
Annotazioni comuni java.xml.ws.annotation API annotazione Javax
Architettura di Common Object Request Broker (CORBA) java.corba GlassFish CORBA ORB
JAVA Transaction API (JTA) java.transaction API delle transazioni Java

-Xbootclasspath/p non è più un'opzione supportata

Il supporto per -Xbootclasspath/p è stato rimosso. Utilizzare invece --patch-module. L'opzione --patch-module è descritta in JEP 261. Cercare la sezione con etichetta "Applicazione di patch al contenuto del modulo". --patch-module può essere usato con javac e con Java per eseguire l'override o aumentare le classi in un modulo.

Quello che --patch-module fa, in effetti, è inserire il modulo patch nel processo di ricerca delle classi del sistema dei moduli. Il sistema dei moduli ottiene prima la classe dal modulo patch. Questo ha lo stesso effetto dell'anteposizione del bootclasspath in Java 8.

UnsupportedClassVersionError

Questa eccezione significa che si sta tentando di eseguire il codice compilato con una versione successiva di Java in una versione precedente di Java. Ad esempio, stai eseguendo Java 11 con un file JAR compilato con JDK 13.

Versione Java Versione del formato del file di classe
8 52
9 53
10 54
11 55
12 56
13 57

Passaggi successivi

Dopo l'esecuzione dell'applicazione in Java 11, prendere in considerazione lo spostamento delle librerie dal percorso di classe e nel percorso del modulo. Cercare le versioni aggiornate delle librerie da cui dipende l'applicazione. Scegliere librerie modulari, se disponibili. Usare il percorso del modulo il più possibile, anche se non si prevede di usare i moduli nell'applicazione. L'uso del percorso modulo offre prestazioni migliori per il caricamento delle classi rispetto a quello del percorso di classe.