背景
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强类型语言的原则,不推荐使用。至于其他的注解,可以简单了解一下,实践中几乎没有通用使用价值。