Lombok

Lombok 是一个非常神奇的 java 类库,会利用注解自动生成 java Bean 中烦人的 Getter、Setter,还能自动生成 logger、ToString、HashCode、Builder 等 java特色的函数或是符合设计模式的函数,能够让你 java Bean 更简洁,更美观。

Lombok 的思想非常先进,它让我们省略繁琐的样板代码,不要在重复的代码上花费太长时间,它也是Java语言演进过程中必然出现的一种思想,要用20% 的时间做 80%的事情。

1-Lombok使用

使用Lombok需要的开发环境Java+Maven+IntelliJ IDEA或者Eclipse(安装Lombok Plugin)

1.1-添加依赖

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

1.2-安装插件

使用Lombok还需要插件的配合,我使用开发工具为idea,这里只讲解idea中安装lombok插件,使用eclipse和myeclipse的小伙伴和自行google安装方法。
打开idea的设置,点击Plugins,点击Browse repositories,在弹出的窗口中搜索lombok,然后安装即可。
IDEA安装Lombok

2-Lombok常用注解

  • @Data

    @Data 是一个很方便的注解,它和@ToString@EqualAndHashCode@Getter/@Setter、和@RequiredArgsConstructor 绑定在一起。换句话说,@Data 生成通常与简单的POJO(Plain Old Java Objects) 和 bean 相关联的所有样板代码,例如:获取所有的属性,设置所有不可继承的属性,适应toString、equals 和 hashcode 的实现,通过构造方法初始化所有final 的属性,以及所有没有使用@NonNull标记的初始化程序的非final字段,以确保该字段永远不为null。

    @Data 就像在类上隐含使用 @toString 、 @EqualAndHashCode、 @Getter、 @Setter 和 @RequiredArgsConstructor 注释一样。@Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor

    但是,@Data 无法设置这些注解的参数,例如callSuper、includeFieldNames 和 exclude

    如果您需要为这些参数中的任何一个设置非默认值,只需显式添加这些注释;

    生成的所有getters/setters 默认都是public 的,为了覆盖访问级别,请使用显式的@Setter \ @Getter批注对字段或类进行注释。你可以使用这个注释(通过与 AccessLevel.NONE结合)来禁止使用 getter或setter。

    所有使用 transient 标记的字段都不会视为 hashcode 和 equals。将完全跳过所有静态字段(不考虑任何生成的方法,并且不会为它们创建setter / getter)。

    如果类已经包含与通常生成的任何方法具有相同名称和参数计数的方法,则不会生成该方法,也不会发出警告或错误。

    例如:如果你使用 equals 标记了一个方法,那么不会再生成 equals 方法,即使从技术上讲,由于具有不同的参数类型,它可能是完全不同的方法。同样的规则适用于构造函数(任何显式构造函数都会阻止 @Data 生成一个),以及toString,equals和所有getter和setter。

    您可以使用@ lombok.experimental.Tolerate 标记任何构造函数或方法,以将它们隐藏在 lombok 中。

    详细解释:
    大致有以下几点:

    1. 此注解会生成 equals(Object other) 和 hashCode() 方法。
    2. 它默认使用非静态,非瞬态的属性
    3. 可通过参数 exclude 排除一些属性
    4. 可通过参数 of 指定仅使用哪些属性
    5. 它默认仅使用该类中定义的属性且不调用父类的方法
    6. 可通过 callSuper=true 解决上一点问题。让其生成的方法中调用父类的方法。

    另:@Data 相当于 @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode 这5个注解的合集。

    通过官方文档,可以得知,当使用 @Data 注解时,则有了 @EqualsAndHashCode 注解,那么就会在此类中存在 equals(Object other) 和 hashCode() 方法,且不会使用父类的属性,这就导致了可能的问题。
    比如,有多个类有相同的部分属性,把它们定义到父类中,恰好id(数据库主键)也在父类中,那么就会存在部分对象在比较时,它们并不相等,却因为lombok自动生成的 equals(Object other) 和 hashCode() 方法判定为相等,从而导致出错。

    修复此问题的方法很简单:

    1. 使用 @Getter @Setter @ToString 代替 @Data 并且自定义 equals(Object other) 和 hashCode() 方法,比如有些类只需要判断主键id是否相等即足矣。
    2. 或者使用在使用 @Data 时同时加上 @EqualsAndHashCode(callSuper=true) 注解。

    例如:

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

    }

    就相当于是不用 lombok 的如下示例:

    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 == thisreturn 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()) != 0return 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 == thisreturn 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;
        }
      }
    }
  • @NonNull

    你可以使用 @NonNull 对方法或者构造器生成 null - check

    如果lombok为您生成整个方法或构造函数(例如@Data),Lombok总是将字段上通常称为@NonNull的各种注释视为生成空值检查的信号。但是,现在,在参数上使用lombok自己的@lombok.NonNull会导致在您自己的方法或构造函数中只插入null-check语句。

    Null - Check 语句看起来像是如下语句:

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

    这条判空语句会在方法的最开始进行判断

    public class NonNullExample {

        @Getter
        private String name;

        public NonNullExample(@NonNull String name){
            this.name = name;
        }
    }

    这个加上 @NonNull 判空的代码就相当于如下代码

    import lombok.NonNull;

    public class NonNullExample {
      private String name;

      public NonNullExample(@NonNull String name) {
        if (name == null) {
          throw new NullPointerException("name is marked @NonNull but is null");
        }
        this.name = name;
      }
    }
  • @Getter和@Setter

    你可以使用 @Getter 和 @Setter 自动生成任何 getter/setter。

    默认的 getter 只返回字段的名称,如果字段的名称为 foo,则返回的是 getFoo(),如果字段类型为 boolean ,则返回 isFoo()。如果字段为 foo 的话,默认的 setter 返回 setFoo,并且类型是 void ,并且带有一个和该属性相同的字段作为参数,用于为此属性字段进行赋值。

    除非你指定AccessLevel 访问级别,否则使用 Getter / Setter 生成的方法默认是 public 的作用范围。AccessLevel的访问级别有 PUBLIC, PROTECTED, PACKAGE, and PRIVATE.

    你也可以在类上使用 @Getter / @Setter ,在这种情况下,就会对该类中的所有非静态属性生成 get and set 方法

    你也可以通过设置 AccessLevel.NONE 禁用任何 get and set 方法的生成。这会使 @Data、@Getter / @Setter 的注解失效。

    public class GetterSetterExample {

        @Setter
        @Getter
        private int age = 10;

        @Setter(AccessLevel.PROTECTED)
        private String name;

        @Getter(AccessLevel.PRIVATE)
        private String high;
    }

    就等同于:

    public class GetterSetterExample {

      private int age = 10;

      private String name;

      private String high;

      public int getAge() {
        return age;
      }

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

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

      private String getHigh(){
        return high;
      }
    }
  • @ToString

    @ToString 注解用来替换掉生成 toString() 方法的实现,默认情况下,它会按顺序打印你的班级名称以及每个字段,并以逗号分隔。

    通过设置 includeFieldNames = true 能够使 toString() 方法打印每个字段的属性值和名称。

    默认情况下,所有非静态属性都被打印,如果你想要排除某些字段的话,需要设置 @ToString.Exclude,或者,你可以指定ToString(onlyExplicitlyIncluded = true)来指定哪些你希望使用的字段。然后使用@ ToString.Include标记要包含的每个字段。

    通过设置 callSuper 为 true ,可以将toString的超类实现的输出包含到输出中。请注意,java.lang.Object 的 toString() 实现没有任何意义,所以你可能不会这样做除非你想要扩展另一个类。

    你还可以在toString 中包含方法调用的输出。只能包含不带参数的实例(非静态)方法,为此,请使用@ ToString.Include标记方法。

    你可以使用 @ToString.Include(name =“some other name”)更改用于标识成员的名称,并且可以通过 @ ToString.Include(rank = -1)更改成员的打印顺序。没有定义等级的成员默认是0级,等级高的成员优先被打印,优先级相同的成员按照它们在源文件中出现的顺序打印。

    @ToString
    public class ToStringExample {

      // 静态属性不会包含
      private static final int STATIC_VAR = 10;
      private String name;
      private String[] tags;
      private Shape shape = new Square(510);

      // 排除指定字段不会被 toString 打印
      @ToString.Exclude
      private int id;

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

      // callSuper 表示是否扩展父类的 toString(), 
      // includeFieldNames 表示是否包含属性名称
      @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;
        }
      }

      public static class Shape {}
    }

    测试一下上面的示例:

    ToStringExample toStringExample = new ToStringExample();
    System.out.println(toStringExample);

    输出如下:

    ToStringExample(name=null, tags=null, shape=ToStringExample.Square(super=com.project.lombok.ToStringExample$Square@1b9e1916, width=5, height=10))

    注释掉 callSuper = true, 测试结果如下:

    ToStringExample(name=null, tags=null, shape=ToStringExample.Square(width=5, height=10))

    从输出可以看出,如果不扩展父类,不会输出关于 Shape 的内部类信息,callSuper 默认为 false

    注释掉 includeFieldNames,测试结果不会发生变化,所以 includeFieldNames 默认值为 true

    更改 includeFieldNames = false,测试结果如下:

    ToStringExample(name=null, tags=null, shape=ToStringExample.Square(super=com.project.lombok.ToStringExample$Square@1b9e1916, 5, 10))

    从输出可以看出,如果设置 includeFieldNames = false ,不会输出Shape 中的字段名称信息。

    上面用@ToString 注解修饰的例子就相当于是下面这段代码:

    public class ToStringExample {
      private static final int STATIC_VAR = 10;
      private String name;
      private Shape shape = new Square(510);
      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) + ")";
      }

      public static class Shape {}
    }
  • @EqualsAndHashCode

    任何类的定义都可以用@EqualsAndHashCode 标注,让 lombok 为其生成 equals和 hashCode 方法。默认情况下,将会用在非静态,非 transient 标记的字段上,但是你可以通过 @EqualsAndHashCode.Include或 @EqualsAndHashCode.Exclude 标记类型成员来修改使用哪些字段。

    或者,你可以通过使用 @EqualsAndHashCode.Include 并使用 @EqualsAndHashCode(onlyExplicitlyIncluded = true)标记它们来准确指定你希望使用的字段或方法。

    如果将 @EqualsAndHashCode 应用于扩展另一个的类,这个特性就会变的很危险。通常来说,对类自动生成equals 和 hashcode 方法不是一个好的选择,因为超类也定义了字段,这些字段也需要equals / hashCode方法。通过设置 callSuper 为 true,可以在生成的方法中包含超类的 equals 和 hachcode 方法。

    对于 hashCode 来说,super.hashCode 的结果包括了哈希算法,对于 equals 来说,如果超类实现认为它不等于传入的对象,生成的方法将返回 false。请注意,不是所有的equals 实现都能正确处理这种情况。然而,lombok生成的 equals实现可以正确处理这种情况。

    如果不扩展类时(只扩展任何java.lang.Object 类)时把 callSuper 设置为 true 会提示编译错误,因为 lombok 会将生成的 equals() 方法和 hashCode() 实现转换为从 Object 继承过来:只有相同的 Object 对象彼此相等并且具有相同的 hashCode 。

    当你继承其他类时没有设置 callSuper 为 true 会进行警告,因为除非父类没有相同的属性,lombok无法为您生成考虑超类声明的字段的实现。你需要自己写实现类或者依赖 callSuper 工具。你还可以使用 lombok.equalsAndHashCode.callSuper 配置key。

    下面是一个例子:

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

        public static class Shape {}
    }
  • @NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

    lombok 有三个生成构造函数的注解,下面一起来看一下它们的使用说明和示例

    @NoArgsConstructor 将会生成无参数的构造函数,如果有final 修饰的字段并且没有为 final 修饰的字段进行初始化的话,那么单纯的使用 @NoArgsConstructor 注解会提示编译错误

    修改建议:需要为 @NoArgsConstructor 指定一个属性@NoArgsConstructor(force=true),lombok会为上面的final 字段默认添加初始值,因为id 是 int 类型,所以 id 的初始值为 0,类似的不同类型的字段的初始值还有 false / null / 0,特定的 Java 构造,像是 hibernate 和 服务提供接口需要无参数的构造方法。此注解主要与 @Data 或生成注解的其他构造函数组合使用。

    这里有个需要注意的地方:@NonNull 不要和 @NoArgsConstructor 一起使用

    @NoArgsConstructor
    @Getter
    public class NoArgsConstructorExample {
        private  Long id ;
        private @NonNull String name;
        private Integer age;

        public static void main(String[] args) {
            System.out.println(new NoArgsConstructorExample().getName());
        }
    }

    输出结果是 null ,因此如果有 @NonNull 修饰的成员的变量就不要用 @NoArgsConstructor 修饰类

    @RequiredArgsConstructor 将为每个需要特殊处理的字段生成一个带有1个参数的构造函数。所有未初始化的 final 字段都会获取一个参数,以及标记为 @NonNull 的任何字段也会获取一个参数。这些字段在声明它们的地方没有初始化。对于这些标记为 @NonNull 的字段,会生成特殊的null 编译检查。如果标记为 @NonNull 的字段的参数为 null,那么构造函数将会抛出 NullPointerException。参数的顺序与字段在类中的显示顺序相匹配。

    例如下面这个例子,只有 @NonNull 和 final 修饰的字段才会加入构造函数:

    @RequiredArgsConstructor
    public class RequiredArgsConstructorExample {

        @NonNull
        private int id;
        private final String name;
        private boolean human;

    }

    生成的结果大概是这样的:

    public class RequiredArgsConstructorExample {
        @NonNull
        private int id;
        private final String name;
        private boolean human;

        public RequiredArgsConstructorExample(@NonNull int id, String name) {
            if (id == null) {
                throw new NullPointerException("id is marked @NonNull but is null");
            } else {
                this.id = id;
                this.name = name;
            }
        }
    }

    @AllArgsConstructor: @AllArgsConstructor 为类中的每个字段生成一个带有1个参数的构造函数生成全参构造器,标有@NonNull 的字段会导致对这些参数进行空检查。

    @AllArgsConstructor
    public class AllArgsConstructorExample {

    private int id;
    private String name;
    private int age;

    }

    相当于自动生成如下代码:

    public AllArgsConstructorExample(int id, String name, int age) {
    this.id = id;
    this.name = name;
    this.age = age;
    }

    这些注解中的每一个都允许使用替代形式,其中生成的构造函数始终是私有的,并且生成包含私有构造函数的附加静态工厂方法,通过为注释提供staticName值来启用此模式,**@RequiredArgsConstructor(staticName =“of”)。**看下面这个例子:

    @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 == nullthrow 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 == nullthrow new NullPointerException("description");
        this.x = x;
        this.y = y;
        this.description = description;
      }

      public static class NoArgsExample {
        @NonNull private String field;

        public NoArgsExample() {
        }
      }
    }
  • @Slf4j

    在需要打印日志的类中使用,当项目中使用了slf4j打印日志框架时使用该注解,会简化日志的打印流程,只需调用info方法即可,注解在类,生成log变量,严格意义来说是常量。private static final Logger log = LoggerFactory.getLogger(UserController.class);

  • @Log4j

    在需要打印日志的类中使用,当项目中使用了log4j打印日志框架时使用该注解,会简化日志的打印流程,只需调用info方法即可;

  • @Cleanup

    自动关闭资源,针对实现了java.io.Closeable接口的对象有效,如:典型的IO流对象;

  • @SneakyThrows

    可以对受检异常进行捕捉并抛出;

  • @Synchronized

    作用于方法级别,可以替换synchronize关键字或lock锁,用处不大;

  • @Builder

    作用于类上,将类转变为建造者模式。

    简述:Builder 使用创建者模式又叫建造者模式。简单来说,就是一步步创建一个对象,它对用户屏蔽了里面构建的细节,但却可以精细地控制对象的构造过程。

    @Builder注释为你的类生成相对略微复杂的构建器API。@Builder可以让你以下面显示的那样调用你的代码,来初始化你的实例对象:

    Student.builder()
    .sno( "001" )
    .sname( "admin" )
    .sage( 18 )
    .sphone( "110" )
    .build();

    @Builder可以放在类,构造函数或方法上。 虽然放在类上和放在构造函数上这两种模式是最常见的用例,但@Builder最容易用放在方法的用例来解释。

    那么@Builder内部帮我们做了什么?

    1. 创建一个名为ThisClassBuilder的内部静态类,并具有和实体类形同的属性(称为构建器)。
    2. 在构建器中:对于目标类中的所有的属性和未初始化的final字段,都会在构建器中创建对应属性。
    3. 在构建器中:创建一个无参的default构造函数。
    4. 在构建器中:对于实体类中的每个参数,都会对应创建类似于setter的方法,只不过方法名与该参数名相同。 并且返回值是构建器本身(便于链式调用),如上例所示。
    5. 在构建器中:一个build()方法,调用此方法,就会根据设置的值进行创建实体对象。
    6. 在构建器中:同时也会生成一个toString()方法。
    7. 在实体类中:会创建一个builder()方法,它的目的是用来创建构建器。

    例子如下:

    @Builder
    public class User {
    private final Integer code = 200;
    private String username;
    private String password;
    }

    // 编译后:
    public class User {
    private String username;
    private String password;
    User(String username, String password) {
    this.username = username; this.password = password;
    }
    public static User.UserBuilder builder() {
    return new User.UserBuilder();
    }

    public static class UserBuilder {
    private String username;
    private String password;
    UserBuilder() {}

    public User.UserBuilder username(String username) {
    this.username = username;
    return this;
    }
    public User.UserBuilder password(String password) {
    this.password = password;
    return this;
    }
    public User build() {
    return new User(this.username, this.password);
    }
    public String toString() {
    return "User.UserBuilder(username=" + this.username + ", password=" + this.password + ")";
    }
    }
    }
  • @Builder中使用 @Singular 组合

    @Builder也可以为集合类型的参数或字段生成一种特殊的方法。 它采用修改列表中一个元素而不是整个列表的方式,可以是增加一个元素,也可以是删除一个元素。

    Student.builder()
    .sno( "001" )
    .sname( "admin" )
    .sage( 18 )
    .sphone( "110" ).sphone( "112" )
    .build();

    这样就可以轻松地将List字段中包含2个字符串。 但是想要这样来操作集合,你需要使用@Singular来注释字段或参数。

    在使用@Singular注释注释一个集合字段(使用@Builder注释类),lombok会将该构建器节点视为一个集合,并生成两个adder方法而不是setter方法。

    • 一个向集合添加单个元素
    • 一个将另一个集合的所有元素添加到集合中

    将不生成仅设置集合(替换已添加的任何内容)的setter。 还生成了clear方法。 这些singular构建器相对而言是有些复杂的,主要是来保证以下特性:

    1. 在调用build()时,生成的集合将是不可变的。
    2. 在调用build()之后调用其中一个adder方法或clear方法不会修改任何已经生成的对象。如果对集合修改之后,再调用build(),则会创建一个基于上一个对象创建的对象实体。
    3. 生成的集合将被压缩到最小的可行格式,同时保持高效。

    @Singular只能应用于lombok已知的集合类型。目前,支持的类型有:

    • java.util:
      • Iterable, Collection, 和List (一般情况下,由压缩的不可修改的ArrayList支持).
      • Set, SortedSet, and NavigableSet (一般情况下,生成可变大小不可修改的HashSet或者TreeSet).
      • Map, SortedMap, and NavigableMap (一般情况下,生成可变大小不可修改的HashMap或者TreeMap).
    • Guava’s com.google.common.collect:
      • ImmutableCollection and ImmutableList
      • ImmutableSet and ImmutableSortedSet
      • ImmutableMap, ImmutableBiMap, and ImmutableSortedMap
      • ImmutableTable

    来看看使用了@Singular注解之后的编译情况:

    @Builder
    public class User {
    private final Integer id;
    private final String zipCode = "123456";
    private String username;
    private String password;
    @Singular
    private List<String> hobbies;
    }

    // 编译后:
    public class User {
    private final Integer id;
    private final String zipCode = "123456";
    private String username;
    private String password;
    private List<String> hobbies;
    User(Integer id, String username, String password, List<String> hobbies) {
    this.id = id; this.username = username;
    this.password = password; this.hobbies = hobbies;
    }

    public static User.UserBuilder builder() {return new User.UserBuilder();}

    public static class UserBuilder {
    private Integer id;
    private String username;
    private String password;
    private ArrayList<String> hobbies;
    UserBuilder() {}
    public User.UserBuilder id(Integer id) { this.id = id; return this; }
    public User.UserBuilder username(String username) { this.username = username; return this; }
    public User.UserBuilder password(String password) { this.password = password; return this; }

    public User.UserBuilder hobby(String hobby) {
    if (this.hobbies == null) {
    this.hobbies = new ArrayList();
    }
    this.hobbies.add(hobby);
    return this;
    }

    public User.UserBuilder hobbies(Collection<? extends String> hobbies) {
    if (this.hobbies == null) {
    this.hobbies = new ArrayList();
    }
    this.hobbies.addAll(hobbies);
    return this;
    }

    public User.UserBuilder clearHobbies() {
    if (this.hobbies != null) {
    this.hobbies.clear();
    }
    return this;
    }

    public User build() {
    List hobbies;
    switch(this.hobbies == null ? 0 : this.hobbies.size()) {
    case 0:
    hobbies = Collections.emptyList();
    break;
    case 1:
    hobbies = Collections.singletonList(this.hobbies.get(0));
    break;
    default:
    hobbies = Collections.unmodifiableList(new ArrayList(this.hobbies));
    }
    return new User(this.id, this.username, this.password, hobbies);
    }
    public String toString() {
    return "User.UserBuilder(id=" + this.id + ", username=" + this.username + ", password=" + this.password + ", hobbies=" + this.hobbies + ")";
    }
    }
    }

    其实,lombok的创作者还是很用心的,在进行build()来创建实例对象时,
    并没有直接使用Collections.unmodifiableList(Collection)此方法来床架实例,而是分为三种情况。

    • 第一种,当集合中没有元素时,创建一个空list
    • 第二种情况,当集合中存在一个元素时,创建一个不可变的单元素list
    • 第三种情况,根据当前集合的元素数量创建对应合适大小的list

    当然我们看编译生成的代码,创建了三个关于集合操作的方法:

    • hobby(String hobby):向集合中添加一个元素
    • hobbies(Collection hobbies):添加一个集合所有的元素
    • clearHobbies():清空当前集合数据
    2. @Singular 注解配置value属性

    我们先来看看 @Singular 注解的详情:

    @Target({FIELD, PARAMETER})
    @Retention(SOURCE)
    public @interface Singular {
    // 修改添加集合元素的方法名
    String value() default "";
    }
    • 测试如何使用注解属性value
    @Builder
    public class User {
    private final Integer id;
    private final String zipCode = "123456";
    private String username;
    private String password;
    @Singular(value = "testHobbies")
    private List<String> hobbies;
    }

    // 测试类
    public class BuilderTest {
    public static void main(String[] args) {
    User user = User.builder()
    .testHobbies("reading")
    .testHobbies("eat")
    .id(1)
    .password("admin")
    .username("admin")
    .build();
    System.out.println(user);
    }
    }

    说明,当我们使用了注解属性value之后,我们在使用添加集合元素时的方法名发生相应的改变。但是,同时生成的添加整个集合的方法名发生改变了吗?我们再来看看编译后的代码:

    / 编译后:
    public class User {
    // 省略部分代码,只看关键部分
    public static class UserBuilder {
    public User.UserBuilder testHobbies(String testHobbies) {
    if (this.hobbies == null) {
    this.hobbies = new ArrayList();
    }
    this.hobbies.add(testHobbies);
    return this;
    }

    public User.UserBuilder hobbies(Collection<? extends String> hobbies) {
    if (this.hobbies == null) {
    this.hobbies = new ArrayList();
    }
    this.hobbies.addAll(hobbies);
    return this;
    }

    public User.UserBuilder clearHobbies() {
    if (this.hobbies != null) {
    this.hobbies.clear();
    }
    return this;
    }
    }
    }

    可以看到,只有添加一个元素的方法名发生了改变。

    3. @Builder.Default 的使用

    比如有这样一个实体类:

    @Builder
    @ToString
    public class User {
    @Builder.Default
    private final String id = UUID.randomUUID().toString();
    private String username;
    private String password;
    @Builder.Default
    private long insertTime = System.currentTimeMillis();
    }

    在类中我在idinsertTime上都添加注解@Builder.Default,当我在使用这个实体对象时,我就不需要在为这两个字段进行初始化值,如下面这样:

    public class BuilderTest {
    public static void main(String[] args) {
    User user = User.builder()
    .password("admin")
    .username("admin")
    .build();
    System.out.println(user);
    }
    }

    // 输出内容:
    User(id=416219e1-bc64-43fd-b2c3-9f8dc109c2e8, username=admin, password=admin, insertTime=1546869309868)

    lombok在实例化对象时就为我们初始化了这两个字段值。

    当然,你如果再对这两个字段进行设值的话,那么默认定义的值将会被覆盖掉,如下面这样:

    public class BuilderTest {
    public static void main(String[] args) {
    User user = User.builder()
    .id("admin")
    .password("admin")
    .username("admin")
    .build();
    System.out.println(user);
    }
    }
    // 输出内容
    User(id=admin, username=admin, password=admin, insertTime=1546869642151)
    4. @Builder 详细配置

    下面我们再来详细看看@Builder这个注解类地详细实现:

    @Target({TYPE, METHOD, CONSTRUCTOR})
    @Retention(SOURCE)
    public @interface Builder {
    // 如果@Builder注解在类上,可以使用 @Builder.Default指定初始化表达式
    @Target(FIELD)
    @Retention(SOURCE)
    public @interface Default {}
    // 指定实体类中创建 Builder 的方法的名称,默认为: builder (个人觉得没必要修改)
    String builderMethodName() default "builder";
    // 指定 Builder 中用来构件实体类的方法的名称,默认为:build (个人觉得没必要修改)
    String buildMethodName() default "build";
    // 指定创建的建造者类的名称,默认为:实体类名+Builder
    String builderClassName() default "";
    // 使用toBuilder可以实现以一个实例为基础继续创建一个对象。(也就是重用原来对象的值)
    boolean toBuilder() default false;

    @Target({FIELD, PARAMETER})
    @Retention(SOURCE)
    public @interface ObtainVia {
    // 告诉lombok使用表达式获取值
    String field() default "";
    // 告诉lombok使用表达式获取值
    String method() default "";

    boolean isStatic() default false;
    }
    }

    以上注解属性,我只测试一个比较常用的toBuilder,因为我们在对实体对象进行操作时,往往会存在对某些实体对象的某个字段进行二次赋值,这个时候就会用到这一属性。但是,这会创建一个新的对象,而不是原来的对象,原来的对象属性是不可变的,除非你自己想要给这个实体类再添加上@Data或者@setter方法。下面就来测试一下:

    @Builder(toBuilder = true)
    @ToString
    public class User {
    private String username;
    private String password;
    }
    // 测试类
    public class BuilderTest {
    public static void main(String[] args) {
    User user1 = User.builder()
    .password("admin")
    .username("admin")
    .build();
    System.out.println(user1);

    User user2 = user1.toBuilder().username("admin2").build();
    // 验证user2是否是基于user1的现有属性创建的
    System.out.println(user2);
    // 验证对象是否是同一对象
    System.out.println(user1 == user2);
    }
    }
    // 输出内容
    User(username=admin, password=admin)
    User(username=admin2, password=admin)
    false
    5. @Builder 全局配置
    # 是否禁止使用@Builder
    lombok.builder.flagUsage = [warning | error] (default: not set)
    # 是否使用Guaua
    lombok.singular.useGuava = [true | false] (default: false)
    # 是否自动使用singular,默认是使用
    lombok.singular.auto = [true | false] (default: true)
    • 总的来说@Builder还是很好用的。

