Lombok foi uma salvação. Durante anos, nos poupou de escrever getters, setters, construtores e métodos repetitivos toString. equalsMas hashCodeo Java moderno alcançou esse nível. Entre registros, classes seladas, casamento de padrões e algumas outras adições, a maior parte do que Lombok fazia agora está integrada à linguagem.
Este artigo analisa cada anotação do Lombok que você provavelmente está usando, mostra o equivalente moderno em Java e explica por que você não precisa dela.
Por que reconsiderar Lombok?
Em essência, o Lombok é uma dependência adicional no seu projeto. E como qualquer dependência, aumenta a sua superfície de ataque. É mais uma coisa para auditar, corrigir e manter atualizada. Sempre sou cauteloso ao adicionar dependências quando não há necessidade real delas, e esse cuidado se torna ainda maior quando a própria linguagem passa a abranger o mesmo.
Além da questão da dependência, o Lombok traz consigo desvantagens que se acumulam ao longo do tempo:
- Fragilidade dos plugins do compilador : o Lombok se integra a
javaccomponentes internos. Cada versão principal do JDK apresenta riscos de quebra, e o ciclo de correções pode bloquear seu caminho de atualização. - Riscos de segurança e da cadeia de suprimentos : Cada dependência é uma vulnerabilidade em potencial. O Lombok funciona como um processador de anotações dentro do seu compilador e tem acesso profundo à sua compilação. Mesmo que o Lombok em si seja seguro hoje, ele representa mais um artefato na sua cadeia de suprimentos para monitorar e mais um ponto de entrada em caso de comprometimento. Se você estava por perto durante o período de festas de 2021, quando a vulnerabilidade do Log4j foi descoberta, sabe o quão problemático pode ser um patch urgente de dependência. Quanto menos dependências você tiver, menor será o impacto quando a próxima vulnerabilidade for revelada.
- Lacunas no suporte da IDE : o processamento de anotações surpreende novos membros da equipe. A navegação no código, as ferramentas de refatoração e a análise estática nem sempre reconhecem o código gerado pelo Lombok.
- Pontos cegos de depuração : os rastreamentos de pilha fazem referência a métodos gerados que você não pode acessar passo a passo ou ler no código-fonte.
- Dependência de uma única biblioteca : Lombok é mantido por uma equipe pequena. Se o projeto desacelerar, seu código dependerá dele.
O Java moderno não apenas substitui o Lombok, mas o substitui por recursos de linguagem de primeira classe que todas as IDEs, depuradores e ferramentas entendem nativamente.
IDEs e IA já geram o código padrão.
Um dos principais atrativos iniciais de Lombok era a economia de digitação. Mas, em 2026, esse argumento perdeu grande parte da sua relevância.
As IDEs fazem isso há anos. IntelliJ IDEA, Eclipse e VS Code geram construtores, getters, setters equalse hashCodebuilders toStringa partir de um menu ou atalho. O código gerado é Java puro: visível, pesquisável e depurável. Não há processador de anotações na sua compilação, nem com que se preocupar em relação à compatibilidade de plugins.
O desenvolvimento assistido por IA preenche a lacuna restante. GitHub Copilot, Gemini, Windsurf, Cursor, Claude Code e outros assistentes de codificação com IA geram código boilerplate tão rápido quanto você consegue descrevê-lo. Precisa de um construtor? Digite um comentário ou pergunte no chat. Precisa equalsde algo hashCodebaseado em campos específicos? A IA escreve em segundos, com todo o contexto da sua classe. Ao contrário do Lombok, o resultado é Java padrão que qualquer desenvolvedor pode ler sem precisar conhecer nenhuma biblioteca específica.
A combinação de registros (para tipos imutáveis), geração de IDE (para classes mutáveis) e assistência de IA (para qualquer coisa personalizada) cobre todos os cenários que o Lombok suporta, sem adicionar nenhuma dependência.
Lombok nos livrou da digitação. As ferramentas modernas também nos livram da digitação, mas sem ocultar o código.
Migração anotação por anotação
@Valor → Registros (substituição direta)
@ValueTorna uma classe imutável com getters, toString, equals, e hashCode. Registros são uma substituição direta, em uma única linha.
Lombok:
@Value public class Product { String name; double price; String category; } |
Java moderno (16+):
public record Product(String name, double price, String category) {} |
Essa única linha fornece:
- Um construtor que recebe todos os campos.
- Métodos de acesso (
name(),price(),category()) toString(),equals(), ehashCode()com base em todos os campos- Imutabilidade por padrão: os campos são
final
Você pode adicionar validação em um construtor compacto:
public record Product(String name, double price, String category) { public Product { if (price < 0) throw new IllegalArgumentException("Price cannot be negative"); name = name.strip(); } } |
Se você estiver usando @Valuea versão atual, os registros são o caminho de atualização. Sem ressalvas.
@Data → Registros quando possível, classes simples quando não for possível
@DataGera getters, setters , toStringmétodos equalse propriedades hashCodeem uma classe mutável. Registros são imutáveis, portanto, não substituem @Dataem todos os casos. Mas, na prática, muitas @Dataclasses não precisam de mutabilidade. São DTOs, respostas, objetos de configuração ou objetos de valor que são criados uma única vez e nunca alterados. Para esses casos, um registro é a melhor escolha.
Se a classe for efetivamente imutável (criada uma única vez, com os campos nunca reatribuídos), mude para um registro:
// Before: @Data, but nothing ever calls the setters @Data public class Product { private String name; private double price; private String category; } // After: record, immutability is explicit public record Product(String name, double price, String category) {} |
Se a classe realmente precisar de mutabilidade , os registros não funcionarão. Isso inclui entidades JPA, construtores mutáveis ou classes em que os campos são definidos após a construção. Para esses casos, descarte a mutabilidade @Datae use uma classe simples com acessadores gerados pela IDE.
public class Product { private String name; private double price; private String category; public Product(String name, double price, String category) { this.name = name; this.price = price; this.category = category; } // IDE-generated getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } } |
Sim, há mais linhas de código. Mas você controla a mutabilidade explicitamente, seus métodos setters podem incluir validação e não há processador de anotações em sua compilação.
Os registros também possuem algumas restrições estruturais que vale a pena conhecer:
- Os registros são finais. Eles não podem ser estendidos por subclasses. Se sua
@Dataclasse estiver em uma hierarquia de herança, um registro não funcionará. - Registros não podem estender outras classes (eles estendem implicitamente
java.lang.Record). Eles podem implementar interfaces. - Os acessadores de registros usam `get`
name(), não `get`getName(). Algumas estruturas e bibliotecas esperam métodos no estilo JavaBeangetXxx(). A maioria das estruturas modernas (Jackson, Spring) lida bem com registros, mas verifique se você está integrando com bibliotecas mais antigas.
Uma regra prática: se você puder criar um registro, crie um registro. Se não puder, escreva uma classe simples. De qualquer forma, Lombok não é necessário.
@Getter e @Setter → Registros ou acessadores simples
Para dados imutáveis, os registros lidam com isso automaticamente. Para classes mutáveis, seu IDE gera acessadores em segundos, e o código fica visível para todos.
Lombok:
@Getter @Setter public class UserPreferences { private String theme; private String language; private boolean notifications; } |
Java moderno:
Se o comportamento imutável funcionar (e geralmente funciona):
public record UserPreferences(String theme, String language, boolean notifications) {} |
Se você precisa de mutabilidade, basta escrever a classe. As IDEs geram o código padrão instantaneamente, e você pode ler cada linha:
public class UserPreferences { private String theme; private String language; private boolean notifications; // IDE-generated getters and setters public String getTheme() { return theme; } public void setTheme(String theme) { this.theme = theme; } // ... remaining accessors } |
@ToString → Geração de registros ou IDE
Os registros geram um log de limpeza toString()automaticamente. Para classes que não são registros, use a geração automática do IDE ou sobrescreva manualmente. São apenas algumas linhas de código e você controla exatamente o que será registrado (importante quando os campos contêm dados sensíveis).
Lombok:
@ToString(exclude = "password") public class User { private String name; private String email; private String password; } |
Java moderno:
public record User(String name, String email, String password) { @Override public String toString() { return "User[name=%s, email=%s]".formatted(name, email); } } |
O uso explícito toStringtem uma vantagem: você vê imediatamente quais campos estão incluídos. Sem mágica de anotações para esconder o que passwordestá sendo excluído.
@EqualsAndHashCode → Registros
Os registros são gerados equals()com hashCode()base em todos os componentes. Se você precisar de igualdade personalizada, substitua-a explicitamente.
Lombok:
@EqualsAndHashCode(of = {"id"}) public class Order { private Long id; private String description; private BigDecimal total; } |
Java moderno:
public record Order(Long id, String description, BigDecimal total) { @Override public boolean equals(Object o) { return o instanceof Order other && Objects.equals(id, other.id); } @Override public int hashCode() { return Objects.hashCode(id); } } |
@AllArgsConstructor, @NoArgsConstructor, @RequiredArgsConstructor → Registros e construtores flexíveis
Os registros vêm com um construtor canônico. Para classes que precisam de vários formatos de construtor, o Java 25 introduziu corpos de construtor flexíveis, para que você possa validar e definir campos antes de chamar o método super().
Lombok:
@AllArgsConstructor @NoArgsConstructor @RequiredArgsConstructor public class Connection { private final String host; private final int port; private String label; } |
Java moderno:
public record Connection(String host, int port, String label) { public Connection(String host, int port) { this(host, port, host + ":" + port); } } |
Para classes que não são registros, os construtores flexíveis do Java 25 permitem que você valide antes da delegação:
public class Connection { private final String host; private final int port; public Connection(String host, int port) { // Validate before assigning (Java 25+) if (host == null || host.isBlank()) throw new IllegalArgumentException("Host required"); this.host = host; this.port = port; } } |
@Builder → Construtor manual ou fábricas estáticas
Esta é a única anotação do Lombok para a qual o Java moderno não possui uma substituição direta em uma única linha. Mas as alternativas são claras e, muitas vezes, melhores.
Lombok:
@Builder public class HttpRequest { private String url; private String method; private Map<String, String> headers; private String body; private Duration timeout; } |
Métodos de fábrica estáticos
Para tipos com alguns campos opcionais, as fábricas estáticas são mais simples do que um construtor:
public record HttpRequest( String url, String method, Map<String, String> headers, String body, Duration timeout ) { public static HttpRequest get(String url) { return new HttpRequest(url, "GET", Map.of(), null, Duration.ofSeconds(30)); } public static HttpRequest post(String url, String body) { return new HttpRequest(url, "POST", Map.of(), body, Duration.ofSeconds(30)); } } |
Construtor explícito
Para tipos com muitos campos opcionais, escreva o construtor. É mais código, mas cada linha é visível e depurável:
public record HttpRequest( String url, String method, Map<String, String> headers, String body, Duration timeout ) { public static Builder builder(String url) { return new Builder(url); } public static class Builder { private final String url; private String method = "GET"; private Map<String, String> headers = Map.of(); private String body; private Duration timeout = Duration.ofSeconds(30); Builder(String url) { this.url = url; } public Builder method(String method) { this.method = method; return this; } public Builder headers(Map<String, String> headers) { this.headers = headers; return this; } public Builder body(String body) { this.body = body; return this; } public Builder timeout(Duration timeout) { this.timeout = timeout; return this; } public HttpRequest build() { return new HttpRequest(url, method, headers, body, timeout); } } } |
Uso:
var request = HttpRequest.builder("https://api.example.com/data") .method("POST") .body("{\"key\": \"value\"}") .timeout(Duration.ofSeconds(10)) .build(); |
Sim, são mais linhas. Mas você pode percorrer cada chamada de método passo a passo, seu IDE indexa todos os campos e nenhum processador de anotações está envolvido.
@Slf4j e @Log → System.getLogger ou declaração direta
Lombok:
@Slf4j public class OrderService { public void process(Order order) { log.info("Processing order: {}", order.id()); } } |
Java moderno (9+) com System.getLogger:
public class OrderService { private static final System.Logger LOG = System.getLogger(OrderService.class.getName()); public void process(Order order) { LOG.log(System.Logger.Level.INFO, "Processing order: " + order.id()); } } |
Ou diretamente com SLF4J (mais comum):
public class OrderService { private static final Logger log = LoggerFactory.getLogger(OrderService.class); public void process(Order order) { log.info("Processing order: {}", order.id()); } } |
Uma única linha de declaração. Era só isso que Lombok estava te poupando.
@Limpeza → tente-com-recursos
Essa funcionalidade tornou-se desnecessária desde o Java 7.
Lombok:
public void readFile(String path) throws IOException { @Cleanup InputStream in = new FileInputStream(path); // use in } |
Java moderno (7+):
public void readFile(String path) throws IOException { try (var in = new FileInputStream(path)) { // use in } } |
try-with-resources é idiomático, universalmente compreendido e lida com múltiplos recursos de forma adequada.
@NonNull → Validação Jakarta, Objects.requireNonNull ou registros
Lombok:
public class UserService { public User createUser(@NonNull String name, @NonNull String email) { // Lombok inserts null check return new User(name, email); } } |
Java moderno com validação Jakarta (Spring Boot):
Se você estiver usando o Spring Boot, já terá o Jakarta Bean Validation no classpath. Adicione-o @NotNullaos parâmetros do seu método e anote a classe com @Validated:
@Validated @Service public class UserService { public User createUser(@NotNull String name, @NotNull String email) { return new User(name, email); } } |
O Spring intercepta a chamada e lança uma exceção ConstraintViolationExceptionse algum dos parâmetros for nulo. Não são necessárias verificações manuais.
O Spring Framework 7 (Spring Boot 4) leva isso adiante com a segurança contra nulos baseada em JSpecify, integrada ao framework. As anotações `@null` @NonNulle @Nullable`@null` do JSpecify são reconhecidas pelo framework, IDEs e ferramentas de análise estática sem necessidade de configuração adicional.
Java puro, sem Spring:
public class UserService { public User createUser(String name, String email) { Objects.requireNonNull(name, "name must not be null"); Objects.requireNonNull(email, "email must not be null"); return new User(name, email); } } |
Os registros também rejeitam componentes nulos por padrão se você adicionar uma verificação de construtor compacto:
public record User(String name, String email) { public User { Objects.requireNonNull(name, "name"); Objects.requireNonNull(email, "email"); } } |
@SneakyThrows → lide com suas exceções
Isso sempre foi controverso. @SneakyThrowsIgnora exceções verificadas enganando o compilador.
Lombok:
@SneakyThrows public String readConfig() { return Files.readString(Path.of("config.yml")); } |
Java moderno, simplesmente lide com isso:
public String readConfig() throws IOException { return Files.readString(Path.of("config.yml")); } |
Ou encapsule-o quando a interface não permitir exceções verificadas:
public String readConfig() { try { return Files.readString(Path.of("config.yml")); } catch (IOException e) { throw new UncheckedIOException(e); } } |
As exceções verificadas existem por um motivo. Ocultá-las dificulta a depuração.
val → var
Lombok introduziu valvariáveis locais imutáveis antes mesmo do Java var. Desde o Java 10, a inferência de tipo de variável local é nativa.
Lombok:
val items = List.of("a", "b", "c"); val count = items.size(); |
Java moderno (10+):
var items = List.of("a", "b", "c"); var count = items.size(); |
varÉ efetivamente final quando você não o reatribui. O compilador infere o tipo.
O perigo oculto: métodos setters @Data sem validação.
Isso é algo que não recebe a atenção necessária. Quando você adiciona @Datauma classe, o Lombok gera métodos setters públicos para cada campo. Esses métodos não realizam nenhuma validação. Eles apenas atribuem o valor. Isso significa que qualquer código, em qualquer lugar, pode colocar seu objeto em um estado inválido.
@Data public class Customer { private String name; private String email; private int age; } // Anywhere in the codebase: var customer = new Customer(); customer.setName(""); // empty name, valid? customer.setEmail("not-an-email"); // no format check customer.setAge(-5); // negative age, accepted silently |
O objeto existe. Está preenchido. É inválido. E nada o impediu de chegar a esse ponto.
Registros e construtores impõem a validade na criação.
Com um registro, você valida uma única vez, na construção, e o objeto tem sua validade garantida a partir desse ponto. Não há métodos setters para serem ignorados:
public record Customer(String name, String email, int age) { public Customer { if (name == null || name.isBlank()) throw new IllegalArgumentException("Name is required"); if (email == null || !email.contains("@")) throw new IllegalArgumentException("Valid email required"); if (age < 0) throw new IllegalArgumentException("Age cannot be negative"); } } |
Você ainda pode combinar isso com o Jakarta Bean Validation para verificações em nível de framework (Spring MVC, endpoints REST):
public record Customer( @NotBlank String name, @Email String email, @Min(0) int age ) { public Customer { Objects.requireNonNull(name, "name"); Objects.requireNonNull(email, "email"); } } |
Agora você tem o melhor dos dois mundos: imutabilidade em tempo de compilação, validação em tempo de execução na construção e validação do framework no limite da API. Nenhum método setter pode colocar o objeto em um estado inválido porque não existem métodos setter.
Para entidades JPA mutáveis onde os métodos setters são inevitáveis, adicione lógica de validação dentro do setter em vez de deixá-lo como uma atribuição cega:
public void setAge(int age) { if (age < 0) throw new IllegalArgumentException("Age cannot be negative"); this.age = age; } |
Os setters gerados pelo Lombok não fazem isso. Se você escrever os seus próprios, você controla as regras.
E quanto às entidades JPA?
Essa é a objeção mais comum: “Registros não funcionam com JPA.” Isso é verdade. Entidades JPA precisam de um construtor sem argumentos, campos mutáveis e getters/setters concretos.
Mas mesmo para entidades da JPA, o Lombok não é obrigatório:
@Entity @Table(name = "products") public class ProductEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private BigDecimal price; protected ProductEntity() {} // JPA requires this public ProductEntity(String name, BigDecimal price) { this.name = name; this.price = price; } public Long getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } } |
É mais código do que isso @Data? Sim. Mas:
- Você controla exatamente o que é mutável e o que não é.
equalsOhashCodeuso de entidades JPA com Lombok é uma fonte conhecida de bugs (proxies, campos carregados sob demanda, entidades desanexadas).- Seu IDE gera isso em segundos. É um custo único.
Utilize registros para DTOs e objetos de valor que saem da camada de persistência:
// DTO returned from service layer: clean, immutable public record ProductDTO(Long id, String name, BigDecimal price) { public static ProductDTO from(ProductEntity entity) { return new ProductDTO(entity.getId(), entity.getName(), entity.getPrice()); } } |
Estratégia de migração
Você não precisa remover o Lombok de todo o seu código de uma só vez. Aqui está uma abordagem prática:
- Novo código : Pare de usar Lombok em novas classes. Use registros para armazenar dados e classes simples para estado mutável.
- Delombok gradualmente : Execute
delombokem arquivos individuais para visualizar o código gerado e, em seguida, substitua por registros ou métodos gerados pela IDE. - Comece com registros : Converta
@Valueclasses@Dataque sejam portadoras de dados imutáveis. Essas são as soluções mais fáceis. - Deixe as entidades JPA por último : elas são as que menos se beneficiam dos registros. Use a geração do IDE e siga em frente.
- Remova a dependência : Assim que não houver mais anotações do Lombok, remova-a
lombokdo seupom.xmlarquivo de configuraçãobuild.gradlee da configuração do processador de anotações.
Maven, remover:
<!-- Remove from dependencies --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!-- Remove from annotation processor --> <annotationProcessorPath> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </annotationProcessorPath> |
Gradle, remover:
// Remove these lines compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' |
Tabela de referência rápida
| Anotação de Lombok | Substituição moderna do Java | Desde |
|---|---|---|
@Data |
record(se imutável) ou classe simples com acessadores gerados pela IDE |
Java 16 |
@Value |
record |
Java 16 |
@Getter/@Setter |
recordacessadores ou geração de IDE |
Java 16 |
@ToString |
recordou controle manual |
Java 16 |
@EqualsAndHashCode |
recordou controle manual |
Java 16 |
@AllArgsConstructor |
recordconstrutor canônico |
Java 16 |
@RequiredArgsConstructor |
recordou construtor explícito |
Java 16 |
@Builder |
Fábricas estáticas ou construtor explícito (a IDE pode gerar isso facilmente) | – |
@Slf4j/@Log |
System.getLogger()ou declaração direta |
Java 9 |
@Cleanup |
try-com-recursos |
Java 7 |
@NonNull |
Jacarta @NotNull+ @Validated, Objects.requireNonNull(), ou JSpecify |
Java 1.7 |
@SneakyThrows |
Tratamento adequado de exceções | – |
val |
var |
Java 10 |
“Mas a equipe Spring ainda usa Lombok”
Sim, usam. O próprio código-fonte do Spring Framework utiliza o Lombok, assim como muitos dos guias e exemplos oficiais do Spring. Isso é um fato, e vale a pena entender o porquê antes de descartá-lo.
A base de código do Spring é enorme e antecede os registros em mais de uma década. Remover o Lombok de um projeto desse porte exigiria um esforço significativo de refatoração com benefícios práticos limitados. O código já funciona, a equipe está ciente disso e provavelmente está focada na próxima geração de novos recursos e melhorias para o framework.
Mas observe a direção, não o estado atual:
- O Spring Data adota registros para projeções e DTOs desde o Spring Boot 3.x. A documentação recomenda ativamente o uso de registros para tipos de valores imutáveis.
- A configuração automática do Spring Boot não requer o Lombok. Trata-se de uma conveniência para o desenvolvimento, não de uma dependência em tempo de execução.
- O Spring AI , um dos projetos mais recentes do Spring, utiliza registros extensivamente em sua API.
- A equipe do Spring afirmou repetidamente que o Lombok é uma escolha da equipe , não uma recomendação. Juergen Hoeller (líder do Spring Framework) observou que o próprio framework poderá eventualmente abandonar o Lombok à medida que o Java evolui.
Há também uma diferença prática entre a equipe do Spring usar o Lombok e você usar o Lombok. O Spring Framework é uma biblioteca utilizada por milhões de desenvolvedores, e seu estilo de código interno não afeta o seu classpath. O código da sua aplicação é diferente: o Lombok está presente no seu pipeline de build, na configuração da sua IDE e no processo de integração da sua equipe.
O fato da equipe do Spring usar Lombok em sua base de código não significa que você deva usá-lo na sua. Significa que eles ainda não migraram, e migrar um framework de 20 anos é um problema diferente de começar do zero ou evoluir um projeto menor.
Quando Lombok ainda faz sentido
Para ser justo, existem alguns cenários em que manter Lombok é razoável:
- Grandes bases de código legadas, onde a remoção seria um esforço enorme com baixo retorno, são exemplos disso. O próprio Spring Framework é um bom exemplo. No entanto, se você tiver um bom conjunto de testes e tiver adotado o desenvolvimento assistido por IA, essa tarefa pode se tornar muito mais fácil, caso sua base de código não seja tão grande quanto o ecossistema do Spring Framework.
Minha recomendação pessoal: da próxima vez que você criar novas classes, opte por usar registros. Deixe as classes Java tradicionais apenas para entidades JPA. Comece não adicionando o Lombok ao seu novo código. Seja crítico: o fato de algo sempre ter sido feito de uma determinada maneira não significa que não haja necessidade de mudar e fazer as coisas de uma maneira melhor no futuro.
Lombok não é o inimigo. Ele resolveu problemas reais quando o Java não conseguia. Mas o Java agora consegue, e a linguagem continua melhorando a cada seis meses. A melhor dependência é aquela que você não precisa.
Referências
- JEP 395: Registros (Java 16)
- JEP 409: Classes Seladas (Java 17)
- JEP 286: Inferência de Tipo de Variável Local (Java 10)
- JEP 513: Corpos Construtores Flexíveis (Java 25)
- Projeto Lombok
- Ferramenta Delombok
Boa programação!




