Lombok使用指南(四)

9、试验性注解

除了前面介绍的核心注解之外,Lombok还提供了若干实验性注解,它们和核心注解一样使用是没问题的,但其支持程度不如核心注解的功能那么完善。它们都位于 lombok.experimental 包中。

具体而言,实验性功能包括:

  • 其测试不如核心功能严格
  • 修复错误的速度也不如核心功能快
  • 可能其 API 会发生变化,如果发现有更好的解决相同问题的方法,这种变化可能会非常显著
  • 如果某个功能太难支持或者产生的样板代码太多,可能会完全消失

那些得到社区积极反馈且似乎能生成简洁、灵活代码的功能,最终会作为核心功能被接受,并从实验性包中移出。

9.1、Accessors注解

Accessors注解用于修饰类和属性,单独使用该注解或使用其默认属性值没有效果,它通常与其它章节一起使用,如 Setter 注解或 Data 注解,使用方式如下:

@Setter
@Getter
@Accessors(fluent = true, chain = true,makeFinal = true)
public class User {
    private int id;
    private String name;
}

产生的源码如下:

public class User {
    private int id;
    private String name;

    @Generated
    public final User id(int id) {
        this.id = id;
        return this;
    }

    @Generated
    public final User name(String name) {
        this.name = name;
        return this;
    }

    @Generated
    public final int id() {
        return this.id;
    }

    @Generated
    public final String name() {
        return this.name;
    }
}

属性说明:

  • fluent:默认值是false;如果为true,则getter与setter方法将根据字段命名,而不包含get或set前缀。如果为true且省略了chain属性,则chain默认为true
  • chain:默认值为false;如果为true,setter方法将返回 this 而不是 void。但若 fluent=true,则其值为 true
  • makeFinal:默认值为false;若为 true, getter与setter方法将使用 final 修饰

9.2、ExtensionMethod注解

ExtensionMethod注解允许在不创建新的派生类型、不重新编译代码或不修改原始类型的情况下,为现有的类型“添加”方法。扩展方法是一种至少带1个参数的public静态方法,但它们的调用方式却如同它们是扩展类型上的实例方法一样。示例:

import java.util.Arrays;

@ExtensionMethod(Arrays.class)
public class ExtensionMethodDemo {
    /**
     * 对整型数组排序
     *
     * @param nums 数组
     * @return 排序之后的原数组
     */
    public int[] sort(int[] nums) {
        if (nums == null) {
            return null;
        }
        nums.sort();
        return nums;
    }
}

上面的方法用于对整型数组排序,如果想直接使用 nums.sort() 这句话,显然会出错,因为 Java 中的数组没有这个方法。但是我们可以为数组扩展一个方法,该方法来自Arrays中,该类提供了一个如下的public 和 static修饰的方法:

public static void sort(int[] a) {
    DualPivotQuicksort.sort(a, 0, 0, a.length);
}

这样在类上使用 @ExtensionMethod(Arrays.class),就是想表明,Arrays中的的带 int[] 参数的sort方法,可以为自己代码中的int[] 类型变量调用sort方法,最终产生的代码如下:

import java.util.Arrays;

public class ExtensionMethodDemo {
    public int[] sort(int[] nums) {
        if (nums == null) {
            return null;
        } else {
            Arrays.sort(nums);
            return nums;
        }
    }
}

可以看到,最终 nums.sort() 这句话的 nums 作为了Arrays中sort方法的参数。也就是调用扩展方法的代码最终是将自身作为了扩展方法的第一个参数。

ExtensionMethod注解的属性value是一个数组,可以接收多个class。

下面我们自己声明一个类,在其中也提供一个 public static 修饰的方法,该方法接受2个参数:

public class MyString {
    /**
     * 翻转字符串,并只保留指定的长度
     *
     * @param s   字符串
     * @param len 保留的长度
     * @return 最终的结果
     */
    public static String reverse(String s, int len) {
        return new StringBuilder(s).reverse().substring(0, len);
    }
}

