【注意】最后更新于 July 31, 2021,文中内容可能已过时,请谨慎使用。
Lombok 是什么
Lombok 是为了解决 Java 语法啰嗦而产生的工具,能够自动为编辑器和编译器产生代码,避免一部分体力劳动。比如我有一个Pojo,本来它是这个样子的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public class Test {
public static void main(String[] args) {
final Pojo build = new Pojo();
build.setA("a");
build.setB(true);
build.setNames(Arrays.asList("name1","name2"));
System.out.println(build);
}
public static class Pojo {
String a;
Boolean b;
List<String> names;
public String getA() {
return a;
}
public void setA(final String a) {
this.a = a;
}
public Boolean getB() {
return b;
}
public void setB(final Boolean b) {
this.b = b;
}
public List<String> getNames() {
return names;
}
public void setNames(final List<String> names) {
this.names = names;
}
@Override
public String toString() {
return "Test.Pojo{" +
"a='" + a + '\'' +
", b=" + b +
", names=" + names +
'}';
}
}
}
|
但是我用了Lombok以后,它是这个样子的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class Test {
public static void main(String[] args) {
final Pojo build = Pojo.builder()
.a("a")
.b(true)
.name("name1")
.name("name2")
.build();
System.out.println(build);
}
@Data
@Builder
@NoArgsConstructor
static class Pojo {
String a;
Boolean b;
@Singular
List<String> names;
}
}
|
输出是Test.Pojo(a=a, b=true, names=[name1, name2])
。
是不是可读性高了不少?这就是Lombok的效果了。
Lombok 怎么用
Lombok 是通过注解来完成其功能的。我把Lombok的注释分为如下几类:
- Pojo代码生成类
- 语义增强类
- 变量的注入和局部变量增强类
Pojo代码生成类
这一类注解主要用于Pojo里,可以避免体力劳动。
@Getter @Setter
最常用的,莫过于Getter和Setter,正如名字所示,它们可以为类和字段生成getter和setter。默认可以通过AccessLevel控制生成的函数的公开程度。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Test {
public static void main(String[] args) {
final Pojo pojo = new Pojo();
pojo.setA("a");
pojo.setB(true);
pojo.setNames(Arrays.asList("name1","name2"));
System.out.println(pojo);
}
@Setter
@Getter
public static class Pojo {
String a;
Boolean b;
List<String> names;
}
}
|
@NoArgsConstructor @AllArgsConstructor @RequiredArgsConstructor
这几个也很常用,分别是无参构造函数,所有参数的构造函数,以及必填参数的构造函数。无参构造函数和所有参数的构造函数都很好理解,最后这个必填参数怎么去找呢?是找到打了@NonNull注解,或者是final的字段,会当成是必填字段。
这三个构造函数的注解还有一个能力,可以通过staticName,来生成一个静态的工厂,此工厂可以根据参数来使用相应的构造函数。这个能力通常是用来接受一些类型参数来构造一个具有某种类型信息的Pojo。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class Test {
public static void main(String[] args) {
final Pojo pojo = new Pojo(Arrays.asList("name1","name2"));
pojo.setA("a");
pojo.setB(true);
// pojo.setNames(Arrays.asList("name1","name2")); 不能再set
// pojo.getNames().add("name3"); java.lang.UnsupportedOperationException
System.out.println(pojo);
}
@Data
@AllArgsConstructor
@RequiredArgsConstructor
public static class Pojo {
String a;
Boolean b;
final List<String> names;
}
}
|
@ToString @EqualsAndHashCode
这两个注解能够为被注解的类增加toString,equals和hashCode函数。如果有继承关系,一般还要把callSupper=true打开,可以将父类的字段囊括到计算中。如果对象里有一些字段是不用参与相等判断的,可以用exclude来排除一些字段。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class Test {
public static void main(String[] args) {
final Pojo pojo = new Pojo();
pojo.setA("a");
pojo.setB(true);
pojo.setNames(Arrays.asList("name1","name2"));
System.out.println(pojo);
}
@ToString
@EqualsAndHashCode
@Setter
@Getter
public static class Pojo {
String a;
Boolean b;
List<String> names;
}
}
|
@Data @Value
@Data其实是@Getter,@Setter,@RequiredArgConstructor,@ToString,@EqualsAndHashCode的集合,而@Value则是@Getter,@FieldDefault,@AllArgsConstructor,@ToString,@EqualsAndHashCode和@Data的集合。基本上打了这两个注解的,一定是当成了一个Pojo来使用。它们的区别是什么呢?@Data的对象没有什么特别的,但是@Value的对象,是当作不可变对象来使用的,一旦构造结束,也就不能变更了。但是要注意的是,上述@Data不包括@NoArgsConstrutor,当你的Pojo在经历反序列化过程的时候有可能会报错,需要人为补充@NoArgsConstructor。
@Builder @Singular
@Builder 可以为指定的Pojo生成一个Builder,通过Builder去实现一个类的Builder模式,解决构造的参数复杂异变带来的问题,如果说这个Builder还有哪里难用,那就是它默认对数组的设置需要填写一个完整的数组。众所周知,Java构造一个数组的语法是相当的啰嗦。而@Singular则可以为Builder中的数组元素提供单个元素的增加方法,build一个复杂对象更加容易了。开篇的示例就是这两个注解的使用方法。
@With
通过在字段上的注解With,我们可以通过With,将原有对象复制一份,所有字段都一样,除了with所指定的字段是新的。
语义增强类
@CustomLog
这是为了给被打注解的类注入一个logger。对于lombok.log.custom.declaration=my.cool.Logger my.cool.LoggerFactory.getLogger(NAME)的配置来说,会对目标类
1
2
3
|
@CustomLog
public class LogExample {
}
|
生成如下代码
1
|
private static final my.cool.Logger log = my.cool.LoggerFactory.getLogger(LogExample.class.getName());
|
但是现在通常使用已经封装好的Logger,所以,它有@Log,@CommonsLog,@Log4j,@Log4j2,@Slf4j,@XSlf4j,@JBossLog,@Flogger一系列注解,可以直接使用项目里已有的logger。
@SneakyThrows
Java面试题里经常问,怎么才能偷偷在一个没有声明Throws的函数上抛出一个checkedException?这个大家知道了原理,但是用的时候,大可不必自己造轮子,因为Lombok已经把轮子造好了,原理是利用了泛型抹除。但是我觉得这个注解应该慎用,因为用了这个注解,就把函数里的异常抛出情况掩盖了。比如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class Test {
public static void main(String[] args) {
final Pojo pojo = new Pojo();
pojo.setA("a");
pojo.setB(true);
pojo.setNames(Arrays.asList("name1","name2"));
pojo.test();
}
@Data
public static class Pojo {
String a;
Boolean b;
List<String> names;
@SneakyThrows
public void test(){
throw new OperationNotSupportedException();
}
}
}
|
会输出Exception in thread "main" javax.naming.OperationNotSupportedException
@Synchronized
这个注释名字大家太熟悉了,我们先看个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public class Test {
@SneakyThrows
public static void main(String[] args) {
final Pojo pojo = new Pojo();
pojo.setA("a");
pojo.setB(true);
pojo.setNames(Arrays.asList("name1","name2"));
int concurrency = 10;
final ExecutorService executorService = Executors.newFixedThreadPool(concurrency);
final CountDownLatch countDownLatch = new CountDownLatch(concurrency);
final List<Callable<Object>> collect = IntStream.range(0,concurrency).mapToObj(i -> new Callable<Object>() {
@Override
public Object call() throws Exception {
pojo.counterAdd(i);
countDownLatch.countDown();
return null;
}
}).collect(Collectors.toList());
final List<Future<Object>> futures = executorService.invokeAll(collect);
countDownLatch.await();
executorService.shutdown();
System.out.println(pojo.getCounter());
}
@Data
public static class Pojo {
String a;
Boolean b;
List<String> names;
volatile int counter = 0;
@Synchronized
public void counterAdd(final int i){
counter += i;
}
}
}
|
由于counter的add是有并发的情况的,通过注解,保证调用时不会导致加法失效。
乍一看这个注释跟我直接在函数上加修饰符synchronized,难道不一样吗?嘿,别说,虽然例子里的效果一样,但是原理还就是不一样。在函数上加的修饰符,锁的是对象的实例;用Lombok的注解,加的锁是对象里生成的一个字段,默认名称lock,我们可以根据自己需要调整不同的名称,达到更细粒度的锁。而且这能防止this在对象外被别人拿去当锁了,会造成不可以意料的死锁。
变量的注入和局部变量增强类
@Cleanup
在try with语法之前,关闭closable通常都写得很罗嗦,尤其是有嵌套的时候——这种情况还挺常见。即便是现在有了try with语法,多层嵌套还是很难看。但是用了@Cleanup,那就简单多了。比如这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class Test {
@SneakyThrows
public static void main(String[] args) {
@Cleanup final Pojo pojo1 = Pojo.builder().name("pojo1").build();
System.out.println(pojo1+ " created.");
@Cleanup final Pojo pojo2 = Pojo.builder().name("pojo2").build();
System.out.println(pojo2+ " created.");
System.out.println(pojo1);
System.out.println(pojo2);
}
@Data
@Builder
public static class Pojo implements Closeable {
String name;
@Override
public void close() throws IOException {
System.out.println(this + " closed.");
}
}
}
|
最后输出是
1
2
3
4
5
6
|
Test.Pojo(name=pojo1) created.
Test.Pojo(name=pojo2) created.
Test.Pojo(name=pojo1)
Test.Pojo(name=pojo2)
Test.Pojo(name=pojo2) closed.
Test.Pojo(name=pojo1) closed.
|
我们看关闭时顺序与创建的顺序是反的,完全正确。
@val @var
在Java支持var 和 val之前,一个类的名字如果要尽可能准确,那长度就很难减少;但是Java语法要赋值一个变量,就得为这个变量指定类型,最终,一个赋值都能写一百多列,非常难看。用了这组注释,再也不用写超级长的类名了。正如大家常用的其它语言,val是不可变量,var是可变量。不过现在Java已经支持了val和var,这个注解就用不到了。
Lombok 的原理
说完了用法,我们肯定会好奇,它为什么引入了依赖,就能自动帮我们完成这么多复杂的工作?通常我们写注解,都会自己写一些注解解析工具来完成注解的的使用工作,而这种工具通常得明确定义出来,或者放到框架的某个生命周期才能生效。Lombok是怎么做到的呢?
原来Lombok使用了一个Java的特性:JSR 269,也就是Pluggable Annotation Processing API。这样在Java编译的时候,Lombok就会被Javac唤起,用来增强实现的效果。如果想了解其具体的实现方案,可以看lombok.javac.handlers包下的各种handler。
Lombok 的注意事项
构造函数的易变性
如果过度依赖Lombok提供的注解来生成构造函数的话,一定要留意对象变更时这几个构造函数的重叠问题。比如当对象里的字段都废弃掉的时候,无参构造函数和全参构造函数参数一致了,编译过程会报错;或者增减字段的时候,原先的全参构造函数被直接使用的地方编译就报错了,也得改代码。更不要说调整字段顺序的时候全参构造函数一定会失效。我认为最好使用builder进行构造,语义清晰,而且修改的时候不容易错。
默认构造函数的丢失问题 @AllArgsConstructor @Builder
如果一个Pojo,只打了@AllArgsConstructor或@Builder,那它会吃掉默认的无参构造函数,在一些序列化框架上会报错。所以这个通常都会把无参构造函数的注解打上去。
注解的继承问题
@Data默认是有@EqualsAndHashCode注解的,但是callSuper并没有打开,也就是说,两个打了@Data的子类,在做Equals等判断的时候,没有考虑父类的字段。这种情况还需要大家自己再打一下@EqualsAndHashCode注解,并且把callSuper打开。
Kotlin与Lombok的集成问题
Kotlin与Lombok不同,它有单独的Compiler,执行的时机早于Lombok的注解工具执行,这会导致如果想在Kotlin里调用Lombok的生成的代码,会找不到实现。这个问题可以通过Delombok来解决,Delombok会先将Lombok处理的代码编译到编译后的目录,保障Kotlin能访问到增强后的函数,避免编译失败。
目前Kotlin项目组应该也注意到了这个问题,已经在开发Kotlin的lombok编译插件,其原理是生成stub,避免编译器失败。目前还在实验阶段,注解的支持有限,感兴趣的可以看https://kotlinlang.org/docs/lombok.html
附: 阅读 Lombok项目
Lombok项目是开源的,托管在github上边,地址https://github.com/projectlombok/lombok
Lombok项目是通过ant管理的,并且ant执行的最低版本是Java11。项目下载后在根目录可以使用ant IDE方式为使用的ide生成项目文件,可以使用的是ant eclipse和ant intellij。由于项目组成员全部是eclipse,所以不保证intellij的脚本可以使用,而且,经过测试,它确实不能使用LoL。好在intellij可以使用导入项目的功能,在生成eclipse文档后,当成是eclipse项目导入即可。
由于是Ant项目,跟Maven项目的默认结构差异还是不小的。不过阅读其源代码,主要关注的是src目录下的core目录。我们日常使用的注解就在这个目录下。
目录下还有若干子目录,bytecode是它对字节码操作的一些工具,core是核心处理,javac是它对javac的支持。core\handlers目录下是各个注解的处理方式。