Lombok, sources.jar y depuraci贸n conveniente

En nuestro equipo, realmente amamos a Lombok . Le permite escribir menos c贸digo y refactorizar menos, lo cual es ideal para desarrolladores perezosos. Pero si, adem谩s del artefacto del proyecto, publica el c贸digo fuente con la documentaci贸n tambi茅n, entonces puede encontrarse con un problema: el c贸digo fuente no coincidir谩 con el c贸digo de bytes. Hablar茅 sobre c贸mo resolvimos este problema y qu茅 dificultades encontramos en el proceso en esta publicaci贸n.



Por cierto, si escribe en Java y, por alguna raz贸n, todav铆a no utiliza Lombok en su proyecto, le recomiendo que lea los art铆culos sobre Habr茅 ( uno y dos ). Estoy seguro de que te gustar谩!

El problema


El proyecto en el que estamos trabajando consta de varios m贸dulos. Algunos de ellos (llam茅moslos condicionalmente backend) cuando se lanza la versi贸n se empaquetan en el archivo (entrega), se cargan en el repositorio y posteriormente se implementan en los servidores de aplicaciones. La otra parte es la llamada M贸dulo cliente: publicado en el repositorio como un conjunto de artefactos, incluidos sources.jar y javadoc.jar. Usamos Lombok en todas partes, y Maven lo har谩 todo.

Hace alg煤n tiempo, uno de los consumidores de nuestro servicio abord贸 un problema: intent贸 depurar nuestro m贸dulo, pero no pudo hacerlo, porque sources.jar carec铆a de m茅todos (e incluso clases) en los que le gustar铆a establecer un punto de interrupci贸n. 隆En nuestro equipo creemos que un intento de identificar y resolver un problema de forma independiente, en lugar de hacer un defecto sin pensar, es un acto de un marido digno que debe ser alentado! :-) Por lo tanto, se decidi贸 traer sources.jar de acuerdo con el c贸digo de bytes.

Ejemplo


Imaginemos que tenemos una aplicaci贸n simple que consta de dos clases:

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); } } 


Y nuestra aplicaci贸n est谩 construida con 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> 


Si compila este proyecto ( mvn compile ) y luego descompila el c贸digo de bytes resultante, la clase SomePojo se ver谩 as铆:

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 + ")"; } } } 


Bastante diferente de lo que entra en nuestras fuentes .jar, 驴no? ;) Como puede ver, si conect贸 el c贸digo fuente para la depuraci贸n de SomePojo y quer铆a establecer un punto de interrupci贸n, por ejemplo, en el constructor, entonces se encontrar铆a con un problema: no hay ning煤n lugar para establecer el punto de interrupci贸n, y la clase SomePojoBuilder no est谩 all铆 en absoluto.

驴Qu茅 hacer al respecto?


Como suele suceder, este problema tiene varias soluciones. Veamos cada uno de ellos.

No uses Lombok


Cuando encontramos este problema por primera vez, est谩bamos hablando de un m贸dulo en el que solo hab铆a un par de clases que usaban Lombok. Por supuesto, no quer铆a rechazarlo, as铆 que inmediatamente pens茅 en hacer delombok. Despu茅s de investigar esta pregunta, encontr茅 algunas soluciones extra帽as con el complemento Lombok de Maven : lombok-maven-plugin . En uno de ellos se sugiri贸, por ejemplo, mantener las fuentes en las que se utiliza Lombok en un directorio separado para el que se ejecutar谩 delombok, y las fuentes generadas ir谩n a las fuentes generadas, desde donde se compilar谩 y se dirigir谩 a sources.jar. Esta opci贸n probablemente est茅 funcionando, pero en este caso el resaltado de sintaxis en las fuentes originales no funcionar谩 en el IDE, ya que un directorio con ellos no se considerar谩 un directorio fuente. Esta opci贸n no me conven铆a, y dado que el costo de abandonar Lombok en este m贸dulo era bajo, se decidi贸 no perder tiempo en esto, deshabilitar Lombok y simplemente generar los m茅todos necesarios a trav茅s del IDE.

En general, me parece que esta opci贸n tiene derecho a la vida, pero solo si las clases que usan Lombok son realmente peque帽as y rara vez cambian.


Delombok plugin + sources.jar build usando Ant


Despu茅s de un tiempo, tuve que volver a este problema nuevamente, cuando ya se trataba de un m贸dulo en el que Lombok se usaba mucho m谩s intensamente. Volviendo nuevamente al estudio de este problema, me encontr茅 con una pregunta sobre stackoverflow , que suger铆a ejecutar delombok para las fuentes y luego usar fuentes en Ant generate sources.jar.
Aqu铆 debe divagar acerca de por qu茅 necesita generar sources.jar usando Ant, en lugar de usar el complemento Source ( maven-source-plugin ). El hecho es que no puede configurar el directorio de origen para este complemento. Siempre usar谩 el contenido de la propiedad sourceDirectory del proyecto.

Entonces, en el caso de nuestro ejemplo, pom.xml se ver谩 as铆:

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 puede ver, la configuraci贸n ha crecido mucho, y est谩 lejos de ser solo lombok-maven-plugin y maven-antrun-plugin . 驴Por qu茅 sucedi贸 esto? El hecho es que, dado que ahora recopilamos sources.jar con Ant, Maven no sabe nada sobre este artefacto. Y tenemos que decirle claramente c贸mo instalar este artefacto, c贸mo implementarlo y c贸mo empaquetar recursos en 茅l.

