
Nós do Grubhub usamos Java em quase todo o back-end. Essa é uma linguagem comprovada que comprovou sua velocidade e confiabilidade nos últimos 20 anos. Mas ao longo dos anos, a idade do "velho homem" ainda começou a afetar.
Java é
uma das linguagens JVM mais populares , mas não a única. Nos últimos anos, ele vem competindo com Scala, Clojure e Kotlin, que fornecem novas funcionalidades e recursos de linguagem otimizados. Em resumo, eles permitem que você faça mais com códigos mais concisos.
Essas inovações no ecossistema da JVM são muito interessantes. Devido à concorrência, o Java é forçado a mudar para permanecer competitivo. O novo cronograma de lançamento de seis meses e várias JEP (propostas de aprimoramento do JDK) no Java 8 (Valhalla, Inferência de tipo variável local, tear) são a prova de que o Java permanecerá uma linguagem competitiva por anos.
No entanto, o tamanho e a escala do Java significam que o desenvolvimento está progredindo mais lentamente do que gostaríamos, sem mencionar o forte desejo de manter a compatibilidade com versões anteriores a todo custo. Em qualquer desenvolvimento, a primeira prioridade deve ser funções, mas aqui as funções necessárias foram desenvolvidas por muito tempo, se é que há, na linguagem. Portanto, no Grubhub, usamos o Projeto Lombok para otimizar e aprimorar o Java à nossa disposição no momento. O projeto Lombok é um plug-in de compilador que adiciona novas “palavras-chave” ao Java e transforma anotações em código Java, reduzindo o esforço de desenvolvimento e fornecendo algumas funcionalidades adicionais.
Configurar Lombok
O Grubhub sempre se esforça para melhorar o ciclo de vida do software, mas cada nova ferramenta e processo tem um custo a considerar. Felizmente, para conectar o Lombok, basta adicionar algumas linhas ao arquivo gradle.
O Lombok converte anotações no código-fonte em instruções Java antes que o compilador as processe: a dependência do
lombok
não
lombok
em tempo de execução, portanto, o uso do plug-in não aumentará o tamanho do assembly. Para
configurar o Lombok com Gradle (também funciona com o Maven), basta adicionar as
seguintes linhas ao arquivo
build.gradle :
plugins { id 'io.franzbecker.gradle-lombok' version '1.14' id 'java' } repositories { jcenter()
Com o Lombok, nosso código fonte
não será um código Java válido. Portanto, você precisará instalar um plug-in para o IDE, caso contrário, o ambiente de desenvolvimento não entenderá com o que está lidando. O Lombok suporta todos os principais IDEs Java. Integração perfeita. Todas as funções como “show use” e “go to deployment” continuam funcionando como antes, movendo você para o campo / classe correspondente.
Lombok em ação
A melhor maneira de conhecer o Lombok é vê-lo em ação. Considere alguns exemplos típicos.
Reviva o objeto POJO
Com os "bons e antigos objetos Java" (POJOs), separamos os dados do processamento para facilitar a leitura do código e otimizar as transferências de rede. Um POJO simples possui vários campos particulares, bem como getters e setters correspondentes. Eles fazem o trabalho, mas requerem muito código padrão.
O Lombok ajuda a usar o POJO de maneira mais flexível e estruturada, sem código adicional. É
@Data
que simplificamos o POJO subjacente com a anotação
@Data
:
@Data public class User { private UUID userId; private String email; }
@Data
é apenas uma anotação conveniente que aplica várias anotações do Lombok de uma só vez.
@ToString
gera uma implementação para o toString()
, que consiste em uma representação clara do objeto: o nome da classe, todos os campos e seus valores.
@EqualsAndHashCode
gera implementações de equals
e hashCode
, que por padrão usam campos não estáticos e não estacionários, mas são personalizáveis.
@Getter / @Setter
gera getters e setters para campos particulares.
@RequiredArgsConstructor
cria um construtor com os argumentos necessários, onde os campos finais e a anotação @NonNull
são @NonNull
(mais sobre isso abaixo).
Somente esta anotação cobre de forma simples e elegante muitos casos de uso típicos. Mas o POJO nem sempre cobre a funcionalidade necessária.
@Data
é uma classe totalmente modificável, cujo abuso pode aumentar a complexidade e limitar a simultaneidade, o que afeta negativamente a capacidade de sobrevivência do aplicativo.
Existe outra solução. Vamos voltar à nossa classe
User
, torná-la imutável e adicionar algumas outras anotações úteis.
@Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; }
A anotação
@Value
semelhante a
@Data
exceto que todos os campos são privados e finais por padrão, e os setters não são criados. Graças a isso, os objetos
@Value
imediatamente se tornam imutáveis. Como todos os campos são finais, não há construtor de argumentos. Em vez disso, o Lombok usa o
@AllArgsConstructor
. O resultado é um objeto totalmente funcional e imutável.
Mas a imutabilidade não é muito útil se você precisar apenas criar um objeto usando o construtor all-args. Como Joshua Bloch explica em seu livro Effective Java Programming, você deve usar construtores se tiver um grande número de parâmetros de designer. Aqui a classe
@Builder
entra em
@Builder
, gerando automaticamente a classe interna do construtor:
User user = User.builder() .userId(UUID.random()) .email(“grubhub@grubhub.com”) .favoriteFood(“burritos”) .favoriteFood(“dosas”) .build()
A geração de um construtor facilita a criação de objetos com um grande número de argumentos e a inclusão de novos campos no futuro. O método estático retorna uma instância do construtor para configurar todas as propriedades do objeto. Depois disso, a chamada para
build()
retorna a instância.
A
@NonNull
pode ser usada para
@NonNull
que esses campos não são nulos ao criar uma instância do objeto; caso contrário, uma
NullPointerException
lançada. Observe que o campo do avatar é anotado com
@NonNull
mas não definido. O fato é que a anotação
@Builder.Default
aponta para
default.png por padrão.
Observe também como o construtor usa
favoriteFood
, o único nome de propriedade em nosso objeto. Ao colocar anotações
@Singular
em uma propriedade de coleção, o Lombok cria métodos especiais do construtor para adicionar itens à coleção individualmente e não para adicionar a coleção inteira ao mesmo tempo. Isso é especialmente bom para testes, porque as maneiras de criar pequenas coleções em Java não podem ser chamadas de simples e rápidas.
Por fim, o parâmetro
toBuilder = true
adiciona o método da instância
toBuilder()
, que cria um objeto construtor preenchido com todos os valores dessa instância. É tão fácil criar uma nova instância, preenchida previamente com todos os valores do original, para que resta alterar apenas os campos necessários. Isso é especialmente útil para
@Value
classes
@Value
, porque os campos são imutáveis.
Algumas notas personalizam ainda mais as funções especiais do setter.
@Wither
cria métodos
@Wither
para cada propriedade. Na entrada, o valor; na saída, o clone da instância com o valor atualizado de um campo.
@Accessors
permite que você configure setters criados automaticamente. O parâmetro
fluent=true
desativa as convenções get e set para getters e setters. Em certas situações, isso pode ser um substituto útil para o
@Builder
.
Se a implementação do Lombok não for adequada para a sua tarefa (e você analisou os modificadores de anotação), sempre poderá tirar e escrever sua própria implementação. Por exemplo, se você possui a classe
@Data
, mas um getter precisa de lógica personalizada, basta implementar esse getter. O Lombok verá que a implementação já foi fornecida e não a substituirá pela implementação criada automaticamente.
Com apenas algumas anotações simples, o POJO básico recebeu tantos recursos avançados que simplificam seu uso sem carregar o trabalho de nossos engenheiros, sem perder tempo ou aumentar os custos de desenvolvimento.
Removendo o Código do Modelo
O Lombok é útil não apenas para POJO: pode ser aplicado em qualquer nível do aplicativo. Os seguintes usos do Lombok são especialmente úteis em classes de componentes, como controladores, serviços e DAOs (objetos de acesso a dados).
O registro é um requisito básico para todas as partes do programa. Qualquer classe que faça um trabalho significativo deve escrever um log. Assim, o criador de logs padrão se torna um modelo para cada classe. O Lombok simplifica esse modelo em uma única anotação que identifica e instancia automaticamente um criador de logs com o nome de classe correto. Existem várias anotações diferentes, dependendo da estrutura do diário.
@Slf4j
Após declarar o logger, adicione nossas dependências:
@Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; }
A
@FieldDefaults
adiciona modificadores finais e privados a todos os campos.
@RequiredArgsConstructor
cria um construtor que configura uma instância do
UserDao
. A
@NonNull
adiciona validação no construtor e
UserDao
NullPointerException
se a instância
UserDao
for zero.
Mas espere, isso não é tudo!
Existem muitas outras situações em que Lombok faz o seu melhor. As seções anteriores mostraram exemplos específicos, mas o Lombok pode facilitar o desenvolvimento em muitas áreas. Aqui estão alguns pequenos exemplos de como usá-lo com mais eficiência.
Embora a palavra-chave
var
aparecido no Java 9, a variável ainda pode ser reatribuída. Lombok tem a palavra-chave
val
, que imprime o tipo final de uma variável local.
Algumas classes com funções puramente estáticas não devem ser inicializadas. Uma maneira de impedir a instanciação é declarar um construtor privado que lança uma exceção. Lombok codificou esse modelo na anotação
@UtilityClass
. Ele gera um construtor privado que lança uma exceção, finalmente gera a classe e torna todos os métodos estáticos.
@UtilityClass
O Java é frequentemente criticado por verbosidade devido a exceções verificadas. Uma anotação separada do Lombok os corrige:
@SneakyThrows
. Como esperado, a implementação é bastante complicada. Ele não captura exceções nem quebra exceções em uma
RuntimeException
. Em vez disso, depende do fato de a JVM não verificar a consistência das exceções verificadas no tempo de execução. Somente javac faz isso. Portanto, o Lombok usa a conversão de bytecode no momento da compilação para desativar essa verificação. O resultado é um código executável.
public class SneakyThrows { @SneakyThrows public void sneakyThrow() { throw new Exception(); } }
Comparação lado a lado
As comparações diretas mostram melhor quanto código o Lombok salva. O plug-in IDE possui uma função "de-lombok" que converte aproximadamente a maioria das anotações do Lombok em código Java nativo (
@NonNull
anotações
@NonNull
não
@NonNull
convertidas). Portanto, qualquer IDE com o plug-in instalado poderá converter a maioria das anotações em código Java nativo e vice-versa. Voltar à nossa classe de
User
.
@Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; }
A classe Lombok é apenas 13 linhas simples, legíveis e compreensíveis. Mas depois de executar o de-lombok, a classe se transforma em mais de cem linhas de código padrão!
public class User { @NonNull UUID userId; @NonNull String email; Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = "default.png"; @java.beans.ConstructorProperties({"userId", "email", "favoriteFoods", "avatar"}) User(UUID userId, String email, Set<String> favoriteFoods, String avatar) { this.userId = userId; this.email = email; this.favoriteFoods = favoriteFoods; this.avatar = avatar; } public static UserBuilder builder() { return new UserBuilder(); } @NonNull public UUID getUserId() { return this.userId; } @NonNull public String getEmail() { return this.email; } public Set<String> getFavoriteFoods() { return this.favoriteFoods; } @NonNull public String getAvatar() { return this.avatar; } public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof User)) return false; final User other = (User) o; final Object this$userId = this.getUserId(); final Object other$userId = other.getUserId(); if (this$userId == null ? other$userId != null : !this$userId.equals(other$userId)) return false; final Object this$email = this.getEmail(); final Object other$email = other.getEmail(); if (this$email == null ? other$email != null : !this$email.equals(other$email)) return false; final Object this$favoriteFoods = this.getFavoriteFoods(); final Object other$favoriteFoods = other.getFavoriteFoods(); if (this$favoriteFoods == null ? other$favoriteFoods != null : !this$favoriteFoods.equals(other$favoriteFoods)) return false; final Object this$avatar = this.getAvatar(); final Object other$avatar = other.getAvatar(); if (this$avatar == null ? other$avatar != null : !this$avatar.equals(other$avatar)) return false; return true; } public int hashCode() { final int PRIME = 59; int result = 1; final Object $userId = this.getUserId(); result = result * PRIME + ($userId == null ? 43 : $userId.hashCode()); final Object $email = this.getEmail(); result = result * PRIME + ($email == null ? 43 : $email.hashCode()); final Object $favoriteFoods = this.getFavoriteFoods(); result = result * PRIME + ($favoriteFoods == null ? 43 : $favoriteFoods.hashCode()); final Object $avatar = this.getAvatar(); result = result * PRIME + ($avatar == null ? 43 : $avatar.hashCode()); return result; } public String toString() { return "User(userId=" + this.getUserId() + ", email=" + this.getEmail() + ", favoriteFoods=" + this.getFavoriteFoods() + ", avatar=" + this.getAvatar() + ")"; } public UserBuilder toBuilder() { return new UserBuilder().userId(this.userId).email(this.email).favoriteFoods(this.favoriteFoods).avatar(this.avatar); } public static class UserBuilder { private UUID userId; private String email; private ArrayList<String> favoriteFoods; private String avatar; UserBuilder() { } public User.UserBuilder userId(UUID userId) { this.userId = userId; return this; } public User.UserBuilder email(String email) { this.email = email; return this; } public User.UserBuilder favoriteFood(String favoriteFood) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.add(favoriteFood); return this; } public User.UserBuilder favoriteFoods(Collection<? extends String> favoriteFoods) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.addAll(favoriteFoods); return this; } public User.UserBuilder clearFavoriteFoods() { if (this.favoriteFoods != null) this.favoriteFoods.clear(); return this; } public User.UserBuilder avatar(String avatar) { this.avatar = avatar; return this; } public User build() { Set<String> favoriteFoods; switch (this.favoriteFoods == null ? 0 : this.favoriteFoods.size()) { case 0: favoriteFoods = java.util.Collections.emptySet(); break; case 1: favoriteFoods = java.util.Collections.singleton(this.favoriteFoods.get(0)); break; default: favoriteFoods = new java.util.LinkedHashSet<String>(this.favoriteFoods.size() < 1073741824 ? 1 + this.favoriteFoods.size() + (this.favoriteFoods.size() - 3) / 3 : Integer.MAX_VALUE); favoriteFoods.addAll(this.favoriteFoods); favoriteFoods = java.util.Collections.unmodifiableSet(favoriteFoods); } return new User(userId, email, favoriteFoods, avatar); } public String toString() { return "User.UserBuilder(userId=" + this.userId + ", email=" + this.email + ", favoriteFoods=" + this.favoriteFoods + ", avatar=" + this.avatar + ")"; } } }
Faremos o mesmo para a classe
UserService
.
@Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; }
Aqui está um exemplo de contraparte no código Java padrão.
public class UserService { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class); private final UserDao userDao; @java.beans.ConstructorProperties({"userDao"}) public UserService(UserDao userDao) { if (userDao == null) { throw new NullPointerException("userDao is marked @NonNull but is null") } this.userDao = userDao; } }
Classificação de efeito
A Grubhub possui mais de cem serviços comerciais de entrega de alimentos. Nós pegamos um deles e lançamos a função "de-lombok" no plug-in Lombok IntelliJ. Como resultado, cerca de 180 arquivos foram alterados e a base de código cresceu cerca de 18.000 linhas de código após remover 800 casos de uso do Lombok. Em média, cada linha Lombok salva 23 linhas Java. Com esse efeito, é difícil imaginar Java sem o Lombok.
Sumário
Lombok é um ótimo ajudante que implementa novos recursos de idioma sem exigir muito esforço do desenvolvedor. Obviamente, é mais fácil instalar o plugin do que treinar todos os engenheiros em um novo idioma e portar o código existente. Lombok não é onipotente, mas pronto para uso é poderoso o suficiente para realmente ajudar no trabalho.
Outra vantagem do Lombok é que ele mantém bases de código consistentes. Como temos mais de uma centena de serviços diferentes e uma equipe distribuída em todo o mundo, a coerência das bases de código facilita o dimensionamento das equipes e reduz a carga de alternar contextos ao iniciar um novo projeto. O Lombok funciona para qualquer versão desde o Java 6, para que possamos contar com sua disponibilidade em todos os projetos.
Para o Grubhub, isso é mais do que apenas novos recursos. No final, todo esse código
pode ser escrito manualmente. Mas Lombok simplifica as partes chatas da base de código sem afetar a lógica de negócios. Isso permite que você se concentre nas coisas que são realmente importantes para os negócios e as mais interessantes para nossos desenvolvedores. O código do modelo Monton é uma perda de tempo para programadores, revisores e mantenedores. Além disso, como esse código não é mais escrito manualmente, ele elimina classes inteiras de erros de digitação. Os benefícios da
@NonNull
automática combinados com o poder do
@NonNull
reduzem a probabilidade de erros e ajudam nosso desenvolvimento, que visa entregar comida à sua mesa!