Escreva código Java sem gordura com o Project Lombok
Publicados: 2022-03-11Há uma série de ferramentas e bibliotecas que não consigo me imaginar escrevendo código Java sem esses dias. Tradicionalmente, coisas como Google Guava ou Joda Time (pelo menos para a era pré Java 8) estão entre as dependências que acabo colocando em meus projetos na maioria das vezes, independentemente do domínio específico em mãos.
O Lombok certamente merece seu lugar em meus POMs ou compilações Gradle também, embora não seja um utilitário típico de biblioteca/framework. Lombok já existe há algum tempo (lançado pela primeira vez em 2009) e amadureceu muito desde então. No entanto, sempre achei que merecia mais atenção — é uma maneira incrível de lidar com a verbosidade natural do Java.
Neste post, exploraremos o que torna o Lombok uma ferramenta tão útil.
Java tem muitas coisas a seu favor além da própria JVM, que é um software notável. Java é maduro e tem alto desempenho, e a comunidade e o ecossistema ao seu redor são enormes e animados.
No entanto, como linguagem de programação, Java tem algumas idiossincrasias próprias, bem como escolhas de design que podem torná-lo bastante detalhado. Adicione algumas construções e padrões de classe que os desenvolvedores Java geralmente precisam usar e frequentemente acabamos com muitas linhas de código que trazem pouco ou nenhum valor real além de cumprir algum conjunto de restrições ou convenções de estrutura.
Aqui é onde Lombok entra em jogo. Isso nos permite reduzir drasticamente a quantidade de código “boilerplate” que precisamos escrever. Os criadores do Lombok são dois caras muito inteligentes e certamente têm um gosto pelo humor - você não pode perder esta introdução que eles fizeram em uma conferência passada!
Vamos ver como o Lombok faz sua mágica e alguns exemplos de uso.
Como funciona o Lombok
O Lombok atua como um processador de anotações que “adiciona” código às suas classes em tempo de compilação. O processamento de anotações é um recurso adicionado ao compilador Java na versão 5. A ideia é que os usuários possam colocar processadores de anotações (escritos por eles mesmos ou por meio de dependências de terceiros, como Lombok) no classpath de compilação. Então, à medida que o processo de compilação está acontecendo, sempre que o compilador encontra uma anotação, ele meio que pergunta: “Ei, alguém no caminho de classe está interessado nesta @Annotation?.” Para aqueles processadores levantando suas mãos, o compilador então transfere o controle para eles junto com o contexto de compilação para que eles, bem... processem.
Talvez o caso mais comum para processadores de anotação seja gerar novos arquivos de origem ou realizar algum tipo de verificação em tempo de compilação.
O Lombok realmente não se enquadra nessas categorias: O que ele faz é modificar as estruturas de dados do compilador usadas para representar o código; ou seja, sua árvore de sintaxe abstrata (AST). Ao modificar o AST do compilador, o Lombok está alterando indiretamente a própria geração de bytecode final.
Essa abordagem incomum e bastante intrusiva resultou tradicionalmente no Lombok sendo visto como uma espécie de hack. Embora eu mesmo concorde com essa caracterização até certo ponto, em vez de ver isso no mau sentido da palavra, veria Lombok como uma “alternativa inteligente, tecnicamente meritória e original”.
Ainda assim, há desenvolvedores que o consideram um hack e não usam o Lombok por esse motivo. Isso é compreensível, mas, na minha experiência, os benefícios de produtividade da Lombok superam qualquer uma dessas preocupações. Estou feliz em usá-lo para projetos de produção há muitos anos.
Antes de entrar em detalhes, gostaria de resumir as duas razões pelas quais valorizo especialmente o uso do Lombok em meus projetos:
- O Lombok ajuda a manter meu código limpo, conciso e direto ao ponto. Acho minhas classes anotadas do Lombok muito expressivas e geralmente acho que o código anotado é bastante revelador de intenções, embora nem todos na Internet concordem necessariamente.
- Quando estou iniciando um projeto e pensando em um modelo de domínio, costumo começar escrevendo classes que são um trabalho em andamento e que mudo de forma iterativa à medida que penso mais e as refino. Nesses estágios iniciais, o Lombok me ajuda a me mover mais rápido por não precisar me movimentar ou transformar o código clichê que ele gera para mim.
Padrão de Bean e Métodos de Objeto Comum
Muitas das ferramentas e estruturas Java que usamos dependem do padrão Bean. Java Beans são classes serializáveis que possuem um construtor padrão zero-args (e possivelmente outras versões) e expõem seu estado por meio de getters e setters, normalmente apoiados por campos privados. Escrevemos muitos deles, por exemplo, ao trabalhar com JPA ou estruturas de serialização, como JAXB ou Jackson.
Considere este bean User que contém até cinco atributos (propriedades), para os quais gostaríamos de ter um construtor adicional para todos os atributos, representação de string significativa e definir igualdade/hashing em termos de seu campo de email:
public class User implements Serializable { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; // Empty constructor implementation: ~3 lines. // Utility constructor for all attributes: ~7 lines. // Getters/setters: ~38 lines. // equals() and hashCode() as per email: ~23 lines. // toString() for all attributes: ~3 lines. // Relevant: 5 lines; Boilerplate: 74 lines => 93% meaningless code :( }Para resumir aqui, em vez de incluir a implementação real de todos os métodos, apenas forneci comentários listando os métodos e o número de linhas de código que as implementações reais levaram. Esse código padrão teria totalizado mais de 90% do código para esta classe!
Além disso, se mais tarde eu quisesse, digamos, alterar o email para emailAddress ou fazer o registrationTs ser uma Date em vez de um Instant , precisaria dedicar tempo (com a ajuda do meu IDE para alguns casos, reconhecidamente) para alterar coisas como obter /set nomes e tipos de métodos, modifique meu construtor de utilitários e assim por diante. Mais uma vez, tempo inestimável para algo que não traz nenhum valor comercial prático ao meu código.
Vamos ver como o Lombok pode ajudar aqui:
import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString @EqualsAndHashCode(of = {"email"}) public class User { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; } Voilá! Acabei de adicionar um monte de anotações lombok.* e consegui exatamente o que eu queria. A listagem acima é exatamente todo o código que preciso escrever para isso. O Lombok está se conectando ao meu processo do compilador e gerou tudo para mim (veja a captura de tela abaixo do meu IDE).
Como você percebe, o inspetor do NetBeans (e isso acontecerá independentemente do IDE) detecta o bytecode da classe compilada, incluindo as adições que o Lombok trouxe para o processo. O que aconteceu aqui é bastante simples:
- Usando
@Gettere@Setter, instruí o Lombok a gerar getters e setters para todos os atributos. Isso ocorre porque eu usei as anotações em um nível de classe. Se eu quisesse especificar seletivamente o que gerar para quais atributos, eu poderia ter anotado os próprios campos. - Graças a
@NoArgsConstructore@AllArgsConstructor, obtive um construtor vazio padrão para minha classe, bem como um adicional para todos os atributos. - A anotação
@ToStringgera automaticamente umtoString()útil, mostrando por padrão todos os atributos de classe prefixados por seu nome. - Por fim, para ter o par de métodos
equals()ehashCode()definidos em termos do campo email usei@EqualsAndHashCodee o parametrizei com a lista de campos relevantes (apenas o email neste caso).
Personalizando anotações do Lombok
Vamos agora usar algumas customizações do Lombok seguindo este mesmo exemplo:
- Eu gostaria de diminuir a visibilidade do construtor padrão. Como eu só preciso disso por motivos de conformidade com o bean, espero que os consumidores da classe chamem apenas o construtor que recebe todos os campos. Para impor isso, estou personalizando o construtor gerado com
AccessLevel.PACKAGE. - Quero garantir que meus campos nunca recebam valores nulos atribuídos, nem por meio do construtor nem por meio dos métodos setter. Anotar atributos de classe com
@NonNullé suficiente; O Lombok gerará verificações nulas lançandoNullPointerExceptionquando apropriado nos métodos construtor e setter. - Vou adicionar um atributo de
password, mas não quero que ele seja mostrado ao chamartoString()por motivos de segurança. Isso é feito por meio do argumento excludes de@ToString. - Estou bem expondo o estado publicamente por meio de getters, mas preferiria restringir a mutabilidade externa. Então, estou deixando
@Gettercomo está, mas novamente usandoAccessLevel.PROTECTEDpara@Setter. - Talvez eu queira forçar alguma restrição no campo de
emailpara que, se for modificado, algum tipo de verificação seja executado. Para isso, eu mesmo implemento o métodosetEmail(). O Lombok apenas omitirá a geração de um método que já existe.
É assim que a classe User ficará:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName; private @NonNull Instant registrationTs; private boolean payingCustomer; protected void setEmail(String email) { // Check for null (=> NullPointerException) // and valid email code (=> IllegalArgumentException) this.email = email; } }Observe que, para algumas anotações, estamos especificando atributos de classe como strings simples. Não é um problema, porque o Lombok lançará um erro de compilação se, por exemplo, digitarmos incorretamente ou nos referirmos a um campo inexistente. Com Lombok, estamos seguros.
Além disso, assim como para o método setEmail() , o Lombok estará apenas OK e não gerará nada para um método que o programador já implementou. Isso se aplica a todos os métodos e construtores.
Estruturas de dados imutáveis
Outro caso de uso em que o Lombok se destaca é ao criar estruturas de dados imutáveis. Estes são geralmente referidos como “tipos de valor”. Algumas linguagens têm suporte embutido para isso, e existe até uma proposta para incorporar isso em futuras versões do Java.
Suponha que queremos modelar uma resposta a uma ação de login do usuário. Esse é o tipo de objeto que gostaríamos de instanciar e retornar a outras camadas do aplicativo (por exemplo, para ser serializado em JSON como o corpo de uma resposta HTTP). Tal LoginResponse não precisaria ser mutável e o Lombok pode ajudar a descrever isso de forma sucinta. Claro, existem muitos outros casos de uso para estruturas de dados imutáveis (elas são compatíveis com multithreading e cache, entre outras qualidades), mas vamos nos ater a este exemplo simples:
import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.experimental.Wither; @Getter @RequiredArgsConstructor @ToString @EqualsAndHashCode public final class LoginResponse { private final long userId; private final @NonNull String authToken; private final @NonNull Instant loginTs; @Wither private final @NonNull Instant tokenExpiryTs; }Vale a pena notar aqui:

- Uma anotação
@RequiredArgsConstructorfoi introduzida. Apropriadamente nomeado, o que ele faz é gerar um construtor para todos os campos finais que ainda não foram inicializados. - Nos casos em que queremos reutilizar um LoginResonse emitido anteriormente (imagine, por exemplo, uma operação de “refresh token”), certamente não queremos modificar nossa instância existente, mas sim gerar uma nova com base nela . Veja como a anotação
@Withernos ajuda aqui: Ela diz ao Lombok para gerar umwithTokenExpiryTs(Instant tokenExpiryTs)que cria uma nova instância de LoginResponse com todos os valores de instância with'ed, exceto o novo que estamos especificando. Você gostaria deste comportamento para todos os campos? Basta adicionar@Witherà declaração de classe.
@Data e @Value
Ambos os casos de uso discutidos até agora são tão comuns que o Lombok envia algumas anotações para torná-los ainda mais curtos: Anotar uma classe com @Data fará com que o Lombok se comporte como se tivesse sido anotado com @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor . Da mesma forma, usar @Value tornará sua classe imutável (e final), também como se tivesse sido anotada com a lista acima.
Padrão do Construtor
Voltando ao nosso exemplo User, se quisermos criar uma nova instância, precisaremos usar um construtor com até seis argumentos. Este já é um número bastante grande, que ficará ainda pior se adicionarmos mais atributos à classe. Suponha também que desejamos definir alguns valores padrão para os campos lastName e payingCustomer .
O Lombok implementa um recurso @Builder muito poderoso, permitindo-nos usar um Builder Pattern para criar novas instâncias. Vamos adicioná-lo à nossa classe User:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) @Builder public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName = ""; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }Agora podemos criar novos usuários fluentemente como este:
User user = User .builder() .email("[email protected]") .password("secret".getBytes(StandardCharsets.UTF_8)) .firstName("Miguel") .registrationTs(Instant.now()) .build();É fácil imaginar o quão conveniente essa construção se torna à medida que nossas classes crescem.
Delegação/Composição
Se você quer seguir a regra muito sensata de “favorecer composição sobre herança”, isso é algo que Java não ajuda muito, em termos de verbosidade. Se você deseja compor objetos, normalmente precisa escrever chamadas de métodos de delegação em todo lugar.
Lombok propõe uma solução para isso via @Delegate . Vamos dar uma olhada em um exemplo.
Imagine que queremos introduzir um novo conceito de ContactInformation . Essas são algumas informações que nosso User possui e podemos querer que outras classes também tenham. Podemos então modelar isso por meio de uma interface como esta:
public interface HasContactInformation { String getEmail(); String getFirstName(); String getLastName(); } Em seguida, introduziríamos uma nova classe ContactInformation usando o Lombok:
import lombok.Data; @Data public class ContactInformation implements HasContactInformation { private String email; private String firstName; private String lastName; } E, finalmente, poderíamos refatorar User para compor com ContactInformation e usar o Lombok para gerar todas as chamadas de delegação necessárias para corresponder ao contrato de interface:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; import lombok.experimental.Delegate; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"contactInformation"}) public class User implements HasContactInformation { @Getter(AccessLevel.NONE) @Delegate(types = {HasContactInformation.class}) private final ContactInformation contactInformation = new ContactInformation(); private @NonNull byte[] password; private @NonNull Instant registrationTs; private boolean payingCustomer = false; } Observe como eu não precisei escrever implementações para os métodos de HasContactInformation : isso é algo que estamos dizendo ao Lombok para fazer, delegando chamadas para nossa instância ContactInformation .
Além disso, como não quero que a instância delegada seja acessível de fora, estou personalizando-a com um @Getter(AccessLevel.NONE) , impedindo efetivamente a geração de getter para ela.
Exceções verificadas
Como todos sabemos, Java diferencia entre exceções verificadas e não verificadas. Esta é uma fonte tradicional de controvérsias e críticas à linguagem, pois o tratamento de exceções às vezes atrapalha muito, especialmente quando lidamos com APIs projetadas para lançar exceções verificadas e, portanto, forçando os desenvolvedores a capturá-las ou declarar nossos métodos para jogá-los.
Considere este exemplo:
public class UserService { public URL buildUsersApiUrl() { try { return new URL("https://apiserver.com/users"); } catch (MalformedURLException ex) { // Malformed? Really? throw new RuntimeException(ex); } } } Este é um padrão muito comum: certamente sabemos que nossa URL está bem formada, mas – porque o construtor de URL lança uma exceção verificada – somos forçados a capturá-la ou declarar nosso método para lançá-la e colocar os chamadores na mesma situação. Envolver essas exceções verificadas dentro de um RuntimeException é uma prática muito estendida. E isso fica ainda pior se o número de exceções verificadas com as quais precisamos lidar cresce à medida que codificamos.
Portanto, é exatamente para isso que serve o @SneakyThrows do Lombok, ele envolverá todas as exceções verificadas sujeitas a serem lançadas em nosso método em um não verificado e nos livrará do aborrecimento:
import lombok.SneakyThrows; public class UserService { @SneakyThrows public URL buildUsersApiUrl() { return new URL("https://apiserver.com/users"); } }Exploração madeireira
Com que frequência você adiciona instâncias de logger às suas classes assim? (amostra SLF4J)
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);Vou adivinhar bastante. Sabendo disso, os criadores do Lombok implementaram uma anotação que cria uma instância de logger com um nome personalizável (o padrão é log), suportando as estruturas de log mais comuns na plataforma Java. Assim (novamente, baseado em SLF4J):
import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @Slf4j public class UserService { @SneakyThrows public URL buildUsersApiUrl() { log.debug("Building users API URL"); return new URL("https://apiserver.com/users"); } }Anotando o código gerado
Se usarmos o Lombok para gerar código, pode parecer que perderemos a capacidade de anotar esses métodos, já que não os estamos escrevendo. Mas isso não é realmente verdade. Em vez disso, o Lombok nos permite dizer como gostaríamos que o código gerado fosse anotado, usando uma notação um tanto peculiar, verdade seja dita.
Considere este exemplo, visando o uso de uma estrutura de injeção de dependência: Temos uma classe UserService que usa injeção de construtor para obter as referências a um UserRepository e UserApiClient .
package com.mgl.toptal.lombok; import javax.inject.Inject; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor(onConstructor = @__(@Inject)) public class UserService { private final UserRepository userRepository; private final UserApiClient userApiClient; // Instead of: // // @Inject // public UserService(UserRepository userRepository, // UserApiClient userApiClient) { // this.userRepository = userRepository; // this.userApiClient = userApiClient; // } }A amostra acima mostra como anotar um construtor gerado. O Lombok também nos permite fazer a mesma coisa para métodos e parâmetros gerados.
Aprendendo mais
O uso do Lombok explicado neste post se concentra nos recursos que eu pessoalmente achei mais úteis ao longo dos anos. No entanto, existem muitos outros recursos e personalizações disponíveis.
A documentação do Lombok é muito informativa e completa. Eles têm páginas dedicadas para cada recurso (anotação) com explicações e exemplos muito detalhados. Se você achar este post interessante, eu o encorajo a mergulhar mais fundo no lombok e sua documentação para saber mais.
O site do projeto documenta como usar o Lombok em vários ambientes de programação diferentes. Em resumo, os IDEs mais populares (Eclipse, NetBeans e IntelliJ) são suportados. Eu mesmo mudo regularmente de um para outro por projeto e uso o Lombok em todos eles perfeitamente.
Delombok!
O Delombok faz parte da “cadeia de ferramentas Lombok” e pode ser muito útil. O que ele faz é basicamente gerar o código-fonte Java para seu código anotado do Lombok, executando as mesmas operações que o bytecode gerado pelo Lombok faz.
Esta é uma ótima opção para pessoas que pensam em adotar o Lombok, mas ainda não têm certeza. Você pode começar a usá-lo livremente e não haverá “bloqueio de fornecedor”. Caso você ou sua equipe se arrependam da escolha, você sempre pode usar o delombok para gerar o código-fonte correspondente, que pode ser usado sem nenhuma dependência restante do Lombok.
O Delombok também é uma ótima ferramenta para saber exatamente o que o Lombok fará. Existem maneiras muito fáceis de conectá-lo ao seu processo de construção.
Alternativas
Existem muitas ferramentas no mundo Java que fazem uso semelhante de processadores de anotação para enriquecer ou modificar seu código em tempo de compilação, como Immutables ou Google Auto Value. Esses (e outros, com certeza!) se sobrepõem aos recursos do Lombok. Eu particularmente gosto muito da abordagem Immutables e também a usei em alguns projetos.
Também vale a pena notar que existem outras ótimas ferramentas que fornecem recursos semelhantes para “aprimoramento de bytecode”, como Byte Buddy ou Javassist. No entanto, eles normalmente funcionam em tempo de execução e compreendem um mundo próprio além do escopo deste post.
Java conciso
Existem várias linguagens modernas direcionadas à JVM que fornecem abordagens de design mais idiomáticas — ou até mesmo em nível de linguagem — ajudando a resolver alguns dos mesmos problemas. Certamente Groovy, Scala e Kotlin são bons exemplos. Mas se você estiver trabalhando em um projeto somente Java, o Lombok é uma boa ferramenta para ajudar seus programas a serem mais concisos, expressivos e de fácil manutenção.
