lombok深度指南

Contents

介绍

Project Lombok是一个java库,它可以自动插入你的编辑器并构建工具,增强你的java代码。
永远不要再写另一个getter或equals方法,使用一个注解,您的类具有一个功能齐全的构建器,自动化您的日志记录变量等等:

Image illustrating the availability of new methods on a data class in Eclipse.

安装

idea 安装lombok

Jetbrains的IntelliJ IDEA的编辑与lombok兼容。

添加Lombok IntelliJ插件以添加对IntelliJ的lombok支持:

  • File > Settings > Plugins
  • 点击 Browse repositories...
  • 搜索 Lombok Plugin
  • 点击 Install plugin
  • 重启IntelliJ IDEA

Lombok 注解

典型的Java项目将数百行代码用于定义简单数据类所需的样板文件并不罕见。这些类通常包含这些字段的若干字段,getter和setter,以及equals和 hashCode实施方式。在最简单的场景中,Lombok项目可以将这些类减少到必需的字段和单个@Data注解。

当然,最简单的场景不一定是开发人员日常所面临的场景。出于这个原因,Project Lombok中有许多注解允许对类的结构和行为进行更细粒度的控制。

@Getter and @Setter

@Getter@Setter注解分别为字段生成getter和setter。生成的getter方法正确地遵循了布尔属性的约定,从而为任何布尔字段foo生成一个isFoo getter方法名,而不是getFoo

需要注意的是,如果带注解字段所属的类包含与要生成的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

@NonNull注释用于指示对对应成员进行快速失败null检查的需要。当放置在Lombok正在为其生成setter方法的字段上时,将生成null检查,如果提供了null值,将导致NullPointerException。此外,如果Lombok正在为拥有的类生成构造函数,那么字段将被添加到构造函数签名中,null检查将包含在生成的构造函数代码中。

IntelliJ IDEA和FindBugs等中的 注解镜像@NotNull@NonNull注解。对于主题的这些变化,Lombok是注解不可知的。如果Lombok遇到任何使用该名称的注解注解的成员,@NotNull或者@NonNull它将通过生成适当的相应代码来遵守它。Project Lombok的作者进一步评论说,如果将此类型的注解添加到Java,则Lombok版本将被删除。

Family的Lombok注解代码:

@Getter @Setter @NonNull
private List<Person> members;

等价Java源代码::

@NonNull
private List<Person> members;

public Family(@NonNull final List<Person> members) {
    if (members == null) throw new java.lang.NullPointerException("members");
    this.members = members;
}
    
@NonNull
public List<Person> getMembers() {
    return members;
}

public void setMembers(@NonNull final List<Person> members) {
    if (members == null) throw new java.lang.NullPointerException("members");
    this.members = members;
}

@ToString

此注释生成toString方法的实现。默认情况下,任何非静态字段都将以名称-值对的形式包含在方法的输出中。如果需要,可以通过将注释参数includeFieldNames设置为false来排除在输出中包含属性名。

通过在exclude 参数中包含其字段名称,可以从生成的方法的输出中排除特定字段。或者,该of参数可用于仅列出输出中所需的那些字段。toString通过将callSuper参数 设置为,还可以包括超类方法的输出true

Lombok 注解代码:

@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 + ")";
    }
}

@EqualsAndHashCode

这个类级注解将导致Lombok生成两者 equalshashCode方法,因为这两者是由hashCode契约本质上绑定在一起的。默认情况下,两个方法都将考虑类中非静态或瞬态的任何字段。很像@ToStringexclude提供参数是为了防止字段包含在生成的逻辑中。也可以使用该 of参数仅列出应考虑的那些字段。

同样@ToStringcallSuper 这个注解有一个参数。将其设置为true将导致 在考虑当前类中的字段之前equals通过调用equals超类来验证相等性。对于该hashCode方法,它导致在hashCode计算散列时结合超类的结果。设置callSuper为true时,请注意确保父类中的equals方法正确处理实例类型检查。如果父类检查该类是否属于特定类型而不仅仅是两个对象的类相同,则可能导致不希望的结果。如果超类使用生成的Lombok 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

@Data注解很可能在项目lombok工具箱中最常用的注解。它结合的功能@ToString@EqualsAndHashCode@Getter@Setter。本质上,使用 @Data的一类是一样的带有默认注解类@ToString@EqualsAndHashCode 以及与两个注解每个字段@Getter 和@Setter。对类进行注解@Data 也会触发Lombok的构造函数生成。这会添加一个公共构造函数,它将任何@NonNullfinal 字段作为参数。这提供了普通旧Java对象(PO​​JO)所需的一切。

