1. 官方网站
官网地址
API 文档
Lombok
是一款Java
开发插件,使得Java
开发者可以通过其定义的一些注解来消除业务工程中冗长和繁琐的代码,尤其对于简单的Java
模型对象(POJO
)。在开发环境中使用Lombok
插件后,Java
开发人员可以节省出重复构建,诸如hashCode
和equals
这样的方法以及各种业务对象模型的accessor
和ToString
等方法的大量时间。对于这些方法,它能够在编译源代码期间自动帮我们生成这些方法,并不会像反射那样降低程序的性能。
2. 准备工作
2.1. 安装 Lombok 插件
我使用的开发工具是IntelliJ IDEA 2019.2
版本(公司电脑上安装的版本),直接在File--->Settings--->Plugins
里面搜索lombok
,会看到下图所示的插件列表,Marketplace
显示的是市场上的相关插件列表,Installed
显示的是本地已安装的插件列表。
点击Install
按钮安装,安装完毕该按钮会变为Installed
,上图为已安装完的状态。
接下来会在Installed
列表中看到已安装的Lombok
插件。(安装完插件需重启 DIEA 才会生效)
2.2. 设置实时编译
IDEA
默认状态为不自动编译,Eclipse
默认为自动编译,很多朋友都是从Eclipse
转到Intellij IDEA
的,这常常导致我们在需要操作class
文件时忘记对java
类文件进行编译从而对旧文件进行了操作。为了能实时查看编译完的class
文件,我们可以将IDEA
设置为实时编译,在File--->Settings--->Build,Execution,Deployment--->Compiler
中,对Build project automatically
选项打钩即可,注意自动编译只有在服务没有运行的时候才可用。另外,会在IDEA
的下方任务栏出现Problems
选项卡,该选项卡会在代码编译的时候给出进度提示和编译结果提示。
IDEA
默认集成了反编译插件,通过设置实时编译,我们可以很方便的查看我们class
文件的反编译结果。使用的时候只需要找到你的class
文件(maven
项目直接在target
下找对应目录的class
文件),直接双击打开即可。
2.3. 文件重新编译
也可以不用设置实时编译,直接对修改的Java
文件进行手动编译,在类上鼠标右键,选择Recompile'xxxx.java'
进行重新编译。或者直接使用快捷键Ctrl+Shift+F9
进行重新编译。
重新编译完毕,找到对应的class
文件,可能是没有更新的,这时可以直接在对应class
文件上或者对应包上点击鼠标右键,选择Synchronize 'xxxxx'
进行同步即可更新为最新的class
文件。
2.4. 引入相关依赖
对于Maven
项目,直接在pom.xml
中引入Lombok
的依赖。当前最新的是1.18.8
版本。
org.projectlombok
lombok
1.18.8
3. 注解使用
3.1. @Getter@Setter
@Getter
和@Setter
注解为字段生成getter
和setter
方法。
1、注解在类或字段上,注解在类时为所有字段生成getter
和setter
方法,注解在字段上时只为该字段生成getter
和setter
方法。
2、不会对final
字段生成setter
方法,会生成getter
方法。
3、对于boolean
类型的属性,生成的getter
方法遵循布尔属性的约定,例如对于属性boolean foo
生成的getter
方法为isFoo
,而不是getFoo
。
4、如果使用注解的字段所属的类包含与要生成的getter
或setter
名称相同的方法,无论参数或返回类型是否一样,都不会生成相应的方法。
5、这两个注解通过使用可选参数AccessLevel
可以指定生成的方法的访问级别。一共有六种级别:
PUBLIC, MODULE, PROTECTED, PACKAGE, PRIVATE, NONE;
示例代码
public class UserInfo {
@Getter@Setter
private final String nid = "nid";
@Getter(value = AccessLevel.PROTECTED)
private String userId;
@Setter(value = AccessLevel.PACKAGE)
private String userName;
@Getter@Setter(value = AccessLevel.NONE)
private boolean sex;
}
等效代码
public class UserInfo {
private final String nid = "nid";
private String userId;
private String userName;
private boolean sex;
public UserInfo() {}
//final属性只生成了getter方法
public String getNid() {
this.getClass();
return "nid";
}
//生成了protected级别的getter方法
protected String getUserId() {
return this.userId;
}
void setUserName(String userName) {
this.userName = userName;
}
//没有生成setter方法,生成的getter方法的名字是is开头的
public boolean isSex() {
return this.sex;
}
}
3.2. @NonNull
@NonNull
注解用于字段上,表示对该字段进行null
检查。
1、当放置在使用@Setter
注解的字段上时,将在生成的setter
方法中进行null
检查。
2、如果使用Lombok
为所属类生成构造函数,那么使用了该注解的字段将添加到生成的构造函数的参数中,并且在生成的构造函数中对该参数进行null
检查。
示例代码
@RequiredArgsConstructor
public class UserInfo {
@Getter@Setter@NonNull
private String userId;
private String userName;
}
等效代码
import lombok.NonNull;
public class UserInfo {
@NonNull
private String userId;
private String userName;
public UserInfo(@NonNull String userId) {
if (userId == null) {//构造方法中生成的null检查
throw new NullPointerException("userId is marked non-null but is null");
} else {
this.userId = userId;
}
}
@NonNull
public String getUserId() {
return this.userId;
}
public void setUserId(@NonNull String userId) {
if (userId == null) {//setter方法中生成的null检查
throw new NullPointerException("userId is marked non-null but is null");
} else {
this.userId = userId;
}
}
}
3.3. @ToString
类上使用该注解会自动生成toString
方法。默认情况下,任何非静态字段都将以名称-值对的形式包含在toString
方法的输出中。该注解有几个可选属性,可相应控制toString
的输出内容。
示例代码
@ToString(exclude = {"age","sex"})
public class UserInfo {
private String userId;
private String userName;
private Integer age;
private Boolean sex;
}
等效代码
public class UserInfo {
private String userId;
private String userName;
private Integer age;
private Boolean sex;
public UserInfo() {
}
//生成的toString方法
public String toString() {
return "UserInfo(userId=" + this.userId + ", userName=" + this.userName + ")";
}
}
可选属性
//该属性设置为 false 表示输出没有属性名和等号,只有属性值,多个属性值用逗号隔开
boolean includeFieldNames() default true;
//该属性表示输出中不包指定字段。可指定多个字段
String[] exclude() default {};
//该属性表示输出中只包含指定字段。可指定多个字段
String[] of() default {};
//该属性设置为 true,表示输出中会包含父类的 toString 方法输出
boolean callSuper() default false;
//该属性设置为 true,表示输出的字段值不通过 getter 方法获取,而是直接访问字段
boolean doNotUseGetters() default false;
//该属性设置为 true,不输出任何字段信息,只输出了构造方法的名字
boolean onlyExplicitlyIncluded() default false;
3.4. @EqualsAndHashCode
该注解用在类上会同时生成equals
和hashCode
方法,因为这两个方法本质上是由hashCode
契约绑定在一起的。默认情况下,这两种方法都将考虑类中不是静态或瞬态的任何字段。
(1)通过exclude
参数可以指定需要排除的属性。还以通过of
参数来指定所希望使用的字段。
(2)callSuper
参数。将其设置为true
将导致equals
方法首先会调用父类的equals
来验证等式。对于hashCode
方法,它将会把父类的hashCode
的结果合并到子类的hashCode
计算中。
示例代码
@EqualsAndHashCode
public class UserInfo {
private String userId;
private String userName;
}
等效代码
public class UserInfo {
private String userId;
private String userName;
public UserInfo() {
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof UserInfo)) {
return false;
} else {
UserInfo other = (UserInfo)o;
if (!other.canEqual(this)) {
return false;
} else {
Object this$userId = this.userId;
Object other$userId = other.userId;
if (this$userId == null) {
if (other$userId != null) {
return false;
}
} else if (!this$userId.equals(other$userId)) {
return false;
}
Object this$userName = this.userName;
Object other$userName = other.userName;
if (this$userName == null) {
if (other$userName != null) {
return false;
}
} else if (!this$userName.equals(other$userName)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(Object other) {
return other instanceof UserInfo;
}
public int hashCode() {
int PRIME = true;
int result = 1;
Object $userId = this.userId;
int result = result * 59 + ($userId == null ? 43 : $userId.hashCode());
Object $userName = this.userName;
result = result * 59 + ($userName == null ? 43 : $userName.hashCode());
return result;
}
}
这里引用一段话,对equals
和hashCode
方法的解释:
hashCode()
方法和equals()
方法的作用其实一样,在Java
里都是用来对比两个对象是否相等一致。重写的equals()
里一般比较的全面且复杂,这样效率就比较低,而利用hashCode()
进行对比,则只要生成一个hash
值进行比较就可以了,效率很高。但是,hashCode()
并不是完全可靠,有时候不同的对象它们生成的hashcode
也会一样(生成hash
值的公式可能存在的问题),所以hashCode()
只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:
(1)equal()
相等的两个对象它们的hashCode()
肯定相等,也就是用equals()
对比是绝对可靠的。
(2)hashCode()
相等的两个对象他们的equals()
不一定相等,也就是hashCode()
不是绝对可靠的。
因此,我们可以得出以下结论:
所有对于需要大量并且快速的对比的情况如果都用equals()
去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()
去对比,如果hashCode()
不一样,则表示这两个对象肯定不相等(也就不必再用equals()
去对比了),如果hashCode()
相同,这时再对比它们的equals()
,如果equals()
也相同,则表示这两个对象是真的相同了,这样既能大大提高效率也保证了对比的绝对正确性!
3.5. @Data
@Data
注解在类上,会为类的所有属性自动生成setter/getter、equals、canEqual、hashCode、toString
方法,还有一个构造方法,如为final
属性,则不会为该属性生成setter
方法。
(1)该注解是使用Lombok
的项目中使用最频繁的注解了。它结合了@ToString
,@EqualsAndHashCode
,@Getter
和 @Setter
的功能。实际上,在类上使用@Data
等同于使用了默认的@ToString
和@EqualsAndHashCode
注解,以及使用@Getter
和@Setter
注释每个字段。也会触发Lombok
的构造函数生成。将添加一个公共构造函数,会将@NonNull
或final
字段作为参数。
(2)虽然@Data
非常有用,但它不能提供与其他Lombok
注解相同的控制粒度。为了覆盖默认的方法生成行为,请使用其他Lombok
注解对类、字段或方法进行注释,并指定必要的参数值,以达到预期的效果。
(3)提供了一个参数选项staticConstructor
,可以用来生成静态工厂方法。将参数的值设置为所需的方法名,会将构造函数设置为私有的,并生成一个公开的的静态工厂方法,名称就是指定的方法名。并且会将final
字段和@NonNull
字段作为构造方法和生成的静态工厂方法的参数。
示例代码
@Data(staticConstructor = "getInstance")
public class UserInfo {
private final Integer id;
@NonNull
private String userId;
private String userName;
}
等效代码
public class UserInfo {
private final Integer id;
@NonNull
private String userId;
private String userName;
//生成的构造方法,将@NonNull和fina字段作为了参数
private UserInfo(Integer id, @NonNull String userId) {
if (userId == null) {
throw new NullPointerException("userId is marked non-null but is null");
} else {
this.id = id;
this.userId = userId;
}
}
//生成了一个公开的静态的工厂方法,方法名为注解的staticConstructor参数指定的方法名
public static UserInfo getInstance(Integer id, @NonNull String userId) {
return new UserInfo(id, userId);
}
//生成的setter、getter方法,final字段不会生成setter方法
public Integer getId() {
return this.id;
}
@NonNull
public String getUserId() {
return this.userId;
}
public String getUserName() {
return this.userName;
}
public void setUserId(@NonNull String userId) {
if (userId == null) {
throw new NullPointerException("userId is marked non-null but is null");
} else {
this.userId = userId;
}
}
public void setUserName(String userName) {
this.userName = userName;
}
//生成的equals方法
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof UserInfo)) {
return false;
} else {
UserInfo other = (UserInfo)o;
if (!other.canEqual(this)) {
return false;
} else {
label47: {
Object this$id = this.getId();
Object other$id = other.getId();
if (this$id == null) {
if (other$id == null) {
break label47;
}
} else if (this$id.equals(other$id)) {
break label47;
}
return false;
}
Object this$userId = this.getUserId();
Object other$userId = other.getUserId();
if (this$userId == null) {
if (other$userId != null) {
return false;
}
} else if (!this$userId.equals(other$userId)) {
return false;
}
Object this$userName = this.getUserName();
Object other$userName = other.getUserName();
if (this$userName == null) {
if (other$userName != null) {
return false;
}
} else if (!this$userName.equals(other$userName)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(Object other) {
return other instanceof UserInfo;
}
//生成的hashCode方法
public int hashCode() {
int PRIME = true;
int result = 1;
Object $id = this.getId();
int result = result * 59 + ($id == null ? 43 : $id.hashCode());
Object $userId = this.getUserId();
result = result * 59 + ($userId == null ? 43 : $userId.hashCode());
Object $userName = this.getUserName();
result = result * 59 + ($userName == null ? 43 : $userName.hashCode());
return result;
}
//生成的toString方法
public String toString() {
return "UserInfo(id=" + this.getId() + ", userId=" + this.getUserId() + ", userName=" + this.getUserName() + ")";
}
}
3.6. @Cleanup
@Cleanup
注释可用于确保已分配的资源被释放。当用@Cleanup
注释局部变量时,任何后续代码都封装在try/finally
块中,该块保证在当前范围的末尾处调用清理方法。默认情况下@Cleanup
生成的清理方法名为 close
,与输入和输出流一样。可以通过参数提供一个不同的方法名。注意方法名必须是该变量可以使用的方法。不然会报错。在使用@Cleanup
注释时还需要注意一点。如果清理方法抛出异常,它将抢占方法主体中抛出的任何异常(覆盖了实际异常)。这可能导致问题的实际原因被掩盖,在选择使用Lombok
的资源管理时应该考虑这种情况。此外,随着Java 7
中出现了自动资源管理,以后会很少需要使用该注解。
示例代码
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();
}
}
等效代码
public void testCleanUp() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write(new byte[]{89, 101, 115});
System.out.println(baos.toString());
} finally {//进行了资源关闭
if (Collections.singletonList(baos).get(0) != null) {
baos.close();
}
}
} catch (IOException var6) {
var6.printStackTrace();
}
}
3.7. @Synchronized
使用@Synchronized
注释实例方法将使该方法变为一个同步方法,生成一个名为$lock
的私有锁定字段,该方法将在执行之前会锁定该字段。类似地,以同样的方式注释静态方法将生成一个私有静态对象$LOCK
,以便静态方法以相同的方式使用。可以通过向注释的值参数提供字段名来指定不同的锁定对象。当提供字段名时,开发人员必须确保Lombok
不会生成该字段。
示例代码
private DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
@Synchronized
public String synchronizedFormat(Date date) {
return format.format(date);
}
private static DateFormat format1 = new SimpleDateFormat("yyyy-MM-dd");
@Synchronized
public static String synchronizedFormat1(Date date) {
return format1.format(date);
}
等效代码
private DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
private final Object $lock = new Object[0];
public String synchronizedFormat(Date date) {
synchronized(this.$lock) {
return this.format.format(date);
}
}
private static DateFormat format1 = new SimpleDateFormat("yyyy-MM-dd");
private static final Object $LOCK = new Object[0];
public static String synchronizedFormat1(Date date) {
synchronized($LOCK) {
return format1.format(date);
}
}
3.8. @SneakyThrows
如果一个类里面抛出一个Exception
,但是类上没进行抛出;或者父类抛出了一个异常,但是子类没有进行处理,这种情况都会产生编译期错误,会提示有一个“未处理的异常”错误。当在类上使用@SneakyThrows
注释时,错误将消失。默认情况下,@SneakyThrows
将允许抛出任何检查过的异常,而不需要在throw
子句中声明。通过向注释的值参数提供一个可抛出类(Class
)数组,可以将此限制为一组特定的异常。
示例代码
@SneakyThrows
public void testSneakyThrows() {
throw new IllegalAccessException();
}
等效代码
public void testSneakyThrows() {
try {
throw new IllegalAccessException();
} catch (Throwable var2) {
throw var2;
}
}
3.9. @NoArgsConstructor
注解在类上,生成无参数的构造方法。
(1)使用@NoArgsConstructor
会生成没有参数的构造函数,但如果存在final
修饰的成员字段,会编译出错,除非使用@NoArgsConstructor(force=true)
,那么所有的final
字段会被定义为0
,false
,null
等。
(2)使用无参数的构造函数构造出来的实例成员变量是null
,如果存在@NonNull
修饰的成员字段,那么就矛盾了。所以如果有@NonNull
修饰的成员的变量就不要用@NoArgsConstructor
修饰类。
示例代码
@NoArgsConstructor(force = true)
public class UserInfo {
private final Integer Id;
@NonNull
private String userId;
private String userName;
}
等效代码
public class UserInfo {
//final字段被强制初始化为null了
private final Integer Id = null;
@NonNull
private String userId;
private String userName;
//生成的无参构造方法
public UserInfo() {
}
}
3.10. @RequiredArgsConstructor
注解在类上,为类中需要特殊处理的字段生成构造方法,比如final
和被@NonNull
注解的字段。
示例代码
@RequiredArgsConstructor
public class UserInfo {
private final Integer Id;
@NonNull
private String userId;
private String userName;
}
等效代码
public class UserInfo {
private final Integer Id;
@NonNull
private String userId;
private String userName;
//final字段和@NonNull字段作为了构造函数的参数。
public UserInfo(final Integer Id, @NonNull final String userId) {
if (userId == null) {
throw new NullPointerException("userId is marked non-null but is null");
} else {
this.Id = Id;
this.userId = userId;
}
}
}
3.11. @AllArgsConstructor
注解在类上,生成包含类中所有字段的构造方法。
这三个都是处理构造函数的注解,都只能修饰类,都能通过staticName
创建静态工厂方法,使用access
控制访问级别。不同之处在于@AllArgsConstructor
会把所有的成员变量都纳入到构造函数中, @RequiredArgsConstructor
只会把final
和@NonNull
修饰的成员变量纳入,@NoArgsConstructor
所有的成员变量都不会纳入到构造函数。
示例代码
@AllArgsConstructor(staticName = "getInstance")
public class UserInfo {
private final Integer Id;
@NonNull
private String userId;
private String userName;
}
等效代码
public class UserInfo {
private final Integer Id;
@NonNull
private String userId;
private String userName;
private UserInfo(final Integer Id, @NonNull final String userId, final String userName) {
if (userId == null) {
throw new NullPointerException("userId is marked non-null but is null");
} else {
this.Id = Id;
this.userId = userId;
this.userName = userName;
}
}
public static UserInfo getInstance(final Integer Id, @NonNull final String userId, final String userName) {
return new UserInfo(Id, userId, userName);
}
}
3.12. @Slf4j
注解在类上,生成log
变量。
@Log,@Log4j,@Log4j2,@Slf4j,@XSlf4j,@CommonsLog,@JBossLog,@Flogger
这几个注解都是不同框架的日志注解。
@Log4j
是使用Log4j
框架时用的。
@Slf4j
是使用Logback
框架时用的。
示例代码
@Slf4j
public class UserInfo {
private String userId;
private String userName;
}
等效代码
public class UserInfo {
//生成的日志变量
private static final Logger log = LoggerFactory.getLogger(UserInfo.class);
private String userId;
private String userName;
public UserInfo() {
}
}
3.13. @Builder
面对复杂的数据结构,使用builder
模式可以抽离复杂的构造方式,能保证线程安全,比如:
UserInfo userInfo = UserInfo.builder().userId("22").userName("张三").build();
示例代码
@Builder
public class UserInfo {
private String userId;
private String userName;
}
等效代码
public class UserInfo {
private String userId;
private String userName;
UserInfo(final String userId, final String userName) {
this.userId = userId;
this.userName = userName;
}
//生成的静态工厂方法
public static UserInfo.UserInfoBuilder builder() {
return new UserInfo.UserInfoBuilder();
}
//生成的静态内部类
public static class UserInfoBuilder {
private String userId;
private String userName;
UserInfoBuilder() {
}
public UserInfo.UserInfoBuilder userId(final String userId) {
this.userId = userId;
return this;
}
public UserInfo.UserInfoBuilder userName(final String userName) {
this.userName = userName;
return this;
}
public UserInfo build() {
return new UserInfo(this.userId, this.userName);
}
public String toString() {
return "UserInfo.UserInfoBuilder(userId=" + this.userId + ", userName=" + this.userName + ")";
}
}
}
3.14. val 类型
使用val
可以作为局部变量的声明类型,类型将从初始化表达式中推断出来。例如: val x = 10.0;
将推断为double
,val y = new ArrayList
将推断为ArrayList
。局部变量会被设置为 final
。
4. 实现原理
Lombok
这款插件是依靠可插件化的Java
自定义注解处理API(JSR 269: Pluggable Annotation Processing API)
来实现在javac
编译阶段利用Annotation Processor
(注解处理) 对自定义的注解进行预处理后生成真正在JVM
上面执行的Class
文件。大致执行原理图如下:
从上面的这个原理图上可以看出Annotation Processing
是编译器在解析Java
源代码和生成Class
文件之间的一个步骤。其中Lombok
插件具体的执行流程如下:
从上面的Lombok
执行的流程图中可以看出,在javac
解析成AST
抽象语法树之后,Lombok
根据自己编写的注解处理器,动态地修改AST
,增加新的节点(即Lombok
自定义注解所需要生成的代码),最终通过分析生成JVM
可执行的字节码Class
文件。使用Annotation Processing
自定义注解是在编译阶段进行修改,而JDK
的反射技术是在运行时动态修改,两者相比,反射虽然更加灵活一些,但是带来的性能损耗更加大。
5. 写在最后
(1)优点
- 能通过注解的形式自动生成构造器、
getter/setter
、equals
、hashcode
、toString
等方法,提高了一定的开发效率。 - 让代码变得简洁,不用过多的去关注相应的方法。
- 属性做修改时,也简化了维护为这些属性所生成的
getter/setter
方法等。
(2)缺点
- 不支持多种参数构造器的重载。
- 降低了源代码的可读性和完整性,降低了阅读源代码的舒适度。
(3)总结
Lombok
虽然有很多优点,但Lombok
更类似于一种IDE
插件,项目也需要依赖相应的jar
包。Lombok
依赖jar
包是因为编译时要用它的注解,为什么说它又类似插件?因为在使用时,eclipse
或IntelliJ IDEA
都需要安装相应的插件,在编译器编译时通过操作AST
(抽象语法树)改变字节码生成,变向的就是说它在改变 java
语法。它不像spring
的依赖注入或者mybatis
的ORM
一样是运行时的特性,而是编译时的特性。
这里引用下一些人对Lombok
的看法:
这是一种低级趣味的插件,不建议使用。
Java
发展到今天,各种插件层出不穷,如何甄别各种插件的优劣?能从架构上优化你的设计的,能提高应用程序性能的,实现高度封装可扩展的..., 像Lombok
这种,已经不仅仅是插件了,它改变了你如何编写源码,事实上,少去的代码你写上去又如何? 如果Java
家族到处充斥着这样的东西,那只不过是一坨披着金属颜色的屎,迟早会被其它语言取代。
如果一个项目有非常多类似Lombok
这样的插件,真的会极大的降低阅读源代码的舒适度。Lombok
有它得天独厚的优点,也有它避之不及的缺点,在实战中需要灵活运用。其实我觉得在项目中适度使用还是可以的,用几个最简单基本的注解,能大大简化一些模版型代码的开发。比如我常用的是@Data
、@Slf4j
、@Setter
、@Getter
、@EqualsAndHashCode
。