Lombok, sources.jar e depuração conveniente

Em nossa equipe, realmente amamos o Lombok . Permite escrever menos código e refatorar menos, o que é ideal para desenvolvedores preguiçosos. Mas se, além do artefato do projeto, você também publicar o código-fonte com a documentação, poderá encontrar um problema - o código-fonte não coincidirá com o bytecode. Vou falar sobre como resolvemos esse problema e que dificuldades encontramos no processo neste post.



A propósito, se você escreve em Java e, por algum motivo, ainda não usa o Lombok em seu projeto, recomendo que você leia os artigos sobre Habré ( um e dois ). Tenho certeza que você vai gostar!

O problema


O projeto em que estamos trabalhando consiste em vários módulos. Alguns deles (vamos chamá-los de back-end condicional) quando o release é lançado são empacotados no arquivo morto (entrega), carregados no repositório e subsequentemente implantados nos servidores de aplicativos. A outra parte é a chamada módulo cliente - publicado no repositório como um conjunto de artefatos, incluindo sources.jar e javadoc.jar. Usamos Lombok em todas as partes, e Maven fará tudo isso.

Há algum tempo, um dos consumidores de nosso serviço solucionou um problema - ele tentou depurar nosso módulo, mas não conseguiu, porque O sources.jar não possuía métodos (e nem classes) nos quais ele gostaria de definir o ponto de interrupção. Em nossa equipe, acreditamos que uma tentativa de identificar e resolver um problema de forma independente, em vez de fazer um defeito sem pensar, é um ato de um marido digno que precisa ser incentivado! :-) Portanto, foi decidido trazer o sources.jar de acordo com o bytecode.

Exemplo


Vamos imaginar que temos um aplicativo simples que consiste em duas classes:

SomePojo.java
package com.github.monosoul.lombok.sourcesjar; import lombok.Builder; import lombok.Value; @Value @Builder(toBuilder = true) class SomePojo { /** * Some string field */ String someStringField; /** * Another string field */ String anotherStringField; } 


Main.java
 package com.github.monosoul.lombok.sourcesjar; import lombok.val; public final class Main { public static void main(String[] args) { if (args.length != 2) { throw new IllegalArgumentException("Wrong arguments!"); } val pojo = SomePojo.builder() .someStringField(args[0]) .anotherStringField(args[1]) .build(); System.out.println(pojo); } } 


E nosso aplicativo é construído usando o Maven:

pom.xml
 <?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> <artifactId>lombok-sourcesjar</artifactId> <groupId>com.github.monosoul</groupId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.2</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.1</version> <configuration> <archive> <manifest> <mainClass>com.github.monosoul.lombok.sourcesjar.Main</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.0.1</version> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project> 


Se você compilar este projeto ( mvn compile ) e, em seguida, descompilar o bytecode resultante, a classe SomePojo terá a seguinte aparência:

SomePojo.class
 package com.github.monosoul.lombok.sourcesjar; final class SomePojo { private final String someStringField; private final String anotherStringField; SomePojo(String someStringField, String anotherStringField) { this.someStringField = someStringField; this.anotherStringField = anotherStringField; } public static SomePojo.SomePojoBuilder builder() { return new SomePojo.SomePojoBuilder(); } public SomePojo.SomePojoBuilder toBuilder() { return (new SomePojo.SomePojoBuilder()).someStringField(this.someStringField).anotherStringField(this.anotherStringField); } public String getSomeStringField() { return this.someStringField; } public String getAnotherStringField() { return this.anotherStringField; } public boolean equals(Object o) { if (o == this) { return true; } else if (!(o instanceof SomePojo)) { return false; } else { SomePojo other = (SomePojo)o; Object this$someStringField = this.getSomeStringField(); Object other$someStringField = other.getSomeStringField(); if (this$someStringField == null) { if (other$someStringField != null) { return false; } } else if (!this$someStringField.equals(other$someStringField)) { return false; } Object this$anotherStringField = this.getAnotherStringField(); Object other$anotherStringField = other.getAnotherStringField(); if (this$anotherStringField == null) { if (other$anotherStringField != null) { return false; } } else if (!this$anotherStringField.equals(other$anotherStringField)) { return false; } return true; } } public int hashCode() { int PRIME = true; int result = 1; Object $someStringField = this.getSomeStringField(); int result = result * 59 + ($someStringField == null ? 43 : $someStringField.hashCode()); Object $anotherStringField = this.getAnotherStringField(); result = result * 59 + ($anotherStringField == null ? 43 : $anotherStringField.hashCode()); return result; } public String toString() { return "SomePojo(someStringField=" + this.getSomeStringField() + ", anotherStringField=" + this.getAnotherStringField() + ")"; } public static class SomePojoBuilder { private String someStringField; private String anotherStringField; SomePojoBuilder() { } public SomePojo.SomePojoBuilder someStringField(String someStringField) { this.someStringField = someStringField; return this; } public SomePojo.SomePojoBuilder anotherStringField(String anotherStringField) { this.anotherStringField = anotherStringField; return this; } public SomePojo build() { return new SomePojo(this.someStringField, this.anotherStringField); } public String toString() { return "SomePojo.SomePojoBuilder(someStringField=" + this.someStringField + ", anotherStringField=" + this.anotherStringField + ")"; } } } 


