SpringBoot 干货教程 | 第三章:lombok 介绍及安装使用教程

原创 springboot

Lombok 简介

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. Early access to future java features such as val, and much more.

这是 Lombok 官网的一段简介,大意为:

Project Lombok 是一个 java 库,它可以作为插件自动加载到你的编辑器和构建工具中,让你的 java 开发添加丰富的特性。

永远都不需要再写 getterequals 方法。 可以早早地使用未来 Java 可能添加的功能,如 val 等等。

其实一般我们说起 Lombok,是分为两部分的,一部分是 Lombok 工具本身,一部分是作为开发工具的辅助插件,提供语法高亮、跳转等便捷功能,简单的说,就是让开发工具可以认识 Lombok。

Lombok 官方地址:https://projectlombok.org

Lombok 插件安装

工欲善其事,必先利其器。在上手开发之前,我们先讲 Lombok 插件的安装。

IntelliJ IDEA 安装 Lombok

Jetbrains IntelliJ IDEA 编辑器是官方支持 lombok 的,插件地址:https://plugins.jetbrains.com/plugin/6317

安装方式:

  • 点击菜单 File > Settings > Plugins
  • 点击按钮 Browse repositories...
  • 搜索 Lombok Plugin
  • 选中搜索结果,点击按钮 Install 来安装插件
  • 安装完成后点击按钮 Restart IntelliJ IDEA 重启 IDEA 工具

如果在线安装网速不是特别好的,可以访问上面的插件地址,下载和 IDEA 版本匹配的插件压缩包,然后从本地安装。点击菜单 File > Settings > Plugins,点击按钮 Install plugin from disk... 选择插件压缩包来完成安装。

Eclipse 安装 Lombok

Eclipse 作为 Java 开发使用最广泛的 IDE,也是支持 Lombok 插件的,包括各种基于 Eclipse 的衍生版本开发工具都支持,包括:

整个 Lombok 只有一个 lombok.jar 包,可到这里下载:https://projectlombok.org/download

双击 lombok.jar(可从此官网下载,或从 maven 仓库下载,是一样的),如果操作系统没有关联 Jar 可执行命令,可以从命令行使用 java -jar lombok.jar 来启动界面。这将启动 eclipse 插件安装程序,它将找到 eclipse(以及上面列出的 eclipse 变体版本)的安装路径,并提供将 lombok 安装到这些 eclipse 安装中。 同样的,这个工具也可以卸载 lombok 插件:

你可以在 eclipse 关于对话框中检查你的 eclipse 是否安装启用了 lombok 插件。 lombok 版本将列在版权文本的末尾:

Netbeans 安装 Lombok

作为被我偏爱的 Netbeans 也是可以支持 Lombok 插件的。

  • 把 lombok.jar 添加到工程的 libraries 依赖中
  • 在工程属性的 Build – Compiling 里,勾选 Enable Annotation Processing in Editor

这样就完成了 NetBeans Lombok 插件的安装。

Microsoft Visual Studio Code 安装 Lombok

Microsoft Visual Studio Code 越来越强大了,相信还是有 Java 开发会使用或者尝鲜的。

VS Code 中 Lombok 插件叫 vscode-lombok

  • 使用快捷键 Ctrl + Shift + X 打开扩展管理
  • 输入 lombok 搜索插件,然后安装
  • 当询问你是否 Reload VS Code 的时候点击 Reload

Lombok 工程依赖

Maven 与 Lombok

在 pom.xml 文件中添加 lombok,并且作用域范围为 provided

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.2</version>
    <scope>provided</scope>
</dependency>

Gradle 与 Lombok

Gradle 依赖 Lombok 稍微麻烦一点,在 build.gradle 文件中添加:

lombok {
    version = '1.18.2'
    sha256 = ""
}
repositories {
    mavenCentral()
}

plugins {
    id 'net.ltgt.apt' version '0.10'
}

dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.2'
    apt "org.projectlombok:lombok:1.18.2"
}

其它情况

对于一般工程,可以直接在 https://projectlombok.org/download 下载 lombok.jar,然后放到工程的 lib 依赖目录。

Lombok 特性讲解

