Lombok一文通

发布于:2024-06-01 ⋅ 阅读:(123) ⋅ 点赞:(0)

1、Lombok简介

作为java的忠实粉丝,但也不得不承认,java是一门比较啰嗦的语言,很多代码的编写远不如其他静态语言方便,更别说跟脚本语言比较了。

因此,lombok应运而生。

Lombok是一种工具库,它提供了一组注解,用于简化Java代码的编写。它的主要目标是通过自动生成代码来减少样板代码的数量,从而提高开发人员的生产力。

使用Lombok,开发人员可以通过添加注解来自动生成getter和setter方法、构造函数、equals和hashCode方法,以及其他常见的样板代码。此外,Lombok还提供了一些其他的注解,可以自动生成日志记录、单例模式、Builder模式等。

Lombok的使用非常简单,只需在类或字段上添加注解即可。在编译时,Lombok会自动生成相应的代码,并将其写入源代码中。这意味着生成的代码会被编译器视为原始代码的一部分,从而允许开发人员在使用生成的代码时获得代码补全和其他IDE功能的支持。

总的来说,Lombok是一个能够减少Java代码冗余的有用工具,它可以帮助开发人员更高效地编写代码,提高开发速度和代码质量。

项目地址: lombok官网

2、Lombok原理

用了那么久lombok,就是觉得写代码方便很多了,但它底层的原理是什么呢?

Lombok 的核心原理是在编译时通过注解处理器(Annotation Processor)来操作抽象语法树(Abstract Syntax Tree, AST),从而扩展 Java 编译器的功能。

2.1.Lombok工作原理

Lombok 的工作原理主要包括以下几个步骤:

  1. 注解定义: Lombok 定义了一系列注解,这些注解可以被应用于 Java 类、方法、字段等元素上。

  2. 注解处理器: Lombok 包含了一个或多个注解处理器,这些处理器在编译时期被触发。当处理器检测到 Lombok 注解时,它会根据注解的配置生成相应的代码。

  3. 编译时代码生成: 使用 Lombok 注解的 Java 类在编译时,Lombok 的注解处理器会读取注解信息,并根据这些信息生成额外的 Java 代码。例如,当在字段上使用 @Getter@Setter 注解时,Lombok 会生成对应的 getter 和 setter 方法。

  4. AST 操作: Lombok 通过操作 Java 源文件的 AST 来插入生成的代码。AST 是源代码的树状结构表示,Lombok 通过修改 AST 来实现代码的自动添加。

  5. 源代码注入: 生成的代码会被注入到源文件中,通常作为单独的文件或在原有文件中以特殊的方式添加,以便编译器能够识别并编译这些新增的代码。

  6. IDE 集成: Lombok 还提供了对 IntelliJ IDEA 和 Eclipse 等 IDE 的支持。这意味着在使用这些 IDE 时,你可以获得完整的 Lombok 注解支持,包括代码补全、导航、重构等特性。

  7. 构建工具集成: Lombok 与 Maven 和 Gradle 等构建工具集成,确保在构建项目时能够正确处理 Lombok 注解。

  8. 无运行时依赖: 由于 Lombok 在编译时生成代码,因此它不会在运行时添加任何依赖或性能开销。

2.2.idea为什么需要引入lombok插件

Lombok 通过注解来自动生成代码,但这些注解发生在编译期。但我们使用ide进行开发的时候,在编写代码就已经需要知道注解的语义,因此通过插件,以便 IDE 能够理解 Lombok 注解并提供相应的代码补全、导航和错误检查功能。

  1. 注解处理器集成: Lombok 作为一个 Java 库,通过注解处理器在编译时生成额外的代码。IntelliJ IDEA 插件确保了这些注解处理器被正确集成到 IDE 中。

  2. 代码补全: 插件为 Lombok 提供的注解(如 @Getter, @Setter, @ToString 等)提供代码补全功能,使得开发更加高效。

  3. 导航和查找: 使用 Lombok 插件,IDEA 可以正确导航到 Lombok 生成的方法和构造函数,就像它们是手写的一样。

  4. 错误检查和代码分析: 插件允许 IDEA 对 Lombok 注解进行语义分析,提供错误和警告信息,帮助开发者避免潜在的问题。

  5. 预览生成的代码: 在某些情况下,插件允许开发者预览 Lombok 注解生成的代码,这有助于理解背后的行为。

3、插入式注解处理器