Bem diferente do que entra em nosso sources.jar, não é? ;) Como você pode ver, se você conectasse o código-fonte para a depuração do SomePojo e desejasse definir o ponto de interrupção, por exemplo, no construtor, haveria um problema - não há lugar para definir o ponto de interrupção, e a classe SomePojoBuilder não existe.

O que fazer sobre isso?


Como costuma acontecer, esse problema tem várias soluções. Vamos olhar para cada um deles.

Não use Lombok


Quando encontramos esse problema, conversávamos sobre um módulo no qual havia apenas algumas classes usando o Lombok. É claro que eu não queria recusar, então pensei imediatamente em fazer o delombok. Depois de investigar essa pergunta, encontrei algumas soluções estranhas usando o plugin Lombok do Maven - lombok-maven-plugin . Em um deles, foi sugerido, por exemplo, manter as fontes em que o Lombok é usado em um diretório separado para o qual o delombok será executado, e as fontes geradas irão para fontes geradas, de onde serão compiladas e acessadas para sources.jar. Essa opção provavelmente está funcionando, mas, neste caso, o destaque da sintaxe nas fontes originais não funcionará no IDE, pois um diretório com eles não será considerado um diretório de origem. Essa opção não me agradou e, como o custo de abandonar o Lombok neste módulo era baixo, foi decidido não perder tempo com isso, desabilitar o Lombok e simplesmente gerar os métodos necessários por meio do IDE.

Em geral, parece-me que essa opção tem direito à vida, mas apenas se as aulas que usam Lombok forem realmente pequenas e raramente mudarem.


Delombok plugin + sources.jar compilação usando Ant


Depois de algum tempo, tive que voltar a esse problema novamente, quando já se tratava de um módulo no qual o Lombok era usado com muito mais intensidade. Voltando ao estudo desse problema, deparei-me com uma pergunta sobre o stackoverflow , que sugeria executar o delombok para as fontes e, em seguida, usar fontes no Ant para gerar sources.jar.
Aqui você precisa discernir por que precisa gerar o sources.jar usando o Ant, em vez de usar o plug-in Source ( maven-source-plugin ). O fato é que você não pode configurar o diretório de origem para este plug-in. Ele sempre usará o conteúdo da propriedade sourceDirectory do projeto.

Portanto, no caso do nosso exemplo, o pom.xml ficará assim:

pom.xml
 <?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> <artifactId>lombok-sourcesjar</artifactId> <groupId>com.github.monosoul</groupId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <lombok.version>1.18.2</lombok.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.1</version> <configuration> <archive> <manifest> <mainClass>com.github.monosoul.lombok.sourcesjar.Main</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.projectlombok</groupId> <artifactId>lombok-maven-plugin</artifactId> <version>${lombok.version}.0</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>delombok</goal> </goals> </execution> </executions> <configuration> <sourceDirectory>src/main/java</sourceDirectory> <outputDirectory>${project.build.directory}/delombok</outputDirectory> <addOutputDirectory>false</addOutputDirectory> <encoding>UTF-8</encoding> <formatPreferences> <generateDelombokComment>skip</generateDelombokComment> </formatPreferences> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <id>copy-to-lombok-build</id> <phase>process-resources</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <resources> <resource> <directory>${project.basedir}/src/main/resources</directory> </resource> </resources> <outputDirectory>${project.build.directory}/delombok</outputDirectory> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <version>1.8</version> <executions> <execution> <id>generate-delomboked-sources-jar</id> <phase>package</phase> <goals> <goal>run</goal> </goals> <configuration> <target> <jar destfile="${project.build.directory}/${project.build.finalName}-sources.jar" basedir="${project.build.directory}/delombok"/> </target> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> <executions> <execution> <id>install-source-jar</id> <goals> <goal>install-file</goal> </goals> <phase>install</phase> <configuration> <file>${project.build.directory}/${project.build.finalName}-sources.jar</file> <classifier>sources</classifier> <generatePom>true</generatePom> <pomFile>${project.basedir}/pom.xml</pomFile> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-deploy-plugin</artifactId> <version>3.0.0-M1</version> <executions> <execution> <id>deploy-source-jar</id> <goals> <goal>deploy-file</goal> </goals> <phase>deploy</phase> <configuration> <file>${project.build.directory}/${project.build.finalName}-sources.jar</file> <classifier>sources</classifier> <generatePom>true</generatePom> <pomFile>${project.basedir}/pom.xml</pomFile> <repositoryId>someRepoId</repositoryId> <url>some://repo.url</url> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> 


