简介

ProjectLombok官网 介绍:Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java. Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more.

永远不要再写另一个getter或者equals方法,一个注解就可以让你的类有一个功能全面的生成器,自动化你的日志变量等等。

在业务开发中,我们不可避免的需要去定义数据库表的实体Entity类、中间数据传输对象DTO类、前端视图对象VO类等,这些类有一个共同的特点就是都会有很多很多的业务字段,承载着系统的业务数据。基于面向对象的封装性,类中的字段会被定义成private,然后提供出publicgettersetter方法暴露给外部操作。于是代码中就出现了一大片一大片的使用IDE生成的gettersetter方法。

Lombok要解决的问题就是去消除这些模版式的gettersetter方法。当然还有一些其它的模版式的代码也可以使用Lombok来消除。

基本使用

引入依赖

引入Maven依赖:

1
2
3
4
5
6
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>

如果你是第一次使用Lombok,还需要在IDE上安装Lombok的插件。(插件的安装方式请自行谷歌)

如果是Spring Boot项目,Spring Boot 2.1.x版本的parent中默认已经预定义了Lombok的依赖,直接引入Maven坐标即可。

@Getter@Setter

这两个注解可以单独写在某些字段上,也可以写在类上。编译后会在字节码文件中加入相应字段的gettersetter方法。

单独写在类上的使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class GetterAndSetterTest {

@Getter
private String getField;

@Setter
private String setField;

@Getter
@Setter(AccessLevel.PROTECTED)
private String otherField;

@Getter
@Setter
private final String fs = null;
}

@Getter注解会生成对应字段的getter方法,@Setter注解会生成对应字段的setter方法,可同时作用于同一个字段,还可以定义生成的方法的访问修饰符,例如@Setter(AccessLevel.PROTECTED)生成的setter方法就是protected级别的。

需要注意的是,如果字段被声明成final类型,@Setter注解不会为其生成对应的setter方法。

编译后的字节码如下:

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
public class GetterAndSetterTest {
private String getField;
private String setField;
private String otherField;

public GetterAndSetterTest() {
}

public String getGetField() {
return this.getField;
}

public void setSetField(String setField) {
this.setField = setField;
}

public String getOtherField() {
return this.otherField;
}

protected void setOtherField(String otherField) {
this.otherField = otherField;
}

public String getFs() {
return this.fs;
}
}

如果不想细粒度的控制到每个字段,可以直接在类上添加@GetterSetter注解,会为所有的字段生成gettersetter方法。

@ToString

我们经常会去重写Object#toString()方法以便在控制台输出时打印的是类的基本信息而不是十六进制的地址值。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ToString(exclude = {"id"})
public class ToStringTest {
private Long id;
private String address;
private UserInfo userInfo;

@ToString
public static class BaseInfo {
private String username;
private String password;
}

@ToString(callSuper = true,includeFieldNames = false)
public static class UserInfo extends BaseInfo {
private Integer age;
private Integer gender;
}
}

@ToString注解会生成一个toString()方法,默认输出类名,所有字段的名称和值。可以使用注解的exclude属性来排除某些字段;还可以设置includeFieldNames = false屏蔽所有字段名称的输出;如果字段存在基类,可以设置callSuper = true来输出基类中的字段。

编译后生成的字节码如下:

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
public class ToStringTest {
private Long id;
private String address;
private ToStringTest.UserInfo userInfo;

public ToStringTest() {
}

public String toString() {
return "ToStringTest(address=" + this.address + ", userInfo=" + this.userInfo + ")";
}

public static class UserInfo extends ToStringTest.BaseInfo {
private Integer age;
private Integer gender;

public UserInfo() {
}

public String toString() {
return "ToStringTest.UserInfo(super=" + super.toString() + ", " + this.age + ", " + this.gender + ")";
}
}

public static class BaseInfo {
private String username;
private String password;

public BaseInfo() {
}

public String toString() {
return "ToStringTest.BaseInfo(username=" + this.username + ", password=" + this.password + ")";
}
}
}

@EqualsAndHashCode

自定义类对象进行相等比较时一般需要重写equals()方法,同时最好也重写hashCode()方法。

使用示例:

1
2
3
4
5
6
7
@EqualsAndHashCode(exclude = {"ex"})
public class EqualsAndHashCodeTest {
private String equal;
private String hc;
transient String tr;
private String ex;
}

默认情况下,会使用所有非静态non-static和非瞬态non-transient的字段来生成equals()hashCode()方法。类似地,可以使用exclude属性来排除某些字段;如果存在基类,也可以使用callSuper = true来囊括基类中的字段。

编译后生成的字节码如下:

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
49
50
51
52
53
54
55
56
57
58
public class EqualsAndHashCodeTest {
private String equal;
private String hc;
transient String tr;
private String ex;

public EqualsAndHashCodeTest() {
}

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

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

return true;
}
}
}

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

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

可以看到还加入了一个canEqual方法用来预先判断是否能和当前类对象进行equals比较。

@Data

