Partilhar via


Transição do Java 8 para o Java 11

Não há uma solução única para fazer a transição de código do Java 8 para o Java 11. Para uma aplicação não trivial, mudar do Java 8 para o Java 11 pode ser uma quantidade significativa de trabalho. Os problemas potenciais incluem API removida, pacotes preteridos, uso de API interna, alterações em carregadores de classes e alterações na coleta de lixo.

Em geral, as abordagens são tentar executar em Java 11 sem recompilar, ou compilar com JDK 11 primeiro. Se o objetivo é colocar um aplicativo em funcionamento o mais rápido possível, apenas tentar executar em Java 11 geralmente é a melhor abordagem. Para uma biblioteca, o objetivo será publicar um artefato compilado e testado com o JDK 11.

Mudar para Java 11 vale o esforço. Novos recursos foram adicionados e melhorias foram feitas desde o Java 8. Esses recursos e aprimoramentos melhoram a inicialização, o desempenho, o uso de memória e fornecem uma melhor integração com contêineres. E há adições e modificações na API que melhoram a produtividade do desenvolvedor.

Este documento aborda as ferramentas para inspecionar o código. Ele também cobre problemas que você pode encontrar e recomendações para resolvê-los. Você também deve consultar outros guias, como o Oracle JDK Migration Guide. Como tornar o código existente modular não é abordado aqui.

A caixa de ferramentas

O Java 11 tem duas ferramentas, jdeprscan e jdeps, que são úteis para detetar possíveis problemas. Essas ferramentas podem ser executadas em arquivos de classe ou jar existentes. Você pode avaliar o esforço de transição sem ter que recompilar.

jdeprscan procura o uso de API preterida ou removida. O uso de API preterida não é um problema de bloqueio, mas é algo a ser analisado. Existe um arquivo jar atualizado? Você precisa registrar um problema para resolver o uso de API obsoleta? O uso da API removida é um problema de bloqueio que deve ser resolvido antes de tentar executar no Java 11.

jdeps, que é um analisador de dependência de classe Java. Quando usado com a --jdk-internals opção, jdeps informa qual classe depende de qual API interna. Você pode continuar a usar a API interna no Java 11, mas substituir o uso deve ser uma prioridade. A página wiki do OpenJDK Java Dependency Analysis Tool recomendou substituições para algumas APIs internas do JDK comumente usadas.

Existem plugins jdeps e jdeprscan para Gradle e Maven. Recomendamos adicionar essas ferramentas aos seus scripts de compilação.

O próprio compilador Java, javac, é outra ferramenta na sua caixa de ferramentas. Os avisos e erros que você recebe de jdeprscan e jdeps sairão do compilador. A vantagem de usar jdeprscan e jdeps é que você pode executar essas ferramentas em jars e arquivos de classe existentes, incluindo bibliotecas de terceiros.

O que jdeprscan e jdeps não podem fazer é avisar sobre o uso de reflexão para acessar a API encapsulada. O acesso reflexivo é verificado em tempo de execução. Em última análise, você tem que executar o código em Java 11 para saber com certeza.

Usando jdeprscan

A maneira mais fácil de usar o jdeprscan é dar-lhe um arquivo jar de uma compilação existente. Você também pode dar a ele um diretório, como o diretório de saída do compilador, ou um nome de classe individual. Use a --release 11 opção para obter a lista mais completa de APIs obsoletas. Se quiser priorizar qual API obsoleta seguir, ajuste a configuração de volta para --release 8. É provável que a API que foi preterida no Java 8 seja removida mais cedo do que a API que foi preterida mais recentemente.

jdeprscan --release 11 my-application.jar

A ferramenta jdeprscan gera uma mensagem de erro se tiver problemas para resolver uma classe dependente. Por exemplo, error: cannot find class org/apache/logging/log4j/Logger. Recomenda-se adicionar classes dependentes ao --class-path ou usar o caminho de classes do aplicativo, mas a ferramenta continuará a verificação sem estas. O argumento é --class-path. Nenhuma outra variação do argumento class-path funcionará.

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

