Compartilhar via


Transição do Java 8 para o Java 11

Não há uma solução única para fazer a transição do Java 8 para o Java 11. Para um aplicativo não trivial, mover do Java 8 para o Java 11 pode ser uma quantidade significativa de trabalho. Os possíveis problemas incluem API removida, pacotes preteridos, uso de API interna, alterações em carregadores de classe e alterações na coleta de lixo.

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

A mudança para Java 11 vale a pena o esforço. Novos recursos foram adicionados e aprimoramentos foram feitos 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 aborda problemas que você pode encontrar e recomendações para resolvê-los. Você também deve consultar outros guias, como o Guia de Migração do Oracle JDK. 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 detectar possíveis problemas. Essas ferramentas podem ser executadas em arquivos jar ou de classe existentes. Você pode avaliar o esforço de transição sem precisar recompilar.

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

jdeps, que é um analisador de dependência de classe Java. Quando usado com a opção --jdk-internals, jdeps informa qual classe depende de uma 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.

jdeps plug-ins e jdeprscan plug-ins para Gradle e Maven. Recomendamos adicionar essas ferramentas aos scripts de build.

O próprio compilador Java, javac, é outra ferramenta em sua caixa de ferramentas. Os avisos e erros obtidos 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 é alertar sobre o uso da reflexão para acessar a API encapsulada. O acesso reflexivo é verificado em runtime. Em última análise, você precisa executar o código no Java 11 para saber com certeza.

Usar jdeprscan

A maneira mais fácil de usar jdeprscan é dar a ele um arquivo jar de um build existente. Você também pode fornecer a ele um diretório, como o diretório de saída do compilador ou um nome de classe individual. Use a opção --release 11 para obter a lista mais completa da API preterida. Se desejar priorizar qual API preterida procurar, volte a configuração para --release 8. A API que foi preterida no Java 8 provavelmente será removida mais cedo do que a API que foi preterida mais recentemente.

jdeprscan --release 11 my-application.jar

A ferramenta jdeprscan gerará uma mensagem de erro se tiver problemas para resolver uma classe dependente. Por exemplo, error: cannot find class org/apache/logging/log4j/Logger. É recomendável adicionar classes dependentes ao --class-path ou usar o caminho de classe do aplicativo, mas a ferramenta continuará a verificação sem isso. O argumento é --class-path. Nenhuma outra variação do argumento de caminho de classe 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á chamando um construtor obsoleto da classe java.lang.Double. O javadoc recomendará qual API usar em vez da API preterida. Nenhum volume de trabalho resolverá o error: cannot find class sun/misc/BASE64Encoder porque é a API que foi removida. Desde Java 8, java.util.Base64 deve ser usado.

Execute jdeprscan --release 11 --list para compreender quais APIs foram preteridas desde o Java 8. Para obter uma lista de API que foi removida, execute jdeprscan --release 11 --list --for-removal.

Usar jdeps

Use jdeps, com a opção --jdk-internals de 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 arquivo jar de várias versões. Sem essa opção, jdeps reclamará se encontrar um arquivo jar de várias versões. A opção especifica qual versão dos arquivos de classe inspecionar.

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 fornece alguns bons conselhos sobre como eliminar o uso da API interna do JDK! Sempre que possível, a API de substituição é sugerida. O nome do módulo em que o pacote é encapsulado é fornecido nos parênteses. O nome do módulo pode ser usado com --add-exports ou --add-opens se for necessário interromper 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 referenciará o JEP (JDK Enhancement Proposal) 260 como uma substituição sugerida. Em poucas palavras, o JEP 260 diz que o uso da API interna terá suporte até que a API de substituição esteja disponível. Embora seu código possa usar a API interna do JDK, ele continuará sendo executado, pelo menos por um tempo. Dê uma olhada no JEP 260, pois ele aponta para substituições para alguma API interna. manipuladores de variáveis podem ser usados no lugar de algumas APIs sun.misc.Unsafe, por exemplo.