接着更新ExtensionMethodDemo类如下:

@ExtensionMethod({Arrays.class, MyString.class})
public class ExtensionMethodDemo {
    /**
     * 对整型数组排序
     *
     * @param nums 数组
     * @return 排序之后的原数组
     */
    public int[] sort(int[] nums) {
        if (nums == null) {
            return null;
        }
        nums.sort();
        return nums;
    }

    public String reverse(String s, int len) {
        if (s == null || s.length() <= len) {
            return null;
        }
        return s.reverse(len);
    }
}

在第21行,出现了对字符串调用reverse方法,但是String是没有这个方法的,此时就可以将MyString.class 增加到ExtensionMethod的 value 属性中。MyString提供了名为reverse且第一个参数是String类型的public static方法。这样就能将String类多"加"一个方法。

如果有多个类都提供了满足要求的方法,则按照value中的顺序依次匹配,一旦前面的匹配成就终止匹配过程。

属性:

  • value:class数组;设置所有其静态方法将被当作扩展方法公开使用的类型
  • suppressBaseMethods:默认值为true,为true的话,则会使用适用的扩展方法(如果存在的话),即便方法调用原本已经可以编译通过。如果为false,则仅在方法调用并非由该类型自身定义的情况下才使用扩展方法。

9.3、NonFinal注解

修饰类,属性,参数,方法等,用于将其他注解产生的 final 修饰符去掉,如:

@NonFinal
@Value
public class User {
    @NonFinal
    int id;
    @NonFinal
    String name;
}

最终产生的代码中本来类与属性前有@Value产生的final,但现在有了@NonFinal注解后,就没有了。

public class User {
    private int id;
    private String name;
    // 其他省略
}

9.4、PackagePrivate注解

PackagePrivate注解修饰类,属性,参数,方法,构造方法等,用于将其他注解产生的修饰符改为包内私有级别(即去掉访问控制修饰符),如:

@PackagePrivate
public class MyClass {
    @PackagePrivate
    public void method() {
    }
}

产生的源码如下:

class MyClass {
    void method() {
    }
}

但目前IDEA插件未支持。

9.5、FieldDefaults注解

FieldDefaults注解修饰类,用于为带有此注解的类型中的每个实例字段(非static)添加修饰符。例如:

@FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true)
public class User {
    private int id = 0;
    String name = "";
}

产生的源码如下:

public class User {
    private final int id = 0;
    protected final String name = "";
}

属性:

  • level:AccessLevel枚举类型,默认值值NONE;为每个包内私有的实例字段(即没有访问修饰符)且未带有 @PackagePrivate 注解的字段将添加指定的访问控制修饰符
  • makeFinal:默认为false,如果为 true,则每个未用 @NonFinal 注解标注的(实例)字段都将添加 final 修饰符

9.6、FieldNameConstants注解

FieldNameConstants注解用于修饰类,生成一个内部类型,其中包含一些字符串常量,这些字符串常量包含了每个非静态字段的字段名。如:

@FieldNameConstants
public class User {
    private int id;
    private String name;
}

产生源码如下:

public class User {
    private int id;
    private String name;

    @Generated
    public static final class Fields {
        public static final String id = "id";
        public static final String name = "name";
    }
}

属性:

  • level:类型是AccessLevel枚举,默认值为PUBLIC,设置内部类的访问控制级别
  • asEnum:默认为false,若为true,则产生的是枚举而不是类
@Generated
public static enum Fields {
    id,
    name;
}
  • innerTypeName:设置产生的内部类/枚举的名称
  • onlyExplicitlyIncluded:与@FieldNameConstants.Include 配合指定产生的类/枚举只包含哪些属性

内部注解@FieldNameConstants.Exclude 用于排除哪些属性不出现在产生的类/枚举中

9.7、Delegate注解

Delegate注解修饰属性或方法,属性必须是具体类,不能是数组,基本数据类型等,方法只能是无参且返回值类型与与其修饰的属性类型一致。