Essa saída nos informa que a classe com.company.Util está a chamar um construtor obsoleto da classe java.lang.Double. O javadoc recomendará a API para usar no lugar da API obsoleta. Nenhuma quantidade de trabalho resolverá o problema, pois é a API que foi removida. Desde Java 8, java.util.Base64 deve ser usado.

Execute jdeprscan --release 11 --list para ter uma noção de qual API foi preterida desde o Java 8. Para obter uma lista de API que foi removida, execute jdeprscan --release 11 --list --for-removal.

Usando jdeps

Use jdeps, com a opção --jdk-internals para localizar dependências na API interna do JDK. A opção --multi-release 11 de linha de comando é necessária para este exemplo porque log4j-core-2.13.0.jar é um ficheiro JAR de multi-versão. Sem essa opção, o jdeps reclamará se encontrar um arquivo jar de várias versões. A opção especifica qual versão dos arquivos de classe deve ser inspecionada.

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   

A saída dá alguns bons conselhos sobre como eliminar o uso da API interna do JDK! Sempre que possível, sugere-se a API de substituição. O nome do módulo onde o pacote está encapsulado é dado entre parênteses. O nome do módulo pode ser usado com --add-exports ou --add-opens se for necessário quebrar explicitamente o encapsulamento.

O uso de sun.misc.BASE64Encoder ou sun.misc.BASE64Decoder resultará em um java.lang.NoClassDefFoundError em Java 11. O código que usa essas APIs deve ser modificado para usar java.util.Base64.

Tente eliminar o uso de qualquer API proveniente do módulo jdk.unsupported. A API deste módulo fará referência ao JDK Enhancement Proposal (JEP) 260 como uma substituição sugerida. Em resumo, o JEP 260 diz que o uso de API interna será suportado até que a API de substituição esteja disponível. Mesmo que seu código possa usar a API interna do JDK, ele continuará a ser executado, pelo menos por um tempo. Dê uma olhada no JEP 260, pois ele aponta para substituições para algumas APIs internas. identificadores variáveis podem ser usados no lugar de algumas APIs sun.misc.Unsafe, por exemplo.

jdeps pode fazer mais do que apenas analisar o uso de internos do JDK. É uma ferramenta útil para analisar dependências e para gerar ficheiros module-info. Dê uma olhada na documentação para mais informações.

Usando javac

A compilação com o JDK 11 exigirá atualizações para criar scripts, ferramentas, estruturas de teste e bibliotecas incluídas. Use a opção -Xlint:unchecked para o javac para obter os detalhes sobre o uso da API interna do JDK e outros avisos. Também pode ser necessário usar --add-opens ou --add-reads expor pacotes encapsulados ao compilador (consulte JEP 261).

As bibliotecas podem considerar o empacotamento como um arquivo JAR multi-release. Os arquivos jar multi-release permitem que você suporte os tempos de execução Java 8 e Java 11 a partir do mesmo arquivo jar. Eles adicionam complexidade à construção. Como criar arquivos JAR de múltiplas versões está além do escopo deste documento.

Executando em Java 11

A maioria dos aplicativos deve ser executada em Java 11 sem modificação. A primeira coisa a tentar é executar em Java 11 sem recompilar o código. O objetivo de apenas executar é ver quais avisos e erros são gerados durante a execução. Esta abordagem obtém um
aplicação para executar em Java 11 mais rapidamente, concentrando-se no mínimo que precisa ser feito.

A maioria dos problemas que você pode encontrar pode ser resolvida sem ter que recompilar o código. Se um problema tiver que ser corrigido no código, faça a correção, mas continue a compilar com o JDK 8. Se possível, trabalhe para que o aplicativo seja executado com java a versão 11 antes de compilar com o JDK 11.

Verificar opções de linha de comando

Antes de executar no Java 11, faça uma verificação rápida das opções de linha de comando. As opções que foram removidas farão com que a Java Virtual Machine (JVM) saia. Essa verificação é especialmente importante se você usar opções de log GC, uma vez que elas mudaram drasticamente do Java 8. A ferramenta JaCoLine é uma boa para usar para detetar problemas com as opções de linha de comando.

Verifique bibliotecas de terceiros

