Lombok详解

简介

Lombok能通过注解的方式,在编译时自动为属性生成构造函数、getter/setter、equals、hashcode、toString等方法。奇妙之处在于源码中没有getter和setter方法,但是在编译生成的字节码文件中有getter和setter方法。这样就省去了手动重建这些代码的麻烦,使代码看起来更简洁明了。

优缺点

优点

1)简化代码,提高效率

Lombok能以简单的注解形式来简化java代码,提高开发人员的开发效率。例如开发中经常需要写的javabean,都需要花时间去添加相应的getter/setter,也许还要去写构造器、equals等方法,而且需要维护,当属性多时会出现大量的getter/setter方法,这些显得很冗长也没有太多技术含量,一旦修改属性,就容易出现忘记修改对应方法的失误。

Lombok能通过注解的方式,在编译时自动为属性生成构造函数、getter/setter、equals、hashcode、toString等方法。奇妙之处在于源码中没有getter和setter方法,但是在编译生成的字节码文件中有getter和setter方法。这样就省去了手动重建这些代码的麻烦,使代码看起来更简洁明了。

缺点

1) 高侵入性,强迫队友

Lombok插件的使用,要求开发者一定要在IDE中安装对应的插件。不仅自己要安装,任何和你协同开发的人都要安装。如果有谁未安装插件的话,使用IDE打开一个基于Lombok的项目的话会提示找不到方法等错误,导致项目编译失败。更重要的是,如果我们定义的一个jar包中使用了Lombok,那么就要求所有依赖这个jar包的所有应用都必须安装插件,这种侵入性是很高的。

2)代码可调试性降低

Lombok确实可以帮忙减少很多代码,因为Lombok会帮忙自动生成很多代码。但是,这些代码是要在编译阶段才会生成的,所以在开发的过程中,其实很多代码其实是缺失的。这就给代码调试带来一定的问题,我们想要知道某个类中的某个属性的getter方法都被哪些类引用的话,就没那么简单了。

3) 影响版本升级

Lombok对于代码有很强的侵入性,就可能带来一个比较大的问题,那就是会影响我们对JDK的升级。按照如今JDK的升级频率,每半年都会推出一个新的版本,但是Lombok作为一个第三方工具,并且是由开源团队维护的,那么他的迭代速度是无法保证的。所以,如果我们需要升级到某个新版本的JDK的时候,若其中的特性在Lombok中不支持的话就会受到影响。还有一个可能带来的问题,就是Lombok自身的升级也会受到限制。因为一个应用可能依赖了多个jar包,而每个jar包可能又要依赖不同版本的Lombok,这就导致在应用中需要做版本仲裁,而我们知道,jar包版本仲裁是没那么容易的,而且发生问题的概率也很高。

4)注解使用有风险

在使用Lombok过程中,如果对于各种注解的底层原理不理解的话,很容易产生意想不到的结果。举一个简单的例子:我们知道,当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,会默认是@EqualsAndHashCode(callSuper=false),这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放,这就可能得到意想不到的结果。

5)可能会破坏封装性

使用过程中如果不小心,在一定程度上就会破坏代码的封装性。举个简单的例子,我们定义一个购物车类,并且使用了@Data注解:

1
2
3
4
5
6
7
8
9
@Data
public class ShoppingCart {
//商品数目
private int itemsCount;
//总价格
private double totalPrice;
//商品明细
private List items = new ArrayList<>();
}

我们知道,购物车中商品数目、商品明细以及总价格三者之前其实是有关联关系的,如果需要修改的话是要一起修改的。但是,我们使用了Lombok的@Data注解,对于itemsCount 和 totalPrice这两个属性,虽然我们将它们定义成 private 类型,但是提供了 public 的 getter、setter 方法。

外部可以通过 setter 方法随意地修改这两个属性的值,我们可以随意调用 setter 方法,来重新设置 itemsCount、totalPrice 属性的值,这也会导致其跟 items 属性的值不一致。

而面向对象封装的定义是:通过访问权限控制,隐藏内部数据,外部仅能通过类提供的有限的接口访问、修改内部数据。所以,暴露不应该暴露的 setter 方法,明显违反了面向对象的封装特性。

好的做法应该是不提供getter/setter,而是只提供一个public的addItem方法,同时取修改itemsCount、totalPrice以及items三个属性。