修饰的属性类型的所有公共实例方法和所有父类字段的公共实例方法,都会被委托执行,但不包括存在于 Object 类中的所有方法、canEqual(Object) 方法,以及任何出现在 excludes 属性所列出的类型中的方法。

如:

public class User {
    @Delegate
    private List<String> names;

    @Delegate
    public String add(){
        return "";
    }
}

产生的源码如下:

public class User {
    private List<String> names;

    public String add() {
        return "";
    }

    @Generated
    public int size() {
        return this.names.size();
    }

    @Generated
    public boolean isEmpty() {
        return this.names.isEmpty();
    }
    
    // 省略若干与List声明一样的方法
    
    @Generated
    public int length() {
        return this.add().length();
    }

    @Generated
    public char charAt(int index) {
        return this.add().charAt(index);
    }
    // 省略若干与String声明一样的方法
}

属性:

  • types:通常情况下,字段的类型会被用作委托类型。然而,若要选择不同的类型作为委托类型,可以在此处列出一个(或多个)类型。请注意,带有类型参数的类型只能作为字段类型使用。
  • excludes:这里列出的所有类型中的任何一种方法(包括超类型中的方法)都不会被委托执行。

9.8、WithBy注解

WithBy注解修饰类与属性,用于产生一个名为withFieldNameBy的方法,产生实例的克隆,需要与全参构造方法配合使用。

@AllArgsConstructor
public class User {
    private @WithBy int id;
    private String name;
}

产生的源码如下:

import java.util.function.IntUnaryOperator;
import lombok.Generated;

public class User {
    private int id;
    private String name;

    @Generated
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Generated
    public User withIdBy(IntUnaryOperator transformer) {
        return new User(transformer.applyAsInt(this.id), this.name);
    }
}

使用时方式如下:

User user1 = new User(1, "老谭");
User user2 = user1.withIdBy(id -> id);

我在使用当前IDEA进行测试时,代码可以编译成功,但是IDEA提示找不到withIdBy,这样就只能使用java命令执行。

9.8、UtilityClass注解

UtilityClass注解修饰类,如果一个类被标注为 @UtilityClass,那么它会发生以下这些变化:

  • 它会被标记为 final。
  • 如果该类中声明了任何构造函数,就会产生一个错误。否则,会生成一个私有的无参构造函数;该构造函数会抛出 UnsupportedOperationException 异常。
  • 该类中的所有方法、内部类和字段都会被标记为静态的。

不要使用非星号的静态导入来导入这些成员;Javac 无法识别这种导入方式。可以使用以下任一种方式:import static ThisType.*;或者不进行静态导入。

@UtilityClass
public class MathUtil {
    private int num;

    public void method() {
    }
}

产生的源码如下:

public final class MathUtil {
    private static int num;

    public static void method() {
    }

    @Generated
    private MathUtil() {
        throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
    }
}

9.9、Helper注解

在方法的局部类中使用此标记,表示该方法内部的所有方法都应像辅助方法一样向方法的其他部分公开。

比如下面的写法:

public class MathUtil {

    public static int add(int num) {
        int delta = 10;
        @Helper
        class HelperClass {
            int addDelta(int num) {
                return num + delta;
            }
        }
        return addDelta(num);//IDEA提示:Cannot resolve method 'addDelta' in 'MathUtil'
    }
}

这种写法极为少见,且IDEA目前不支持这个注解,会报错,不过Lombok 会正常处理class,产生的源码如下:

public class MathUtil {
    public static int add(int num) {
        final int delta = 10;

        class HelperClass {
            int addDelta(int num) {
                return num + delta;
            }
        }

        HelperClass $HelperClass = new HelperClass();
        return $HelperClass.addDelta(num);
    }
}

从产生的源码也可以看出来,Helper注解修饰的类要有无参构造方法。

9.10、SuperBuilder注解

