Eu tenho uma pequena biblioteca StreamEx que estende a API Java 8 Stream. Tradicionalmente, coleciono a biblioteca através do Maven e, na maioria das vezes, estou feliz com tudo. No entanto, eu queria experimentar.
Algumas coisas na biblioteca devem funcionar de maneira diferente em diferentes versões do Java. O exemplo mais impressionante é o novo método da API de fluxo, como o takeWhile
, que apareceu apenas no Java 9. Minha biblioteca também fornece uma implementação desses métodos no Java 8, mas quando você expande a API do fluxo, você sofre algumas restrições que não mencionarei aqui. Eu gostaria que os usuários do Java 9+ tivessem acesso a uma implementação padrão.
Para que o projeto continue compilando usando Java 8, isso geralmente é feito usando ferramentas de reflexão: descobrimos se existe um método correspondente na biblioteca padrão e, se assim for, o chamamos e, se não, usamos nossa implementação. No entanto, decidi usar a API MethodHandle , porque declara menos sobrecarga de chamada. Você pode obter o MethodHandle
antecipadamente e salvá-lo em um campo estático:
MethodHandles.Lookup lookup = MethodHandles.publicLookup(); MethodType type = MethodType.methodType(Stream.class, Predicate.class); MethodHandle method = null; try { method = lookup.findVirtual(Stream.class, "takeWhile", type); } catch (NoSuchMethodException | IllegalAccessException e) {
E então use-o:
if (method != null) { return (Stream<T>)method.invokeExact(stream, predicate); } else {
Tudo isso é bom, mas parece feio. E o mais importante, em todo momento em que é possível uma variação de implementações, você deve escrever essas condições. Uma abordagem ligeiramente alternativa é separar as estratégias do Java 8 e Java 9 como uma implementação da mesma interface. Ou, para salvar o tamanho da biblioteca, simplesmente implemente tudo para o Java 8 em uma classe não final separada e substitua um herdador pelo Java 9. Foi feito algo como isto:
Então, nos pontos de uso, você pode simplesmente escrever o return Internals.VER_SPEC.takeWhile(stream, predicate)
. Agora toda a mágica com alças de método está apenas na classe Java9Specific
. A propósito, essa abordagem salvou a biblioteca para usuários do Android que já haviam reclamado anteriormente que ela não funcionava em princípio. A máquina virtual Android não é Java, nem sequer implementa a especificação Java 7. Em particular, não há métodos com uma assinatura polimórfica como invokeExact
, e a própria presença dessa chamada no bytecode quebra tudo. Agora, essas chamadas são feitas em uma classe que nunca é inicializada.
No entanto, tudo isso ainda é feio. Uma solução bonita (pelo menos em teoria) é usar o Multi Release Jar, que acompanha o Java 9 ( JEP-238 ). Para fazer isso, parte das classes deve ser compilada no Java 9 e os arquivos de classe compilados colocados no META-INF/versions/9
dentro do arquivo Jar. Além disso, você precisa adicionar a linha Multi-Release: true
ao manifesto. Em seguida, o Java 8 ignorará tudo isso com êxito e o Java 9 e mais recente carregará novas classes em vez de classes com os mesmos nomes localizados no local usual.
A primeira vez que tentei fazer isso mais de dois anos atrás, pouco antes do lançamento do Java 9. Foi muito difícil e eu parei. Mesmo a compilação do projeto com o compilador Java 9 foi difícil: muitos plug-ins do Maven simplesmente quebraram devido a APIs internas alteradas java.version
string java.version
alterado ou qualquer outra coisa.
Uma nova tentativa este ano foi mais bem sucedida. Os plug-ins foram em sua maioria atualizados e funcionam adequadamente no novo Java. A primeira etapa foi traduzida do assembly inteiro para Java 11. Para isso, além de atualizar as versões do plug-in, tive que fazer o seguinte:
O próximo passo foi a adaptação dos testes. Como o comportamento da biblioteca é obviamente diferente no Java 8 e Java 9, seria lógico executar testes para ambas as versões. Agora, estamos executando tudo no Java 11, para que o código específico do Java 8 não seja testado. Este é um código bastante grande e não trivial. Para consertar isso, fiz uma caneta artificial:
static final VersionSpecific VER_SPEC = System.getProperty("java.version", "").compareTo("1.9") > 0 && !Boolean.getBoolean("one.util.streamex.emulateJava8") ? new Java9Specific() : new VersionSpecific();
Agora basta passar -Done.util.streamex.emulateJava8=true
ao executar os testes,
para testar o que geralmente funciona no Java 8. Agora, adicione um novo bloco <execution>
à configuração do maven-surefire-plugin
com argLine = -Done.util.streamex.emulateJava8=true
e os testes passam duas vezes.
No entanto, eu gostaria de considerar os testes de cobertura total. Eu uso o JaCoCo e, se você não contar nada a ele, a segunda execução simplesmente substituirá os resultados da primeira. Como funciona o JaCoCo? Primeiro, ele executa o destino do prepare-agent
, que define a propriedade argLine Maven, assinando algo como -javaagent:blah-blah/.m2/org/jacoco/org.jacoco.agent/0.8.4/org.jacoco.agent-0.8.4-runtime.jar=destfile=blah-blah/myproject/target/jacoco.exec
. Eu quero que dois arquivos exec diferentes sejam formados. Você pode hackear dessa maneira. Inclua destFile=${project.build.directory}
configuração do prepare-agent
. Áspero, mas eficaz. Agora o argLine
terminará em blah-blah/myproject/target
. Sim, este não é um arquivo, mas um diretório. Mas podemos substituir o nome do arquivo já no início dos testes. Retornamos ao maven-surefire-plugin
e configuramos argLine = @{argLine}/jacoco_java8.exec -Done.util.streamex.emulateJava8=true
para a execução do Java 8 e argLine = @{argLine}/jacoco_java11.exec
para a execução do Java 11. Depois, é fácil combinar esses dois arquivos usando o destino de merge
, que o plug-in JaCoCo também fornece, e obtemos a cobertura geral.
Atualização: godin nos comentários disse que era desnecessário, você pode gravar em um arquivo e colará automaticamente o resultado no anterior. Agora não sei por que esse cenário não funcionou para mim inicialmente.
Bem, estamos bem preparados para mudar para o Jar Multi-Release. Encontrei várias recomendações sobre como fazer isso. O primeiro sugeriu o uso de um projeto Maven multi-modular. Não me apetece: esta é uma grande complicação da estrutura do projeto: existem cinco pom.xml, por exemplo. Brincar por causa de alguns arquivos que precisam ser compilados no Java 9 parece ser demais. Outro sugeriu iniciar a compilação através do maven-antrun-plugin
. Aqui eu decidi olhar apenas como último recurso. É claro que qualquer problema no Maven pode ser resolvido usando o Ant, mas isso é de alguma forma bastante desajeitado. Por fim, vi uma recomendação para usar um plug-in de terceiros multi-release-jar-maven-plugin . Já parecia delicioso e certo.
O plug-in recomenda colocar códigos-fonte específicos para novas versões do Java em diretórios como src/main/java-mr/9
, o que eu fiz. Eu ainda decidi evitar colisões nos nomes de classe ao máximo, então a única classe (mesmo a interface) que está presente no Java 8 e Java 9, tenho o seguinte:
A velha constante mudou-se para um novo local, mas nada realmente mudou. Somente agora a classe Java9Specific
tornou muito mais simples: todos os agachamentos com MethodHandle foram substituídos com sucesso por chamadas de método diretas.
O plugin promete fazer o seguinte:
- Substitua o
maven-compiler-plugin
padrão e compile em duas etapas por uma versão de destino diferente. - Substitua o
maven-jar-plugin
padrão e maven-jar-plugin
resultado maven-jar-plugin
compilação pelos caminhos corretos. - Adicione a linha
Multi-Release: true
ao MANIFEST.MF
.
Para que ele funcionasse, foram necessárias algumas etapas.
Mude a embalagem do jar
para o jar
multi-release-jar
.
Adicione build-extension:
<build> <extensions> <extension> <groupId>pw.krejci</groupId> <artifactId>multi-release-jar-maven-plugin</artifactId> <version>0.1.5</version> </extension> </extensions> </build>
Copie a configuração do maven-compiler-plugin
. Eu tinha apenas a versão padrão no espírito de <source>1.8</source>
e <arg>-Xlint:all</arg>
Eu pensei que o maven-compiler-plugin
agora pode ser removido, mas o novo plugin não substitui a compilação de testes; portanto, a versão Java foi redefinida para o padrão (1.5!) E o -Xlint:all
argumentos desapareceram. Então eu tive que sair.
Para não duplicar a origem e o destino dos dois plugins, descobri que ambos respeitam as propriedades de maven.compiler.source
e maven.compiler.target
. Eu os instalei e removi as versões das configurações do plugin. No entanto, de repente, descobriu-se que o maven-javadoc-plugin
usa o source
- source
das configurações do maven-compiler-plugin
para descobrir a URL do JavaDoc padrão, que deve ser vinculada ao se referir a métodos padrão. E agora ele não respeita maven.compiler.source
. Portanto, tive que retornar <source>${maven.compiler.source}</source>
para as configurações do maven-compiler-plugin
. Felizmente, nenhuma outra alteração foi necessária para gerar o JavaDoc. Pode muito bem ser gerado a partir de fontes Java 8, porque todo o carrossel com versões não afeta a API da biblioteca.
O maven-bundle-plugin
, o que transformou minha biblioteca em um artefato OSGi. Ele simplesmente se recusou a trabalhar com packaging = multi-release-jar
. Em princípio, nunca gostei dele. Ele escreve um conjunto de linhas adicionais no manifesto, ao mesmo tempo estragando a ordem de classificação e adicionando mais lixo. Felizmente, descobriu-se que não é difícil se livrar dele escrevendo tudo à mão. Apenas, é claro, não no maven-jar-plugin
, mas no novo. Toda a configuração do plugin multi-release-jar
acabou se tornando assim (eu defini algumas propriedades como project.package
eu mesmo):
<plugin> <groupId>pw.krejci</groupId> <artifactId>multi-release-jar-maven-plugin</artifactId> <version>0.1.5</version> <configuration> <compilerArgs><arg>-Xlint:all</arg></compilerArgs> <archive> <manifestEntries> <Automatic-Module-Name>${project.package}</Automatic-Module-Name> <Bundle-Name>${project.name}</Bundle-Name> <Bundle-Description>${project.description}</Bundle-Description> <Bundle-License>${license.url}</Bundle-License> <Bundle-ManifestVersion>2</Bundle-ManifestVersion> <Bundle-SymbolicName>${project.package}</Bundle-SymbolicName> <Bundle-Version>${project.version}</Bundle-Version> <Export-Package>${project.package};version="${project.version}"</Export-Package> </manifestEntries> </archive> </configuration> </plugin>
Testes. Não temos mais one.util.streamex.emulateJava8
, mas você pode obter o mesmo efeito modificando os testes de caminho de classe. Agora, o oposto é verdadeiro: por padrão, a biblioteca funciona no modo Java 8 e, para o Java 9, você precisa escrever:
<classesDirectory>${basedir}/target/classes-9</classesDirectory> <additionalClasspathElements>${project.build.outputDirectory}</additionalClasspathElements> <argLine>@{argLine}/jacoco_java9.exec</argLine>
Um ponto importante: as classes-9
devem seguir os arquivos de classes comuns, portanto tivemos que transferir os usuais para os additionalClasspathElements
, que são adicionados depois.
Fontes. Vou usar o jar de código-fonte e seria interessante incluir fontes do Java 9 para que, por exemplo, o depurador no IDE possa exibi-las corretamente. Não estou muito preocupado com o VerSpec
duplicado, porque há uma linha que é executada apenas durante a inicialização. Não há problema em deixar apenas a opção do Java 8. No entanto, seria bom Java9Specific.java
. Isso pode ser feito adicionando manualmente um diretório de origem adicional:
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <phase>test</phase> <goals><goal>add-source</goal></goals> <configuration> <sources> <source>src/main/java-mr/9</source> </sources> </configuration> </execution> </executions> </plugin>
Depois de coletar o artefato, conectei-o a um projeto de teste e o verifiquei no depurador IntelliJ IDEA. Tudo funciona lindamente: dependendo da versão da máquina virtual usada para executar o projeto de teste, acabamos em uma fonte diferente durante a depuração.
Seria legal fazer isso pelo próprio plugin multi-release-jar, então fiz uma sugestão.
JaCoCo. Acabou sendo o mais difícil para ele, e eu não poderia prescindir de ajuda externa. O fato é que o plug-in gerava perfeitamente os arquivos exec para Java-8 e Java-9, normalmente os colava em um arquivo; no entanto, ao gerar relatórios em XML e HTML, eles teimosamente ignoravam as fontes do Java-9. Revistando a fonte , vi que ela gera apenas um relatório para os arquivos de classe encontrados em project.getBuild().getOutputDirectory()
. Esse diretório, é claro, pode ser substituído, mas na verdade eu tenho dois deles: classes
e classes-9
. Teoricamente, você pode copiar todas as classes em um diretório, alterar o outputDirectory
e iniciar o JaCoCo e, em seguida, alterar o outputDirectory
volta para não interromper o assembly JAR. Mas isso parece completamente feio. Em geral, decidi adiar a solução para esse problema no meu projeto por enquanto, mas escrevi para a equipe da JaCoCo que seria bom poder especificar vários diretórios com arquivos de classe.
Para minha surpresa, poucas horas depois, um dos desenvolvedores da JaCoCo godin veio ao meu projeto e trouxe uma solicitação de recebimento, que resolve o problema. Como isso decide? Usando Ant, é claro! Verificou-se que o plugin Ant para JaCoCo é mais avançado e pode gerar um relatório de resumo para vários diretórios de origem e arquivos de classe. Ele nem precisou de uma etapa de merge
separada, porque podia alimentar vários arquivos exec de uma só vez. Em geral, Ant não pode ser evitado, que assim seja. A principal coisa que funcionou e o pom.xml cresceu apenas seis linhas.

Eu até twitei em corações:
Então, eu tenho um projeto muito funcional que constrói um lindo Jar Multi-Release. Ao mesmo tempo, a porcentagem de cobertura aumentou até porque removi todos os tipos de catch (NoSuchMethodException | IllegalAccessException e)
que eram inatingíveis no Java 9. Infelizmente, essa estrutura de projeto não é suportada pelo IntelliJ IDEA, então tive que abandonar a importação de POM e configurar o projeto no IDE manualmente. . Espero que, no futuro, ainda exista uma solução padrão que seja automaticamente suportada por todos os plugins e ferramentas.