
我们在Grubhub几乎整个后端都使用Java。 这是一种经过验证的语言,在过去的20年中证明了它的速度和可靠性。 但是多年来,“老人”的年龄仍然开始受到影响。
Java是
最流行的JVM语言之一 ,但不是唯一的一种。 近年来,他一直与Scala,Clojure和Kotlin竞争,后者提供新功能和优化的语言功能。 简而言之,它们使您可以使用更简洁的代码执行更多操作。
JVM生态系统中的这些创新非常有趣。 由于竞争,Java被迫改变以保持竞争力。 新的六个月发布时间表和Java 8中的几个JEP(JDK增强建议)(Valhalla,local-Variable Type Inference,Loom)证明了Java多年来仍将是一种竞争性语言。
但是,Java的规模和规模意味着开发的进展比我们期望的要慢,更不用说不惜一切代价保持向后兼容性的强烈愿望了。 在任何开发中,首要任务都应该是功能,但是在这里,必要的功能用语言开发的时间太长了。 因此,我们Grubhub现在使用Project Lombok来优化和改进Java。 Lombok项目是一个编译器插件,可以向Java添加新的“关键字”并将注释转换为Java代码,从而减少了开发工作并提供了一些其他功能。
配置龙目岛
Grubhub一直致力于改善软件生命周期,但是每个新工具和过程都需要考虑成本。 幸运的是,要连接Lombok,只需在gradle文件中添加几行即可。
Lombok在编译器处理它们之前将源代码中的注释转换为Java语句:
lombok
依赖项不在运行时中,因此使用插件不会增加程序集的大小。 要
使用Gradle配置Lombok (它也适用于Maven),只需
将以下行添加到
build.gradle文件中:
plugins { id 'io.franzbecker.gradle-lombok' version '1.14' id 'java' } repositories { jcenter()
使用Lombok,我们的源代码
将不是有效的Java代码。 因此,您将需要为IDE安装插件,否则开发环境将无法理解其处理的内容。 Lombok支持所有主要的Java IDE。 无缝集成。 “显示使用”和“执行”之类的所有功能将继续像以前一样工作,将您移至相应的字段/类。
龙目岛行动
认识龙目岛的最佳方法是亲自观看龙目岛。 考虑几个典型的例子。
重新体验POJO对象
使用“良好的旧Java对象”(POJO),我们将数据与处理分开,以使代码更易于阅读和简化网络传输。 一个简单的POJO具有几个私有字段,以及相应的getter和setter。 他们可以完成工作,但是需要大量样板代码。
Lombok有助于以更加灵活和结构化的方式使用POJO,而无需其他代码。 这就是我们使用
@Data
注释简化基础POJO的方式:
@Data public class User { private UUID userId; private String email; }
@Data
只是一个方便的注释,它可以一次应用多个Lombok注释。
仅此注释即可简单优雅地涵盖许多典型用例。 但是POJO并不总是涵盖必要的功能。
@Data
是一个完全可修改的类,其滥用会增加复杂性并限制并发性,从而对应用程序的可生存性产生负面影响。
还有另一种解决方案。 让我们回到
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”; }
@Value
批注与
@Data
相似,不同之处在于,默认情况下所有字段都是私有字段和最终字段,并且不会创建设置器。 因此,
@Value
对象立即变得不可变。 由于所有字段都是最终字段,因此没有参数构造函数。 相反,Lombok使用
@AllArgsConstructor
。 结果是一个功能齐全的,不变的对象。
但是,如果只需要使用all-args构造函数创建一个对象,那么不变性不是很有用。 正如Joshua Bloch在他的《有效的Java编程》一书中解释的那样,如果您有大量的设计器参数,则应该使用构建器。 在这里,
@Builder
类开始
@Builder
,自动生成构建器的内部类:
User user = User.builder() .userId(UUID.random()) .email(“grubhub@grubhub.com”) .favoriteFood(“burritos”) .favoriteFood(“dosas”) .build()
生成生成器可以轻松创建带有大量参数的对象,并在将来添加新字段。 静态方法返回构建器的实例以设置对象的所有属性。 之后,对
build()
的调用返回实例。
@NonNull
可用于
@NonNull
在创建对象的实例时这些字段不为null;否则,将引发
NullPointerException
。 请注意,头像字段使用
@NonNull
注释,
@NonNull
设置。 事实是
@Builder.Default
注释默认情况下指向
default.png 。
还要注意,构建器如何使用
favoriteFood
,这是对象中唯一的属性名称。 将
@Singular
批注放置在集合属性上时,Lombok会创建特殊的生成器方法,以分别向集合添加项目,而不是同时添加整个集合。 这对测试特别有用,因为不能用Java来简单快速地创建小型集合的方法。
最后,
toBuilder = true
参数添加了
toBuilder()
实例方法,该方法创建了一个用此实例的所有值填充的生成器对象。 创建一个新实例,并使用原始实例中的所有值进行预填充非常容易,因此只需更改必要的字段即可。 这对于
@Value
类特别有用,因为这些字段是不可变的。
一些说明进一步定制了设置器的特殊功能。
@Wither
为每个属性创建
@Wither
方法。 在输入处为值;在输出处为实例的克隆,其中一个字段的更新值为。
@Accessors
允许您配置自动创建的setter。
fluent=true
参数禁用getter和setter的get和set约定。 在某些情况下,这可以替代
@Builder
。
如果Lombok实现不适合您的任务(并且您查看了注释修饰符),则始终可以采用并编写自己的实现。 例如,如果您具有
@Data
类,但是一个getter需要自定义逻辑,则只需实现此getter。 Lombok将看到已经提供了实现,并且不会用自动创建的实现覆盖它。
仅需几个简单的注释,基本的POJO就具有许多丰富的功能,可以简化其使用,而不会增加我们的工程师的工作量,而不会浪费时间或增加开发成本。
删除模板代码
Lombok不仅对POJO有用:它可以在应用程序的任何级别上应用。 Lombok的以下用法在组件类(例如控制器,服务和DAO(数据访问对象))中特别有用。
日志记录是程序所有部分的基本要求。 任何从事有意义工作的类都应编写日志。 因此,标准记录器成为每个类的模板。 Lombok将此模板简化为一个注释,该注释会自动标识并实例化具有正确类名的记录器。 根据期刊的结构,有几种不同的注释。
@Slf4j
声明记录器后,添加我们的依赖项:
@Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; }
@FieldDefaults
将final和private修饰符添加到所有字段。
@RequiredArgsConstructor
创建一个构造函数,该构造函数设置
UserDao
的实例。
@NonNull
在构造函数中添加验证,如果
UserDao
实例为零,则
UserDao
NullPointerException
。
但是,等等,还不止这些!
在许多情况下,龙目岛都尽力而为。 前面的部分显示了具体示例,但是Lombok可以促进许多领域的开发。 这是一些如何更有效地使用它的小例子。
尽管
var
关键字出现在Java 9中,但仍可以重新分配变量。 龙目岛具有关键字
val
,它显示局部变量的最终类型。
某些具有纯静态功能的类不打算初始化。 防止实例化的一种方法是声明一个引发异常的私有构造函数。 Lombok在
@UtilityClass
批注中将此模板编成
@UtilityClass
。 它生成一个引发异常的私有构造函数,最后输出该类并使所有方法变为静态。
@UtilityClass
由于检查异常,Java经常因冗长而受到批评。 一个单独的Lombok批注修复了它们:
@SneakyThrows
。 如预期的那样,实现非常棘手。 它不会捕获异常,甚至不会在
RuntimeException
包装异常。 相反,它依赖于JVM在运行时不检查检查的异常的一致性的事实。 只有javac可以做到这一点。 因此,Lombok在编译时使用字节码转换来禁用此检查。 结果是可执行代码。
public class SneakyThrows { @SneakyThrows public void sneakyThrow() { throw new Exception(); } }
并排比较
直接比较最能说明Lombok节省了多少代码。 IDE插件具有“ de-lombok”功能,可以将大多数Lombok注释近似转换为本地Java代码(
@NonNull
注释不转换)。 因此,任何安装了插件的IDE都将能够将大多数注释转换为本地Java代码,反之亦然。 回到我们的
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”; }
Lombok类只有13条简单,易读,易懂的行。 但是在运行de-lombok之后,该类将变成一百多行样板代码!
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 + ")"; } } }
我们将对
UserService
类执行相同的操作。
@Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; }
这是标准Java代码中的对应示例。
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; } }
效果等级
Grubhub拥有一百多种食品配送业务服务。 我们选择了其中一个,并在Lombok IntelliJ插件中启动了“ de-lombok”功能。 结果,在删除了800个使用Lombok的案例之后,大约180个文件被更改,并且代码库增加了大约18,000行代码。 平均而言,每条龙目岛行节省23条Java行。 有了这种效果,很难想象没有Lombok的Java。
总结
龙目岛(Lombok)是很好的帮手,无需开发人员的大量努力即可实现新的语言功能。 当然,安装插件要比用新语言培训所有工程师并移植现有代码要容易得多。 龙目岛不是万能的,但开箱即用的功能足以真正帮助完成工作。
Lombok的另一个优点是它保持一致的代码库。 我们在全球拥有一百多个不同的服务和分布的团队,因此代码库的一致性促进了团队的扩展,并减轻了在开始新项目时切换上下文的负担。 Lombok适用于Java 6以后的任何版本,因此我们可以指望它在所有项目中的可用性。
对于Grubhub,这不仅仅是新功能。 最后,所有这些代码
都可以手动编写。 但是Lombok在不影响业务逻辑的情况下简化了代码库中无聊的部分。 这使您可以专注于对业务真正重要且对我们的开发人员最有趣的事情。 Monton模板代码对于程序员,审阅者和维护者来说是浪费时间。 另外,由于此代码不再是手动编写的,因此消除了整个拼写错误类别。 自动
@NonNull
的优点与
@NonNull
的功能相结合,减少了出错的可能性,并有助于我们的发展,旨在为您的餐桌提供食物!