在 Java 中,插入式注解处理器(Pluggable Annotation Processor)允许开发者在编译时期自动处理特定的注解。下面是一个简单的示例,演示如何创建和使用自定义注解以及相应的注解处理器。

1.创建自定义注解

首先,定义一个注解,用来标记需要生成额外代码的类。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE) // 标记在类上
public @interface GenerateClass {
    String value() default "DefaultClassName";
}

2.创建注解处理器

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.Processor;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
import com.google.auto.service.AutoService;
import com.example.annotations.GenerateClass;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.annotation.processing.Filer;
import javax.tools.JavaFileObject;
import java.io.Writer;
import java.util.Set;

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.example.annotations.GenerateClass")
@SupportedSourceVersion(javax.lang.model.SourceVersion.RELEASE_8)
public class GenerateClassProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(GenerateClass.class)) {
            GenerateClass generateClass = element.getAnnotation(GenerateClass.class);
            String className = generateClass.value();
            try {
                Filer filer = processingEnv.getFiler();
                JavaFileObject builderFile = filer.createSourceFile(className);
                try (Writer writer = builderFile.openWriter()) {
                    writer.write("public class " + className + " {\n");
                    writer.write("    public " + className + "() {\n");
                    writer.write("        System.out.println(\\\"New class generated!\\\");\n");
                    writer.write("    }\n");
                    writer.write("}\n");
                }
            } catch (Exception e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
            }
        }
        return true;
    }
}

3.注册注解处理器

如果你使用了 Google 的 AutoService,则注解处理器会自动注册。否则,你需要在 META-INF/services 目录下创建一个名为 javax.annotation.processing.Processor 的文件,并填入你的处理器的全限定名。

import com.example.annotations.GenerateClass;

@GenerateClass(value = "MyGeneratedClass")
public class MyClass {
    // 这个类会被注解处理器处理,并生成 MyGeneratedClass
}

4.编译项目

编译你的项目,注解处理器将自动运行,并根据 GenerateClass 注解生成新的类文件。

注意事项:

  • 注解处理器是在编译时执行的,因此它们不会影响运行时性能。
  • 确保你的注解处理器类被正确注册,以便编译器能够发现并使用它。
  • 使用 @SupportedAnnotationTypes 和 @SupportedSourceVersion 注解来声明你的处理器支持哪些注解和 Java 版本。
  • 处理过程中发生的任何异常都需要被捕获并适当处理,通常通过 processingEnv.getMessager().printMessage() 打印错误信息。

4、Lombok示例

3.1.javabean注解

注解名称 作用
@Getter 用于自动生成字段的get方法。作为类注解,则生成该类所有字段的getter方法(static字段不参与);作为字段注解,则生成该字段的getter方法。
@Setter 用于自动生成字段的set方法。作为类注解,则生成该类所有字段的setter方法(final/static字段不参与);作为字段注解,则生成该字段的setter方法。
@ToString

自动生成 toString() 方法,包括类的所有实例字段和final字段,不包括static字段。

@EqualsAndHashCode 自动生成 equals() 和 hashCode() 方法
@NoArgsConstructor 自动生成无参构造函数
@AllArgsConstructor 自动生成包含所有字段的构造函数
@RequiredArgsConstructo 自动生成包含所有非 final 和非静态字段的构造函数,并标记为 @NonNull 的字段
@Data 组合注解,相当于 @Getter@Setter@RequiredArgsConstructor@ToString, 和 @EqualsAndHashCode
@Value 类似 @Data,但生成不可变对象(所有字段都是 final

 现在对@Data注解进行演示

假设一个普通的javabean定义如下:

import lombok.Data;

@Data
public class Person {

    private int age;

    private String name;

    private static String key = "key";

    private final String finalField = "FINALFIELD";
}

经过lombok插件的翻译之后,变成:

import java.util.Objects;

public class Person {
    private int age;
    private String name;
    private static String key = "key";
    private final String finalField = "FINALFIELD";

    public Person() {
    }

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

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

    public String getFinalField() {
        Objects.requireNonNull(this);
        return "FINALFIELD";
    }

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

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

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Person)) {
            return false;
        } else {
            Person other = (Person)o;
            if (!other.canEqual(this)) {
                return false;
            } else if (this.getAge() != other.getAge()) {
                return false;
            } else {
                Object this$name = this.getName();
                Object other$name = other.getName();
                if (this$name == null) {
                    if (other$name != null) {
                        return false;
                    }
                } else if (!this$name.equals(other$name)) {
                    return false;
                }

                Object this$finalField = this.getFinalField();
                Object other$finalField = other.getFinalField();
                if (this$finalField == null) {
                    if (other$finalField != null) {
                        return false;
                    }
                } else if (!this$finalField.equals(other$finalField)) {
                    return false;
                }

                return true;
            }
        }
    }

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

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        result = result * 59 + this.getAge();
        Object $name = this.getName();
        result = result * 59 + ($name == null ? 43 : $name.hashCode());
        Object $finalField = this.getFinalField();
        result = result * 59 + ($finalField == null ? 43 : $finalField.hashCode());
        return result;
    }

    public String toString() {
        int var10000 = this.getAge();
        return "Person(age=" + var10000 + ", name=" + this.getName() + ", finalField=" + this.getFinalField() + ")";
    }
}

