个性化阅读
专注于IT技术分析

用Project Lombok编写无脂的Java代码

本文概述

这些天我无法想象自己编写了Java代码的许多工具和库。传统上, 像谷歌Guava或Joda Time(至少在Java 8之前的时代)这样的东西都是我最终在大多数时间都投入到我的项目中的依赖项, 而与手头的特定领域无关。

尽管不是典型的库/框架实用程序, 但Lombok在我的POM或Gradle构建中当然也应得到应有的地位。Lombok已经存在了很长一段时间(2009年首次发布), 并且从那时起已经成熟了很多。但是, 我一直觉得它值得更多关注-这是一种处理Java自然冗长的惊人方法。

在这篇文章中, 我们将探讨使Lombok成为方便工具的原因。

龙目岛计划

Java除了JVM本身(这是一款了不起的软件)之外, 还有很多其他用途。 Java是成熟和高性能的, 它周围的社区和生态系统也非常活跃。

但是, 作为一种编程语言, Java具有其自身的一些特质以及可以使其相当冗长的设计选择。添加一些Java开发人员经常需要使用的构造和类模式, 并且我们最终会得到许多行代码, 这些代码行除了遵守某些约束或框架约定外, 几乎没有或没有任何实际价值。

这是Lombok(Lombok)发挥作用的地方。它使我们能够大大减少需要编写的”样板”代码的数量。Lombok(Lombok)的创建者是几个非常聪明的人, 当然有幽默感-你不能错过他们在过去的会议上所作的介绍!

让我们看看Lombok(Lombok)如何发挥其魔力以及一些用法示例。

Lombok如何运作

Lombok充当注释处理器, 在编译时将代码”添加”到你的类中。注释处理是在版本5上添加到Java编译器的功能。该想法是, 用户可以将注释处理器(由自己编写, 或通过第三方依赖性, 例如Lombok编写)放入构建类路径中。然后, 随着编译过程的进行, 每当编译器找到注解时, 它都会询问:”嘿, 类路径中有人对此@Annotation感兴趣吗?”。对于那些举手的处理器, 编译器随后将控制权以及编译上下文移交给他们, 以便进行处理。

注释处理器最常见的情况是生成新的源文件或执行某种编译时检查。

Lombok并没有真正属于这些类别:它所做的是修改用于表示代码的编译器数据结构;即其抽象语法树(AST)。通过修改编译器的AST, Lombok间接改变了最终的字节码生成本身。

传统上, 这种不寻常且颇具侵略性的方法导致Lombok被视为某种骇客。尽管我本人会在某种程度上同意这种特征, 而不是从字面上看这很不好, 但我会把Lombok视为”聪明, 技术上有功的, 原始的选择”。

尽管如此, 仍有一些开发人员认为它是黑客, 因此不使用Lombok。这是可以理解的, 但以我的经验, Lombok的生产力收益超过了所有这些担忧。多年来, 我一直很高兴将其用于生产项目。

在详细介绍之前, 我想总结一下我特别重视在项目中使用Lombok的两个原因:

  1. Lombok有助于保持我的代码简洁, 简洁。我发现我的Lombok带注释的类非常有表现力, 而且我发现带注释的代码非常有意图, 尽管并非互联网上的每个人都一定同意。
  2. 当我开始一个项目并考虑一个领域模型时, 我倾向于从编写类而开始, 这些类在很大程度上是一个正在进行的工作, 并且随着我的进一步思考和改进它们会进行迭代地更改。在这些早期阶段, Lombok不需要四处移动或转换它为我生成的样板代码, 从而帮助我更快地移动。

Bean模式和通用对象方法

我们使用的许多Java工具和框架都依赖Bean模式。 Java Bean是可序列化的类, 具有默认的零参数构造函数(可能还有其他版本), 并通过通常由私有字段支持的getter和setter公开其状态。例如, 在使用JPA或序列化框架(例如JAXB或Jackson)时, 我们会写很多这样的文章。

