“Boilerplate”是一个术语,用于描述在应用程序的许多部分中重复执行的代码,几乎不做任何更改。对Java语言最频繁的批评之一是大多数项目中都存在这种类型的代码。这个问题通常是各个图书馆设计决策的结果,但由于语言本身的局限性而加剧。Lombok项目旨在通过用一组简单的注释来替代它们,以减少一些最严重的违法者的流行率。
虽然注解用于指示使用情况,实现绑定甚至生成框架使用的代码并不罕见,但它们通常不用于生成应用程序直接使用的代码。部分原因是因为这样做需要在开发时刻对注释进行热切处理。龙目计划就是这样做的。通过集成到IDE中,Project Lombok能够注入可立即供开发人员使用的代码。例如,简单地将@Data
注释添加到数据类中,如下所示,会在IDE中产生许多新方法:
Project Lombok在项目网站上作为单个jar文件提供 。它包含用于开发的API,作为IDE集成的安装程序。在大多数系统上,只需双击jar文件即可启动安装程序。如果系统未配置为正确启动jar文件,则也可以从命令行运行,如下所示:
java -jar lombok.jar
安装程序将尝试检测受支持的IDE的位置。如果无法正确确定IDE的安装位置,则可以手动指定位置。只需点击“安装/更新”即可完成IDE集成。在撰写本文时,仅支持Eclipse和NetBeans。然而,IntelliJ IDEA源代码的发布已经将IDEA支持作为未来版本的可能性,并且JDeveloper已经报告了有限的成功。
jar文件仍然需要包含在任何将使用Project Lombok注释的项目的类路径中。Maven用户可以将Lombok作为依赖项添加到项目pom.xml文件中:
org.projectlombok
lombok
0.9.2
projectlombok.org
http://projectlombok.org/mavenrepo
典型的Java项目将数百行代码投入到定义简单数据类所需的样板文件中并不罕见。这些类通常包含这些字段的若干字段,getter和setter,以及equals
和hashCode
实施方式。在最简单的情况下,Project Lombok可以将这些类减少到必需的字段和单个@Data
注释。
当然,最简单的情况并不一定是开发人员在日常工作中所面对的情况。出于这个原因,龙目岛项目中有许多注释允许对类的结构和行为进行更细粒度的控制。
的@Getter
和@Setter
注解分别产生一个字段的获取和设置。生成的getters正确遵循布尔属性的约定,导致 isFoo
getter方法名称而不是getFoo
任何boolean
字段foo
。应该注意的是,如果注释字段所属的类包含与要生成的getter或setter相同名称的方法,则不管参数或返回类型如何,都不会生成相应的方法。
两个@Getter
和@Setter
注释一个可选参数,来指定所生成的方法的访问级别。
Lombok注释的代码:
@Getter @Setter private boolean employed = true;
@Setter(AccessLevel.PROTECTED) private String name;
等效的Java源代码:
private boolean employed = true;
private String name;
public boolean isEmployed() {
return employed;
}
public void setEmployed(final boolean employed) {
this.employed = employed;
}
protected void setName(final String name) {
this.name = name;
}
该@NonNull
注释用于指示对相应成员进行快速失败空检查的需要。当放置在Lombok正在生成setter方法的字段上时,将生成空值检查,这将导致NullPointerException
应该提供空值。此外,如果Lombok正在为拥有的类生成一个构造函数,那么该字段将被添加到构造函数签名中,而空检查将包含在生成的构造函数代码中。
此批注 在IntelliJ IDEA和FindBugs中发现了镜像@NotNull
和@NonNull
批注,等等。关于主题上的这些变体,龙目标注释是不可知的。如果Lombok的遇到的名称的任何注解任何构件@NotNull
或者@NonNull
,将通过产生适当的相应代码履行它。Project Lombok的作者进一步评论说,如果这种类型的注释被添加到Java中,那么Lombok版本将被删除。
Lombok注解了该类的代码Family
:
@Getter @Setter @NonNull
private List members;
等效的Java源代码:
@NonNull
private List members;
public Family(@NonNull final List members) {
if (members == null) throw new java.lang.NullPointerException("members");
this.members = members;
}
@NonNull
public List getMembers() {
return members;
}
public void setMembers(@NonNull final List members) {
if (members == null) throw new java.lang.NullPointerException("members");
this.members = members;
}
该注释生成该toString
方法的实现。默认情况下,任何非静态字段将以名称/值对的形式包含在方法的输出中。如果需要,可以通过设置注释参数includeFieldNames
来 抑制在输出中包含属性名称false
。
通过在exclude
参数中包含它们的字段名称,可以从生成方法的输出中排除特定字段。或者,该of
参数可用于仅列出输出中所需的那些字段。toString
通过设置callSuper
参数, 还可以包含超类方法的输出true
。
龙目岛注释的代码:
@ToString(callSuper=true,exclude="someExcludedField")
public class Foo extends Bar {
private boolean someBoolean = true;
private String someStringField;
private float someExcludedField;
}
等效的Java源代码:
public class Foo extends Bar {
private boolean someBoolean = true;
private String someStringField;
private float someExcludedField;
@java.lang.Override
public java.lang.String toString() {
return "Foo(super=" + super.toString() +
", someBoolean=" + someBoolean +
", someStringField=" + someStringField + ")";
}
}
这个类级别的注释会导致龙目岛同时生成 equals
和hashCode
方法,因为这两个是由内在的联系在一起hashCode
的合同。默认情况下,两个方法都会考虑非静态或瞬态的类中的任何字段。很像@ToString
,exclude
提供参数是为了防止字段被包含在生成的逻辑中。也可以使用该 of
参数仅列出应该考虑的那些字段。
还有@ToString
,callSuper
这个注释有一个参数。将其设置为true将导致 在考虑当前类中的字段之前equals
通过调用equals
来自超类的验证相等性。对于该hashCode
方法,它会导致将超类的结果并入hashCode
散列计算中。设置callSuper
为true时,请注意确保父类中的equals方法正确处理实例类型检查。如果父类检查该类是特定类型的,而不仅仅是这两个对象的类是相同的,则这可能会导致不希望的结果。如果超类正在使用生成的龙目岛 equals
方法,这不是一个问题。但是,其他实现可能无法正确处理这种情况。还要注意,callSuper
只有当类只扩展时Object
,才能设置 为true ,因为它会导致实例相等性检查将字段的比较短路。这是由于生成的方法调用了equals
实现 Object
,如果比较的两个实例不是相同的实例,则返回false。因此,在这种情况下,Lombok会产生编译时错误。
Lombok注释的代码:
@EqualsAndHashCode(callSuper=true,exclude={"address","city","state","zip"})
public class Person extends SentientBeing {
enum Gender { Male, Female }
@NonNull private String name;
@NonNull private Gender gender;
private String ssn;
private String address;
private String city;
private String state;
private String zip;
}
等效的Java源代码:
public class Person extends SentientBeing {
enum Gender {
/*public static final*/ Male /* = new Gender() */,
/*public static final*/ Female /* = new Gender() */;
}
@NonNull
private String name;
@NonNull
private Gender gender;
private String ssn;
private String address;
private String city;
private String state;
private String zip;
@java.lang.Override
public boolean equals(final java.lang.Object o) {
if (o == this) return true;
if (o == null) return false;
if (o.getClass() != this.getClass()) return false;
if (!super.equals(o)) return false;
final Person other = (Person)o;
if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
if (this.gender == null ? other.gender != null : !this.gender.equals(other.gender)) return false;
if (this.ssn == null ? other.ssn != null : !this.ssn.equals(other.ssn)) return false;
return true;
}
@java.lang.Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = result * PRIME + super.hashCode();
result = result * PRIME + (this.name == null ? 0 : this.name.hashCode());
result = result * PRIME + (this.gender == null ? 0 : this.gender.hashCode());
result = result * PRIME + (this.ssn == null ? 0 : this.ssn.hashCode());
return result;
}
}
该@Data
注释很可能在Lombok目岛工具箱中最常用的注释。它结合的功能@ToString
,@EqualsAndHashCode
, @Getter
和@Setter
。本质上,使用 @Data
的一类是一样的带有默认注释类@ToString
和@EqualsAndHashCode
以及与两个注释每个字段@Getter
和@Setter
。注释一个类@Data
也会触发龙目岛的构造函数生成。这增加了一个公共构造函数,它将任何@NonNull
或final
字段作为参数。这提供了简单的旧Java对象(POJO)所需的一切。
虽然@Data
非常有用,但它不提供与其他Lombok注释相同的控制粒度。为了覆盖默认的方法生成行为,请使用其他Lombok注释之一注释该类,字段或方法,并指定必要的参数值以实现所需的效果。
@Data
确实提供了可用于生成静态工厂方法的单个参数选项。将staticConstructor
参数的值设置为 所需的方法名称将导致Lombok将生成的构造函数设置为私有的,并公开给定名称的静态工厂方法。
龙目岛注释的代码:
@Data(staticConstructor="of")
public class Company {
private final Person founder;
private String name;
private List employees;
}
等效的Java源代码:
public class Company {
private final Person founder;
private String name;
private List employees;
private Company(final Person founder) {
this.founder = founder;
}
public static Company of(final Person founder) {
return new Company(founder);
}
public Person getFounder() {
return founder;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public List getEmployees() {
return employees;
}
public void setEmployees(final List employees) {
this.employees = employees;
}
@java.lang.Override
public boolean equals(final java.lang.Object o) {
if (o == this) return true;
if (o == null) return false;
if (o.getClass() != this.getClass()) return false;
final Company other = (Company)o;
if (this.founder == null ? other.founder != null : !this.founder.equals(other.founder)) return false;
if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
if (this.employees == null ? other.employees != null : !this.employees.equals(other.employees)) return false;
return true;
}
@java.lang.Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = result * PRIME + (this.founder == null ? 0 : this.founder.hashCode());
result = result * PRIME + (this.name == null ? 0 : this.name.hashCode());
result = result * PRIME + (this.employees == null ? 0 : this.employees.hashCode());
return result;
}
@java.lang.Override
public java.lang.String toString() {
return "Company(founder=" + founder + ", name=" + name + ", employees=" + employees + ")";
}
}
该@Cleanup
注释可以用来保证分配的资源被释放。当注释一个局部变量时@Cleanup
,任何后续代码都被包装在一个 try/finally
块中,以保证在当前作用域的末尾调用清理方法。默认情况下,@Cleanup
假定清理方法被命名为“关闭”,就像输入和输出流一样。但是,可以为注释的value
参数提供不同的方法名称。只有不带参数的清理方法才能用于此批注。
使用@Cleanup注解时还需要注意一点。如果清理方法抛出异常,它将抢占抛出方法体内的任何异常。这可能导致问题的实际原因被掩埋,因此在选择使用Project Lombok的资源管理时应该考虑这个问题。此外,随着Java 7中的自动化资源管理技术的发展,这种特殊的注释可能会相对短暂。
龙目岛注释的代码:
public void testCleanUp() {
try {
@Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(new byte[] {'Y','e','s'});
System.out.println(baos.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
等效的Java源代码:
public void testCleanUp() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write(new byte[]{'Y', 'e', 's'});
System.out.println(baos.toString());
} finally {
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
synchronized
在一个方法上 使用关键字可能会导致不幸的后果,因为任何从事多线程软件开发的开发人员都可以证明。synchronized关键字将在this
实例方法的情况下锁定当前对象()或在class
静态方法的对象上 锁定当前对象。这意味着开发人员无法控制的代码可能会锁定在同一个对象上,导致死锁。通常建议明确地锁定一个单独的对象,该对象专门专门用于此目的,而不是以允许未经请求的锁定的方式公开。龙目岛项目@Synchronized
为此目的提供了注释。
使用注释实例方法@Synchronized
将提示Lombok生成一个专用锁定字段$lock
,该字段在执行之前将被锁定。同样,以同样的方式注释一个静态方法将生成一个私有静态对象$LOCK
,该静态对象以静态方法命名 ,以相同的方式使用。通过为注解的value
参数提供一个字段名称可以指定一个不同的锁定对象。当提供字段名称时,开发人员必须定义属性,因为龙目岛不会生成它。
龙目岛注释的代码:
private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");
@Synchronized
public String synchronizedFormat(Date date) {
return format.format(date);
}
等效的Java源代码:
private final java.lang.Object $lock = new java.lang.Object[0];
private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");
public String synchronizedFormat(Date date) {
synchronized ($lock) {
return format.format(date);
}
}
@SneakyThrows
可能是Lombok项目批注中最受批评的人,因为它是对检查异常的直接攻击。关于检查异常的使用方面存在很多分歧,有大量开发人员认为他们是一个失败的实验。这些开发者会喜欢@SneakyThrows
。在选中/未选中的异常栏的另一边的那些开发人员很可能会将其视为隐藏潜在问题。
IllegalAccessException
如果IllegalAccessException
或者某个父类未在throws子句中列出,则 投掷通常会生成“未处理的异常”错误:
注释时@SneakyThrows
,错误消失。
默认情况下,@SneakyThrows
将允许在没有在throws
子句中声明的情况下引发任何检查的异常。通过为注释Class extends Throwable>
的value
参数提供一个可抛类(())数组,可以将其限制为一组特定的异常 。
龙目岛注释的代码:
@SneakyThrows
public void testSneakyThrows(){
throw new IllegalAccessException();
}
等效的Java源代码:
public void testSneakyThrows(){
try {
throw new IllegalAccessException();
} catch(java.lang.Throwable $ ex){
throw lombok.Lombok.sneakyThrow($ ex);
}
}
看上面的代码和签名 Lombok.sneakyThrow(Throwable)
会导致大多数人认为异常被包装在一个RuntimeException
并重新抛出,但事实并非如此。该sneakyThrow
方法将永远不会正常返回,而是将提供的throwable完全不改变。
与任何技术选择一样,使用Project Lombok也有正面和负面影响。将Lombok的注释合并到项目中可以大大减少在IDE中生成或手动编写的样板代码的行数。这样可以减少维护开销,减少错误并提高可读性。
这并不是说在项目中使用Project Lombok注释没有缺点。Lombok项目主要是为了填补Java语言的空白。因此,有可能会改变语言,以免使用Lombok的注释,例如增加一流的财产支持。此外,当与基于注释的对象关系映射(ORM)框架结合使用时,数据类的注释数量可能开始变得笨拙。这很大程度上被Lombok注释取代的代码量所抵消。但是,避免频繁使用注释的人可能会选择另一种方式。
Lombok项目提供了delombok
用等效源代码替换Lombok注释的工具。这可以通过命令行为整个源目录完成。
java -jar lombok.jar delombok src -d src-delomboked
另外,还提供了一个Ant任务,用于合并到构建过程中。
target>
两者delombok
和相应的Ant任务都打包在核心lombok.jar
下载中。除了允许Lombok注释在使用Google Web Toolkit(GWT)或其他不兼容框架构建的应用程序中有用之外,delombok
在Person
该类上运行 还可以轻松地将使用Lombok注释编写的类与包含等效样板文本的代码进行对比。
package com.ociweb.jnb.lombok;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
@Data
@EqualsAndHashCode(exclude={"address","city","state","zip"})
public class Person {
enum Gender { Male, Female }
@NonNull private String firstName;
@NonNull private String lastName;
@NonNull private final Gender gender;
@NonNull private final Date dateOfBirth;
private String ssn;
private String address;
private String city;
private String state;
private String zip;
}
使用Project Lombok注解的代码比包含样板的同等类更加简洁。
package com.ociweb.jnb.lombok;
import java.util.Date;
import lombok.NonNull;
public class Person {
enum Gender {
/*public static final*/ Male /* = new Gender() */,
/*public static final*/ Female /* = new Gender() */;
}
@NonNull
private String firstName;
@NonNull
private String lastName;
@NonNull
private final Gender gender;
@NonNull
private final Date dateOfBirth;
private String ssn;
private String address;
private String city;
private String state;
private String zip;
public Person(@NonNull final String firstName, @NonNull final String lastName,
@NonNull final Gender gender, @NonNull final Date dateOfBirth) {
if (firstName == null)
throw new java.lang.NullPointerException("firstName");
if (lastName == null)
throw new java.lang.NullPointerException("lastName");
if (gender == null)
throw new java.lang.NullPointerException("gender");
if (dateOfBirth == null)
throw new java.lang.NullPointerException("dateOfBirth");
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
this.dateOfBirth = dateOfBirth;
}
@NonNull
public String getFirstName() {
return firstName;
}
public void setFirstName(@NonNull final String firstName) {
if (firstName == null)
throw new java.lang.NullPointerException("firstName");
this.firstName = firstName;
}
@NonNull
public String getLastName() {
return lastName;
}
public void setLastName(@NonNull final String lastName) {
if (lastName == null)
throw new java.lang.NullPointerException("lastName");
this.lastName = lastName;
}
@NonNull
public Gender getGender() {
return gender;
}
@NonNull
public Date getDateOfBirth() {
return dateOfBirth;
}
public String getSsn() {
return ssn;
}
public void setSsn(final String ssn) {
this.ssn = ssn;
}
public String getAddress() {
return address;
}
public void setAddress(final String address) {
this.address = address;
}
public String getCity() {
return city;
}
public void setCity(final String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(final String state) {
this.state = state;
}
public String getZip() {
return zip;
}
public void setZip(final String zip) {
this.zip = zip;
}
@java.lang.Override
public java.lang.String toString() {
return "Person(firstName=" + firstName + ", lastName=" + lastName +
", gender=" + gender + ", dateOfBirth=" + dateOfBirth +
", ssn=" + ssn + ", address=" + address + ", city=" + city +
", state=" + state + ", zip=" + zip + ")";
}
@java.lang.Override
public boolean equals(final java.lang.Object o) {
if (o == this) return true;
if (o == null) return false;
if (o.getClass() != this.getClass()) return false;
final Person other = (Person)o;
if (this.firstName == null
? other.firstName != null
: !this.firstName.equals(other.firstName))
return false;
if (this.lastName == null
? other.lastName != null
: !this.lastName.equals(other.lastName))
return false;
if (this.gender == null
? other.gender != null
: !this.gender.equals(other.gender))
return false;
if (this.dateOfBirth == null
? other.dateOfBirth != null
: !this.dateOfBirth.equals(other.dateOfBirth))
return false;
if (this.ssn == null
? other.ssn != null
: !this.ssn.equals(other.ssn))
return false;
return true;
}
@java.lang.Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = result * PRIME +
(this.firstName == null ? 0 : this.firstName.hashCode());
result = result * PRIME +
(this.lastName == null ? 0 : this.lastName.hashCode());
result = result * PRIME +
(this.gender == null ? 0 : this.gender.hashCode());
result = result * PRIME +
(this.dateOfBirth == null ? 0 : this.dateOfBirth.hashCode());
result = result * PRIME +
(this.ssn == null ? 0 : this.ssn.hashCode());
return result;
}
}
请记住,这不仅仅是通常必须编写的代码,还必须通过维护开发人员来阅读。这意味着,在使用Project Lombok提供的注释时,开发人员无需通过无数代码行来确定所讨论的类是简单的数据类还是更险恶的东西。
虽然Project Lombok做了一些戏剧性的事情来让开发人员的生活更轻松,但它有其局限性。浏览 问题清单 将快速阐明目前的一些缺点,其中大部分都是次要的。一个重要的问题是无法检测超类的构造函数。这意味着如果一个超类没有默认构造函数,那么任何子类都不能使用@Data注释,而不显式编写构造函数来使用可用的超类构造函数。由于Lombok项目尊重任何匹配要生成方法名称的方法,因此可以使用此方法克服其大部分功能缺陷。
针对使用Lombok项目提出了一些问题。最常见的观点认为,注释是针对“元”信息的,并且不会以这种方式使用,导致代码库被删除时无法编译。这当然是Lombok注释的情况。新的方法来自这些注释,这些注释不仅被框架使用,而且被应用程序的其他部分使用。Lombok项目的开发时间支持是它的面包和黄油,但这确实会带来一些后果,尤其是对IDE支持的限制。
如前所述,@SneakyThrows
必然会激起古老的争论,而不是经过检查和未经检查的例外。关于这场辩论的意见往往是凶狠的宗教。因此,反对使用的论点@SneakyThrows
也肯定会激发激情之间的激情。
争论的另一点是实现支持IDE集成的代码以及javac
注释处理器。这两个Lombok项目都使用非公开的API来完成他们的魔法。这意味着在后续的IDE或JDK版本中,Project Lombok可能会被破坏。以下是项目创始人之一Reinier Zwitserloot如何描述情况:
这是一个彻头彻尾的黑客。使用非公开的API。放肆的投射(知道
在javac中运行的注释处理器将获得JavacAnnotationProcessor的一个实例,
这是AnnotationProcessor(一个接口)的内部实现,它
恰好有一些额外的方法用于获取实时AST) 。
在eclipse上,它可能会更糟糕(并且更健壮) - 一个java代理用于将
代码注入eclipse语法和解析器类,这当然完全是非公共
API并且完全没有限制。
如果你可以做Lombok用标准API做什么,我会这样做,但
你不能。不过,为了它的价值,我开发了Eclipse eclipse v3.5的eclipse插件
运行在java 1.6上,并且不需要做任何修改就可以运行在Eclipse
1.5 上的eclipse v3.4 上,所以它不是完全脆弱的。
Lombok项目是实用开发人员的强大工具。它为从Java类中消除大量样板代码提供了一组有用的注释。在最好的情况下,只有五个字符可以代替数百行代码。其结果是Java类清晰,简洁并易于维护。这些好处确实会带来成本。在IntelliJ IDEA商店中使用Project Lombok简直不是一个可行的选择。IDE和JDK升级有破坏的风险,以及围绕项目目标和实施的争议。所有这些转化为与任何技术选择必须考虑的相同。总会有收获和损失。问题在于Project Lombok是否可以为手头的项目提供比成本更高的价值。如果没有其他的事情,龙目岛项目肯定会为迄今为止已经枯萎的语言特征的讨论注入新的生命,并且从任何角度都是一个胜利。
感谢Mark Volkmann,Eric Burke,Mario Aquino和Lance Finney对本文的评论和建议,并特别感谢Mark Volkmann向我介绍龙目岛项目。