很多脚本语言和 c++ 最新特性都支持常量和变量的类型推导声明关键字,Java 目前到 8 为止还没有支持,Lombok 为我们带来了这种体验的可能性。

val

可以使用 val 作为局部变量声明的类型,而不是实际类型。 这样,将从初始化表达式推断出类型。 局部变量也将成为 final 变量。 这个特性仅适用于局部变量和 foreach 循环,而不适用于类字段(fields)。 val 修饰的表达式必需被初始化。

val 实际上是一种“类型”,在 lombok 包中作为一个真正的类存在。 你必须导入它才能使 val 工作(或使用 lombok.val 作为类型)。 在局部变量声明中存在这种类型会触发添加 final 关键字以及复制初始化表达式的类型,该类型会覆盖 "伪" val 类型。

注意,目前 NetBeans 不支持这个特性。

使用示例:

import java.util.ArrayList;
import java.util.HashMap;
import lombok.val;

public class ValExample {
  public String example() {
    val example = new ArrayList<String>();
    example.add("Hello, World!");
    val foo = example.get(0);
    return foo.toLowerCase();
  }

  public void example2() {
    val map = new HashMap<Integer, String>();
    map.put(0, "zero");
    map.put(5, "five");
    for (val entry : map.entrySet()) {
      System.out.printf("%d: %s\n", entry.getKey(), entry.getValue());
    }
  }
}

这等价于下面的 Java 代码。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class ValExample {
  public String example() {
    final ArrayList<String> example = new ArrayList<String>();
    example.add("Hello, World!");
    final String foo = example.get(0);
    return foo.toLowerCase();
  }

  public void example2() {
    final HashMap<Integer, String> map = new HashMap<Integer, String>();
    map.put(0, "zero");
    map.put(5, "five");
    for (final Map.Entry<Integer, String> entry : map.entrySet()) {
      System.out.printf("%d: %s\n", entry.getKey(), entry.getValue());
    }
  }
}

var

var 的工作原理与 val 完全相同,除了局部变量不会标记为 final。就是说 var 声明的局部变量是可变的,而 val 声明的局部变量是不可变的,是 final 的。

var 类型仍然完全由强制初始化的表达式导出。虽然现在任何进一步的赋值是合法的(因为变量不再是 final 的),但不会再次查看以确定适当的类型。

例如下面的代码不会正常工作。

var x = "Hello";
x = Color.RED; // Color.RED 返回为 java.awt.Color 类型

首先 x 变量的类型会被分配为 java.lang.String,因此 x = Color.RED 赋值将会失败。如果 x 被指向 java.lang.Object,那么编译就会通过,但是很可惜,Lombok var 不是这么定义的。

var 类型是根据 JEP 286 引入的,目前仍然处于实验阶段,在 Lombok 2.0.0 可能被正式接纳为稳定特性。

@NonNull

你可以在方法或构造函数的参数上使用 @NonNull 让 lombok 为你生成 null-check 语句。

如果 Lombok 为你生成整个方法或构造函数,例如使用 @Data,Lombok 始终将任何字段上名为 @NonNull 的注解视为生成 null-check 的信号。

null-check 代码类似下面的样子:

if (param == null) {
    throw new NullPointerException("param is marked @NonNull but is null");
}

null-check 代码一般会插入到方法体的最前面。在构造方法中,如果显式调用了 this()super(),那么 null-check 代码就会插入到 this()super() 后面。

如果你在方法前手动检查了 null,那么 null-check 代码就不会生成。

@NonNull 使用示例:

import lombok.NonNull;

public class NonNullExample extends Something {
  private String name;

  public NonNullExample(@NonNull Person person) {
    super("Hello");
    this.name = person.getName();
  }
}

等价于:

import lombok.NonNull;

public class NonNullExample extends Something {
  private String name;

  public NonNullExample(@NonNull Person person) {
    super("Hello");
    if (person == null) {
      throw new NullPointerException("person is marked @NonNull but is null");
    }
    this.name = person.getName();
  }
}

@Cleanup

你可以使用 @Cleanup 确保在代码执行路径退出当前作用域之前自动清理给定资源。可以通过使用 @Cleanup 来注解任何局部变量声明:

@Cleanup InputStream in = new FileInputStream("some/file");

这将在这段代码所在的作用域最后调用 in.close() 方法,和 try/finally 是一样的。

如果要清理的对象类型没有 close() 方法,而是其他一些无参数方法,则可以指定此方法的名称,如下所示:

@Cleanup("dispose") org.eclipse.swt.widgets.CoolBar bar = new CoolBar(parent, 0);

默认情况下,清除方法假定为 close()。 不能通过 @Cleanup 调用带有1个或多个参数的清理方法,例如 close(true) 这样的清理方法是不行的。

@Cleanup 使用示例:

import lombok.Cleanup;
import java.io.*;

public class CleanupExample {
  public static void main(String[] args) throws IOException {
    @Cleanup InputStream in = new FileInputStream(args[0]);
    @Cleanup OutputStream out = new FileOutputStream(args[1]);
    byte[] b = new byte[10000];
    while (true) {
      int r = in.read(b);
      if (r == -1) break;
      out.write(b, 0, r);
    }
  }
}

等价于普通 Java 书写方式:

import java.io.*;

public class CleanupExample {
  public static void main(String[] args) throws IOException {
    InputStream in = new FileInputStream(args[0]);
    try {
      OutputStream out = new FileOutputStream(args[1]);
      try {
        byte[] b = new byte[10000];
        while (true) {
          int r = in.read(b);
          if (r == -1) break;
          out.write(b, 0, r);
        }
      } finally {
        if (out != null) {
          out.close();
        }
      }
    } finally {
      if (in != null) {
        in.close();
      }
    }
  }
}

@Getter/@Setter

这是 Lombok 最常用的两个注解,可以在任何类字段上使用 @Getter@Setter 来让 Lombok 自动生成 setter/getter 方法。

如果字段名为 foo,默认的 getter 只简单的返回字段,则方法命名为 getFoo(如果字段的类型为 boolean,则命名为 isFoo)。 如果字段名为 foo,则默认 setter 名为 setFoo,返回 void,并且采用与该字段相同类型的 1 个参数,它只是简单地将字段设置为此参数值。

生成的 setter/getter 方法默认是 public 的,除非你像下面的示例一样,明确指定一个 AccessLevel,合法的访问控制级别有 PUBLICPROTECTEDPACKAGEPRIVATE

你也可以在一个类声明上加一个 @Getter@Setter 注解,这种情况下,和在这个类的所有非 static 的字段上加注解是等价的。

可以使用特殊的 AccessLevel.NONE 访问级别手动禁用任何字段的 getter/setter 生成。 这使你可以覆盖类上的 @Getter@Setter@Data 注解的默认行为。

其它的细节可以参考 API 文档,下面是一个注解使用示例:

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

public class GetterSetterExample {
  /**
   * Age of the person. Water is wet.
   *
   * @param age New value for this person's age. Sky is blue.
   * @return The current value of this person's age. Circles are round.
   */
  @Getter @Setter private int age = 10;

  /**
   * Name of the person.
   * -- SETTER --
   * Changes the name of this person.
   *
   * @param name The new value.
   */
  @Setter(AccessLevel.PROTECTED) private String name;

  @Override public String toString() {
    return String.format("%s (age: %d)", name, age);
  }
}

等价于:

 public class GetterSetterExample {
  /**
   * Age of the person. Water is wet.
   */
  private int age = 10;

  /**
   * Name of the person.
   */
  private String name;

  @Override public String toString() {
    return String.format("%s (age: %d)", name, age);
  }

  /**
   * Age of the person. Water is wet.
   *
   * @return The current value of this person's age. Circles are round.
   */
  public int getAge() {
    return age;
  }

  /**
   * Age of the person. Water is wet.
   *
   * @param age New value for this person's age. Sky is blue.
   */
  public void setAge(int age) {
    this.age = age;
  }

  /**
   * Changes the name of this person.
   *
   * @param name The new value.
   */
  protected void setName(String name) {
    this.name = name;
  }
}

@ToString

任何类定义都可以用 @ToString 注解,让 lombok 生成 toString() 方法的实现。 默认情况下,它会按顺序打印类名以及每个字段,并以逗号分隔。