考虑一个最多包含五个属性(属性)的User Bean, 我们希望为其提供一个用于所有属性的附加构造函数, 有意义的字符串表示形式, 并根据其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 :(
    
}

为了简洁起见, 我没有提供所有方法的实际实现, 而是提供了列出方法的注释以及实际实现所用的代码行数。该样板代码总计将占该类代码的90%以上!

而且, 如果以后我想将电子邮件更改为emailAddress或将RegistrationTs设为Date而不是Instant, 那么我需要花费时间(诚然, 在某些情况下借助IDE的帮助)来更改诸如get / set方法名称和类型, 修改我的实用程序构造函数, 依此类推。同样, 无价的时间可以为我的代码带来实际的商业价值。

让我们看看Lombok如何在这里提供帮助:

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;

}

瞧!我刚刚添加了很多lombok。*批注, 并且实现了我想要的。上面的清单正是我为此编写的所有代码。 Lombok正在参与我的编译器过程并为我生成了所有内容(请参阅下面的IDE屏幕快照)。

IDE屏幕截图

如你所见, NetBeans检查器(无论是否使用IDE都会发生这种情况)确实会检测到已编译的类字节码, 包括Lombok引入到流程中的附加内容。这里发生的事情非常简单:

  • 我使用@Getter和@Setter指示Lombok为所有属性生成getter和setter。这是因为我在类级别使用了注释。如果我想选择性地指定为哪些属性生成内容, 则可以对字段本身进行注释。
  • 感谢@NoArgsConstructor和@AllArgsConstructor, 为我的类提供了一个默认的空构造函数, 并为所有属性提供了一个额外的空构造函数。
  • @ToString批注自动生成一个方便的toString()方法, 默认情况下显示所有以其名称为前缀的类属性。
  • 最后, 为了根据电子邮件字段定义一对equals()和hashCode()方法, 我使用了@EqualsAndHashCode并使用相关字段的列表对其进行了参数化(在这种情况下仅为电子邮件)。

自定义Lombok注释

现在, 按照以下相同示例, 使用一些Lombok自定义设置:

  • 我想降低默认构造函数的可见性。因为我仅出于bean兼容的原因需要它, 所以我希望类的使用者仅调用采用所有字段的构造函数。为了实现这一点, 我使用AccessLevel.PACKAGE自定义生成的构造函数。
  • 我想确保我的字段永远不会通过构造函数或setter方法分配空值。用@NonNull注释类属性就足够了;在适当的构造方法和设置方法中, Lombok将生成引发NullPointerException的空检查。
  • 我将添加一个密码属性, 但是出于安全原因, 在调用toString()时不希望显示该属性。这是通过@ToString的excludes参数完成的。
  • 我可以通过吸气剂公开公开状态, 但希望限制外部可变性。因此, 我将按原样保留@Getter, 但再次将AccessLevel.PROTECTED用于@Setter。
  • 也许我想对电子邮件字段强加一些约束, 以便在修改后对其进行检查。为此, 我自己实现了setEmail()方法。 Lombok只会忽略已经存在的方法的生成。

这是User类的外观:

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;
    } 
    
}

请注意, 对于某些注释, 我们将类属性指定为纯字符串。没问题, 因为例如, 如果我们输入错误或引用了不存在的字段, Lombok就会抛出编译错误。使用Lombok, 我们很安全。

同样, 就像setEmail()方法一样, Lombok可以运行, 并且不会为程序员已经实现的方法生成任何内容。这适用于所有方法和构造函数。

不变的数据结构

Lombok擅长的另一个用例是在创建不可变数据结构时。这些通常称为”值类型”。某些语言对此提供了内置支持, 甚至还提出了将其合并到将来的Java版本中的建议。

假设我们要对用户登录操作的响应进行建模。我们只想实例化这种对象并返回到应用程序的其他层(例如, 将JSON序列化为HTTP响应的主体)。这样的LoginResponse根本不需要是可变的, Lombok可以帮助简洁地描述这一点。当然, 不可变数据结构还有许多其他用例(它们具有多线程和缓存友好性, 还有其他特质), 但让我们继续下面的简单示例:

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;
    
}

