Lombok使用说明

本文介绍了Lombok工具包,它能减少重复代码、提高代码整洁度和可维护性,但滥用会有隐患。详细阐述了Lombok常用注解,如@Setter、@ToString等的使用方法,还说明了部分注解使用时的注意事项,如@EqualsAndHashCode重写方法可能忽略父类属性等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

Lombok是个使用过就离不开的工具包,开发可以少写很多重复代码😀且显著提高代码的整洁度和可维护性,是个超级语法糖,但随意乱用也会带来难以排查的隐患。
leader最近review团队代码的时候发现了Lombok滥用问题:类继承关系没仔细斟酌,甚至有些框架代码也可见Lombok身影,因此趁小周末梳理一下相关内容,在下次例会的时候和大家说明一下。

本文主题内容是介绍Lombok的常用注解及使用注意事项

1.使用介绍

Lombok常用注解可以分为以下几种类型:
【1】@Setter 和 @Setter 和 @Accessor
【2】@ToString 和 @EqualsAndHashCode
【3】@NoArgsConstructor 和 AllArgsConstructor 和 @RequiredArgsConstructor
【4】@Data 和 @Value
【5】@Builder 和 @Singular
【6】@Delegate
【7】@Synchronized
【8】@Cleanup
【9】@SneakyThrows
【10】其他类型:@NonNull、@Slf4j、@val、@var、@with、@Generated等

1.1 @Setter 和 @Setter 和 @Accessor

@Setter 和 @Setter

@Setter和@Setter用于为实体类的属性生成getter/setter方法,可注解在具体属性上,也可注解在类上(注解在类上时,对全体属性生效), 需要注意:Getter和Setter对static变量无效。
源码:

@Getter
public class GetAndSetSample {
    private Integer id;

    @Setter
    private String name;

    private static String address;
}

编译后字节码:

public class GetAndSetSample {
    private Integer id;
    private String name;
    private static String address;

    public GetAndSetSample() {
    }

    public Integer getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public void setName(final String name) {
        this.name = name;
    }
}
@Accessor

@Accessor注解使得Lombok在生成Getter和Setter时,对生成的方法进行一些设置。可以注解在具体属性上,也可注解在类上(对全部属性生效)。
@Accessor注解存在3个属性:fluent和prefix用于设置生成的方法名(不常用),chain用于链式编程(较为常见);

源码:

@Setter
@Accessors(chain = true)
public class SetterAccessorSample {
    private Integer id;

    private String name;
}

编译后字节码:

public class SetterAccessorSample {
    private Integer id;
    private String name;

    public SetterAccessorSample() {
    }

    public SetterAccessorSample setId(final Integer id) {
        this.id = id;
        return this;
    }

    public SetterAccessorSample setName(final String name) {
        this.name = name;
        return this;
    }
}

注意:@Accessors需要与与Getter或Setter与搭配使用,单独使用时无效;
源码:

@Accessors(chain = true)
public class AccessorSample {
    private Integer id;

    private String name;
}

编译后字节码:

public class AccessorSample {
    private Integer id;
    private String name;

    public AccessorSample() {
    }
}

1.2 @ToString 和 @EqualsAndHashCode

@ToString

@ToString注解在类上,为类重写toString方法;
源码:

@ToString
public class ToStringSample {
    private Integer id;
    private String name;
}

编译后字节码:

public class ToStringSample {
    private Integer id;
    private String name;

    public ToStringSample() {
    }

    public String toString() {
        return "ToStringSample(id=" + this.id + ", name=" + this.name + ")";
    }
}
@EqualsAndHashCode

@EqualsAndHashCode注解在类上,为类重写equals和hashcode方法;
源码:

@EqualsAndHashCode
public class EqualsAndHashcodeSample {
    private Integer id;
    private String name;
}

编译后字节码:

public class EqualsAndHashcodeSample {
    private Integer id;
    
    private String name;

