📜 ⬆️ ⬇️

Lombok, sources.jar and convenient debug

In our team, we love Lombok very much. It allows you to write less code and refactor less, which is ideal for lazy developers. But if, in addition to the project artifact, you also publish source files with documentation, you may encounter a problem - the source code will not be the same as the bytecode. How we solved this problem and what difficulties we encountered in the process, I will tell you in this post.



By the way, if you write in Java and for some reason still do not use Lombok in your project, then I recommend to get acquainted with articles on Habré ( one and two times ). I bet you enjoy it!

Problem


The project we are working on consists of several modules. Some of them (let's call them conditionally backend) when releasing a release are packaged in an archive (delivery), loaded into the repository and subsequently deposited on application servers. The other part - the so-called. client module - published in the repository as a set of artifacts, including sources.jar and javadoc.jar. Lombok we use in all parts, and all this is going to Maven 'om.

Some time ago, one of the consumers of our service addressed a problem - he tried to debug our module, but could not do it, because There were no methods (or even classes) in sources.jar in which he would like to set a breakpoint. We in our team believe that an attempt to independently identify and solve a problem, instead of mindlessly making a defect, is an act of a worthy husband who needs to be encouraged! :-) Therefore, it was decided to align sources.jar with bytecode.

Example


Let's imagine that we have a simple application consisting of two 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); } } 


And our application is built using 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> 


If you compile this project ( mvn compile ) and then compile the resulting bytecode, the SomePojo class will look like this:

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


Pretty much different from what gets into our sources.jar, isn’t it? ;) As you can see, if you connected the source code for SomePojo debug and wanted to set a breakpoint, for example, in the constructor, then you would encounter a problem - there is no place to put a breakpoint, and there is no class at all SomePojoBuilder .

What to do with it?


As it often happens - this problem has several solutions. Let's look at each of them.

Do not use Lombok


When we encountered this problem for the first time - it was about a module that contained only a couple of classes using Lombok. Of course, I didn’t want to refuse him, so I immediately thought about doing delombok. Having investigated this question, I found some strange solutions using Lombok-plugin for Maven - lombok-maven-plugin . In one of them, it was suggested, for example, to keep sources in which Lombok is used in a separate directory for which delombok will be run, and the generated sources will be sent to generated-sources, from where it will be compiled and sent to sources.jar. The variant is probably a worker, but in this case syntax highlighting in the original source code will not work in the IDE, since a directory with them will not be considered a source directory. This option did not suit me, and since the price of abandoning Lombok in this module was small, it was decided not to waste time on it, disable Lombok and simply generate the necessary methods via the IDE.

In general, it seems to me that such an option has the right to life, but only if the classes using Lombok are really small and they rarely change.


Delombok plugin + sources.jar build with Ant


After some time, we had to return to this problem again, when we were already talking about the module in which Lombok was used much more intensively. Returning again to the study of this problem, I came across a question on stackoverflow , where it was proposed to run for delombok sources, and then use the task in Ant to generate sources.jar.
Here we need to make a digression about why sources.jar should be generated using Ant, rather than using a source plugin ( maven-source-plugin ). The fact is that for this plugin you cannot configure the source directory. It will always use the contents of the project's sourceDirectory property.

So, in the case of our example, pom.xml will look like this:

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> 


As you can see, the configuration has greatly expanded, and there is not only lombok-maven-plugin and maven-antrun-plugin . Why did it happen? The fact is that since we now collect sources.jar with Ant, Maven does not know anything about this artifact. And we need to explicitly tell him how to install this artifact, how to deploy it, and how to pack resources into it.

In addition, I found that when running delombok by default, Lombok adds a comment to the header of the generated files. In this case, the format of the generated files is not controlled by the options in the lombok.config file, but by using the plugin options. The list of these options was not easy to find. It was possible, of course, to call Lombok's jar-nickname with the delombok and --help keys, but I’m too lazy to do this , so the programmer, so I found them in the source code on the githabe .

But neither the volume of the configuration, nor its features can be compared with the main drawback of this method. He does not solve the problem . Bytecode is compiled from some sources, and others are in sources.jar. And despite the fact that delombok is executed by the same Lombok, there will still be differences between the bytecode and the generated source code, i.e. for debug they are still unsuitable. To put it mildly, I was upset when I realized this.


Delombok plugin + profile in maven


So what to do? I had sources.jar with the “correct” sources, but they still differed from bytecode. In principle, the problem could be solved by compiling from source generated by delombok. But the problem is that maven-compiler-plugin 'you can not specify the path to the source. It always uses the sources specified in the sourceDirectory project, as well as the maven-source-plugin . It would be possible to specify there the directory into which the source code is generated delomboked, but in this case, when importing the project into the IDE, the real source directory will not be considered as such and syntax highlighting and other features will not work for files in it. This option did not suit me either.

You can use profiles! Create a profile that would be used only when building the project and in which the sourceDirectory value would be replaced! But there is a nuance. The sourceDirectory tag can only be declared inside the build tag in the project root.

Fortunately, there is a workaround for this problem. You can declare a property that will be inserted into the sourceDirectory tag, and change the value of this property in the profile!

In this case, the project configuration will look like this:

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> 


It works as follows. In the origSourceDir property we substitute the path to the directory with the original sources, and in the default property origSourceDir we substitute the value from origSourceDir . In the delombokedSourceDir property delombokedSourceDir we specify the path to the source code generated by delombok. Thus, when importing a project into the IDE, the directory from origSourceDir , and when building a project, if you specify the build profile, the delombokedSourceDir directory will be used.

As a result, we will get the bytecode compiled from the same sources that go to sources.jar, i.e. debag will finally work. At the same time, we do not need to configure the installation and deploy the source artifact, since we use the maven-source-plugin to generate the artifact. True, magic with variables can confuse a person unfamiliar with the nuances of Maven.

You can lombok.config add the lombok.addJavaxGeneratedAnnotation = true option to lombok.addJavaxGeneratedAnnotation = true , then the generated source code will @javax.annotation.Generated("lombok") summary above the generated code, which will help avoid questions like “Why does your code look so weird? ! :)


Use gradle


I think if you are already familiar with Gradle , then you should not explain all its advantages over Maven. If you are not familiar with it, then there is a whole Hub for this. A great reason to look into it! :)
Generally, when I thought about using Gradle, I expected that it would be much easier to do what I needed in it, because I knew that I could easily tell from it what to collect sources.jar and what compile to bytecode. The problem was waiting for me where I expected the least - there is no delombok plug-in for Gradle.

There is this plugin , but it seems that it is impossible to specify options for formatting delomboked sources, which did not suit me.

There is another plugin , it generates a text file from the options passed to it, and then passes it as an argument to lombok.jar. I did not manage to force it to add the generated source code to the correct directory, it seems that this is due to the fact that the path in the text file with the arguments is not taken in quotes and is not escaped properly. Maybe later I'll make a pull request to the plugin author with a suggestion for correction.

In the end, I decided to go the other way and just described the task with the Ant call to execute delombok, Lombok just has an Ant task for this , and it looks quite good:

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


According to the result, this option is equivalent to the previous one.


findings


A rather trivial, in fact, the task turned out to be a series of attempts to find workarounds around the strange decisions of the Maven authors. As for me, the Gradle script, against the background of Maven’s resulting config files, looks much more obvious and logical. However, maybe I just could not find a better solution? Tell us in the comments if you solved a similar problem, and if so, how.

Thank you for reading!

Sources

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