值得一提的是:

  • 引入了@RequiredArgsConstructor批注。恰当地命名, 它的作用是为所有尚未初始化的最终字段生成一个构造函数。
  • 如果我们想重用先前发布的LoginResonse(例如, “刷新令牌”操作), 我们当然不想修改我们现有的实例, 而是想基于它来生成一个新实例。 。在这里查看@Wither注释如何为我们提供帮助:它告诉Lombok生成一个withTokenExpiryTs(Instant tokenExpiryTs)方法, 该方法创建一个LoginResponse的新实例, 该实例具有所有withed实例值, 除了我们指定的新实例值。你是否希望所有字段都具有这种行为?只需将@Wither添加到类声明中即可。

@Data和@Value

到目前为止讨论的两个用例都很常见, 以至于Lombok附带了几个注释, 以使它们更短:使用@Data注释类将触发Lombok的行为就像使用@Getter + @Setter + @ToString +进行注释一样@EqualsAndHashCode + @RequiredArgsConstructor。同样, 使用@Value将把你的类变成一个不变的(也是最终的)类, 就像在上面的列表中注释一样。

建造者模式

回到我们的用户示例, 如果要创建新实例, 则需要使用最多包含六个参数的构造函数。这已经是一个相当大的数字, 如果我们进一步将属性添加到类中, 它将变得更糟。另外, 假设我们要为lastName和payingCustomer字段设置一些默认值。

Lombok实现了非常强大的@Builder功能, 使我们能够使用构建器模式来创建新实例。让我们将其添加到我们的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;

}

现在, 我们可以像这样流畅地创建新用户:

User user = User
        .builder()
            .email("[email protected]")
            .password("secret".getBytes(StandardCharsets.UTF_8))
            .firstName("Miguel")
            .registrationTs(Instant.now())
        .build();

不难想象, 随着我们班级的增长, 这种构造变得多么方便。

代表团/组成

如果你要遵循”偏重于继承而不偏向继承”的理智规则, 那么冗长的话, Java并不能真正帮助你。如果要合成对象, 通常需要在各处编写委托方法调用。

Lombok通过@Delegate为此提出了一个解决方案。让我们来看一个例子。

想象一下, 我们想引入一个ContactInformation的新概念。这是用户拥有的一些信息, 我们可能也希望其他类也具有。然后, 我们可以通过类似这样的界面对此进行建模:

public interface HasContactInformation {

    String getEmail();
    String getFirstName();
    String getLastName();

}

然后, 我们将使用Lombok引入一个新的ContactInformation类:

import lombok.Data;

@Data
public class ContactInformation implements HasContactInformation {

    private String email;

    private String firstName;
    private String lastName;

}

最后, 我们可以重构User以使其与ContactInformation组合, 并使用Lombok生成所有必需的委派调用以匹配接口协定:

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;

}

请注意, 我不需要为HasContactInformation的方法编写实现:这是我们告诉Lombok要做的事, 将调用委托给我们的ContactInformation实例。

另外, 由于我不希望从外部访问委派的实例, 因此我使用@Getter(AccessLevel.NONE)对其进行了自定义, 从而有效地防止了生成该方法。

检查异常

众所周知, Java区分已检查异常和未检查异常。这是对语言的争议和批评的传统来源, 因为异常处理有时会导致我们的处理方式过多, 特别是在处理旨在引发检查异常的API时, 因此迫使我们开发人员要么捕获它们, 要么声明我们的方法来处理该语言。扔他们。

考虑以下示例:

public class UserService {

    public URL buildUsersApiUrl() {
        try {
            return new URL("https://apiserver.com/users");
        } catch (MalformedURLException ex) {
            // Malformed? Really?
            throw new RuntimeException(ex);
        }
    }

}