Adem谩s, descubr铆 que al ejecutar delombok por defecto, Lombok agrega un comentario al encabezado de los archivos generados. En este caso, el formato de los archivos generados se controla no usando las opciones en el archivo lombok.config , sino usando las opciones del complemento. La lista de estas opciones no fue f谩cil de encontrar. Era posible, por supuesto, llamar al apodo jar de Lombok con las teclas delombok y --help , pero soy demasiado vago para este programador, as铆 que los encontr茅 en los c贸digos fuente en el github .

Pero ni el volumen de la configuraci贸n ni sus caracter铆sticas se pueden comparar con el principal inconveniente de este m茅todo. El no resuelve el problema . Bytecode se compila a partir de una fuente, mientras que otras ingresan en sources.jar. Y a pesar del hecho de que delombok es ejecutado por el mismo Lombok, todav铆a habr谩 diferencias entre el c贸digo de bytes y las fuentes generadas, es decir. Todav铆a no son adecuados para la depuraci贸n. Para decirlo suavemente, me molest茅 cuando me di cuenta de esto.


Complemento Delombok + perfil en maven


Entonces que hacer? Ten铆a sources.jar con las fuentes "correctas", pero a煤n eran diferentes del bytecode. En principio, el problema podr铆a resolverse compilando a partir de c贸digos fuente generados como resultado de delombok. Pero el problema es que maven-compiler-plugin 'no puede especificar la ruta a la fuente. Siempre usa las fuentes especificadas en el sourceDirectory proyecto, como el maven-source-plugin . Podr铆amos indicar el directorio donde se generan las fuentes no combinadas, pero en este caso, al importar un proyecto al IDE, el directorio con fuentes reales no se considerar谩 como tal y el resaltado de sintaxis y otras caracter铆sticas no funcionar谩n para los archivos que contiene. Esta opci贸n tampoco me conven铆a.

隆Puedes usar perfiles! 隆Cree un perfil que se usar铆a solo al construir el proyecto y en el que se reemplazar铆a el valor de sourceDirectory ! Pero hay un matiz. La etiqueta sourceDirectory solo se puede declarar dentro de la etiqueta de build en la ra铆z del proyecto.

Afortunadamente, hay una soluci贸n para este problema. 隆Puede declarar una propiedad que se sustituir谩 en la etiqueta sourceDirectory y cambiar el valor de esta propiedad en el perfil!

En este caso, la configuraci贸n del proyecto se ver谩 as铆:

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 de la siguiente manera. En la propiedad origSourceDir sustituimos la ruta al directorio con las fuentes originales, y en la propiedad sourceDir sustituimos el valor de origSourceDir . En la propiedad delombokedSourceDir especificamos la ruta a las fuentes generadas por delombok. Por lo tanto, al importar un proyecto en el IDE, origSourceDir el directorio de origSourceDir , y al origSourceDir el proyecto, si especifica el perfil de build , se delombokedSourceDir directorio delombokedSourceDir .

Como resultado, obtenemos un c贸digo de bytes compilado de las mismas fuentes que caer谩 en sources.jar, es decir. la depuraci贸n finalmente funcionar谩. En este caso, no necesitamos configurar la instalaci贸n y la implementaci贸n del artefacto con las fuentes, ya que usamos el maven-source-plugin para generar el artefacto. Es cierto que la magia con variables puede confundir a una persona que no est谩 familiarizada con los matices de Maven.

Tambi茅n puede agregar la opci贸n lombok.addJavaxGeneratedAnnotation = true en lombok.config , luego la anotaci贸n @javax.annotation.Generated("lombok") ubicar谩 por encima del c贸digo generado en las fuentes generadas, lo que ayudar谩 a evitar preguntas como "驴Por qu茅 su c贸digo se ve tan extra帽o? ! " :)


Utilizar gradle


Creo que si ya est谩s familiarizado con Gradle , entonces no deber铆as explicar todas sus ventajas sobre Maven. Si a煤n no est谩 familiarizado con 茅l, entonces hay un centro completo en el centro 隆Una gran raz贸n para investigarlo! :)
En general, cuando pens茅 en usar Gradle, esperaba que fuera mucho m谩s f谩cil hacer lo que necesitaba en 茅l, porque sab铆a que en 茅l pod铆a se帽alar f谩cilmente de qu茅 construir sources.jar y qu茅 compilar a bytecode. El problema me estaba esperando donde menos esperaba: no hay un complemento delombok que funcione para Gradle.

Existe este complemento , pero parece que no puede especificar opciones para formatear fuentes no combinadas en 茅l, lo que no me conven铆a.

Hay otro complemento , genera un archivo de texto a partir de las opciones que se le pasan y luego lo pasa como argumento a lombok.jar. No pude lograr que coloque las fuentes generadas en el directorio correcto, parece que esto se debe al hecho de que la ruta en el archivo de texto con los argumentos no se cita y no se escapa correctamente. Tal vez m谩s tarde har茅 una solicitud de extracci贸n al autor del complemento con una sugerencia para una soluci贸n.

Al final, decid铆 ir hacia otro lado y acabo de describir una tarea con Ant llamando a ejecutar delombok, en Lombok solo hay una tarea Ant para esto , y se ve bastante bien:

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 opci贸n es equivalente a la anterior.


Conclusiones


Una tarea bastante trivial, de hecho, result贸 ser una serie de intentos de encontrar soluciones a las decisiones extra帽as de los autores de Maven. En cuanto a m铆, el script Gradle, en el contexto de las configuraciones de Maven resultantes, parece mucho m谩s obvio y l贸gico. Sin embargo, 驴tal vez no podr铆a encontrar una mejor soluci贸n? D铆ganos en los comentarios si resolvi贸 un problema similar y, de ser as铆, c贸mo.

Gracias por leer!

C贸digo fuente

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


All Articles