虽然@Data非常有用,但它不提供与其他Lombok注解相同的控制粒度。要覆盖默认方法生成行为,请使用其他Lombok注解之一注解类,字段或方法,并指定必要的参数值以实现所需的效果。

@Data确实提供了一个可用于生成静态工厂方法的参数选项。将staticConstructor参数的值设置为 所需的方法名称将导致Lombok将生成的构造函数设置为private,并公开给定名称的静态工厂方法。

Lombok 注解代码:

@Data(staticConstructor="of")
public class Company {
    private final Person founder;
    private String name;
    private List<Person> employees;
}

等价Java源代码:

public class Company {
    private final Person founder;
    private String name;
    private List<Person> 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<Person> getEmployees() {
        return employees;
    }
    
    public void setEmployees(final List<Person> 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注解可以用来保证分配的资源被释放。当使用带注解的局部变量时@Cleanup,任何后续代码都包含在一个 try/finally块中,该块保证在当前作用域的末尾调用cleanup方法。默认情况下,@Cleanup 假设清理方法命名为“close”,与输入和输出流一样。但是,可以为注解的value参数提供不同的方法名称。只有不带参数的清理方法才能与此注解一起使用。

使用@Cleanup注解时还需要注意一点。如果清理方法抛出异常,它将抢占方法体中引发的任何异常。这可能导致问题的实际原因被掩盖,并且在选择使用Project Lombok的资源管理时应该考虑到这一点。此外,随着Java 7中的自动资源管理,这个特定的注解可能相对短暂。

Lombok注解代码:

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关键字可能会导致不好的结果,任何一个开发多线程软件的开发人员都可以证明这一点。synchronized关键字在实例方法或静态方法的类对象中会锁定当前对象。这意味着开发人员控制之外的代码有可能锁定同一个对象,从而导致死锁。通常建议在单独的对象上显式锁定,该对象专门用于此目的,而不是以允许非请求锁定的方式公开。Project Lombok正是为此目的提供了@Synchronized注解。

@Synchronized注解一个实例方法将提示Lombok生成一个名为lock的私有锁定字段,该方法将在执行之前锁定该字段。类似地,以相同的方式注解静态方法将为静态方法生成一个名为`LOCK`的私有静态对象,以便以相同的方式使用。可以通过为注解的值参数提供字段名来指定不同的锁定对象。当提供字段名时,开发人员必须定义该属性,因为Lombok不会生成它。

Lombok注解代码:

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

@SneakyThrows可能是具有最多批评者的项目lombok注解,因为它是对已检查异常的直接攻击。关于使用已检查异常的问题存在很多分歧,大量开发人员认为这是一个失败的实验。这些开发人员会喜欢@SneakyThrows。在已检查/未检查的异常栏的另一侧的那些开发人员很可能将此视为隐藏潜在问题。

IllegalAccessException如果IllegalAccessException或某些父类未在throws子句中列出,则 抛出通常会生成“未处理的异常”错误:

Screenshot of Eclipse generating an error message regarding unhandled exceptions.

当用@ sneakythrow注解时,错误就消失了.

Screenshot of a method annotated with @SneakyThrows and generating no error in Eclipse.

默认情况下,@SneakyThrows将允许抛出任何已检查的异常而不在 throws子句中声明。通过向注解Class<? extends Throwable>value参数提供一个throwable classes()数组,可以将其限制为一组特定的异常 .

Lombok 注解代码:

@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注解取代的代码量所抵消。但是,那些避免经常使用注解的人可能会选择另一种方式。

缺什么?

Project Lombok提供了delombok用等效源代码替换Lombok注解的实用程序。可以通过命令行对整个源目录执行此操作

java -jar lombok.jar delombok src -d src-delomboked

或者,提供Ant任务以结合到构建过程中。

<target name="delombok">
    <taskdef classname="lombok.delombok.ant.DelombokTask"
        classpath="WebRoot/WEB-INF/lib/lombok.jar" name="delombok" />
    <mkdir dir="src-delomboked" />
    <delombok verbose="true" encoding="UTF-8" to="src-delomboked"
        from="src" />
</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
   }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335