龙目岛,sources.jar和方便的调试

在我们的团队中,我们真的很喜欢Lombok 。 它使您可以减少编写代码和重构的次数,这对于懒惰的开发人员来说是理想的选择。 但是,如果除了项目工件之外,您还随文档一起发布了源代码,那么您可能会遇到问题-源代码将与字节码不一致。 我将在本文中谈论我们如何解决此问题以及在此过程中遇到的困难。



顺便说一句,如果您使用Java编写并且由于某种原因仍未在项目中使用Lombok,那么我建议您阅读有关Habré的文章( 两次 )。 我相信你会喜欢的!

问题


我们正在从事的项目包括几个模块。 其中一些(在发布时)称为打包(有条件地称为后端),打包在存档(交付)中,上传到存储库,然后部署到应用程序服务器。 另一部分是所谓的 客户端模块-作为一组工件发布到资源库,包括 sources.jar和javadoc.jar。 我们在所有部分都使用Lombok,而Maven将会完成所有工作。

前段时间,我们服务的使用者之一解决了一个问题-他尝试调试我们的模块,但无法完成,因为 sources.jar缺少他想设置断点的方法(甚至类)。 我们团队中的每个人都认为,试图独立地发现和解决问题,而不是无所顾忌地制造缺陷,是值得鼓励的丈夫的行为! :-)因此,决定根据字节码导入sources.jar。

例子


假设我们有一个包含两个类的简单应用程序:

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


我们的应用程序是使用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> 


如果编译该项目( mvn compile ),然后反编译生成的字节码,则SomePojo类将如下所示:

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


与进入sources.jar的内容大不相同,不是吗? ;)如您所见,例如,如果您连接SomePojo调试的源并想在构造函数中设置断点,那么您将遇到问题-无处设置断点,并且SomePojoBuilder类根本存在。

怎么办呢?


通常,此问题有几种解决方案。 让我们看看它们中的每一个。

不要使用龙目岛


当我们第一次遇到这个问题时,我们正在谈论一个模块,其中只有两个使用Lombok的类。 当然,我不想拒绝它,所以我立即想到了做delombok。 在研究了这个问题之后,我发现了使用Maven的 Lombok插件-lombok-maven-plugin的一些奇怪的解决方案。 例如,在其中一个建议中,将使用Lombok的源代码保留在一个单独的目录中,将对delombok执行该目录,并且生成的源代码将进入生成源代码,从该源代码进行编译并转到sources.jar。 此选项可能有效,但是在这种情况下,原始源代码中的语法高亮将无法在IDE中使用,因为 包含它们的目录将不被视为源目录。 此选项不适合我,并且由于在此模块中放弃Lombok的成本很低,因此决定不浪费时间,禁用Lombok并仅通过IDE生成必要的方法。

总的来说,在我看来,这种选择具有生命权,但前提是使用Lombok的班级确实很小并且很少改变。


使用Ant构建Delombok插件+ sources.jar


一段时间之后,当已经是Lombok被更广泛使用的模块的问题时,我不得不再次回到这个问题。 再次回到对该问题的研究,我遇到了一个关于stackoverflow的问题,该问题建议对源执行delombok,然后在Ant中使用source生成sources.jar。
在这里,您需要讨论为什么需要使用Ant而不是使用Source插件( maven-source-plugin )来生成sources.jar的问题。 事实是您不能为此插件配置源目录。 它将始终使用项目的sourceDirectory属性的内容。

因此,在我们的示例中,pom.xml将如下所示:

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> 


如您所见,配置已经有了很大的增长,它远远超过了lombok-maven-pluginmaven-antrun-plugin 。 为什么会这样呢? 事实是,由于我们现在使用Ant收集sources.jar,因此Maven对这种工件一无所知。 我们需要清楚地告诉他如何安装此工件,如何部署它以及如何将资源打包到其中。

另外,我发现默认情况下执行delombok时,Lombok在生成的文件的标题中添加了注释。 在这种情况下,不使用lombok.config文件中的选项来控制生成文件的格式,而是使用插件选项来控制。 这些选项的列表不容易找到。 当然,可以用delombok--help键来调用Lombok的jar昵称,但是我对这个程序员太懒了 ,因此我在github源代码中找到了它们。