Uma fonte potencial de problemas são bibliotecas de terceiros que você não controla. Você pode atualizar proativamente bibliotecas de terceiros para versões mais recentes. Ou você pode ver o que cai fora da execução do aplicativo e atualizar apenas as bibliotecas que são necessárias. O problema com a atualização de todas as bibliotecas para uma versão recente é que torna mais difícil encontrar a causa raiz se houver algum erro no aplicativo. O erro aconteceu por causa de alguma biblioteca atualizada? Ou o erro foi causado por alguma alteração no tempo de execução? O problema com a atualização apenas do que é necessário é que pode levar várias iterações para resolver.

A recomendação aqui é fazer o mínimo de alterações possível e atualizar bibliotecas de terceiros como um esforço separado. Se você atualizar uma biblioteca de terceiros, na maioria das vezes você vai querer a versão mais recente e melhor que é compatível com Java 11. Dependendo de quão atrasada está a sua versão atual, você pode querer adotar uma abordagem mais cautelosa e atualizar para a primeira versão compatível com Java 9+.

Além de examinar as notas de versão, você pode usar jdeps e jdeprscan para avaliar o arquivo jar. Além disso, o OpenJDK Quality Group mantém uma página wiki Quality Outreach que lista o status de teste de muitos projetos de Software Livre de Código Aberto (FOSS) em relação às versões do OpenJDK.

Definir explicitamente a coleta de lixo

O coletor de lixo paralelo (Parallel GC) é o GC padrão do Java 8. Se o aplicativo estiver usando o padrão, o GC deve ser explicitamente definido com a opção -XX:+UseParallelGCde linha de comando. O padrão foi alterado no Java 9 para o coletor de lixo Garbage First (G1GC). Para fazer uma comparação justa de um aplicativo em execução no Java 8 versus o Java 11, as configurações do GC devem ser as mesmas. A experimentação com as configurações de GC deve ser adiada até que o aplicativo tenha sido validado no Java 11.

Definir explicitamente as opções padrão

Se estiver sendo executado na VM HotSpot, definir a opção -XX:+PrintCommandLineFlags de linha de comando despejará os valores das opções definidas pela VM, particularmente os padrões definidos pelo GC. Execute com este sinalizador no Java 8 e use as opções impressas ao executar no Java 11. Na maioria das vezes, os padrões são os mesmos de 8 a 11. Mas usar as configurações de 8 garante a paridade.

Recomenda-se definir a opção --illegal-access=warn de linha de comando. No Java 11, o uso da reflexão para acessar a API interna do JDK resultará em um aviso de acesso reflexivo ilegal. Por padrão, o aviso é emitido apenas para o primeiro acesso ilegal. A configuração --illegal-access=warn causará um aviso em cada acesso refletivo ilegal. Você encontrará mais casos de acesso ilegal se a opção estiver definida para avisar. Mas você também receberá muitos avisos redundantes.
Depois que o aplicativo for executado no Java 11, defina --illegal-access=deny para imitar o comportamento futuro do tempo de execução do Java. A partir do Java 16, o padrão será --illegal-access=deny.

Advertências do ClassLoader

No Java 8, você pode converter o carregador de classes do sistema para um URLClassLoader. Isso geralmente é feito por aplicativos e bibliotecas que desejam injetar classes no classpath em tempo de execução. A hierarquia do carregador de classes foi alterada no Java 11. O carregador de classes do sistema (também conhecido como carregador de classes de aplicativo) agora é uma classe interna. Casting para um URLClassLoader vai lançar um ClassCastException durante a execução. O Java 11 não tem API para aumentar dinamicamente o classpath em tempo de execução, mas isso pode ser feito por meio de reflexão, com as ressalvas óbvias sobre o uso de API interna.

No Java 11, o carregador de classes de inicialização carrega apenas módulos principais. Se você criar um carregador de classes com um pai nulo, ele pode não encontrar todas as classes de plataforma. No Java 11, você precisa passar ClassLoader.getPlatformClassLoader() em vez de null como o carregador de classe pai nesses casos.

Alterações de dados de localidade

A fonte padrão para dados de localidade em Java 11 foi alterada com o JEP 252 para o Common Locale Data Repository do Unicode Consortium. Isso pode ter um impacto na formatação localizada. Defina a propriedade java.locale.providers=COMPAT,SPI do sistema para reverter para o comportamento de localidade Java 8, se necessário.

Potenciais problemas

Aqui estão alguns dos problemas comuns que você pode encontrar. Siga os links para obter mais detalhes sobre esses problemas.

Opções não reconhecidas

Se uma opção de linha de comando tiver sido removida, o aplicativo irá imprimir Unrecognized option: ou Unrecognized VM option seguido pelo nome da opção removida. Uma opção não reconhecida fará com que a VM saia. As opções que foram preteridas, mas não removidas, produzirão um aviso de VM.

Em geral, as opções que foram removidas não têm substituição e o único recurso é remover a opção da linha de comando. A exceção são as opções para registro de coleta de lixo. O log GC foi reimplementado no Java 9 para usar a estrutura unificada de log da JVM. Consulte "Tabela 2-2 Mapeando sinalizadores de log de coleta de lixo herdados para a configuração do Xlog" na seção Habilitar registro em log com a estrutura de log unificado da JVM da Referência das ferramentas Java SE 11.

Avisos de VM

O uso de opções preteridas produzirá um aviso. Uma opção é preterida quando foi substituída ou não é mais útil. Tal como acontece com as opções removidas, estas opções devem ser removidas da linha de comandos. O aviso VM Warning: Option <option> was deprecated significa que a opção ainda é suportada, mas que o suporte pode ser removido no futuro. Uma opção que não é mais suportada e gerará o aviso VM Warning: Ignoring option. As opções que não são mais suportadas não têm efeito no tempo de execução.

A página da Web VM Options Explorer fornece uma lista exaustiva de opções que foram adicionadas ou removidas do Java desde o JDK 7.

Erro: Não foi possível criar a máquina virtual Java

Essa mensagem de erro é impressa quando a JVM encontra uma opção não reconhecida.

AVISO: Ocorreu uma operação ilegal de acesso reflexivo

Quando o código Java usa reflexão para acessar a API interna do JDK, o tempo de execução emitirá um aviso de acesso reflexivo ilegal.

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

O que isso significa é que um módulo não exportou o pacote que está sendo acessado por meio de reflexão. O pacote é encapsulado no módulo e é, basicamente, API interna. O aviso pode ser ignorado como um primeiro esforço para começar a funcionar no Java 11. O tempo de execução do Java 11 permite o acesso reflexivo para que o código legado possa continuar a funcionar.

Para resolver esse aviso, procure o código atualizado que não faz uso da API interna. Se o problema não puder ser resolvido com código atualizado, pode-se usar a opção de linha de comando --add-exports ou --add-opens para abrir o acesso ao pacote. Essas opções permitem o acesso a tipos não exportados de um módulo de outro módulo.

A --add-exports opção permite que o módulo de destino acesse os tipos públicos do pacote nomeado do módulo de origem. Às vezes, o código usará setAccessible(true) para acessar membros não públicos e a API. Isso é conhecido como reflexão profunda. Nesse caso, use --add-opens para dar ao seu código acesso aos membros não públicos de um pacote. Se você não tiver certeza se deseja usar --add-exports ou --add-opens, comece com --add-exports.

As --add-exports opções ou --add-opens devem ser consideradas como uma solução alternativa e não como uma solução a longo prazo. O uso dessas opções interrompe o encapsulamento do sistema do módulo, que se destina a impedir que a API interna do JDK seja usada. Se a API interna for removida ou alterada, o aplicativo falhará. O acesso reflexivo será negado no Java 16, exceto quando o acesso for ativado por opções de linha de comando, como --add-opens. Para imitar o comportamento futuro, defina --illegal-access=deny na linha de comando.

O aviso no exemplo acima é emitido porque o pacote sun.nio.ch não é exportado pelo módulo java.base. Em outras palavras, não há exports sun.nio.ch; no arquivo module-info.java do módulo java.base. Isso pode ser resolvido com --add-exports=java.base/sun.nio.ch=ALL-UNNAMED. As classes que não são definidas em um módulo implicitamente pertencem ao módulo sem nome , literalmente chamado ALL-UNNAMED.

java.lang.reflect.InaccessibleObjectException