Como você pode ver, a configuração cresceu muito e está longe de ser apenas o lombok-maven-plugin e o maven-antrun-plugin . Por que isso aconteceu? O fato é que, agora que coletamos o sources.jar com o Ant, o Maven não sabe nada sobre esse artefato. E precisamos dizer claramente a ele como instalar esse artefato, como implantá-lo e como empacotar recursos nele.

Além disso, descobri que, ao executar o delombok por padrão, o Lombok adiciona um comentário ao cabeçalho dos arquivos gerados. Nesse caso, o formato dos arquivos gerados é controlado não usando as opções no arquivo lombok.config , mas usando as opções do plug-in. A lista dessas opções não foi fácil de encontrar. Era possível, é claro, chamar o apelido do jar de Lombok com as teclas delombok e --help , mas eu sou muito preguiçoso para esse programador, então eu os encontrei nos códigos-fonte no github .

Mas nem o volume da configuração, nem seus recursos podem ser comparados com a principal desvantagem desse método. Ele não resolve o problema . O bytecode é compilado a partir de uma fonte, enquanto outros acessam o sources.jar. E, apesar do delombok ser executado pelo mesmo Lombok, ainda haverá diferenças entre o bytecode e as fontes geradas, ou seja, eles ainda não são adequados para depuração. Para dizer o mínimo, fiquei chateado quando percebi isso.


Delombok plugin + perfil em maven


Então o que fazer? Eu tinha o sources.jar com as fontes "corretas", mas elas ainda eram diferentes do bytecode. Em princípio, o problema poderia ser resolvido pela compilação a partir de códigos-fonte gerados como resultado do delombok. Mas o problema é que o maven-compiler-plugin 'não pode especificar o caminho para a fonte. Ele sempre usa as fontes especificadas no sourceDirectory projeto, como o maven-source-plugin . Poderíamos indicar o diretório em que as fontes delomboked são geradas, mas, neste caso, ao importar um projeto para o IDE, o diretório com fontes reais não será considerado como tal e o realce da sintaxe e outros recursos não funcionarão para arquivos nele. Esta opção também não me agradou.

Você pode usar perfis! Crie um perfil que seria usado apenas na criação do projeto e no qual o valor do sourceDirectory seria substituído! Mas há uma nuance. A tag sourceDirectory pode ser declarada apenas dentro da tag build na raiz do projeto.

Felizmente, há uma solução alternativa para esse problema. Você pode declarar uma propriedade que será substituída na tag sourceDirectory e alterar o valor dessa propriedade no perfil!

Nesse caso, a configuração do projeto terá a seguinte aparência:

pom.xml
 <?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> <artifactId>lombok-sourcesjar</artifactId> <groupId>com.github.monosoul</groupId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <lombok.version>1.18.2</lombok.version> <origSourceDir>${project.basedir}/src/main/java</origSourceDir> <sourceDir>${origSourceDir}</sourceDir> <delombokedSourceDir>${project.build.directory}/delombok</delombokedSourceDir> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> </dependencies> <profiles> <profile> <id>build</id> <properties> <sourceDir>${delombokedSourceDir}</sourceDir> </properties> </profile> </profiles> <build> <sourceDirectory>${sourceDir}</sourceDirectory> <plugins> <plugin> <groupId>org.projectlombok</groupId> <artifactId>lombok-maven-plugin</artifactId> <version>${lombok.version}.0</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>delombok</goal> </goals> </execution> </executions> <configuration> <sourceDirectory>${origSourceDir}</sourceDirectory> <outputDirectory>${delombokedSourceDir}</outputDirectory> <addOutputDirectory>false</addOutputDirectory> <encoding>UTF-8</encoding> <formatPreferences> <generateDelombokComment>skip</generateDelombokComment> <javaLangAsFQN>skip</javaLangAsFQN> </formatPreferences> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.1</version> <configuration> <archive> <manifest> <mainClass>com.github.monosoul.lombok.sourcesjar.Main</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.0.1</version> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project> 


