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 researched 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 need to be generated with the help of Ant, and not with the help of the 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 sourceDirectoryproject 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 is greatly expanded, and it has by no means 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. At the same time, the format of the generated files is controlled not by the options in the file lombok.config, but by the options of the plugin. The list of these options was not easy to find. It was possible, of course, to call Lombok's jar-nickname with keys delombokand --help, but I'm too lazy for this programmer, so I found them insource code on githaba .

    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 sourceDirectoryproject, as well as 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 value was replaced sourceDirectory! But there is a nuance. A tag sourceDirectorycan only be declared inside a tag buildin the project root.

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

    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 property origSourceDirwe substitute the path to the directory with the original source code, and in the sourceDirdefault property we substitute the value from origSourceDir. In the property, delombokedSourceDirwe specify the path to the source generated by delombok. Thus, when importing a project into the IDE, the directory from is used origSourceDir, and when building the project, if you specify a profile build, the directory will be used delombokedSourceDir.

    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 artifact with the source code, since we use the plugin to generate the artifactmaven-source-plugin. True, magic with variables can confuse a person unfamiliar with the nuances of Maven.

    You can also lombok.configadd an option lombok.addJavaxGeneratedAnnotation = true, then in the generated source code there will be a summary above the generated code @javax.annotation.Generated("lombok"), 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! :)
    In general, 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 how to compile sources.jar and what to compile into 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


    Quite a trivial, in fact, the task eventually 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

    Also popular now: