简介 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
,然后提供出public
的getter
和setter
方法暴露给外部操作。于是代码中就出现了一大片一大片的使用IDE
生成的getter
和setter
方法。
Lombok
要解决的问题就是去消除这些模版式的getter
和setter
方法。当然还有一些其它的模版式的代码也可以使用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
这两个注解可以单独写在某些字段上,也可以写在类上。编译后会在字节码文件中加入相应字段的getter
和setter
方法。
单独写在类上的使用示例:
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; } }
如果不想细粒度的控制到每个字段,可以直接在类上添加@Getter
和Setter
注解,会为所有的字段生成getter
和setter
方法。
@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
注解,它等价于这四个注解的组合,会同时生成getter
、setter
、toString
、equals
、canEqual
和hashCode
这六个方法。同时还提供了一个属性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
、@AllArgsConstructor
和RequiredArgsConstructor
@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
字段来生成构造器,可与Spring
的DI
依赖注入搭配使用。
@Slf4j
作为研发我们避免不了日志打印,但我们却要在每一个需要打印日志的类中定义这样一个静态日志常量:private static final Logger log = LoggerFactory.getLogger(XXXService.class);
,其中XXXService
的名称还都不相同,这太影响像我这样的CV
工程师的效率了。Lombok
提供了@Slf4j
注解来解决这个问题。
使用前提:引入日志框架slf4j
的Maven
依赖
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
一定是在编译阶段对注解进行解析然后注入对应的字节码。
而编译阶段对注解的解析有两种机制:
Annotation Processing Tool
:简称apt
apt
随着JDK5
引入注解时产生,在JDK7
中被标记为过期,不推荐使用,JDK8
已彻底将其删除。
从JDK6
开始,可以使用Pluggable Annotation Processing API
来替换它,apt
被替换的主要原因是:
api
都在com.sun.mirror
非标准包下;没有集成到javac
中,需要额外运行。 Pluggable Annotation Processing API
这其实是JSR 269 规范,在JDK6
中被引入作为apt
的替代方案,它解决了apt
的那两个问题,在javac
执行过程中会调用实现了该规范的程序,这样我们就可以对javac
编译器做一些增强。
JSR
是Java Specification Requests
的缩写,意思是Java
规范提案。
Lombok
就是一个实现了JSR 269
规范的程序,在javac
编译过程中会调用Lombok
,从而实现代码的自动加入。
整个编译的过程大致如下:
javac
对源代码进行分析,生成一颗抽象语法树(AST
);调用JSR 269
的实现:Lombok
; Lombok
对抽象语法树进行解析,找到Lombok
定义的注解所在的语法树,然后进行修改,添加getter
和setter
等方法定义的树节点;使用修改后的语法树生成字节码class
文件。 优缺点 优点:
代码简洁优雅,通过注解的形式自动生成一些模版式的代码。 当类字段发生修改时,不用去修改对应的getter
和setter
等方法。 缺点:
IDE
中需要安装插件,否则项目报错。不支持任意个参数的构造器重载。 总结 关于Lombok
,网上一部分人支持,一部分人反对。反对的理由还很多:强依赖插件;组内有一人用则所有人用;操作语法树等于改变Java
语法等等。暂且认为这些都有道理,但有句话怎么说来着:拥抱变化!Java
语言走过这么多年也经过了很多变化,每个大版本也或多或少会出现一些新语法。Lombok
的出现也预示着Java
需要发生变化,而不是局限于当下的安稳。所以,我认为Lombok
是值得去拥抱的!
从另一个角度说,那些反对Lombok
的人可能是没写过业务系统,业务代码中经常出现一个类有二三十个字段,甚至更多,这时如果手动去维护各个类的getter
和setter
方法,效率可想而知,而且也没什么技术含量。打工人讲究的是如何高效打工!