这是一种常见的模式:我们当然知道我们的URL格式正确, 但是由于URL构造函数引发了一个已检查的异常, 我们要么被迫捕获它, 要么被声明为抛出它的方法, 并在相同情况下发出调用者。将这些检查的异常包装在RuntimeException中是一种很扩展的做法。如果我们在编码时需要处理的检查异常的数量增加, 则情况将更加糟糕。

因此, 这正是Lombok的@SneakyThrows的目的所在, 它将将要在我们的方法中抛出的所有受检查的异常包装到未经检查的方法中, 从而使我们摆脱了麻烦:

import lombok.SneakyThrows;

public class UserService {

    @SneakyThrows
    public URL buildUsersApiUrl() {
        return new URL("https://apiserver.com/users");
    }

}

记录中

你多久将记录器实例添加到你的类中的频率如此? (SLF4J样本)

private static final Logger LOG = LoggerFactory.getLogger(UserService.class);

我会猜测很多。知道了这一点, Lombok的创建者实现了一个注释, 该注释创建具有可自定义名称(默认为log)的记录器实例, 从而支持Java平台上最常见的记录框架。就像这样(再次基于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");
    }

}

注释生成的代码

如果我们使用Lombok生成代码, 由于我们实际上并未编写这些方法, 因此似乎无法对其进行注释。但这不是真的。确切地说, Lombok允许我们告诉我们我们希望如何对生成的代码进行批注, 尽管这是说实话。

考虑以下示例, 针对使用依赖项注入框架:我们有一个UserService类, 该类使用构造函数注入来获取对UserRepository和UserApiClient的引用。

package com.mgl.srcmini.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;
    // }

}

上面的示例显示了如何注释生成的构造函数。Lombok允许我们对生成的方法和参数执行相同的操作。

了解更多

本文中介绍的Lombok用法重点介绍了我个人多年来发现最有用的那些功能。但是, 还有许多其他功能和自定义项可用。

Lombok的文档内容丰富且详尽。对于每个功能(注释), 它们都有专用的页面, 其中包含非常详细的说明和示例。如果你觉得这篇文章有趣, 我鼓励你更深入地学习lombok及其文档以了解更多信息。

该项目站点记录了如何在几种不同的编程环境中使用Lombok。简而言之, 支持大多数流行的IDE(Eclipse, NetBeans和IntelliJ)。我本人会根据每个项目定期从一个切换到另一个, 并完美无缺地对所有这些使用Lombok。

Delombok!

Delombok是” Lombok工具链”的一部分, 可以派上用场。它的作用基本上是为Lombok注释的代码生成Java源代码, 并执行与Lombok生成的字节码相同的操作。

对于考虑采用Lombok但还不确定的人来说, 这是一个不错的选择。你可以自由地开始使用它, 并且不会出现”供应商锁定”。万一你或你的团队以后对选择感到遗憾, 你可以始终使用delombok生成相应的源代码, 然后在不依赖Lombok的情况下使用该源代码。

Delombok还是一个很好的工具, 可以准确地了解Lombok将要做什么。有很简单的方法将其插入你的构建过程。

备择方案

Java世界中有很多工具都使用注释处理器来在编译时丰富或修改你的代码, 例如Immutables或Google Auto Value。这些(当然还有其他)与Lombok在功能方面重叠。我特别喜欢Immutables方法, 并且在某些项目中也使用了它。

还值得注意的是, 还有其他一些出色的工具可以为”字节码增强”提供类似的功能, 例如Byte Buddy或Javassist。这些通常在运行时运行, 并且超出了本文的范围, 构成了它们自己的世界。

简洁的Java

有许多针对JVM的现代语言提供了更惯用的甚至语言级别的设计方法, 以帮助解决一些相同的问题。 Groovy, Scala和Kotlin当然是很好的例子。但是, 如果你正在开发仅Java项目, 那么Lombok是一个很好的工具, 可以帮助你的程序更简洁, 更具表现力和可维护性。

相关文章:Dart语言:Java和C#不够清晰时

赞(0)
未经允许不得转载:srcmini » 用Project Lombok编写无脂的Java代码

评论 抢沙发

评论前必须登录!