SuperBuilder注解修饰类,是@Builder 的增强版,特别适用于继承层次结构中的 Builder 模式。当类需要继承其他类,并且希望在构建子类对象时,能够同时设置父类和子类的字段时,@SuperBuilder 就显得尤为重要。比如先声明父类如下:

@SuperBuilder
public class ParentUser {
    private int id;
    private String name;
}

子类如下:

@SuperBuilder
public class User extends ParentUser {
    private String address;
}

测试代码:

User user = User.builder().id(1).name("老谭").address("四川成都").build();

属性:builderMethodName、buildMethodName、toBuilder、setterPrefix 与 @Builder 中的一样

9.11、Tolerate注解

Tolerate注解修饰方法或构造方法,用于实现对冲突的兼容,比如我们有一个类:

@Builder
public class User {
    private int id;
    private String name;
}

@Builder产生的源码中是没有无参构造方法的,内部使用的是全参构造方法。没有无参构造方法的类不符合 JavaBean 规范,在实现与序列化相关的操作时会出错。如果此时增加一个无参构造方法:

@Builder
public class User {
    private int id;
    private String name;

    public User(){}
}

这样的话又不会产生全参构造方法了,这时就可以使用Tolerate注解修饰此构造方法:

@Builder
public class User {
    private int id;
    private String name;

    @Tolerate
    public User() {
    }
}

这样Lombok在处理Builder时就当无参构造方法不能存在了。就可以产生全参构造方法供build方法调用了。

刚才的问题其实使用下面的写法是完全一样的效果

@Builder
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PACKAGE)
public class User {
 private int id;
 private String name;
}

9.12、StandardException注解

StandardException注释修饰异常类(Throwable类型),用于添加 4 个常见的异常构造方法。可以手动实现这 4 个构造方法中的任意一个或全部;lombok 只会生成缺失的那些。除了全参数构造方法之外,其余所有构造方法都实现了为 this(msg, cause) 调用的方式;因此,只需编写全参数构造方法,就有可能在构造时运行相关代码。如:

@StandardException
public class MyException extends Exception {
}

产生的源码如下:

public class MyException extends Exception {
    @Generated
    public MyException() {
        this((String)null, (Throwable)null);
    }

    @Generated
    public MyException(String message) {
        this(message, (Throwable)null);
    }

    @Generated
    public MyException(Throwable cause) {
        this(cause != null ? cause.getMessage() : null, cause);
    }

    @Generated
    public MyException(String message, Throwable cause) {
        super(message);
        if (cause != null) {
            super.initCause(cause);
        }
    }
}

10、配置文件

Lombok提供了使用配置文件进行全局配置的方式。可以在任何目录中创建 lombok.config 文件,并在其中添加配置指令。这些指令将适用于该目录及其所有子目录中的所有源文件。

这种配置对于 lombok 中那些在整个项目中具有相同特性的可配置部分特别有用,例如日志变量的名称。该配置系统还可以用于指示 lombok 将不喜欢的某些 lombok 特性使用情况标记为警告甚至错误。

通常,Lombok 的用户会在工作区或项目根目录中放置一个包含其偏好设置的 lombok.config 文件,并使用特殊的 config.stopBubbling = true 键来告知 Lombok 这是根目录。然后,就可以在任何子目录(通常代表项目或源包)中创建具有不同设置的 lombok.config 文件。

使用的 Lombok 版本支持的所有配置键的最新列表可通过运行以下命令生成:

java -jar lombok-1.18.42.jar config -g --verbose

该配置工具的输出结果本身就是一个有效的 lombok.config 文件。内容比较多,就不贴出来了。

该配置工具还可以通过提供相应的目录或源文件作为参数来显示针对任何给定目录或源文件所使用的完整 lombok 配置。

以下是一些可用的配置选项示例,完整可用的可参考官网每一个注解的详情页:

# 告知 Lombok 这是根目录
config.stopBubbling = true 
# 设置是否在产生的源码中使用Generated注解,默认是true
lombok.addLombokGeneratedAnnotation = false
# 设置日志相关注解产生的日志实例名称,默认名是log
lombok.log.fieldName = log
# 设置toString的属性doNotUseGetters为true
lombok.toString.doNotUseGetters = true
# 下面的配置用于强制禁用或者不建议使用lombok的特性,值一般是WARNING、WARNING、ALLOW,默认一般是没有设置。
# Lombok.(featureName).flagUsage = WARNING | ERROR | ALLOW
# 下面是一些示例
lombok.experimental.flagUsage = [WARNING | ERROR | ALLOW]
lombok.accessors.flagUsage = [WARNING | ERROR | ALLOW]
lombok.allArgsConstructor.flagUsage = [WARNING | ERROR | ALLOW]
lombok.anyConstructor.flagUsage = [WARNING | ERROR | ALLOW]
lombok.builder.flagUsage = [WARNING | ERROR | ALLOW]
lombok.cleanup.flagUsage = [WARNING | ERROR | ALLOW]
lombok.data.flagUsage = [WARNING | ERROR | ALLOW]
lombok.delegate.flagUsage = [WARNING | ERROR | ALLOW]
lombok.equalsAndHashCode.flagUsage = [WARNING | ERROR | ALLOW]
lombok.experimental.flagUsage = [WARNING | ERROR | ALLOW]
lombok.extensionMethod.flagUsage = [WARNING | ERROR | ALLOW]
lombok.fieldDefaults.flagUsage = [WARNING | ERROR | ALLOW]
lombok.fieldNameConstants.flagUsage = [WARNING | ERROR | ALLOW]
lombok.getter.flagUsage = [WARNING | ERROR | ALLOW]
lombok.getter.lazy.flagUsage = [WARNING | ERROR | ALLOW]
lombok.helper.flagUsage = [WARNING | ERROR | ALLOW]
lombok.log.apacheCommons.flagUsage = [WARNING | ERROR | ALLOW]
lombok.log.flagUsage = [WARNING | ERROR | ALLOW]
lombok.log.flogger.flagUsage = [WARNING | ERROR | ALLOW]
lombok.log.javaUtilLogging.flagUsage = [WARNING | ERROR | ALLOW]
lombok.log.jbosslog.flagUsage = [WARNING | ERROR | ALLOW]
lombok.log.log4j.flagUsage = [WARNING | ERROR | ALLOW]
lombok.log.log4j2.flagUsage = [WARNING | ERROR | ALLOW]
lombok.log.slf4j.flagUsage = [WARNING | ERROR | ALLOW]
lombok.log.xslf4j.flagUsage = [WARNING | ERROR | ALLOW]
lombok.noArgsConstructor.flagUsage = [WARNING | ERROR | ALLOW]
lombok.nonNull.flagUsage = [WARNING | ERROR | ALLOW]
lombok.onX.flagUsage = [WARNING | ERROR | ALLOW]
lombok.requiredArgsConstructor.flagUsage = [WARNING | ERROR | ALLOW]
lombok.setter.flagUsage = [WARNING | ERROR | ALLOW]
lombok.sneakyThrows.flagUsage = [WARNING | ERROR | ALLOW]
lombok.synchronized.flagUsage = [WARNING | ERROR | ALLOW]
lombok.toString.flagUsage = [WARNING | ERROR | ALLOW]
lombok.utilityClass.flagUsage = [WARNING | ERROR | ALLOW]
lombok.val.flagUsage = [WARNING | ERROR | ALLOW]
lombok.value.flagUsage = [WARNING | ERROR | ALLOW]
lombok.var.flagUsage = [WARNING | ERROR | ALLOW]
lombok.wither.flagUsage = [WARNING | ERROR | ALLOW]

11、Delombok