但是,配置的数量及其功能都无法与这种方法的主要缺点相提并论。 他没有解决问题 。 字节码从一个来源编译,而其他则进入sources.jar。 尽管事实上delombok是由同一个Lombok执行的,但字节码和生成的源之间仍然存在差异,即 它们仍然不适合调试。 坦率地说,当我意识到这一点时,我很沮丧。


Maven中的Delombok插件+个人资料


那该怎么办呢? 我有带有“正确”源的sources.jar,但它们仍然与字节码不同。 原则上,可以通过编译由delombok生成的源代码来解决该问题。 但是问题在于,“ maven-compiler-plugin ”无法指定源的路径。 它始终使用项目的sourceDirectory指定的源,例如maven-source-plugin 。 可以在其中指示生成delomboked源的目录,但是在这种情况下,当将项目导入IDE时,包含真实源的目录将不被视为真实目录,并且语法高亮显示和其他功能将不适用于其中的文件。 这个选项也不适合我。

您可以使用个人资料! 创建一个仅在构建项目时使用的配置文件,并在其中替换sourceDirectory的值! 但是有细微差别。 只能在项目根目录的build标记内声明sourceDirectory标记。

幸运的是,有解决此问题的方法。 您可以声明一个将被替换为sourceDirectory标记的属性,并在配置文件中更改此属性的值!

在这种情况下,项目配置将如下所示:

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> 


它的工作原理如下。 我们用sourceDir属性中的原始源替换目录的路径,并默认将sourceDir的值origSourceDirsourceDir属性。 在delombokedSourceDir属性中delombokedSourceDir我们指定由delombok生成的源的路径。 因此,将项目导入IDE时,将origSourceDir的目录,而在构建项目时,如果指定了build配置文件,则将使用delombokedSourceDir目录。

结果,我们从相同的源代码中获得了一个字节码,该源代码将被放入sources.jar中,即 调试将最终工作。 在这种情况下,我们不需要使用源配置工件的安装和部署,因为我们使用maven-source-plugin来生成工件。 的确,带有变量的魔术可以使不熟悉Maven细微差别的人感到困惑。

lombok.config可以在lombok.addJavaxGeneratedAnnotation = true添加选项lombok.addJavaxGeneratedAnnotation = true ,然后注释@javax.annotation.Generated("lombok")将位于生成的源代码中生成的代码上方,这将有助于避免诸如“为什么代码看起来如此奇怪? ! :)


使用gradle


我认为,如果您已经熟悉Gradle ,那么您不应该解释它相对于Maven的所有优点。 如果您还不熟悉它,那么集线器上会有一个整个集线器 一个很好的理由去研究它! :)
总的来说,当我考虑使用Gradle时,我期望做它所需的事情会容易得多,因为我知道在其中可以轻松地指出从中构建source.jar的内容以及从中构建内容的内容。编译为字节码。 问题在我最期待的地方等了我-Gradle没有可用的delombok插件。

这个插件 ,但是似乎您不能在其中指定用于格式化delomboked源的选项,这不适合我。

还有另一个插件 ,它通过传递给它的选项生成一个文本文件,然后将其作为参数传递给lombok.jar。 我无法让他将生成的源放在正确的目录中,这似乎是由于以下事实:带有引数的文本文件中的路径未加引号,并且没有正确转义。 也许以后我会向插件的作者发出请求,并提出修复建议。

最后,我决定采用另一种方法,只是用Ant调用描述了执行delombok的任务, 在Lombok中,只有一个Ant任务可以完成 ,它看起来非常不错:

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


因此,此选项等同于上一个。


结论


实际上,事实证明这是一件微不足道的任务,它是针对Maven作者的奇怪决定寻找解决方法的一系列尝试。 对于我来说-在生成的Maven配置的背景下,Gradle脚本看起来更加明显和合乎逻辑。 但是,也许我只是找不到更好的解决方案? 如果您解决了类似的问题,请在评论中告诉我们,如果可以,那么如何解决。

感谢您的阅读!

源代码

Source: https://habr.com/ru/post/zh-CN438548/


All Articles