jdeps pode fazer mais do que apenas verificar o uso de recursos internos do JDK. É uma ferramenta útil para analisar dependências e para gerar arquivos de informações de módulo. Dê uma olhada na documentação para saber mais.

Usar 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 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 de várias versões. Arquivos jar de várias versões permitem que você dê suporte a tempos de execução do Java 8 e Java 11 do mesmo arquivo jar. Eles adicionam complexidade ao build. Como criar jars de várias versões vai além do escopo deste documento.

Executar no Java 11

A maioria dos aplicativos deve ser executada no Java 11 sem modificação. A primeira coisa a tentar é executar no Java 11 sem recompilar o código. A questão de apenas executar é ver quais avisos e erros são provenientes da execução. Essa abordagem obtém um
aplicativo para ser executado no Java 11 de maneira mais rápida concentrando-se no mínimo que precisa ser feito.

A maioria dos problemas que você pode encontrar pode ser resolvida sem precisar recompilar o código. Se um problema precisar 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 as 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 JVM (Máquina Virtual Java) seja encerrada. Essa verificação é especialmente importante se você usar opções de log de GC, pois elas foram alteradas drasticamente desde o Java 8. A ferramenta JaCoLine é boa para ser usada para detectar problemas com as opções de linha de comando.

Verificar bibliotecas de terceiros

Uma possível fonte 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 está fora da execução do aplicativo a atualizar apenas essas bibliotecas 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 ocorreu devido a alguma biblioteca atualizada? Ou o erro foi causado por alguma alteração no runtime? O problema de atualizar apenas o que é necessário é que pode levar várias iterações para resolver.

A recomendação aqui é fazer o menor número possível de alterações e atualizar bibliotecas de terceiros como um esforço separado. Se você atualizar uma biblioteca de terceiros, na maioria das vezes você desejará a versão mais recente e maior compatível com o Java 11. Dependendo de quão distante está sua versão atual, talvez você queira 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 Grupo de Qualidade do OpenJDK mantém uma página wiki de Divulgação de Qualidade que lista o status do teste de muitos projetos do FOSS (Software livre livre) em relação às versões do OpenJDK.

Definir explicitamente a coleta de lixo

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

Definir explicitamente as opções padrão

Se estiver em execução na VM do HotSpot, definir a opção -XX:+PrintCommandLineFlags de linha de comando despejará os valores das opções definidas pela VM, especialmente os padrões definidos pelo GC. Execute com esse sinalizador no Java 8 e use as opções impressas ao executar no Java 11. Na maior parte dos casos, os padrões são os mesmos da versão 8 à versão 11. Mas usar as configurações de 8 garante a paridade.

É recomendável 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 só é emitido para o primeiro acesso ilegal. Definir --illegal-access=warn causará um aviso em todo acesso reflexivo ilícito. 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 runtime 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 classe de sistema em um URLClassLoader. Isso geralmente é feito por aplicativos e bibliotecas que desejam injetar classes no classpath em runtime. A hierarquia do carregador de classe foi alterada no Java 11. O carregador de classe do sistema (também conhecido como carregador de classe de aplicativo) agora é uma classe interna. A conversão em um URLClassLoader gerará um ClassCastException em runtime. O Java 11 não tem API para aumentar dinamicamente o classpath em runtime, mas pode ser feito por meio de reflexão, com as ressalvas óbvias sobre o uso da API interna.

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

Alterações de dados de localidade

A fonte padrão para dados de localidade no Java 11 foi alterada com JEP 252 para o Repositório de Dados de Localidade Comum do Consórcio Unicode. Isso pode ter um impacto na formatação localizada. Defina a propriedade java.locale.providers=COMPAT,SPI do sistema para reverter para o comportamento da localidade Java 8, se necessário.

Possíveis 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 exibirá Unrecognized option: ou Unrecognized VM option seguido pelo nome da opção problemática. 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 opções para registro de coleta de lixo em log. O registro em log do GC foi reimplementado no Java 9 para usar a estrutura de log de JVM unificada. Consulte a "Tabela 2-2 Mapear Sinalizadores de Registro de Coleta de Lixo Herdada em Log para a Configuração Xlog" na seção Habilitar o Registro em Log com a Estrutura Unificada de Registro em Log da JVM da Referência de Ferramentas do Java SE 11.