    public EqualsAndHashcodeSample() {
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof EqualsAndHashcodeSample)) {
            return false;
        } else {
            EqualsAndHashcodeSample other = (EqualsAndHashcodeSample)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$id = this.id;
                Object other$id = other.id;
                if (this$id == null) {
                    if (other$id != null) {
                        return false;
                    }
                } else if (!this$id.equals(other$id)) {
                    return false;
                }

                Object this$name = this.name;
                Object other$name = other.name;
                if (this$name == null) {
                    if (other$name != null) {
                        return false;
                    }
                } else if (!this$name.equals(other$name)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof EqualsAndHashcodeSample;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $id = this.id;
        int result = result * 59 + ($id == null ? 43 : $id.hashCode());
        Object $name = this.name;
        result = result * 59 + ($name == null ? 43 : $name.hashCode());
        return result;
    }
}

其中:hashCode基于所有字段生成,equals会判断所有属性是否相等。
这里需要注意,@EqualsAndHashCode注解为类生成equals和hashcode方法时,只会基于当前类的属性而忽略父类属性,因此重写的hashcode和equal可能是错误代码。以下通过两种场景介绍常见的错误写法。
场景1:

// 父类
public class FoodSample {
    private Integer id;
    private String type;
}

// 子类
@EqualsAndHashCode
public class FruitSample extends FoodSample{
    private String name;
}

FruitSample编译后字节码:

public class FruitSample extends FoodSample {
    private String name;

    public FruitSample() {
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof FruitSample)) {
            return false;
        } else {
            FruitSample other = (FruitSample)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$name = this.name;
                Object other$name = other.name;
                if (this$name == null) {
                    if (other$name != null) {
                        return false;
                    }
                } else if (!this$name.equals(other$name)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof FruitSample;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $name = this.name;
        int result = result * 59 + ($name == null ? 43 : $name.hashCode());
        return result;
    }
}

其中,重写的hashcode和equals仅涉及FruitSample中的name字段,而忽略了父类的id和type属性。
测试demo:

@Slf4j
public class HashcodeTest {
    @Test
    public void testHashCodeAndEquals() {
        FruitSample fruit1 = new FruitSample();
        fruit1.setId(1);
        fruit1.setType("fruit");
        fruit1.setName("tomato");

        FruitSample fruit2 = new FruitSample();
        fruit2.setId(2);
        fruit2.setType("vegetables");
        fruit2.setName("tomato");

        LOGGER.debug("Test fruit1.hashcode is {}.", fruit1.hashCode());
        LOGGER.debug("Test fruit2.hashcode is {}.", fruit2.hashCode());

        LOGGER.debug("Test fruit1 equals with fruit2: {}.", fruit1.equals(fruit2));
    }
}

运行结果如下:
在这里插入图片描述
结果显示fruit1和fruit2的ID和Type属性不相同,但hashcode相同且equals相等; 此误操作可能埋下难以排查的隐患。
在子类上注解@EqualsAndHashCode(callSuper=true),使得equals和hashcode方法涉及父类和子类的所有属性。

场景2:
当父类使用Lombok生成了equals和hashcode,而子类没有复写时:

// 父类
@Getter @Setter
@EqualsAndHashCode
public class FoodSample {
    private Integer id;
    private String type;
}

// 子类
@Getter @Setter
public class FruitSample extends FoodSample {
    private String name;
}

测试类:

@Slf4j
public class HashcodeTest {
    @Test
    public void testHashCodeAndEquals() {
        FruitSample fruit1 = new FruitSample();
        fruit1.setId(1);
        fruit1.setType("fruit");
        fruit1.setName("apple");

        FruitSample fruit2 = new FruitSample();
        fruit2.setId(1);
        fruit2.setType("fruit");
        fruit2.setName("orange");

        LOGGER.debug("Test fruit1.hashcode is {}.", fruit1.hashCode());
        LOGGER.debug("Test fruit2.hashcode is {}.", fruit2.hashCode());

        LOGGER.debug("Test fruit1 equals with fruit2: {}.", fruit1.equals(fruit2));
    }
}

运行结果如下:
在这里插入图片描述
结果表明:fruit1和fruit2的hashcode相同且equals相等。
分析:
子类继承父类时,如果没有复写父类的方法,则会使用父类的方法;因此hash和equals方法只会涉及父类中属性。
因此,需要注意当父类中复写了Object的hashcode和equals方法时,子类必要要再次复写。此时只需要在子类上添加@EqualsAndHashCode(callSuper=true)注解即可。

1.3 @NoArgsConstructor 和 AllArgsConstructor 和 @RequiredArgsConstructor