Essa exceção indica que você está tentando chamar setAccessible(true) um campo ou método de uma classe encapsulada. Você também pode receber um aviso de acesso reflexivo ilegal. Use a --add-opens opção para dar acesso ao seu código aos membros não públicos de um pacote. A mensagem de exceção informará que o módulo "não abre" o pacote para o módulo que está tentando chamar setAccessible. Se o módulo for "módulo sem nome", use UNNAMED-MODULE como módulo de destino na opção --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 é provavelmente causado por um pacote dividido ou por referência a módulos removidos.

NoClassDefFoundError causado por pacotes divididos

Um pacote dividido é quando um pacote é encontrado em mais de uma biblioteca. O sintoma de um problema de pacote dividido é que uma classe que se sabe estar no classpath não é encontrada.

Esse problema só ocorrerá ao usar o caminho do módulo. O sistema de módulo Java otimiza a pesquisa de classe restringindo um pacote a um módulo nomeado . O tempo de execução dá preferência ao caminho do módulo sobre o caminho da classe ao fazer uma pesquisa de classe. Se um pacote for dividido entre um módulo e o caminho da classe, somente o módulo será usado para fazer a pesquisa de classe. Isso pode levar a NoClassDefFound erros.

Uma maneira fácil de verificar se há um pacote dividido é introduzir o caminho do módulo e o caminho da classe em jdeps e usar o caminho para os ficheiros de classe da aplicação como o <caminho>. Se houver um pacote dividido, o jdeps imprimirá um aviso: Warning: split package: <package-name> <module-path> <split-path>.

Esse problema pode ser resolvido usando --patch-module <module-name>=<path>[,<path>] para adicionar o pacote dividido no módulo nomeado.

NoClassDefFoundError causado pelo uso de módulos Java EE ou CORBA

Se o aplicativo é executado em Java 8, mas lança um java.lang.NoClassDefFoundError ou um java.lang.ClassNotFoundException, então é provável que o aplicativo esteja usando um pacote dos módulos Java EE ou CORBA. Esses módulos foram preteridos no Java 9 e removidos no Java 11.

Para resolver o problema, adicione uma dependência de tempo de execução ao seu projeto.

Módulo removido Pacote afetado Dependência sugerida
API Java para XML Web Services (JAX-WS) java.xml.ws Tempo de execução JAX WS RI
Arquitetura Java para vinculação XML (JAXB) java.xml.bind Tempo de execução JAXB
Estrutura de ativação JavaBeans (JAV) java.ativação Estrutura de ativação JavaBeans (TM)
Anotações comuns java.xml.ws.anotação API de anotação Javax
Arquitetura do Common Object Request Broker (CORBA) java.corba Peixe-Vidro CORBA ORB
API de transação Java (JTA) java.transação API de transação Java

-Xbootclasspath/p não é mais uma opção suportada

O suporte para -Xbootclasspath/p foi removido. Utilize --patch-module em substituição. A opção --patch-module é descrita no JEP 261. Procure a seção rotulada "Patching module content". --patch-module pode ser usado com javac e com java para substituir ou aumentar as classes em um módulo.

O que --patch-module faz, na verdade, é inserir o módulo de patch na pesquisa de classe do sistema de módulos. O sistema de módulo pegará a classe do módulo de patch primeiro. Este é o mesmo efeito que antepor o bootclasspath no Java 8.

UnsupportedClassVersionError (Erro de Versão de Classe Não Suportada)

Essa exceção significa que você está tentando executar o código que foi compilado com uma versão posterior do Java em uma versão anterior do Java. Por exemplo, você está executando em Java 11 com um jar que foi compilado com JDK 13.

Versão Java Versão do formato de ficheiro de classe
8 52
9 53
10 54
11 55
12 56
13 57

Próximos passos

Depois que o aplicativo for executado no Java 11, considere mover as bibliotecas do class-path para o module-path. Procure versões atualizadas das bibliotecas das quais seu aplicativo depende. Escolha bibliotecas modulares, se disponíveis. Use o caminho do módulo tanto quanto possível, mesmo que você não planeje usar módulos em seu aplicativo. O uso do caminho do módulo tem melhor desempenho para o carregamento de classe do que o caminho de classe.