Lombok是什么樣的原理

相信大家在項(xiàng)目中都使用過 Lombok,因?yàn)槟軌蚝喕覀冊S多的代碼,但是該有的功能一點(diǎn)也不少。
那么lombok到底是個(gè)什么呢,lombok是一個(gè)可以通過簡單的注解的形式來幫助我們簡化消除一些必須有但顯得很臃腫的 Java 代碼的工具,簡單來說,比如我們新建了一個(gè)類,然后在其中寫了幾個(gè)字段,然后通常情況下我們需要手動(dòng)去建立getter和setter方法啊,構(gòu)造函數(shù)啊之類的,lombok的作用就是為了省去我們手動(dòng)創(chuàng)建這些代碼的麻煩,它能夠在我們編譯源碼的時(shí)候自動(dòng)幫我們生成這些方法。
那么Lombok到底是如何做到這些的呢?其實(shí)底層就是用到了編譯時(shí)注解的功能。
1
Lombok如何使用
Lombok是一個(gè)開源項(xiàng)目,代碼是在lombok中,如果是gradle項(xiàng)目的話直接在項(xiàng)目中引用如下即可。
compile ("org.projectlombok:lombok:1.16.6")功能
那么Lombok是做什么呢?其實(shí)很簡單,一個(gè)最簡單的例子就是能夠通過添加注解自動(dòng)生成一些方法,使我們代碼更加簡潔易懂。例如下面一個(gè)類。
@Data
public class TestLombok {
private String name;
private Integer age;
public static void main(String[] args) {
TestLombok testLombok = new TestLombok();
testLombok.setAge(12);
testLombok.setName("zs");
}
}我們使用Lombok提供的Data注解,在沒有寫get、set方法的時(shí)候也能夠使用其get、set方法。看它編譯過后的class文件,可以看到它給我們自動(dòng)生成了get、set方法。
public class TestLombok {
private String name;
private Integer age;
public static void main(String[] args) {
TestLombok testLombok = new TestLombok();
testLombok.setAge(12);
testLombok.setName("zs");
}
public TestLombok() {
}
public String getName() {
return this.name;
}
public Integer getAge() {
return this.age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
}當(dāng)然Lombok的功能不止如此,還有很多其他的注解幫助我們簡便開發(fā),網(wǎng)上有許多的關(guān)于Lombok的使用方法,這里就不再啰嗦了。正常情況下我們在項(xiàng)目中自定義注解,或者使用Spring框架中@Controller、@Service等等這類注解都是運(yùn)行時(shí)注解,運(yùn)行時(shí)注解大部分都是通過反射來實(shí)現(xiàn)的。而Lombok是使用編譯時(shí)注解實(shí)現(xiàn)的。那么編譯時(shí)注解是什么呢?
2
編譯時(shí)注解
注解(也被成為元數(shù)據(jù))為我們在代碼中添加信息提供了一種形式化的方法,使我們可以在稍后某個(gè)時(shí)刻非常方便地使用這些數(shù)據(jù)。——————摘自《Thinking in Java》
Java中的注解分為運(yùn)行時(shí)注解和編譯時(shí)注解,運(yùn)行時(shí)注解就是我們經(jīng)常使用的在程序運(yùn)行時(shí)通過反射得到我們注解的信息,然后再做一些操作。而編譯時(shí)注解是什么呢?就是在程序在編譯期間通過注解處理器進(jìn)行處理。
編譯期:Java語言的編譯期是一段不確定的操作過程,因?yàn)樗赡苁菍?.java文件轉(zhuǎn)化成*.class文件的過程;也可能是指將字節(jié)碼轉(zhuǎn)變成機(jī)器碼的過程;還可能是直接將*.java編譯成本地機(jī)器代碼的過程
運(yùn)行期:從JVM加載字節(jié)碼文件到內(nèi)存中,到最后使用完畢以后卸載的過程都屬于運(yùn)行期的范疇。
3
注解處理工具apt
注解處理工具apt(Annotation Processing Tool),這是Sun為了幫助注解的處理過程而提供的工具,apt被設(shè)計(jì)為操作Java源文件,而不是編譯后的類。
它是javac的一個(gè)工具,中文意思為編譯時(shí)注解處理器。APT可以用來在編譯時(shí)掃描和處理注解。通過APT可以獲取到注解和被注解對象的相關(guān)信息,在拿到這些信息后我們可以根據(jù)需求來自動(dòng)的生成一些代碼,省去了手動(dòng)編寫。
注意,獲取注解及生成代碼都是在代碼編譯時(shí)候完成的,相比反射在運(yùn)行時(shí)處理注解大大提高了程序性能。APT的核心是AbstractProcessor類。
正常情況下使用APT工具只是能夠生成一些文件(不僅僅是我們想象的class文件,還包括xml文件等等之類的),并不能修改原有的文件信息。
但是此時(shí)估計(jì)會有疑問,那么Lombok不就是在我們原有的文件中新增了一些信息嗎?我在后面會有詳細(xì)的解釋,這里簡單介紹一下,其實(shí)Lombok是修改了Java中的**抽象語法樹AST**才做到了修改其原有類的信息。
接下來我們演示一下如何用APT工具生成一個(gè)class文件,然后我們再說Lombok是如何修改已存在的類中的屬性的。
定義注解
首先,當(dāng)然我們需要定義自己的注解了
@Retention(RetentionPolicy.SOURCE) // 註解只在源碼中保留
@Target(ElementType.TYPE) // 用於修飾類
public @interface GeneratePrint {
String value();
}Retention注解上面有一個(gè)屬性value,它是RetentionPolicy類型的枚舉類,RetentionPolicy枚舉類中有三個(gè)值。
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}SOURCE修飾的注解:修飾的注解,表示注解的信息會被編譯器拋棄,不會留在class文件中,注解的信息只會留在源文件中
CLASS修飾的注解:表示注解的信息被保留在class文件(字節(jié)碼文件)中當(dāng)程序編譯時(shí),但不會被虛擬機(jī)讀取在運(yùn)行的時(shí)候
RUNTIME修飾的注解:表示注解的信息被保留在class文件(字節(jié)碼文件)中當(dāng)程序編譯時(shí),會被虛擬機(jī)保留在運(yùn)行時(shí)。所以它能夠通過反射調(diào)用,所以正常運(yùn)行時(shí)注解都是使用的這個(gè)參數(shù)
Target注解上面也有個(gè)屬性value,它是ElementType類型的枚舉。是用來修飾此注解作用在哪的。
public enum ElementType {
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE
}定義注解處理器
我們要定義注解處理器的話,那么就需要繼承AbstractProcessor類。繼承完以后基本的框架類型如下:
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("aboutjava.annotion.MyGetter")
public class MyGetterProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
return true;
}
}我們可以看到在子類中上面有兩個(gè)注解,注解描述如下
@SupportedSourceVersion:表示所支持的Java版本
@SupportedAnnotationTypes:表示該處理器要處理的注解
繼承了父類的兩個(gè)方法,方法描述如下:
init方法:主要是獲得編譯時(shí)期的一些環(huán)境信息
process方法:在編譯時(shí),編譯器執(zhí)行的方法。也就是我們寫具體邏輯的地方
我們是演示一下如何通過繼承AbstractProcessor類來實(shí)現(xiàn)在編譯時(shí)生成類,所以我們在process方法中書寫我們生成類的代碼。如下所示。
@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
StringBuilder builder = new StringBuilder()
.append("package aboutjava.annotion;\n\n")
.append("public class GeneratedClass {\n\n") // open class
.append("\tpublic String getMessage() {\n") // open method
.append("\t\treturn \"");
// for each javax.lang.model.element.Element annotated with the CustomAnnotation
for (Element element : roundEnv.getElementsAnnotatedWith(MyGetter.class)) {
String objectType = element.getSimpleName().toString();
// this is appending to the return statement
builder.append(objectType).append(" says hello!\\n");
}
builder.append("\";\n") // end return
.append("\t}\n") // close method
.append("}\n"); // close class
try { // write the file
JavaFileObject source = processingEnv.getFiler().createSourceFile("aboutjava.annotion.GeneratedClass");
Writer writer = source.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
// Note: calling e.printStackTrace() will print IO errors
// that occur from the file already existing after its first run, this is normal
}
return true;
}定義使用注解的類(測試類)
上面的兩個(gè)類就是基本的工具類了,一個(gè)是定義了注解,一個(gè)是定義了注解處理器,接下來我們來定義一個(gè)測試類(TestAno.java)。我們在類上面加上我們自定的注解類。
@MyGetter
public class TestAno {
public static void main(String[] args) {
System.out.printf("1");
}
}這樣我們在編譯期就能生成文件了,接下來演示一下在編譯時(shí)生成文件,此時(shí)不要著急直接進(jìn)行javac編譯,MyGetter類是注解類沒錯(cuò),而MyGetterProcessor是注解類的處理器,那么我們在編譯TestAnoJava文件的時(shí)候就會觸發(fā)處理器。因此這兩個(gè)類是無法一起編譯的。
先給大家看一下我的目錄結(jié)構(gòu)
aboutjava2
-- annotion3
-- MyGetter.java4
-- MyGetterProcessor.java5
-- TestAno.java所以我們先將注解類和注解處理器類進(jìn)行編譯
javac aboutjava/annotion/MyGett*
接下來進(jìn)行編譯我們的測試類,此時(shí)在編譯時(shí)需要加上processor參數(shù),用來指定相關(guān)的注解處理類。
javac -processor aboutjava.annotion.MyGetterProcessor aboutjava/annotion/TestAno.java
大家可以看到動(dòng)態(tài)圖中,自動(dòng)生成了Java文件。