@NoArgsConstructor 和 AllArgsConstructor分别为类生成无参构造函数和全参数构造函数;@RequiredArgsConstructor为必要的参数生成构造函数(如被final和@NonNull修饰的字段)

@NoArgsConstructor 和 AllArgsConstructor

源码:

@NoArgsConstructor
@AllArgsConstructor
public class AllAndNonConstructorSample {
    private String type;

    private String name;
}

编译后字节码:

public class AllAndNonConstructorSample {
    private String type;
    private String name;

    public AllAndNonConstructorSample() {
    }

    public AllAndNonConstructorSample(final String type, final String name) {
        this.type = type;
        this.name = name;
    }
}
@RequiredArgsConstructor

源码:

@RequiredArgsConstructor
public class RequiredConstructorSample {
    private final Integer id;

    @NonNull
    private String type;

    private String name;
}

编译后字节码:

public class RequiredConstructorSample {
    private final Integer id;
    @NonNull
    private String type;
    private String name;

    public RequiredConstructorSample(final Integer id, @NonNull final String type) {
        if (type == null) {
            throw new NullPointerException("type");
        } else {
            this.id = id;
            this.type = type;
        }
    }
}

可以看出@RequiredArgsConstructor只给必要的参数生成构造函数;其中涉及的@NonNull也是Lombok较为常见的注解,注解字段为空时,返回空指针。

1.4 @Data 和 @Value

@Data可以理解为一个组合,相当于:@Setter + @Getter + @ToString + @EqualsAndHashCode,因此较为常见。
@Value作用类似@Data,区别是被@Value注解的类为值对象类型——不可修改,因此不会生产Setter方法;此处给出@Value的样例:

源码:

@Value
public class ValueDemo {
    private String type;

    private String name;
}

编译后代码:

public final class ValueDemo {
    private final String type;
    private final String name;

    public ValueDemo(final String type, final String name) {
        this.type = type;
        this.name = name;
    }

    private ValueDemo() {
        this.type = null;
        this.name = null;
    }

    public String getType() {
        return this.type;
    }

    public String getName() {
        return this.name;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof ValueDemo)) {
            return false;
        } else {
            ValueDemo other = (ValueDemo)o;
            Object this$type = this.getType();
            Object other$type = other.getType();
            if (this$type == null) {
                if (other$type != null) {
                    return false;
                }
            } else if (!this$type.equals(other$type)) {
                return false;
            }

            Object this$name = this.getName();
            Object other$name = other.getName();
            if (this$name == null) {
                if (other$name != null) {
                    return false;
                }
            } else if (!this$name.equals(other$name)) {
                return false;
            }

            return true;
        }
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $type = this.getType();
        int result = result * 59 + ($type == null ? 43 : $type.hashCode());
        Object $name = this.getName();
        result = result * 59 + ($name == null ? 43 : $name.hashCode());
        return result;
    }

    public String toString() {
        return "ValueDemo(type=" + this.getType() + ", name=" + this.getName() + ")";
    }
}

其中也可以看出:ValueDemo类被final修改,不可被继承。

1.5 @Builder 和 @Singular

@Builder

被@Builder注解的实体类,会在内部生成一个建造者类,用于构建实体类。

@Builder
@ToString
public class BuilderSample {
    private String type;

    private String name;
}

// 测试类
@Slf4j
public class BuilderTest {
    @Test
    public void testBuilder() {
        BuilderSample builderSample = BuilderSample.builder()
    	  .name("test_name")
     	  .type("test_type")
      	  .build();
        
        LOGGER.debug("BuilderSample is {}.", builderSample);
    }
}
@Singular

当类属性中存在集合时,可通过注解上@Singular为该集合属性提供一个添加元素的方法,如下所示:

@Builder
@ToString
public class BuilderSample {
    private String type;

    private String name;

    @Singular
    private List<String> attributes;
}

测试类:

@Slf4j
public class BuilderTest {
    @Test
    public void testBuilder() {
        BuilderSample builderSample = BuilderSample.builder()
                .name("test_name")
                .type("test_type")
                .attribute("attr1")
                .attribute("attr2")
                .attribute("attr3")
                .build();
        LOGGER.debug("BuilderSample is {}.", builderSample);
    }
}