因此,在此种情况下,就不适合使用Lombok,或者只用@Getter不用@Setter,而别直接使用@Data,在使用过程中,需要多多小心。

安装

1. 安装插件

Lombok插件支持Eclipse、IntelliJ IDEA、MyEclispe等IDE,在这里我们使用IntelliJ IDEA来演示安装:

1)打开File > Settings > Plugins >Marketplace,搜索Lombok,如图,点击install,弹窗Accept,然后安装好后Restart IDEA。

img

2)设置Enable annotation processing

打开File > Settings > Build, Execution, Deployment > Compiler > Annotation Processors,勾选Enable annotation processing,然后Apply和OK。

img

2. 导入jar包

常用jar包引入方法有两种,如下:

方法1:普通项目可以直接官网下载jar包,和使用普通jar包一样,导入项目即可。jar下载地址:点击去下载

方法2:Maven项目可以在pom.xml中配置依赖坐标即可,依赖坐标如下:

1
2
3
4
5
6
7
8
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>

常用注解

@NonNull

@NonNull该注解用在属性或构造器上,Lombok会生成一个非空的声明,可用于校验参数,能帮助避免空指针。

示例代码:

1
2
3
4
//成员方法参数加上@NonNull注解,构造方法也一样,在此不做演示
public String getName(@NonNull Person p){
return p.getName();
}

实际效果相当于:

1
2
3
4
5
6
public String getName(Person p){
if(p==null){
throw new NullPointerException("person");
}
return p.getName();
}

@Getter和@Setter

使用此注解相当于为成员变量生成对应的get和set方法,方法默认修饰符为public,同时还可以使用AccessLevel为生成的方法指定访问修饰符。这两个注解还可以直接用在类上,可以为此类里的所有非静态成员变量生成对应的get和set方法。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
public class Student{
@Getter
@Setter
private String name;

@Setter(AccessLevel.PROTECTED)
private int age;

@Getter(AccessLevel.PUBLIC)
private String language;
}

@ToString

使用此注解会自动重写对应的toStirng方法,默认情况下,会输出类名、所有属性(会按照属性定义顺序),用逗号来分割,通过callSuper参数来指定是否引用父类,includeFieldNames参数设为true,就能明确的输出toString()属性。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ToString(exclude="id")
public class ToStringExample {
private static final int STATIC_VAR = 10;
private String name;
private Shape shape = new Square(5, 10);
private String[] tags;
private int id;

public String getName() {
return this.getName();
}

@ToString(callSuper=true, includeFieldNames=true)
public static class Square extends Shape {
private final int width, height;

public Square(int width, int height) {
this.width = width;
this.height = height;
}
}
}

实际效果相当于:

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
public class ToStringExample {
private static final int STATIC_VAR = 10;
private String name;
private Shape shape = new Square(5, 10);
private String[] tags;
private int id;

public String getName() {
return this.getName();
}

public static class Square extends Shape {
private final int width, height;

public Square(int width, int height) {
this.width = width;
this.height = height;
}

@Override
public String toString() {
return "Square(super=" + super.toString() + ", width=" + this.width + ", height=" + this.height + ")";
}
}

@Override
public String toString() {
return "ToStringExample(" + this.getName() + ", " + this.shape + ", " + Arrays.deepToString(this.tags) + ")";
}
}

@NoArgsConstructor、@RequiredArgsConstructor和@AllArgsConstructor

这三个注解都是用在类上的,第一个和第三个都很好理解,就是为该类产生无参的构造方法和包含所有参数的构造方法,第二个注解则使用类中所有带有@NonNull注解的或者带有final修饰的成员变量生成对应的构造方法,当然,成员变量都是非静态的,另外,如果类中含有final修饰的成员变量,是无法使用@NoArgsConstructor注解的。

三个注解都可以指定生成的构造方法的访问权限,同时,第二个注解还可以用@RequiredArgsConstructor(staticName=”methodName”)的形式生成一个指定名称的静态方法,返回一个调用相应的构造方法产生的对象。

示例代码:

1
2
3
4
5
6
7
8
9
10
@RequiredArgsConstructor(staticName = "myShape")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
public class Shape {
private int x;
@NonNull
private double y;
@NonNull
private String name;
}

@Data