通过将 includeFieldNames 参数设置为 true,可以为 toString() 方法的输出添加字段名称,默认情况下只输出字段值。

默认情况下,所有的非 static 字段都会被打印出来。如果你想在 toString() 方法中忽略某个字段,可以在那个字段上加上 @ToString.Exclude 注解。或者,你可以明确的手动指定只包含那些字段,在类声明上加 @ToString(onlyExplicitlyIncluded = true) 注解,然后在需要包含在 toString() 方法中的字段上加 @ToString.Include 注解。

设置 @ToString 注解的 callSuper 属性为 true,可以在子类的 toString 方法中包含父类的属性。

在字段上使用 @ToString.Include(name = "some other name") 可以为输出的字段指定别名,使用 @ToString.Include(rank = -1) 为输出的字段指定打印顺序。

@ToString 使用示例:

import lombok.ToString;

@ToString
public class ToStringExample {
  private static final int STATIC_VAR = 10;
  private String name;
  private Shape shape = new Square(5, 10);
  private String[] tags;
  @ToString.Exclude private int id;

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

  @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;
    }
  }
}

等价于:

import java.util.Arrays;

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) + ")";
  }
}

@EqualsAndHashCode

生成 hashCode()equals() 方法。在注解属性设置上和 @ToString 比较像,具体参考文档。

使用示例:

import lombok.EqualsAndHashCode;

@EqualsAndHashCode
public class EqualsAndHashCodeExample {
  private transient int transientVar = 10;
  private String name;
  private double score;
  @EqualsAndHashCode.Exclude private Shape shape = new Square(5, 10);
  private String[] tags;
  @EqualsAndHashCode.Exclude private int id;

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

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

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

等价于:

import java.util.Arrays;

public class EqualsAndHashCodeExample {
  private transient int transientVar = 10;
  private String name;
  private double score;
  private Shape shape = new Square(5, 10);
  private String[] tags;
  private int id;

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

  @Override public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof EqualsAndHashCodeExample)) return false;
    EqualsAndHashCodeExample other = (EqualsAndHashCodeExample) o;
    if (!other.canEqual((Object)this)) return false;
    if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
    if (Double.compare(this.score, other.score) != 0) return false;
    if (!Arrays.deepEquals(this.tags, other.tags)) return false;
    return true;
  }

  @Override public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    final long temp1 = Double.doubleToLongBits(this.score);
    result = (result*PRIME) + (this.name == null ? 43 : this.name.hashCode());
    result = (result*PRIME) + (int)(temp1 ^ (temp1 >>> 32));
    result = (result*PRIME) + Arrays.deepHashCode(this.tags);
    return result;
  }

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

  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 boolean equals(Object o) {
      if (o == this) return true;
      if (!(o instanceof Square)) return false;
      Square other = (Square) o;
      if (!other.canEqual((Object)this)) return false;
      if (!super.equals(o)) return false;
      if (this.width != other.width) return false;
      if (this.height != other.height) return false;
      return true;
    }

    @Override public int hashCode() {
      final int PRIME = 59;
      int result = 1;
      result = (result*PRIME) + super.hashCode();
      result = (result*PRIME) + this.width;
      result = (result*PRIME) + this.height;
      return result;
    }

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

@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

这三个是构造函数注解。

  • @NoArgsConstructor:生成无参构造函数
  • @RequiredArgsConstructor:会生成一个包含常量(final),和标识了 @NotNull 的变量的构造方法。
  • @AllArgsConstructor:会生成一个包含所有变量,同时如果变量使用了 @NotNull 注解,会进行是否为空的校验。全部参数的构造函数的自动生成,该注解的作用域也是只有在实体类上,参数的顺序与属性定义的顺序一致。

它们都有三个参数可以设置:

  • String staticName() default "" :如果设置了它,将原来的构造方法的访问修饰符将会变成私有的,而外添加一个静态构造方法,参数相同,名字是设置的字符串的名字,访问修饰符为公有的。
  • AnyAnnotation[] onConstructor() default {} :在构造方法上添加注解。使用方法如 @RequiredArgsConstructor(onConstructor=@__({@SomeAnnotations}))}
  • AccessLevel access() default lombok.AccessLevel.PUBLIC : 构造函数访问修饰符