基本上每一个实体类都会使用到上面四个注解:@Getter@Setter@ToString@EqualsAndHashCode,但是如果在每一个类上都重复的写上这四个注解,就又显得累赘了。于是Lombok提供了@Data注解,它等价于这四个注解的组合,会同时生成gettersettertoStringequalscanEqualhashCode这六个方法。同时还提供了一个属性staticConstructor,例如:staticConstructor="of",这样使用后,会将无参构造函数私有化,同时会提供一个静态成员方法of()用来创建当前类对象,在类外部无法使用new关键字来创建对象。

使用示例:

1
2
3
4
5
@Data(staticConstructor = "of")
public class DataTest {
private String s;
private String c;
}

编译后生成的字节码如下:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class DataTest {
private String s;
private String c;

private DataTest() {
}

public static DataTest of() {
return new DataTest();
}

public String getS() {
return this.s;
}

public String getC() {
return this.c;
}

public void setS(String s) {
this.s = s;
}

public void setC(String c) {
this.c = c;
}

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

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

return true;
}
}
}

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

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

public String toString() {
return "DataTest(s=" + this.getS() + ", c=" + this.getC() + ")";
}
}

@NoArgsConstructor@AllArgsConstructorRequiredArgsConstructor

  • @NoArgsConstructor:无参构造器;
  • @AllArgsConstructor:全参构造器;
  • @RequiredArgsConstructor:部分参数构造器,针对被声明为final的字段生成构造器。

Java类的构造器机制是:如果类中未声明任何一个构造器,则编译器会在编译期自动加上一个无参构造器;如果声明了任意一个构造器,则不会自动加上无参构造器。

所以一般会将无参和全参构造器进行搭配使用:

1
2
3
4
5
@NoArgsConstructor
@AllArgsConstructor
public class ArgsConstructor {
private String ac;
}

编译后生成的字节码如下:

1
2
3
4
5
6
7
8
9
10
public class ArgsConstructor {
private String ac;

public ArgsConstructor() {
}

public ArgsConstructor(String ac) {
this.ac = ac;
}
}

@RequiredArgsConstructor注解跟前两个是互斥的,该注解是针对final字段来生成构造器,可与SpringDI依赖注入搭配使用。

@Slf4j

作为研发我们避免不了日志打印,但我们却要在每一个需要打印日志的类中定义这样一个静态日志常量:private static final Logger log = LoggerFactory.getLogger(XXXService.class);,其中XXXService的名称还都不相同,这太影响像我这样的CV工程师的效率了。Lombok提供了@Slf4j注解来解决这个问题。

使用前提:引入日志框架slf4jMaven依赖

1
2
3
4
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>

使用示例:

1
2
3
@Slf4j
public class LogTest {
}

编译后生成的字节码如下:

1
2
3
4
5
6
7
8
9
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogTest {
private static final Logger log = LoggerFactory.getLogger(LogTest.class);

public LogTest() {
}
}

只用一个相同的注解@Slf4j就可以为每个类分别生成对应的日志常量log对象。CV工程师的效率又提高了!

原理分析

Lombok的使用很简单,只需添加对应注解,就可以在字节码中生成对应的代码。从源文件到字节码之间只经过了编译阶段,所以Lombok一定是在编译阶段对注解进行解析然后注入对应的字节码。

而编译阶段对注解的解析有两种机制:

  1. Annotation Processing Tool:简称apt

apt随着JDK5引入注解时产生,在JDK7中被标记为过期,不推荐使用,JDK8已彻底将其删除。

JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换的主要原因是:

  • api都在com.sun.mirror非标准包下;
  • 没有集成到javac中,需要额外运行。
  1. Pluggable Annotation Processing API

这其实是JSR 269规范,在JDK6中被引入作为apt的替代方案,它解决了apt的那两个问题,在javac执行过程中会调用实现了该规范的程序,这样我们就可以对javac编译器做一些增强。

JSRJava Specification Requests的缩写,意思是Java规范提案。

Lombok就是一个实现了JSR 269规范的程序,在javac编译过程中会调用Lombok,从而实现代码的自动加入。

整个编译的过程大致如下:

  • javac对源代码进行分析,生成一颗抽象语法树(AST);
  • 调用JSR 269的实现:Lombok
  • Lombok对抽象语法树进行解析,找到Lombok定义的注解所在的语法树,然后进行修改,添加gettersetter等方法定义的树节点;
  • 使用修改后的语法树生成字节码class文件。

优缺点

优点:

  • 代码简洁优雅,通过注解的形式自动生成一些模版式的代码。
  • 当类字段发生修改时,不用去修改对应的gettersetter等方法。

缺点:

  • IDE中需要安装插件,否则项目报错。
  • 不支持任意个参数的构造器重载。

总结

关于Lombok,网上一部分人支持,一部分人反对。反对的理由还很多:强依赖插件;组内有一人用则所有人用;操作语法树等于改变Java语法等等。暂且认为这些都有道理,但有句话怎么说来着:拥抱变化!Java语言走过这么多年也经过了很多变化,每个大版本也或多或少会出现一些新语法。Lombok的出现也预示着Java需要发生变化,而不是局限于当下的安稳。所以,我认为Lombok是值得去拥抱的!

从另一个角度说,那些反对Lombok的人可能是没写过业务系统,业务代码中经常出现一个类有二三十个字段,甚至更多,这时如果手动去维护各个类的gettersetter方法,效率可想而知,而且也没什么技术含量。打工人讲究的是如何高效打工!