Avisos da VM

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

O Gerenciador de Opções de VM da página da Web fornece uma lista completa 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 de acesso reflexivo ilícito

Quando o código Java usa reflexão para acessar a API interna do JDK, o runtime 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 da reflexão. O pacote é encapsulado no módulo e é, basicamente, a API interna. O aviso pode ser ignorado como um primeiro esforço para começar a funcionar no Java 11. O runtime do Java 11 permite o acesso reflexivo para que o código herdado possa continuar funcionando.

Para resolver esse aviso, procure o código atualizado que não faça uso da API interna. Se o problema não puder ser resolvido com o código atualizado, a opção de linha de comando --add-exports ou --add-opens pode ser usada para abrir o acesso ao pacote. Essas opções permitem o acesso a tipos não exportados de um módulo para 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 API. Isso é conhecido como reflexão profunda. Nesse caso, use --add-opens para conceder acesso de código 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 opções --add-exports ou --add-opens devem ser consideradas como uma solução provisória, não uma solução de longo prazo. O uso dessas opções interrompe o encapsulamento do sistema de módulos, que se destina a impedir que a API interna do JDK seja usada. Se a API interna for removida ou for alterada, o aplicativo falhará. O acesso reflexivo será negado no Java 16, exceto quando o acesso for habilitado 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 sun.nio.ch pacote não é exportado pelo java.base módulo. Em outras palavras, não há nenhum exports sun.nio.ch; no module-info.java arquivo do módulo java.base. Isso pode ser resolvido com --add-exports=java.base/sun.nio.ch=ALL-UNNAMED. Classes que não são definidas em um módulo pertencem implicitamente ao módulo sem nome , literalmente nomeado ALL-UNNAMED.

java.lang.reflect.InaccessibleObjectException

Essa exceção indica que você está tentando chamar setAccessible(true) em um campo ou método de uma classe encapsulada. Você também pode receber um aviso de acesso reflexivo ilícito. Use a opção --add-opens para conceder acesso de código aos membros não públicos de um pacote. A mensagem de exceção informa 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 o 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 referenciar 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 você sabe que está no class-path não foi encontrada.

Esse problema só ocorrerá ao usar o caminho do módulo. O sistema de módulos Java otimiza a pesquisa de classe restringindo um pacote a um módulo nomeado . O runtime 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 causar erros de NoClassDefFound.

Uma maneira fácil de verificar se há um pacote dividido é conectar seu caminho de módulo e de classe ao jdeps e usar o caminho para o arquivos de classe de aplicativo como o <path>. 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 ao módulo nomeado.

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

Se o aplicativo for executado no Java 8, mas gerar um java.lang.NoClassDefFoundError ou um java.lang.ClassNotFoundException, então provavelmente o aplicativo estará 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 runtime ao seu projeto.

Módulo removido Pacote afetado Dependência sugerida
API Java para Serviços Web XML (JAX-WS) java.xml.ws JAX WS RI Runtime
Arquitetura java para associação XML (JAXB) java.xml.bind JAXB Runtime
JAV (Estrutura de Ativação do JavaBeans) java.activation Estrutura de Ativação do JavaBeans (TM)
Anotações comuns java.xml.ws.annotation API de Anotação javax
Arquitetura Comum de Corretor de Solicitação de Objetos (CORBA) java.corba GlassFish CORBA ORB
JTA (API de Transação Java) java.transaction API de transação Java

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

O suporte a -Xbootclasspath/p foi removido. Use --patch-module em seu lugar. A opção --patch-module é descrita no JEP 261. Procure pela seção rotulada como "Conteúdo do módulo de patch". --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. Primeiro, o sistema de módulos capturará a classe do módulo de patch. Esse é o mesmo efeito que acrescentar o bootclasspath no Java 8.

UnsupportedClassVersionError

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á em execução no Java 11 com um jar compilado com o JDK 13.

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

Próximas etapas

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