@Singular通过attribute方法为attributes属性添加元素;需要注意集合属性名必须是复数,否则会导致编译异常。

1.6 @Delegate

@Delegate的核心功能是委托,按照被注解属性类的信息生产委托方法,并依赖其实现;以下通过案例方式介绍:

// 被委托类
@Slf4j
public class Sample {
    public void insertSample() {
        LOGGER.info("Operation insert sample.");
    }

    protected void deleteSample() {
        LOGGER.info("Operation delete sample.");
    }

    void updateSample() {
        LOGGER.info("Operation update sample.");
    }

    private void selectSample() {
        LOGGER.info("Operation select sample.");
    }

    public static void staticSelectSample() {
        LOGGER.info("Operation static select sample.");
    }
}

// 委托类
@Slf4j
public class DelegateSample {
    @Delegate
    private Sample sample;
}

其中,委托类编译后字节码:

public class DelegateSample {
    private static final Logger LOGGER = LoggerFactory.getLogger(DelegateSample.class);
    private Sample sample;

    public DelegateSample() {
    }

    public void insertSample() {
        this.sample.insertSample();
    }
}

可以看出,@Delegate只会针对(被注解属性的)public—非static方法(为委托类)生成委托方法。

@Delegate生成的方法必须为public-非static,包括继承得到的方法,但不包含Object类中的方法.

另外,对于满足public—非static的所有方法,lombok都会为其在委托类中生成对应的方法,这也会导致一些问题,如下所示:

@Slf4j
public class Sample {
    public void insertSample() {
        LOGGER.info("Operation insert sample.");
    }

    public void deleteSample() {
        LOGGER.info("Operation delete sample.");
    }

    public void updateSample() {
        LOGGER.info("Operation update sample.");
    }

    public void selectSample() {
        LOGGER.info("Operation select sample.");
    }
}

当需要在委托类中定义同名的selectSample方法时,会因存在重复签名的方法而编译报错:
在这里插入图片描述
此时,通过@Delegate的excludes属性排除不需要的委托方法:
在这里插入图片描述
如上所示,可以定义一个Exclude接口,并在该接口内部声明不需要的委托方法;再将该接口的字节码文件赋值给@Delegate的excludes属性。

注意:使用@Delegate时注意避免方法签名的冲突

@Delegate还可以注解在无参方法上, 此时生成的方法依赖于方法的返回类型:

@Slf4j
public class DelegateSample {
    @Delegate
    public Sample selectSampleObj() {
        LOGGER.info("Operation select sample.");
        return new Sample();
    }
}

编译后的字节码:

public class DelegateSample {
    private static final Logger LOGGER = LoggerFactory.getLogger(DelegateSample.class);

    public DelegateSample() {
    }

    public Sample selectSampleObj() {
        LOGGER.info("Operation select sample.");
        return new Sample();
    }

    public void insertSample() {
        this.selectSampleObj().insertSample();
    }

    public void deleteSample() {
        this.selectSampleObj().deleteSample();
    }

    public void updateSample() {
        this.selectSampleObj().updateSample();
    }

    public void selectSample() {
        this.selectSampleObj().selectSample();
    }
}

生成的委托方法会先调用被注解的方法🥷来获取对象实例,再委托给该对象。

1.7 @Synchronized

随着JDK版本的升级,对synchronized的性能不断优化,使得锁的性能基本与JUC的Lock相近。
Lombok也提供了对应的注解用于操作Synchronized关键字,使用实例如下所示:

@Slf4j
public class SynchronizedSample {
    private Object updateLock = new Object();

    @Synchronized
    public void insertSample() {
        LOGGER.info("Operation insert sample.");
    }

    @Synchronized("updateLock")
    public void deleteSample() {
        LOGGER.info("Operation delete sample.");
    }

    @Synchronized("updateLock")
    public void updateSample() {
        LOGGER.info("Operation update sample.");
    }
}

编译后字节码:

public class SynchronizedSample {
    private static final Logger LOGGER = LoggerFactory.getLogger(SynchronizedSample.class);
    private final Object $lock = new Object[0];
    private Object updateLock = new Object();

    public SynchronizedSample() {
    }

    public void insertSample() {
        synchronized(this.$lock) {
            LOGGER.info("Operation insert sample.");
        }
    }