Funciona da seguinte maneira. Na propriedade origSourceDir substituímos o caminho do diretório pelas fontes originais e, na propriedade sourceDir substituímos o valor de origSourceDir . Na propriedade delombokedSourceDir especificamos o caminho para as fontes geradas pelo delombok. Portanto, ao importar um projeto para o IDE, o diretório origSourceDir e, ao criar o projeto, se você especificar o perfil de build , o diretório delombokedSourceDir será usado.

Como resultado, obtemos um bytecode compilado das mesmas fontes que caem em sources.jar, ou seja, depuração finalmente funcionará. Nesse caso, não precisamos configurar a instalação e a implementação do artefato com as fontes, pois usamos o maven-source-plugin para gerar o artefato. É verdade que a magia com variáveis ​​pode confundir uma pessoa não familiarizada com as nuances de Maven.

Você lombok.config pode adicionar a opção lombok.addJavaxGeneratedAnnotation = true no lombok.config e a anotação @javax.annotation.Generated("lombok") ficará acima do código gerado nas fontes geradas, o que ajudará a evitar perguntas como "Por que seu código parece tão estranho? ! " :)


Use gradle


Acho que se você já conhece o Gradle , não deve explicar todas as vantagens do Maven. Se você ainda não está familiarizado com ele, existe um hub inteiro no hub Um ótimo motivo para investigar! :)
Em geral, quando pensei em usar o Gradle, esperava que fosse muito mais fácil fazer o que eu precisava, porque sabia que nele poderia facilmente apontar o que construir o sources.jar e o que compilar no bytecode. O problema estava me esperando onde eu esperava menos - não há nenhum plugin delombok para o Gradle.

Existe esse plug-in , mas parece que você não pode especificar opções para formatar fontes delomboked nele, o que não combina comigo.

outro plug - in , ele gera um arquivo de texto a partir das opções passadas para ele e o passa como argumento para o lombok.jar. Não consegui fazê-lo colocar as fontes geradas no diretório certo, parece que isso se deve ao fato de o caminho no arquivo de texto com os argumentos não estar entre aspas e não ter escapado adequadamente. Talvez mais tarde eu faça uma solicitação pull ao autor do plugin com uma sugestão para uma correção.

No final, decidi seguir o outro caminho e apenas descrevi uma tarefa com Ant chamando para executar o delombok, no Lombok há apenas uma tarefa Ant para isso , e parece muito bom:

build.gradle.kts
 group = "com.github.monosoul" version = "1.0.0" plugins { java } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } dependencies { val lombokDependency = "org.projectlombok:lombok:1.18.2" annotationProcessor(lombokDependency) compileOnly(lombokDependency) } repositories { mavenCentral() } tasks { "jar"(Jar::class) { manifest { attributes( Pair("Main-Class", "com.github.monosoul.lombok.sourcesjar.Main") ) } } val delombok by creating { group = "lombok" val delombokTarget by extra { File(buildDir, "delomboked") } doLast({ ant.withGroovyBuilder { "taskdef"( "name" to "delombok", "classname" to "lombok.delombok.ant.Tasks\$Delombok", "classpath" to sourceSets.main.get().compileClasspath.asPath) "mkdir"("dir" to delombokTarget) "delombok"( "verbose" to false, "encoding" to "UTF-8", "to" to delombokTarget, "from" to sourceSets.main.get().java.srcDirs.first().absolutePath ) { "format"("value" to "generateDelombokComment:skip") "format"("value" to "generated:generate") "format"("value" to "javaLangAsFQN:skip") } } }) } register<Jar>("sourcesJar") { dependsOn(delombok) val delombokTarget: File by delombok.extra from(delombokTarget) archiveClassifier.set("sources") } withType(JavaCompile::class) { dependsOn(delombok) val delombokTarget: File by delombok.extra source = fileTree(delombokTarget) } } 


Como resultado, esta opção é equivalente à anterior.


Conclusões


Uma tarefa bastante trivial, de fato, acabou sendo uma série de tentativas de encontrar soluções alternativas para decisões estranhas dos autores do Maven. Quanto a mim - o script Gradle, no contexto das configurações resultantes do Maven, parece muito mais óbvio e lógico. No entanto, talvez eu simplesmente não consegui encontrar uma solução melhor? Diga-nos nos comentários se você resolveu um problema semelhante e, em caso afirmativo, como.

Obrigado pela leitura!

Código fonte

Source: https://habr.com/ru/post/pt438548/


All Articles