@NoArgsConstructor 无参构造函数中还有个属性:boolean force() default false; 设置为 true 的时候,初始化所有的参数为默认值,否则编译错误。

使用示例:

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.NonNull;

@RequiredArgsConstructor(staticName = "of")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class ConstructorExample<T> {
  private int x, y;
  @NonNull private T description;

  @NoArgsConstructor
  public static class NoArgsExample {
    @NonNull private String field;
  }
}

等价于

public class ConstructorExample<T> {
  private int x, y;
  @NonNull private T description;

  private ConstructorExample(T description) {
    if (description == null) throw new NullPointerException("description");
    this.description = description;
  }

  public static <T> ConstructorExample<T> of(T description) {
    return new ConstructorExample<T>(description);
  }

  @java.beans.ConstructorProperties({"x", "y", "description"})
  protected ConstructorExample(int x, int y, T description) {
    if (description == null) throw new NullPointerException("description");
    this.x = x;
    this.y = y;
    this.description = description;
  }

  public static class NoArgsExample {
    @NonNull private String field;

    public NoArgsExample() {
    }
  }
}

@Data

这个相当于是一个快捷方式,在类声明上使用 @Data 注解相当于同时使用 @ToString@EqualsAndHashCode@Getter@RequiredArgsConstructor,然后在非 final 字段上使用 @Setter

注意的是,同时使用 @Data@AllArgsConstructor 后,默认的无参构造函数失效,如果需要它,要重新设置 @NoArgsConstructor

使用示例:

import lombok.AccessLevel;
import lombok.Setter;
import lombok.Data;
import lombok.ToString;

@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;
  }
}

相当于:

import java.util.Arrays;

public class DataExample {
  private final String name;
  private int age;
  private double score;
  private String[] tags;

  public DataExample(String name) {
    this.name = name;
  }

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

  void setAge(int age) {
    this.age = age;
  }

  public int getAge() {
    return this.age;
  }

  public void setScore(double score) {
    this.score = score;
  }

  public double getScore() {
    return this.score;
  }

  public String[] getTags() {
    return this.tags;
  }

  public void setTags(String[] tags) {
    this.tags = tags;
  }

  @Override public String toString() {
    return "DataExample(" + this.getName() + ", " + this.getAge() + ", " + this.getScore() + ", " + Arrays.deepToString(this.getTags()) + ")";
  }

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

  @Override public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof DataExample)) return false;
    DataExample other = (DataExample) o;
    if (!other.canEqual((Object)this)) return false;
    if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
    if (this.getAge() != other.getAge()) return false;
    if (Double.compare(this.getScore(), other.getScore()) != 0) return false;
    if (!Arrays.deepEquals(this.getTags(), other.getTags())) return false;
    return true;
  }

  @Override public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    final long temp1 = Double.doubleToLongBits(this.getScore());
    result = (result*PRIME) + (this.getName() == null ? 43 : this.getName().hashCode());
    result = (result*PRIME) + this.getAge();
    result = (result*PRIME) + (int)(temp1 ^ (temp1 >>> 32));
    result = (result*PRIME) + Arrays.deepHashCode(this.getTags());
    return result;
  }

  public static class Exercise<T> {
    private final String name;
    private final T value;

    private Exercise(String name, T value) {
      this.name = name;
      this.value = value;
    }

    public static <T> Exercise<T> of(String name, T value) {
      return new Exercise<T>(name, value);
    }

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

    public T getValue() {
      return this.value;
    }

    @Override public String toString() {
      return "Exercise(name=" + this.getName() + ", value=" + this.getValue() + ")";
    }

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

    @Override public boolean equals(Object o) {
      if (o == this) return true;
      if (!(o instanceof Exercise)) return false;
      Exercise<?> other = (Exercise<?>) o;
      if (!other.canEqual((Object)this)) return false;
      if (this.getName() == null ? other.getValue() != null : !this.getName().equals(other.getName())) return false;
      if (this.getValue() == null ? other.getValue() != null : !this.getValue().equals(other.getValue())) return false;
      return true;
    }

    @Override public int hashCode() {
      final int PRIME = 59;
      int result = 1;
      result = (result*PRIME) + (this.getName() == null ? 43 : this.getName().hashCode());
      result = (result*PRIME) + (this.getValue() == null ? 43 : this.getValue().hashCode());
      return result;
    }
  }
}

