Lombok, sources.jar et débogage pratique

Dans notre équipe, nous aimons vraiment Lombok . Il vous permet d'écrire moins de code et de refactoriser moins, ce qui est idéal pour les développeurs paresseux. Mais si, en plus de l'artefact du projet, vous publiez également le code source avec la documentation, vous pouvez rencontrer un problème - le code source ne coïncidera pas avec le bytecode. Je vais parler de la façon dont nous avons résolu ce problème et des difficultés rencontrées dans le processus dans ce post.



Soit dit en passant, si vous écrivez en Java et que vous n'utilisez toujours pas Lombok dans votre projet, je vous recommande de lire les articles sur Habré ( un et deux ). Je suis sûr que vous l'aimerez!

Le problème


Le projet sur lequel nous travaillons se compose de plusieurs modules. Certains d'entre eux (appelons-les conditionnellement backend) lorsque la version est publiée sont empaquetés dans l'archive (livraison), téléchargés dans le référentiel et ensuite déployés sur les serveurs d'applications. L'autre partie est la soi-disant module client - publié dans le référentiel sous la forme d'un ensemble d'artefacts, y compris sources.jar et javadoc.jar. Nous utilisons Lombok dans toutes les parties, et Maven va tout faire.

Il y a quelque temps, l'un des consommateurs de notre service a rencontré un problème - il a essayé de déboguer notre module, mais n'a pas pu le faire, car sources.jar manquait de méthodes (et même de classes) dans lesquelles il aimerait définir un point d'arrêt. Dans notre équipe, nous pensons qu'une tentative d'identifier et de résoudre un problème de manière indépendante, au lieu de faire un défaut sans réfléchir, est un acte d'un mari digne qui doit être encouragé! :-) Par conséquent, il a été décidé de mettre sources.jar en conformité avec le bytecode.

Exemple


Imaginons que nous ayons une application simple composée de deux 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); } } 


Et notre application est construite en utilisant 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 vous compilez ce projet ( mvn compile ), puis décompilez le bytecode résultant, la classe SomePojo ressemblera à ceci:

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


Assez différent de ce qui entre dans notre sources.jar, n'est-ce pas? ;) Comme vous pouvez le voir, si vous connectez le code source du débogage SomePojo et que vous souhaitez définir un point d'arrêt, par exemple, dans le constructeur, vous rencontrerez un problème - il n'y a nulle part où définir un point d'arrêt et la classe SomePojoBuilder n'est pas là du tout.

Que faire à ce sujet?


Comme cela arrive souvent, ce problème a plusieurs solutions. Regardons chacun d'eux.

N'utilisez jamais Lombok


Lorsque nous avons rencontré ce problème pour la première fois, nous parlions d'un module dans lequel il n'y avait que quelques classes utilisant Lombok. Bien sûr, je ne voulais pas le refuser, j'ai donc immédiatement pensé à faire du delombok. Après avoir enquêté sur cette question, j'ai trouvé des solutions étranges en utilisant le plugin Lombok de Maven - lombok-maven-plugin . Dans l'un d'entre eux, il a été suggéré, par exemple, de conserver les sources dans lesquelles Lombok est utilisé dans un répertoire séparé pour lequel delombok sera exécuté, et les sources générées iront à générées-sources, d'où il compilera et ira à sources.jar. Cette option fonctionne probablement, mais dans ce cas, la coloration syntaxique dans les sources d'origine ne fonctionnera pas dans l'IDE, car un répertoire avec eux ne sera pas considéré comme un répertoire source. Cette option ne me convenait pas, et comme le coût de l'abandon de Lombok dans ce module était faible, il a été décidé de ne pas perdre de temps sur cela, de désactiver Lombok et de simplement générer les méthodes nécessaires via l'IDE.

En général, il me semble que cette option a droit à la vie, mais seulement si les classes utilisant Lombok sont vraiment petites et changent rarement.


Plugin Delombok + build sources.jar utilisant Ant


Après un certain temps, j'ai dû revenir sur ce problème, alors qu'il s'agissait déjà d'un module dans lequel Lombok était utilisé de manière beaucoup plus intensive. Revenant à nouveau à l'étude de ce problème, je suis tombé sur une question sur stackoverflow , qui a suggéré d'exécuter delombok pour les sources, puis d'utiliser les sources dans Ant generate sources.jar.
Ici, vous devez vous éloigner des raisons pour lesquelles vous devez générer sources.jar en utilisant Ant, plutôt qu'en utilisant le plug-in Source ( maven-source-plugin ). Le fait est que vous ne pouvez pas configurer le répertoire source de ce plugin. Il utilisera toujours le contenu de la propriété sourceDirectory du projet.

Donc, dans le cas de notre exemple, pom.xml ressemblera à ceci:

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> 


Comme vous pouvez le voir, la configuration s'est beaucoup développée et elle est loin d'être uniquement le lombok-maven-plugin maven-antrun-plugin . Pourquoi est-ce arrivé? Le fait est que depuis que nous collectons maintenant sources.jar avec Ant, Maven ne sait rien de cet artefact. Et nous devons lui dire clairement comment installer cet artefact, comment le déployer et comment y empaqueter des ressources.

De plus, j'ai constaté que lors de l'exécution de delombok par défaut, Lombok ajoute un commentaire à l'en-tête des fichiers générés. Dans ce cas, le format des fichiers générés est contrôlé non pas en utilisant les options du fichier lombok.config , mais en utilisant les options du plugin. La liste de ces options n'a pas été facile à trouver. Il était possible, bien sûr, d'appeler le surnom du pot de Lombok avec les delombok et --help , mais je suis trop paresseux pour ce programmeur, donc je les ai trouvés dans les codes source sur le github .