@Data注解在类上,会为类的所有属性自动生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。@Value注解和@Data类似,区别在于它会把所有成员变量默认定义为private final修饰,并且不会生成set方法。

官方实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data public class DataExample {
private final String name;
@Setter(AccessLevel.PACKAGE) private int age;
private double score;
private String[] tags;

@ToString(includeFieldNames=true)
@Data(staticConstructor="of")
public static class Exercise<T> {
private final String name;
private final T value;
}
}

@SneakyThrows

这个注解用在方法上,可以将方法中的代码用try-catch语句包裹起来,捕获异常并在catch中用Lombok.sneakyThrow(e)把异常抛出,可以使用@SneakyThrows(Exception.class)的形式指定抛出哪种异常,很简单的注解,直接看个例子:

1
2
3
4
5
6
7
8
9
10
11
public class SneakyThrows implements Runnable {
@SneakyThrows(UnsupportedEncodingException.class)
public String utf8ToString(byte[] bytes) {
return new String(bytes, "UTF-8");
}

@SneakyThrows
public void run() {
throw new Throwable();
}
}

实际效果相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SneakyThrows implements Runnable {
public String utf8ToString(byte[] bytes) {
try{
return new String(bytes, "UTF-8");
}catch(UnsupportedEncodingException uee){
throw Lombok.sneakyThrow(uee);
}
}

public void run() {
try{
throw new Throwable();
}catch(Throwable t){
throw Lombok.sneakyThrow(t);
}
}
}

@Synchronized

这个注解用在类方法或者实例方法上,效果和synchronized关键字相同,区别在于锁对象不同,对于类方法和实例方法,synchronized关键字的锁对象分别是类的class对象和this对象,而@Synchronized得锁对象分别是私有静态final对象LOCK和私有final对象lock,当然,也可以自己指定锁对象,例子也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Synchronized {
private final Object readLock = new Object();

@Synchronized
public static void hello() {
System.out.println("world");
}

@Synchronized
public int answerToLife() {
return 42;
}

@Synchronized("readLock")
public void foo() {
System.out.println("bar");
}
}

实际效果相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Synchronized {
private static final Object $LOCK = new Object[0];
private final Object $lock = new Object[0];
private final Object readLock = new Object();

public static void hello() {
synchronized($LOCK) {
System.out.println("world");
}
}

public int answerToLife() {
synchronized($lock) {
return 42;
}
}

public void foo() {
synchronized(readLock) {
System.out.println("bar");
}
}
}

@Log

这个注解用在类上,可以省去从日志工厂生成日志对象这一步,直接进行日志记录,具体注解根据日志工具的不同而不同,同时,可以在注解中使用topic来指定生成log对象时的类名。不同的日志注解总结如下(上面是注解,下面是实际作用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@CommonsLog
private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);
@JBossLog
private static final org.jboss.logging.Logger log = org.jboss.logging.Logger.getLogger(LogExample.class);
@Log
private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());
@Log4j
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogExample.class);
@Log4j2
private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);
@Slf4j
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
@XSlf4j
private static final org.slf4j.ext.XLogger log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);

示例代码:

在实际的使用中完全做到了无缝衔接,与注释调到的功能完全等价,简洁而优雅

1
2
3
4
5
6
7
8
9
10
11
@Controller
@Slf4j
public class SampleController {
//private static Logger log= LogManager.getLogger(SampleController.class);

@RequestMapping("/")
public String home(ModelMap model) {
log.info("aa");
return "main";
}
}

@Builder

建造者的模式一般是这样(减少了成员变量,只有一个成员变量一般不用此模式)

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
public class User {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public User(Builder builder) {
this.name= builder.name;
}

public static Builder builder(){
return new Builder();
}

public static class Builder{
private String name;

public Builder name(String name) {
this.name= name;
return this;
}

public User build(){
return new User(this);
}
}
}

使用了@Builder之后,只需要这样

1
2
3
4
5
6
@Builder
@Getter
@Setter
public class User {
private String name;
}

二者调用方法相同

1
2
3
4
5
@Test
public void main() throws Exception{
User user=User.builder().name("Asens").build();
System.out.println(user.getName());
}

参考文章

Lombok入门使用教程及其优缺点详解

Lombok指南

作者

Hyeee

发布于

2023-09-22

更新于

2023-09-22

许可协议