@Value

@Value 注解非常像 @Data 注解的不可变 final 版本。

示例:

import lombok.AccessLevel;
import lombok.experimental.NonFinal;
import lombok.experimental.Value;
import lombok.experimental.Wither;
import lombok.ToString;

@Value public class ValueExample {
  String name;
  @Wither(AccessLevel.PACKAGE) @NonFinal int age;
  double score;
  protected String[] tags;

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

等价于:

 import java.util.Arrays;

public final class ValueExample {
  private final String name;
  private int age;
  private final double score;
  protected final String[] tags;

  @java.beans.ConstructorProperties({"name", "age", "score", "tags"})
  public ValueExample(String name, int age, double score, String[] tags) {
    this.name = name;
    this.age = age;
    this.score = score;
    this.tags = tags;
  }

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

  public int getAge() {
    return this.age;
  }

  public double getScore() {
    return this.score;
  }

  public String[] getTags() {
    return this.tags;
  }

  @java.lang.Override
  public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof ValueExample)) return false;
    final ValueExample other = (ValueExample)o;
    final Object this$name = this.getName();
    final Object other$name = other.getName();
    if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
    if (this.getAge() != other.getAge()) return false;
    if (Double.compare(this.getScore(), other.getScore()) != 0) return false;
    if (!Arrays.deepEquals(this.getTags(), other.getTags())) return false;
    return true;
  }

  @java.lang.Override
  public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    final Object $name = this.getName();
    result = result * PRIME + ($name == null ? 43 : $name.hashCode());
    result = result * PRIME + this.getAge();
    final long $score = Double.doubleToLongBits(this.getScore());
    result = result * PRIME + (int)($score >>> 32 ^ $score);
    result = result * PRIME + Arrays.deepHashCode(this.getTags());
    return result;
  }

  @java.lang.Override
  public String toString() {
    return "ValueExample(name=" + getName() + ", age=" + getAge() + ", score=" + getScore() + ", tags=" + Arrays.deepToString(getTags()) + ")";
  }

  ValueExample withAge(int age) {
    return this.age == age ? this : new ValueExample(name, age, score, tags);
  }

  public static final class Exercise<T> {
    private final String name;
    private final T value;

    private Exercise(String name, T value) {
      this.name = name;
      this.value = value;
    }

    public static <T> Exercise<T> of(String name, T value) {
      return new Exercise<T>(name, value);
    }

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

    public T getValue() {
      return this.value;
    }

    @java.lang.Override
    public boolean equals(Object o) {
      if (o == this) return true;
      if (!(o instanceof ValueExample.Exercise)) return false;
      final Exercise<?> other = (Exercise<?>)o;
      final Object this$name = this.getName();
      final Object other$name = other.getName();
      if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
      final Object this$value = this.getValue();
      final Object other$value = other.getValue();
      if (this$value == null ? other$value != null : !this$value.equals(other$value)) return false;
      return true;
    }

    @java.lang.Override
    public int hashCode() {
      final int PRIME = 59;
      int result = 1;
      final Object $name = this.getName();
      result = result * PRIME + ($name == null ? 43 : $name.hashCode());
      final Object $value = this.getValue();
      result = result * PRIME + ($value == null ? 43 : $value.hashCode());
      return result;
    }

    @java.lang.Override
    public String toString() {
      return "ValueExample.Exercise(name=" + getName() + ", value=" + getValue() + ")";
    }
  }
}

@SneakyThrows

暗中抛出异常,当我们需要抛出异常,在当前方法上调用,不用显示的在方法名后面写 throw

import lombok.SneakyThrows;

