Em nossa equipe, realmente amamos o
Lombok . Permite escrever menos código e refatorar menos, o que é ideal para desenvolvedores preguiçosos. Mas se, além do artefato do projeto, você também publicar o código-fonte com a documentação, poderá encontrar um problema - o código-fonte não coincidirá com o bytecode. Vou falar sobre como resolvemos esse problema e que dificuldades encontramos no processo neste post.

A propósito, se você escreve em Java e, por algum motivo, ainda não usa o Lombok em seu projeto, recomendo que você leia os artigos sobre Habré (
um e
dois ). Tenho certeza que você vai gostar!
O problema
O projeto em que estamos trabalhando consiste em vários módulos. Alguns deles (vamos chamá-los de back-end condicional) quando o release é lançado são empacotados no arquivo morto (entrega), carregados no repositório e subsequentemente implantados nos servidores de aplicativos. A outra parte é a chamada módulo cliente - publicado no repositório como um conjunto de artefatos, incluindo sources.jar e javadoc.jar. Usamos Lombok em todas as partes, e
Maven fará tudo isso.
Há algum tempo, um dos consumidores de nosso serviço solucionou um problema - ele tentou depurar nosso módulo, mas não conseguiu, porque O sources.jar não possuía métodos (e nem classes) nos quais ele gostaria de definir o ponto de interrupção. Em nossa equipe, acreditamos que uma tentativa de identificar e resolver um problema de forma independente, em vez de fazer um defeito sem pensar, é um ato de um marido digno que precisa ser incentivado! :-) Portanto, foi decidido trazer o sources.jar de acordo com o bytecode.
Exemplo
Vamos imaginar que temos um aplicativo simples que consiste em duas classes:
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); } }
E nosso aplicativo é construído usando o 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>
Se você compilar este projeto (
mvn compile
) e, em seguida, descompilar o bytecode resultante, a classe
SomePojo terá a seguinte aparência:
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 + ")"; } } }
Bem diferente do que entra em nosso sources.jar, não é? ;) Como você pode ver, se você conectasse o código-fonte para a depuração do
SomePojo e desejasse definir o ponto de interrupção, por exemplo, no construtor, haveria um problema - não há lugar para definir o ponto de interrupção, e a classe
SomePojoBuilder não existe.
O que fazer sobre isso?
Como costuma acontecer, esse problema tem várias soluções. Vamos olhar para cada um deles.
Não use Lombok
Quando encontramos esse problema, conversávamos sobre um módulo no qual havia apenas algumas classes usando o Lombok. É claro que eu não queria recusar, então pensei imediatamente em fazer o delombok. Depois de investigar essa pergunta, encontrei algumas soluções estranhas usando
o plugin Lombok
do Maven -
lombok-maven-plugin . Em um deles, foi sugerido, por exemplo, manter as fontes em que o Lombok é usado em um diretório separado para o qual o delombok será executado, e as fontes geradas irão para fontes geradas, de onde serão compiladas e acessadas para sources.jar. Essa opção provavelmente está funcionando, mas, neste caso, o destaque da sintaxe nas fontes originais não funcionará no IDE, pois um diretório com eles não será considerado um diretório de origem. Essa opção não me agradou e, como o custo de abandonar o Lombok neste módulo era baixo, foi decidido não perder tempo com isso, desabilitar o Lombok e simplesmente gerar os métodos necessários por meio do IDE.
Em geral, parece-me que essa opção tem direito à vida, mas apenas se as aulas que usam Lombok forem realmente pequenas e raramente mudarem.
Delombok plugin + sources.jar compilação usando Ant
Depois de algum tempo, tive que voltar a esse problema novamente, quando já se tratava de um módulo no qual o Lombok era usado com muito mais intensidade. Voltando ao estudo desse problema, deparei-me com uma
pergunta sobre o stackoverflow , que sugeria executar o delombok para as fontes e, em seguida, usar fontes no Ant para gerar sources.jar.
Aqui você precisa discernir por que precisa gerar o sources.jar usando o Ant, em vez de usar o plug-in Source (
maven-source-plugin ). O fato é que você não pode configurar o diretório de origem para este plug-in. Ele sempre usará o conteúdo da propriedade
sourceDirectory
do projeto.
Portanto, no caso do nosso exemplo, o pom.xml ficará assim:
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>
Como você pode ver, a configuração cresceu muito e está longe de ser apenas o
lombok-maven-plugin
e o
maven-antrun-plugin
. Por que isso aconteceu? O fato é que, agora que coletamos o sources.jar com o Ant, o Maven não sabe nada sobre esse artefato. E precisamos dizer claramente a ele como instalar esse artefato, como implantá-lo e como empacotar recursos nele.
Além disso, descobri que, ao executar o delombok por padrão, o Lombok adiciona um comentário ao cabeçalho dos arquivos gerados. Nesse caso, o formato dos arquivos gerados é controlado não usando as opções no arquivo
lombok.config
, mas usando as opções do plug-in. A lista dessas opções não foi fácil de encontrar. Era possível, é claro, chamar o apelido do jar de Lombok com as teclas
delombok
e
--help
, mas eu sou
muito preguiçoso para esse programador, então eu os encontrei nos
códigos-fonte no github .
Mas nem o volume da configuração, nem seus recursos podem ser comparados com a principal desvantagem desse método.
Ele não resolve o problema . O bytecode é compilado a partir de uma fonte, enquanto outros acessam o sources.jar. E, apesar do delombok ser executado pelo mesmo Lombok, ainda haverá diferenças entre o bytecode e as fontes geradas, ou seja, eles ainda não são adequados para depuração. Para dizer o mínimo, fiquei chateado quando percebi isso.
Delombok plugin + perfil em maven
Então o que fazer? Eu tinha o sources.jar com as fontes "corretas", mas elas ainda eram diferentes do bytecode. Em princípio, o problema poderia ser resolvido pela compilação a partir de códigos-fonte gerados como resultado do delombok. Mas o problema é que o
maven-compiler-plugin 'não pode especificar o caminho para a fonte. Ele sempre usa as fontes especificadas no
sourceDirectory
projeto, como o
maven-source-plugin
. Poderíamos indicar o diretório em que as fontes delomboked são geradas, mas, neste caso, ao importar um projeto para o IDE, o diretório com fontes reais não será considerado como tal e o realce da sintaxe e outros recursos não funcionarão para arquivos nele. Esta opção também não me agradou.
Você pode usar perfis! Crie um perfil que seria usado apenas na criação do projeto e no qual o valor do
sourceDirectory
seria substituído! Mas há uma nuance. A tag
sourceDirectory
pode ser declarada apenas dentro da tag
build
na raiz do projeto.
Felizmente, há uma solução alternativa para esse problema. Você pode declarar uma propriedade que será substituída na tag
sourceDirectory
e alterar o valor dessa propriedade no perfil!
Nesse caso, a configuração do projeto terá a seguinte aparência:
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>
Funciona da seguinte maneira. Na propriedade
origSourceDir
substituímos o caminho do diretório pelas fontes originais e, na propriedade
sourceDir
substituímos o valor de
origSourceDir
. Na propriedade
delombokedSourceDir
especificamos o caminho para as fontes geradas pelo delombok. Portanto, ao importar um projeto para o IDE, o diretório
origSourceDir
e, ao criar o projeto, se você especificar o perfil de
build
, o diretório
delombokedSourceDir
será usado.
Como resultado, obtemos um bytecode compilado das mesmas fontes que caem em sources.jar, ou seja, depuração finalmente funcionará. Nesse caso, não precisamos configurar a instalação e a implementação do artefato com as fontes, pois usamos o
maven-source-plugin
para gerar o artefato. É verdade que a magia com variáveis pode confundir uma pessoa não familiarizada com as nuances de Maven.
Você
lombok.config
pode adicionar a opção
lombok.addJavaxGeneratedAnnotation = true
no
lombok.config
e a anotação
@javax.annotation.Generated("lombok")
ficará acima do código gerado nas fontes geradas, o que ajudará a evitar perguntas como "Por que seu código parece tão estranho? ! " :)
Use gradle
Acho que se você já conhece o
Gradle , não deve explicar todas as vantagens do Maven. Se você ainda não está familiarizado com ele, existe
um hub inteiro no hub Um ótimo motivo para investigar! :)
Em geral, quando pensei em usar o Gradle, esperava que fosse muito mais fácil fazer o que eu precisava, porque sabia que nele poderia facilmente apontar o que construir o sources.jar e o que compilar no bytecode. O problema estava me esperando onde eu esperava menos - não há nenhum plugin delombok para o Gradle.
Existe
esse plug-in , mas parece que você não pode especificar opções para formatar fontes delomboked nele, o que não combina comigo.
Há
outro plug -
in , ele gera um arquivo de texto a partir das opções passadas para ele e o passa como argumento para o lombok.jar. Não consegui fazê-lo colocar as fontes geradas no diretório certo, parece que isso se deve ao fato de o caminho no arquivo de texto com os argumentos não estar entre aspas e não ter escapado adequadamente. Talvez mais tarde eu faça uma solicitação pull ao autor do plugin com uma sugestão para uma correção.
No final, decidi seguir o outro caminho e apenas descrevi uma tarefa com Ant chamando para executar o delombok,
no Lombok há apenas uma tarefa Ant para isso , e parece muito bom:
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) } }
Como resultado, esta opção é equivalente à anterior.
Conclusões
Uma tarefa bastante trivial, de fato, acabou sendo uma série de tentativas de encontrar soluções alternativas para decisões estranhas dos autores do Maven. Quanto a mim - o script Gradle, no contexto das configurações resultantes do Maven, parece muito mais óbvio e lógico. No entanto, talvez eu simplesmente não consegui encontrar uma solução melhor? Diga-nos nos comentários se você resolveu um problema semelhante e, em caso afirmativo, como.
Obrigado pela leitura!
Código fonte