    public void deleteSample() {
        synchronized(this.updateLock) {
            LOGGER.info("Operation delete sample.");
        }
    }

    public void updateSample() {
        synchronized(this.updateLock) {
            LOGGER.info("Operation update sample.");
        }
    }
}

代码中可见:@Synchronized中可以指定锁对象,此时为updateLock对象,未指定锁对象的方法共用同一把锁;静态同步方法与之类似,仅将对应属性修改为静态属性即可。由于不方便控制锁的粒度,因此不推荐使用。

1.8 @Cleanup

该注解用于关闭资源对象,免除了try-catch-finnally冗长的逻辑,提供了一个相对于try-with-resources更高明的语法糖(使用更为自由,不需要集中在try-with-resources中进行声明)。

public class CleanupExample {
    public static void copy(String sourceFile, String targetFile) throws IOException {
        @Cleanup InputStream source = new FileInputStream(sourceFile);
        @Cleanup OutputStream target = new FileOutputStream(targetFile);
        byte[] byteArr = new byte[1024];
        while (true) {
            int length = source.read(byteArr);
            if (length == -1) {
                break;
            }
            target.write(byteArr, 0, length);
        }
    }
}

编译后字节码为:

public class CleanupExample {
    public CleanupExample() {
    }

    public static void copy(String sourceFile, String targetFile) throws IOException {
        FileInputStream source = new FileInputStream(sourceFile);
        try {
            FileOutputStream target = new FileOutputStream(targetFile);
            try {
                byte[] byteArr = new byte[1024];
                while(true) {
                    int length = source.read(byteArr);
                    if (length == -1) {
                        return;
                    }
                    target.write(byteArr, 0, length);
                }
            } finally {
                if (Collections.singletonList(target).get(0) != null) {
                    target.close();
                }
            }
        } finally {
            if (Collections.singletonList(source).get(0) != null) {
                source.close();
            }
        }
    }
}

1.9 @SneakyThrows

JDK语法要求对于所有的check异常都需要进行捕获,而RuntimeException不需要;Lombok提供了一个机制:捕获所有的异常,并封装为RuntimeException异常抛出,使得方法链不再需要声明被抛出的异常。

public class SneakyThrowsSample {
    @SneakyThrows
    public void insertSample() {
        throw new Throwable();
    }
}

编译后字节码:

public class SneakyThrowsSample {
    public SneakyThrowsSample() {
    }

    public void insertSample() {
        try {
            throw new Throwable();
        } catch (Throwable var2) {
            throw var2;
        }
    }
}

@SneakyThrows毕竟违背了JDK的异常设计理念,请谨慎使用。

1.10 其他类型

除上述注解外,Lombok包还提供了@NonNull、@Slf4j、@val、@var、@with、@Generated等。

@NonNull

其中,@NonNull用于校验非空, 为空时抛出NPE, 可以提高代码的整洁度:
源码:

@Slf4j
public class NonNullSample {
    public void insertSample(@NonNull String name) {
        LOGGER.debug("Param name is {}.", name);
    }
}

编译后源码:

public class NonNullSample {
    private static final Logger LOGGER = LoggerFactory.getLogger(NonNullSample.class);

    public NonNullSample() {
    }

    public void insertSample(@NonNull String name) {
        if (name == null) {
            throw new NullPointerException("name");
        } else {
            LOGGER.debug("Param name is {}.", name);
        }
    }
}

使得开发🥷不需要手动进行非空判断。

@Slf4j

Lombok为各种日志框架都提供了注解,这里以@Slf4j为例:
上述案例中的日志打印,使用的就是@Slf4j, Lombok会在类中生产一个日志对象,如NonNullSample类中:

private static final Logger LOGGER = LoggerFactory.getLogger(NonNullSample.class);

这里顺便提一下,Lombok生成的日志对象默认为log, 并不符合Java命名规范,可在lombok.conf(项目根目录)中进行修改:

// lombok.config文件
lombok.log.fieldname=LOGGER
其他…

@val和@var可以用来声明对象,让编译器自己推断;明显违背了Java强类型语言的原则,不推荐使用。至于其他的注解,可以简单了解一下,实践中几乎没有通用使用价值。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值