public class SneakyThrowsExample implements Runnable {
  @SneakyThrows(UnsupportedEncodingException.class)
  public String utf8ToString(byte[] bytes) {
    return new String(bytes, "UTF-8");
  }

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

等同于:

import lombok.Lombok;

public class SneakyThrowsExample implements Runnable {
  public String utf8ToString(byte[] bytes) {
    try {
      return new String(bytes, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      throw Lombok.sneakyThrow(e);
    }
  }

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

@Synchronized

这个就比较简单直接添加了 synchronized 关键字就好了。不过现在 JDK 也比较推荐的是 Lock 对象,这个可能用的不是特别多。

方法中所有的代码都加入到一个代码块中,默认静态方法使用的是全局锁,普通方法使用的是对象锁,当然也可以指定锁的对象。

例子:

import lombok.Synchronized;

public class SynchronizedExample {
  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");
  }
}

等价于:

public class SynchronizedExample {
  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");
    }
  }
}

@Getter(lazy=true)

你可以让 lombok 生成一个 getter,它会在第一次调用这个 getter 时计算一次值,然后从那里开始缓存它。

如果计算该值需要大量 CPU,或者该值占用大量内存,这可能很有用。

要使用此功能,请创建一个 private final 变量,使用运行成本高的表达式对其进行初始化,并使用 @Getter(lazy=true) 注解字段。

该字段将从其余代码中隐藏,并且在首次调用 getter 时,表达式将被计算不超过一次。

使用示例:

import lombok.Getter;

public class GetterLazyExample {
  @Getter(lazy=true) private final double[] cached = expensive();

  private double[] expensive() {
    double[] result = new double[1000000];
    for (int i = 0; i < result.length; i++) {
      result[i] = Math.asin(i);
    }
    return result;
  }
}

生成的等价代码:

public class GetterLazyExample {
  private final java.util.concurrent.AtomicReference<java.lang.Object> cached = new java.util.concurrent.AtomicReference<java.lang.Object>();

  public double[] getCached() {
    java.lang.Object value = this.cached.get();
    if (value == null) {
      synchronized(this.cached) {
        value = this.cached.get();
        if (value == null) {
          final double[] actualValue = expensive();
          value = actualValue == null ? this.cached : actualValue;
          this.cached.set(value);
        }
      }
    }
    return (double[])(value == this.cached ? null : value);
  }

  private double[] expensive() {
    double[] result = new double[1000000];
    for (int i = 0; i < result.length; i++) {
      result[i] = Math.asin(i);
    }
    return result;
  }
}

@Builder

这个注解是被大多数 Lombok 推崇者最偏爱的,通过简单的使用 @Builder 注解,就可以让 Lombok 生成完美的 Builder 模式的代码。生成器模式是现在比较推崇的一种构建值对象的方式。

Builder 模式是一步一步创建一个复杂对象的创建型模式,它允许用户在不知道内部构建细节的情况下,可以更精细地控制对象的构造流程。该模式是为了将构建复杂对象的过程和它的部件解耦,使得构建过程和部件的表示隔离开来。

Builder模式,在于分工明确,一个抽象建造者,一个具体的建造者,一个指挥者,当然还需要具体的产品。那么我们以一个软件产品为例:技术主管就是抽象建造者,他和产品经理沟通,知道要做一个什么样的产品;而程序员就是具体的劳动者,技术主管说咋做你就咋做;而指挥者就是公司的产品经理,负责和用户沟通,了解客户的需求。

使用示例:

import lombok.Builder;
import lombok.Singular;
import java.util.Set;

@Builder
public class BuilderExample {
  @Builder.Default private long created = System.currentTimeMillis();
  private String name;
  private int age;
  @Singular private Set<String> occupations;
}

生成的等价 Builder 模式代码:

import java.util.Set;

public class BuilderExample {
  private long created;
  private String name;
  private int age;
  private Set<String> occupations;

  BuilderExample(String name, int age, Set<String> occupations) {
    this.name = name;
    this.age = age;
    this.occupations = occupations;
  }

  private static long $default$created() {
    return System.currentTimeMillis();
  }

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

  public static class BuilderExampleBuilder {
    private long created;
    private boolean created$set;
    private String name;
    private int age;
    private java.util.ArrayList<String> occupations;

    BuilderExampleBuilder() {
    }

    public BuilderExampleBuilder created(long created) {
      this.created = created;
      this.created$set = true;
      return this;
    }

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

