Lombok retorna a grandeza do Java



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() // or Maven central, required for Lombok dependency } lombok { version = '1.18.4' sha256 = "" } 

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 // also: @CommonsLog @Flogger @JBossLog @Log @Log4j @Log4j2 @XSlf4j public class UserService { // created automatically // private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class); } 

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.

 // final Map map = new HashMap<Integer, String>(); val map = new HashMap<Integer, String>(); 

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 // will be made final public class UtilityClass { // will be made static private final int GRUBHUB = “ GRUBHUB”; // autogenerated by Lombok // private UtilityClass() { // throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated"); //} // will be made static public void append(String input) { return input + GRUBHUB; } } 

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!

Source: https://habr.com/ru/post/pt438870/


All Articles