通常情况下,Lombok会直接将所有功能集成到集成开发环境(IDE)和编译器中,通过与它们进行集成来实现。然而,Lombok并不涵盖所有工具。例如,它无法与 javadoc 进行集成,也无法与 Google Widget Toolkit 进行集成,因为这两者都是基于 Java 源代码运行的。但 Delombok仍然允许使用 Lombok与这些工具配合使用,通过预处理Java 代码,使其生成已应用了 lombok 所有转换的 Java 代码。
Delombok 当然还能帮助我们直接查看 Lombok 在"内部"所进行的操作。
Delombok的标准操作模式是:它会将整个目录复制到另一个目录中(采用递归方式),跳过类文件,并对遇到的任何 Java 源文件应用 Lombok 转换操作。

为了能在命令行方便执行此操作,我将lombok-1.18.42.jar 拷贝至Maven项目的src/main目录下,接着运行以下命令:

java -jar lombok-1.18.42.jar delombok java -d src-delomboked

执行后,虽然提示有些依赖的类没找到,无法完成编译,但已经在当前目录产生了 src-delomboked 目录,其中就是Lombok转换后的源码。

12、Lombok实现原理

Lombok 的核心功能是通过 注解处理器(Annotation Processor) 在编译期介入 Java 代码的编译过程,动态生成字节码,从而简化代码编写(如消除 getter/setter、构造方法等模板代码)。其底层原理可概括为:在编译阶段解析注解,生成对应代码,并整合到字节码中,而非运行时动态修改。它就是遵循APT实现的。

Pluggable Annotation Processing API(可插拔式注解处理 API,简称APT)是 Java 提供的一套标准 API,用于在编译期处理源代码中的注解(Annotation)。它允许开发者自定义注解处理器(Annotation Processor),通过扫描、解析注解,动态生成新的 Java 代码或修改现有代码结构,最终影响编译结果。

APT 是 Java 编译器(javac)的一部分,从 Java 6 开始正式引入,广泛用于代码生成、校验、简化开发等场景(例如 Lombok、Dagger、MapStruct 等主流库都基于 APT 实现核心功能)。

image-20251018102605387

Lombok 注解处理器的工作流程

  • 注册注解处理器:Lombok 会在 META-INF/services/javax.annotation.processing.Processor 文件中注册自己的注解处理器(lombok.launch.AnnotationProcessorHider$AnnotationProcessor),告诉 Java 编译器在编译时调用它。
  • 扫描注解:编译时,处理器会扫描源代码中所有带有 Lombok 注解的类、字段或方法(如 @Data 标注的类)。
  • 解析与生成代码:根据注解类型,处理器会分析类的结构(字段、方法等),并按照规则生成对应的代码(如为@Getter字段生成 getter 方法),同时修改 AST(抽象语法树)。
  • 传递给编译器后续阶段:修改后的 AST 会被传递给编译器的后续阶段(如类型检查、字节码生成),最终生成包含 Lombok 动态添加代码的 .class 文件。

与反射 / 字节码增强的区别:

  • 反射:运行时动态获取或修改类信息,性能有损耗,且无法修改已编译的字节码。
  • 字节码增强(如 ASM、CGLIB):通常在运行时或类加载时修改字节码,而 Lombok 是在 编译期 直接生成字节码,属于 “源头生成”,性能与原生代码一致。

IDE 支持的原理:

Lombok 能在 IDE(如 IntelliJ IDEA、Eclipse)中被识别,依赖于:

  • IDE 安装的 Lombok 插件:插件会模拟 Lombok 的注解处理逻辑,在 IDE 中动态生成代码的 “虚拟视图”(如在编辑器中显示 getter 方法),避免 IDE 报 “方法不存在” 的错误。
  • 启用注解处理:IDE 需要开启 “Annotation Processing” 功能,确保 Lombok 插件能参与代码解析。

在使用IDEA时,如果发现没有启用Annotation Processing,一般在右下角会弹出一下的窗口,点击按钮启用即可。

image-20251018101011225

手动设置的界面如下:

image-20251018101224523

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值