3.2.@Builder注解

@Builder也是一个非常常用的注解,为类提供构建器模式支持,允许链式调用。对于上面的Person定义,使用该注解之后,编译器会自动生成一个Builder类。代码大致如下:

public class Person$PersonBuilder {
    private int age;
    private String name;

    Person$PersonBuilder() {
    }

    public Person$PersonBuilder age(final int age) {
        this.age = age;
        return this;
    }

    public Person$PersonBuilder name(final String name) {
        this.name = name;
        return this;
    }

    public Person build() {
        return new Person(this.age, this.name);
    }

    public String toString() {
        return "Person.PersonBuilder(age=" + this.age + ", name=" + this.name + ")";
    }
}

如此,客户端便可已链式的方式进行创建实例,如下:

Person p = Person.builder().age(1).name("Tom").build();

3.3.日志类注解

lombok为各种常见的日志框架提供简易注解,有如下几个注解。

需要注意的是,

log注解只能用于类,接口,enum,或者record等。

注解只提供代码上的引用,项目仍然需要引入相关的依赖及配置才能使日志生效。

注解名称 作用
@Log
jdk官方log接口
@Log4j
log4j日志接口
@Log4j2 log4j2日志接口
@Slf4j Slf4j日志门面接口

下面以@slf4j注解进行演示说明

@Slf4j
public class TestLombok {

    public static void main(String[] args) {
        log.info("此处是日志");
    }
}

翻译的代码如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestLombok {
    private static final Logger log = LoggerFactory.getLogger(TestLombok.class);

    public TestLombok() {
    }

    public static void main(String[] args) {
        log.info("此处是日志");
    }
}

3.4.@Cleanup注解

在jdk1.7之前,对于io资源的使用,一般是使用类似的代码,需要在io流使用结束后关闭资源。

import java.io.FileInputStream;

public class Test {

    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("input.txt");
        } catch (Exception e) {

        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (Exception ignore) {

                }
            }
        }
    }
}

jdk1.7引入了try-with-resources语句块,可以自动处理单个或多个资源的关闭。类似下面的语法。

    public static void main(String[] args) {
        try ( FileInputStream fis = new FileInputStream("input.txt")){
           // do sth
        } catch (Exception ignored) {

        } 
    }

@Cleanup做的也是类似的工作。个人觉得该注解比较鸡肋,因为jdk已经有官方的实现了。

源代码如下:

import lombok.Cleanup;

import java.io.FileInputStream;

public class Test {

    public static void main(String[] args) throws Exception {
        @Cleanup var file1 = new FileInputStream("input.txt");
        @Cleanup var file2 = new FileInputStream("input.txt");
        @Cleanup var file3 = new FileInputStream("input.txt");
    }
}

翻译的代码如下:

import java.io.FileInputStream;
import java.util.Collections;

public class Test {
    public Test() {
    }

    public static void main(String[] args) throws Exception {
        FileInputStream file1 = new FileInputStream("input.txt");

        try {
            FileInputStream file2 = new FileInputStream("input.txt");

            try {
                FileInputStream file3 = new FileInputStream("input.txt");
                if (Collections.singletonList(file3).get(0) != null) {
                    file3.close();
                }
            } finally {
                if (Collections.singletonList(file2).get(0) != null) {
                    file2.close();
                }

            }
        } finally {
            if (Collections.singletonList(file1).get(0) != null) {
                file1.close();
            }

        }

    }
}

当然,lombok还有其他很多注解,但可能使用频率没那么广泛,感兴趣的读者可以自行查阅。


网站公告

今日签到

点亮在社区的每一天
去签到