Di tim kami, kami benar-benar mencintai
Lombok . Ini memungkinkan Anda untuk menulis lebih sedikit kode dan lebih sedikit refactor, yang ideal untuk pengembang yang malas. Tetapi jika, selain artefak proyek, Anda mempublikasikan kode sumber dengan dokumentasi juga, maka Anda dapat mengalami masalah - kode sumber tidak akan bertepatan dengan bytecode. Saya akan berbicara tentang bagaimana kami menyelesaikan masalah ini dan kesulitan apa yang kami temui dalam proses dalam posting ini.

Omong-omong, jika Anda menulis di Jawa dan karena alasan tertentu masih tidak menggunakan Lombok dalam proyek Anda, maka saya sarankan Anda membaca artikel tentang Habrรฉ (
sekali dan
dua kali ). Saya yakin Anda akan menyukainya!
Masalah
Proyek yang kami kerjakan terdiri dari beberapa modul. Beberapa dari mereka (sebut saja backend bersyarat) dikemas dalam arsip (pengiriman) ketika rilis dirilis, diunggah ke repositori dan kemudian digunakan untuk server aplikasi. Bagian lainnya adalah yang disebut modul klien - diterbitkan ke repositori sebagai satu set artefak, termasuk sources.jar dan javadoc.jar. Kami menggunakan Lombok di semua bagian, dan
Maven akan melakukan semuanya.
Beberapa waktu yang lalu, salah satu konsumen layanan kami datang dengan masalah - ia mencoba men-debug modul kami, tetapi tidak dapat melakukannya, karena sources.jar tidak memiliki metode (dan bahkan kelas) di mana ia ingin mengatur breakpoint. Kami di tim kami percaya bahwa upaya mengidentifikasi dan memecahkan masalah secara mandiri, alih-alih membuat cacat adalah tindakan suami yang layak yang perlu didorong! :-) Oleh karena itu, diputuskan untuk membawa sources.jar sesuai dengan bytecode.
Contoh
Mari kita bayangkan bahwa kita memiliki aplikasi sederhana yang terdiri dari dua kelas:
SomePojo.javapackage com.github.monosoul.lombok.sourcesjar; import lombok.Builder; import lombok.Value; @Value @Builder(toBuilder = true) class SomePojo { String someStringField; 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); } }
Dan aplikasi kami dibangun menggunakan 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>
Jika Anda mengkompilasi proyek ini (
mvn compile
), dan kemudian mendekompilasi bytecode yang dihasilkan, kelas
SomePojo akan terlihat seperti ini:
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 + ")"; } } }
Sangat berbeda dari apa yang masuk ke sumber kami. Jar, bukan? ;) Seperti yang Anda lihat, jika Anda menghubungkan kode sumber untuk debug
SomePojo dan ingin menetapkan breakpoint, misalnya, di konstruktor, maka Anda akan
mengalami masalah - tidak ada tempat untuk mengatur breakpoint, dan kelas
SomePojoBuilder tidak ada sama sekali.
Apa yang harus dilakukan?
Seperti yang sering terjadi, masalah ini memiliki beberapa solusi. Mari kita lihat masing-masing.
Jangan gunakan Lombok
Ketika kami pertama kali menghadapi masalah ini, kami berbicara tentang modul di mana hanya ada beberapa kelas menggunakan Lombok. Tentu saja, saya tidak mau menolaknya, jadi saya langsung berpikir untuk melakukan delombok. Setelah menyelidiki pertanyaan ini, saya menemukan beberapa solusi aneh menggunakan plugin
Maven Lombok -
lombok-maven-plugin . Dalam salah satu dari mereka disarankan, misalnya, untuk menjaga sumber-sumber di mana Lombok digunakan dalam direktori terpisah yang delombok akan dieksekusi, dan sumber yang dihasilkan akan pergi ke sumber yang dihasilkan, dari mana ia akan dikompilasi dan pergi ke sources.jar. Opsi ini mungkin berfungsi, tetapi dalam hal ini penyorotan sintaks pada sumber asli tidak akan berfungsi di IDE, seperti direktori dengan mereka tidak akan dianggap sebagai direktori sumber. Opsi ini tidak cocok untuk saya, dan karena biaya meninggalkan Lombok dalam modul ini rendah, diputuskan untuk tidak membuang waktu untuk hal ini, menonaktifkan Lombok dan hanya menghasilkan metode yang diperlukan melalui IDE.
Secara umum, menurut saya opsi ini memiliki hak untuk hidup, tetapi hanya jika kelas yang menggunakan Lombok benar-benar kecil dan mereka jarang berubah.
Delombok plugin + sources.jar build menggunakan Ant
Setelah beberapa waktu, saya harus kembali ke masalah ini lagi, ketika itu sudah menjadi pertanyaan tentang modul di mana Lombok digunakan jauh lebih intensif. Kembali lagi ke studi tentang masalah ini, saya menemukan
pertanyaan tentang stackoverflow , yang menyarankan mengeksekusi delombok untuk sumber, dan kemudian menggunakan sumber di Ant menghasilkan sources.jar.
Di sini Anda perlu ngelantur tentang mengapa Anda perlu menghasilkan sources.jar menggunakan Ant, daripada menggunakan plug-in Sumber (
maven-source-plugin ). Faktanya adalah Anda tidak dapat mengonfigurasi direktori sumber untuk plugin ini. Itu akan selalu menggunakan konten properti
sourceDirectory
proyek.
Jadi, dalam contoh kita, pom.xml akan terlihat seperti ini:
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>
Seperti yang Anda lihat, konfigurasi telah berkembang sangat banyak, dan itu jauh dari hanya
lombok-maven-plugin
dan
maven-antrun-plugin
. Mengapa ini terjadi? Faktanya adalah karena kita sekarang mengumpulkan sumber-sumber. Bergabung dengan Ant, Maven tidak tahu apa-apa tentang artefak ini. Dan kita harus memberi tahu dia dengan jelas bagaimana cara menginstal artefak ini, bagaimana cara menyebarkannya dan bagaimana mengemas sumber daya ke dalamnya.
Selain itu, saya menemukan bahwa ketika menjalankan delombok secara default, Lombok menambahkan komentar ke header file yang dihasilkan. Dalam hal ini, format file yang dihasilkan dikendalikan tidak menggunakan opsi di file
lombok.config
, tetapi menggunakan opsi plugin. Daftar opsi ini tidak mudah ditemukan. Tentu saja mungkin untuk memanggil nama panggilan toples Lombok dengan
delombok
dan
--help
keys, tapi saya
terlalu malas untuk programmer
ini , jadi saya menemukannya di
kode sumber di github .
Tetapi volume konfigurasi, maupun fiturnya tidak dapat dibandingkan dengan kelemahan utama dari metode ini.
Dia tidak memecahkan masalah . Bytecode dikompilasi dari satu sumber, sementara yang lain masuk ke sources.jar. Dan terlepas dari kenyataan bahwa delombok dieksekusi oleh Lombok yang sama, masih akan ada perbedaan antara bytecode dan sumber yang dihasilkan, yaitu mereka masih tidak cocok untuk debug. Singkatnya, saya kesal ketika menyadari ini.
Plugin + profil Delombok dalam maven
Jadi apa yang harus dilakukan? Saya memiliki sources.jar dengan sumber yang "benar", tetapi mereka masih berbeda dari bytecode. Pada prinsipnya, masalah dapat diselesaikan dengan kompilasi dari kode sumber yang dihasilkan sebagai hasil dari delombok. Tetapi masalahnya adalah bahwa
maven-compiler-plugin 'tidak dapat menentukan path ke sumbernya. Itu selalu menggunakan sumber yang ditentukan dalam
sourceDirectory
proyek, seperti
maven-source-plugin
. Mungkin saja untuk menunjukkan di sana direktori tempat sumber delomboked dihasilkan, tetapi dalam kasus ini, ketika mengimpor proyek ke IDE, direktori dengan sumber nyata tidak akan dianggap seperti itu dan penyorotan sintaks dan fitur lainnya tidak akan berfungsi untuk file di dalamnya. Pilihan ini juga tidak cocok untukku.
Anda dapat menggunakan profil! Buat profil yang hanya akan digunakan ketika membangun proyek dan di mana nilai
sourceDirectory
akan diganti! Namun ada nuansa. Tag
sourceDirectory
hanya dapat dideklarasikan di dalam tag
build
di root proyek.
Untungnya, ada solusi untuk masalah ini. Anda dapat mendeklarasikan properti yang akan diganti menjadi tag
sourceDirectory
, dan mengubah nilai properti ini di profil!
Dalam hal ini, konfigurasi proyek akan terlihat seperti ini:
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>
Ini berfungsi sebagai berikut. Di properti
origSourceDir
kami mengganti jalur ke direktori dengan sumber asli, dan di properti
sourceDir
mengganti nilai dari
origSourceDir
. Di properti
delombokedSourceDir
kami menentukan jalur ke sumber yang dihasilkan oleh delombok. Jadi, ketika mengimpor proyek ke IDE, direktori dari
origSourceDir
, dan ketika membangun proyek, jika Anda menentukan profil
build
, direktori
delombokedSourceDir
akan digunakan.
Akibatnya, kita mendapatkan bytecode yang dikompilasi dari sumber yang sama yang akan jatuh ke sources.jar, yaitu akhirnya debug akan bekerja. Dalam hal ini, kita tidak perlu mengkonfigurasi instalasi dan penyebaran artefak dengan sumber, karena kita menggunakan
maven-source-plugin
untuk menghasilkan artefak. Benar, sihir dengan variabel dapat membingungkan seseorang yang tidak terbiasa dengan nuansa Maven.
Anda
lombok.config
dapat menambahkan opsi
lombok.addJavaxGeneratedAnnotation = true
di
lombok.config
, lalu anotasi
@javax.annotation.Generated("lombok")
akan berdiri di atas kode yang dihasilkan di sumber yang dihasilkan, yang akan membantu menghindari pertanyaan seperti "Mengapa kode Anda terlihat sangat aneh? ! " :)
Gunakan gradle
Saya pikir jika Anda sudah terbiasa dengan
Gradle , maka Anda seharusnya tidak menjelaskan semua kelebihannya atas Maven. Jika Anda belum terbiasa dengan itu, maka ada
hub seluruh pada hub Alasan yang bagus untuk melihatnya! :)
Secara umum, ketika saya berpikir tentang menggunakan Gradle, saya berharap akan lebih mudah untuk melakukan apa yang saya butuhkan di dalamnya, karena saya tahu bahwa di dalamnya saya dapat dengan mudah menunjukkan apa yang akan membangun sumber. kompilasi ke bytecode. Masalahnya menunggu saya di tempat yang saya harapkan paling tidak - tidak ada plugin delombok yang berfungsi untuk Gradle.
Ada
plugin ini , tetapi tampaknya Anda tidak dapat menentukan opsi untuk memformat sumber yang dihapus di dalamnya, yang tidak cocok untuk saya.
Ada
plugin lain , itu menghasilkan file teks dari opsi yang diteruskan ke sana, dan kemudian meneruskannya sebagai argumen ke lombok.jar. Saya tidak bisa membuatnya menempatkan sumber yang dihasilkan di direktori yang benar, tampaknya ini disebabkan oleh kenyataan bahwa path dalam file teks dengan argumen tidak dikutip dan tidak lolos dengan benar. Mungkin nanti saya akan melakukan permintaan tarik ke pembuat plugin dengan saran untuk memperbaikinya.
Pada akhirnya, saya memutuskan untuk pergi ke arah lain dan hanya menggambarkan tugas dengan panggilan Ant untuk mengeksekusi delombok,
di Lombok hanya ada tugas Ant untuk ini , dan itu terlihat cukup bagus:
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) } }
Akibatnya, opsi ini setara dengan yang sebelumnya.
Kesimpulan
Sebenarnya tugas yang sepele, ternyata merupakan serangkaian upaya untuk menemukan solusi di sekitar keputusan aneh oleh penulis Maven. Bagi saya - skrip Gradle, dengan latar belakang konfigurasi Maven yang dihasilkan, terlihat jauh lebih jelas dan logis. Namun, mungkin saya tidak bisa menemukan solusi yang lebih baik? Beri tahu kami di komentar jika Anda memecahkan masalah yang sama, dan jika demikian, caranya.
Terima kasih sudah membaca!
Kode sumber