Mais ni le volume de la configuration, ni ses fonctionnalités ne peuvent être comparés au principal inconvénient de cette méthode. Il ne résout pas le problème . Le bytecode est compilé à partir d'une source, tandis que d'autres entrent dans sources.jar. Et malgré le fait que delombok est exécuté par le même Lombok, il y aura toujours des différences entre le bytecode et les sources générées, c'est-à-dire ils ne sont toujours pas adaptés au débogage. Pour le dire légèrement, j'étais bouleversé quand je l'ai réalisé.


Plugin Delombok + profil dans Maven


Alors que faire? J'avais sources.jar avec les sources "correctes", mais elles étaient toujours différentes du bytecode. En principe, le problème pourrait être résolu par la compilation à partir de codes sources générés à la suite de delombok. Mais le problème est que maven-compiler-plugin 'ne peut pas spécifier le chemin vers la source. Il utilise toujours les sources spécifiées dans le sourceDirectory projet, comme le maven-source-plugin . Nous pourrions indiquer le répertoire où les sources délombées sont générées, mais dans ce cas, lors de l'importation d'un projet dans l'EDI, le répertoire avec les sources réelles ne sera pas considéré comme tel et la mise en évidence de la syntaxe et d'autres fonctionnalités ne fonctionneront pas pour les fichiers qu'il contient. Cette option ne me convenait pas non plus.

Vous pouvez utiliser des profils! Créez un profil qui ne serait utilisé que lors de la construction du projet et dans lequel la valeur de sourceDirectory serait remplacée! Mais il y a une nuance. La balise sourceDirectory ne peut être déclarée qu'à l'intérieur de la balise build à la racine du projet.

Heureusement, il existe une solution de contournement pour ce problème. Vous pouvez déclarer une propriété qui sera substituée dans la balise sourceDirectory et changer la valeur de cette propriété dans le profil!

Dans ce cas, la configuration du projet ressemblera à ceci:

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> 


Cela fonctionne comme suit. Dans la propriété origSourceDir nous substituons le chemin d'accès au répertoire avec les sources d'origine, et dans la propriété sourceDir substituons la valeur de origSourceDir . Dans la propriété delombokedSourceDir nous delombokedSourceDir le chemin d'accès aux sources générées par delombok. Ainsi, lors de l'importation d'un projet dans l'EDI, le répertoire de origSourceDir , et lors de la construction du projet, si vous spécifiez le profil de build , le répertoire delombokedSourceDir sera utilisé.

En conséquence, nous obtenons un bytecode compilé à partir des mêmes sources qui tomberont dans sources.jar, c'est-à-dire le débogage fonctionnera enfin. Dans ce cas, nous n'avons pas besoin de configurer l'installation et le déploiement de l'artefact avec les sources, car nous utilisons le maven-source-plugin pour générer l'artefact. Certes, la magie avec des variables peut confondre une personne peu familière avec les nuances de Maven.

Vous pouvez lombok.config ajouter l'option lombok.addJavaxGeneratedAnnotation = true dans lombok.config , puis l'annotation @javax.annotation.Generated("lombok") se tiendra au-dessus du code généré dans les sources générées, ce qui vous évitera des questions comme "Pourquoi votre code a-t-il l'air si étrange?" ! " :)


Utilisez gradle


Je pense que si vous connaissez déjà Gradle , vous ne devriez pas expliquer tous ses avantages par rapport à Maven. Si vous ne le connaissez pas déjà, il y a tout un hub sur le hub Une excellente raison de s'y intéresser! :)
En général, quand j'ai pensé à utiliser Gradle, je m'attendais à ce qu'il soit beaucoup plus facile de faire ce dont j'avais besoin, car je savais que je pouvais facilement indiquer de quoi construire sources.jar à partir de quoi compiler en bytecode. Le problème m'attendait là où je m'attendais le moins - il n'y a pas de plugin delombok fonctionnel pour Gradle.

Il y a ce plugin , mais il semble que vous ne puissiez pas spécifier d'options de formatage de sources délombées, ce qui ne me convenait pas.

Il existe un autre plugin , il génère un fichier texte à partir des options qui lui sont transmises, puis le transmet comme argument à lombok.jar. Je n'ai pas réussi à lui faire mettre les sources générées dans le bon répertoire, il semble que cela soit dû au fait que le chemin dans le fichier texte avec les arguments n'est pas cité et ne s'échappe pas correctement. Peut-être que plus tard, je ferai une demande de pull à l'auteur du plugin avec une suggestion de correctif.

En fin de compte, j'ai décidé d'aller dans l'autre sens et je viens de décrire une tâche avec Ant appelant pour exécuter delombok, à Lombok, il n'y a qu'une tâche Ant pour cela , et ça a l'air plutôt 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) } } 


Par conséquent, cette option est équivalente à la précédente.


Conclusions


En fait, une tâche assez banale s'est avérée être une série de tentatives pour trouver des solutions de contournement aux décisions étranges des auteurs Maven. Quant à moi - le script Gradle, dans le contexte des configurations Maven résultantes, semble beaucoup plus évident et logique. Cependant, peut-être que je ne pouvais tout simplement pas trouver une meilleure solution? Dites-nous dans les commentaires si vous avez résolu un problème similaire, et si oui, comment.

Merci d'avoir lu!

Code source

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


All Articles