3-Lombok原理

在Lombok使用的过程中,只需要添加相应的注解,无需再为此写任何代码。自动生成的代码到底是如何产生的呢?

核心之处就是对于注解的解析上。JDK5引入了注解的同时,也提供了两种解析方式。

  • 运行时解析
    运行时能够解析的注解,必须将@Retention设置为RUNTIME,这样就可以通过反射拿到该注解。java.lang.reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。

  • 编译时解析
    编译时解析有两种机制,分别简单描述下:

  1. Annotation Processing Tool

apt自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点原因:

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

JSR 269自JDK6加入,作为apt的替代方案,它解决了apt的两个问题,javac在执行的时候会调用实现了该API的程序,这样我们就可以对编译器做一些增强。

Lombok本质上就是一个实现了“JSR 269 API”的程序。在使用javac的过程中,它产生作用的具体流程如下:

  • javac对源代码进行分析,生成了一棵抽象语法树(AST)
  • 运行过程中调用实现了“JSR 269 API”的Lombok程序
  • 此时Lombok就对第一步骤得到的AST进行处理,找到@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter方法定义的相应树节点
  • javac使用修改后的抽象语法树(AST)生成字节码文件,即给class增加新的节点(代码块)

通过读Lombok源码,发现对应注解的实现都在HandleXXX中,比如@Getter注解的实现在HandleGetter.handle()。还有一些其它类库使用这种方式实现,比如Google Auto、Dagger等等。

4-Lombok的优缺点

  • 优点:
    1. 能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,提高了一定的开发效率
    2. 让代码变得简洁,不用过多的去关注相应的方法
    3. 属性做修改时,也简化了维护为这些属性所生成的getter/setter方法等
  • 缺点:
    1. 不支持多种参数构造器的重载
    2. 虽然省去了手动创建getter/setter方法的麻烦,但大大降低了源代码的可读性和完整性,降低了阅读源代码的舒适度