    public BuilderExampleBuilder age(int age) {
      this.age = age;
      return this;
    }

    public BuilderExampleBuilder occupation(String occupation) {
      if (this.occupations == null) {
        this.occupations = new java.util.ArrayList<String>();
      }

      this.occupations.add(occupation);
      return this;
    }

    public BuilderExampleBuilder occupations(Collection<? extends String> occupations) {
      if (this.occupations == null) {
        this.occupations = new java.util.ArrayList<String>();
      }

      this.occupations.addAll(occupations);
      return this;
    }

    public BuilderExampleBuilder clearOccupations() {
      if (this.occupations != null) {
        this.occupations.clear();
      }

      return this;
    }

    public BuilderExample build() {
      // complicated switch statement to produce a compact properly sized immutable set omitted.
      Set<String> occupations = ...;
      return new BuilderExample(created$set ? created : BuilderExample.$default$created(), name, age, occupations);
    }

    @java.lang.Override
    public String toString() {
      return "BuilderExample.BuilderExampleBuilder(created = " + this.created + ", name = " + this.name + ", age = " + this.age + ", occupations = " + this.occupations + ")";
    }
  }
}

@Log 日志标签集合

在类声明上添加 @Log 注解(包括为其它几种日志定义的注解), Lombok 将会在生成的代码中添加一个 private static final 修饰的日志实例,并且变量名为 log,你不需要再重复的在类中定义日志实例。

默认情况下,logger 的topic(或名称)是使用 @Log 注解的类的类名。 可以通过指定 topic 参数来自定义。 例如:@XSlf4j(topic="reporting")

Lombok 提供了很多主流的日志框架注解,包括 apache logging、log4j、log4j2、jdk logging 等。

  • @CommonsLog:生成 private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);
  • @Flogger:生成 private static final com.google.common.flogger.FluentLogger log = com.google.common.flogger.FluentLogger.forEnclosingClass();
  • @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);

日志注解使用示例:

import lombok.extern.java.Log;
import lombok.extern.slf4j.Slf4j;

@Log
public class LogExample {

  public static void main(String... args) {
    log.error("Something's wrong here");
  }
}

@Slf4j
public class LogExampleOther {

  public static void main(String... args) {
    log.error("Something else is wrong here");
  }
}

@CommonsLog(topic="CounterLog")
public class LogExampleCategory {

  public static void main(String... args) {
    log.error("Calling the 'CounterLog' with a message");
  }
}

生成的代价代码:

public class LogExample {
  private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());

  public static void main(String... args) {
    log.error("Something's wrong here");
  }
}

public class LogExampleOther {
  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExampleOther.class);

  public static void main(String... args) {
    log.error("Something else is wrong here");
  }
}

public class LogExampleCategory {
  private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog("CounterLog");

  public static void main(String... args) {
    log.error("Calling the 'CounterLog' with a message");
  }
}

Lombok 原理

说道 Lombok,我们就得去提到 JSR 269: Pluggable Annotation Processing API (https://www.jcp.org/en/jsr/detail?id=269) 。

JSR 269 之前我们也有注解这样的神器,可是我们比如想要做什么必须使用反射,反射的方法局限性较大。

首先,它必须定义 @RetentionRetentionPolicy.RUNTIME,只能在运行时通过反射来获取注解值,使得运行时代码效率降低。

其次,如果想在编译阶段利用注解来进行一些检查,对用户的某些不合理代码给出错误报告,反射的使用方法就无能为力了。

而 JSR 269 之后我们可以在 javac 的编译期利用注解做这些事情。所以我们发现核心的区分是在 运行期 还是 编译期

从上图可知,Annotation Processing 是在解析和生成之间的一个步骤。

上图是 Lombok 处理流程,在 javac 解析成抽象语法树之后(AST), Lombok 根据自己的注解处理器,动态的修改 AST,增加新的节点(所谓代码),最终通过分析和生成字节码。


参考资料:


本文收录在 SpringBoot 干货系列教程:SpringBoot 干货教程 | 第一章:教程目录大纲和前戏

如果觉得这对你有用,请随意赞赏,给与作者支持
评论 0
最新评论