lombok 是什么?
lombok 是一個非常神奇的 java 類庫,會利用注解自動生成 java Bean 中煩人的 Getter、Setting,還能自動生成 logger、ToString、HashCode、Builder 等 java
特色的函數或是符合設計模式的函數,能夠讓你 java Bean 更簡潔,更美觀。
來先看下使用 lombok 后的 java bean 看起來是怎樣的
@Data
@AllArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
public static void main(String[] args) {
User user = new User(1L,"張三",18);
System.out.println("toString:"+user);
System.out.println("name:"+user.getName());
}
}
輸出如下
toString():User(id=1, name=張三, age=18)
getName:張三
看到了嗎,僅僅在類中添加 @Data 注解就能做到自動生成 Getter
和 ToString
函數了! 僅僅添加了 @AllArgsConstructor
注解就再也不用寫那些讓你心煩的構造函數了!
我第一次使用 lombok 的時候就很喜歡它了。覺得它的思想非常好的,即是不應該花時間去寫重復的代碼,應該讓之自動化。
當然自動化的手段是什么也很重要,我可以通過 ide 的生成器功能、或者是自己寫的代碼生成器,自動生成 Getter、Setting、ToString,就算不用 lombok 也是可以做到的。而我喜歡 lombok 最主要原因就在於它會讓代碼更簡潔、閱讀起來更清晰。
可能會有人說這玩意只適合個人項目,不適合大型合作。。。但知乎有位大神說過亞馬遜(重度使用 java 的公司) 內部的項目都在用。不管如何還是先了解再決定吧,我個人還是很喜歡用這種方式。
上面的介紹了一下 lombok 有點簡陋,所以下面會介紹得更詳細的一點。當然也許會有讀者對 lombok 這種像是魔法的寫法感到好奇,究竟它是怎么自動生成代碼的呢?所以文章的后面會對 lombok 進行簡單的探討,希望讀者會喜歡。
lombok 的安裝
安裝
得現代的依賴管理 ,引入 lombok 依賴及其簡單
meavn
使用 meavn 的朋友在 pom.xml 文件中添加依賴即可
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
</dependencies>
gradle
用 gradle 的朋友 在 build.gradle 在添加依賴即可
repositories {
mavenCentral()
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.6'
annotationProcessor 'org.projectlombok:lombok:1.18.6'
}
idea 的插件
你看 pom.xml 的依賴的作用域(scope) 是 provided,也就是說 lombok 是在編譯的時候才起作用的。因此 idea 在正確使用 lombok 的時候也會報錯的
所以你要安裝 lombok 插件才能正常使用。
使用
注解的類型
類型 | 解釋 |
---|---|
val,var | 神奇的類型推到,可以代表任意類型 |
@Getter and @Setter | |
@ToString | |
@EqualsAndHashCode | |
@NonNull | |
@AllArgsConstructor、@RequiredArgsConstructor、@NoArgsConstructor | 構造函數部分,針對不同情況的構造函數 |
@Data | 相當於 @Getter + @Setter + @ToString + @EqualsAndHashCode + RequiredArgsConstructor |
@Value | 類變成只讀模式 |
@Builder | builder 模式,會創建內 Builder |
@Singular | 要配合 builder 使用,會對(List、Set)等生成更方便函數 |
@Cleanup | 告別煩人的釋放的資源 |
@Log, @Log4j, @Log4j2, @Slf4j, @XSlf4j @CommonsLog, @JBossLog, @Flogger |
不同框架的日志注解 |
@SneakyThrows | 偷偷摸摸地拋出異常 |
@Delegate | 帶實驗性質的,能非常方便實現代理模式 |
@Accessors | 帶實驗性質的存取器 |
@Wither | 帶實驗性質的,根據被修飾的成員變量創建類 |
val,var
可以表示任何類型!
var 可以用來表示變量,類似其他語言中的 let
val 可以用來表示常量(final),類似其他語言中的 const
var str = "hello world";
val list = Arrays.asList(1,2,3,4);
System.out.println(str);
for(val item : list){
System.out.printf("%d\t",item);
}
等價於
String str = "hello world";
final List<Integer> list = Arrays.asList(1,2,3,4);
System.out.println(str);
for(final Integer item : list){
System.out.printf("%d\t",item);
}
@Getter、@Setter
添加了注解后會根據字段生成對應的 get、set 函數,可以修飾成員變量或者類
@Getter
@Setter
public class User {
private Long id;
private String name;
private Integer age;
}
靈活的 lombok 可以通過,下面的方式指定訪問級別(PUBLIC、PROTECTED、PACKAGE、PRIVATE)
@Getter
@Setter
public class User {
private Long id;
private String name;
@Setter(AccessLevel.PROTECTED)
private Integer age;
}
@ToString
@ToString
public class User {
private Long id;
private String name;
private Integer age;
}
ToString 生成后代碼大概如下
public String toString() {
return "User(id=" + this.id + ", name=" + this.name + ", age=" + this.age + ")";
}
選項:
1、@ToString(includeFieldNames=false) 不顯示變量名,會直接輸出值
public String toString() {
return "User(" + this.id + ", " + this.name + ", " + this.age + ")";
}
2、@ToString(exclude = {"age"}) 生成的結果會排出 age 變量
public String toString(){
return "User(id=" + this.id + ", name=" + this.name + ")";
}
3、@ToString(of = {"id","name"}) 生成的結果包括
public String toString(){
return "User(id=" + this.id + ", name=" + this.name + ")";
}
@EqualsAndHashCode
只能用於修飾類。
@EqualsAndHashCode
public class User {
//...
}
和 ToString 類似,可以用 of 以及 exclude 來排出成員變量
@NonNull
可以用於成員變量、本地變量、參數、方法
@Setter
public class User {
private Long id;
@NonNull
private String name;
private Integer age;
}
setName 函數實際上會變成這樣
public void setName(@NonNull String name) {
if (name == null) {
throw new NullPointerException("name is marked @NonNull but is null");
} else {
this.name = name;
}
}
構造函數 @AllArgsConstructor、@RequiredArgsConstructor、@NoArgsConstructor
這三者都是處理構造函數的注解,都只能修飾類,都能通過staticName
創建靜態工廠方法,使用access
控制訪問級別。
不同之處在於 @AllArgsConstructor
會把所有的成員變量都納入到構造函數中, @RequiredArgsConstructor
只會把 final
和 @NonNull
修飾的成員變量納入、@NoArgsConstructor
所有的成員變量都不會納入到構造函數。
@AllArgsConstructor
構造函數會包含所有字段
@AllArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
}
會自動生成
public User(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
關於 staticName
和 access
的選項,可以看下面的例子
@AllArgsConstructor(staticName = "of",access = AccessLevel.PRIVATE)
public class User {
private Long id;
private String name;
private Integer age;
}
會看到構造函數和靜態工廠函數的訪問級別都變成 private
了
public class User {
private Long id;
private String name;
private Integer age;
private User(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
private static User of(Long id, String name, Integer age) {
return new User(id, name, age);
}
}
@RequiredArgsConstructor
用 final
修飾和 @NonNull
修飾的參數才會加入構造函數
@RequiredArgsConstructor
public class User {
@NonNull
private Long id;
private final String name;
private Integer age;
}
生成的結果大概是這樣
public class User {
@NonNull
private Long id;
private final String name;
private Integer age;
public User(@NonNull Long id, String name) {
if (id == null) {
throw new NullPointerException("id is marked @NonNull but is null");
} else {
this.id = id;
this.name = name;
}
}
}
@NoArgsConstructor
顧名思義,使用 @NoArgsConstructor
會生成沒有參數的構造函數
但如果是用
final
修飾的成員函數呢?
答:這樣會編譯出錯的,除非是用 @NoArgsConstructor(force=true)
,那么所有的 final 字段會被定義為0,false,null等。
如果使用使用的是
@NonNull
修飾的成員字段呢?那么使用無參數的構造函數構造出來的實例成員變量不就是 null 了嗎?不就矛盾了嗎?
答:是的。。。
比如
@NoArgsConstructor
@Getter
public class User {
private Long id ;
private @NonNull String name;
private Integer age;
public static void main(String[] args) {
System.out.println(new User().getName());
}
}
輸出結果是 null
因此如果有 @NonNull
修飾的成員的變量就不要用 @NoArgsConstructor
修飾類
@Data
@Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor 就是那么的方便
選項,可以通過 staticConstructor
創建靜態工廠函數
@Data(staticConstructor = "of")
public class User {
private Long id ;
private @NonNull String name;
private Integer age;
}
@Value
將類變成只讀模式。會讓所有類成員變量都變成 final,然后 + @RequiredArgsConstructor + @ToString + @EqualsAndHashCode
@Builder
面對復雜的數據結構,使用 builder 模式可以抽離復雜的構造方式,能保證線程安全,我在這篇文章中也有對 Builder 的進行粗略的探討。
使用 Builder 模式很爽,比如是這樣的
User user = User.builder().id(1L)
.name("張三")
.age(12).build();
這樣雖然好爽,但
代價是什么呢?
就是好多冗余的東西要寫。比如是這樣
public class User {
private Long id;
private String name;
private Integer age;
User(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
public static User.UserBuilder builder() {
return new User.UserBuilder();
}
public static class UserBuilder {
private Long id;
private String name;
private Integer age;
UserBuilder() {
}
public User.UserBuilder id(Long id) {
this.id = id;
return this;
}
public User.UserBuilder name(String name) {
this.name = name;
return this;
}
public User.UserBuilder age(Integer age) {
this.age = age;
return this;
}
public User build() {
return new User(this.id, this.name, this.age);
}
public String toString() {
return "User.UserBuilder(id=" + this.id + ", name=" + this.name + ", age=" + this.age + ")";
}
}
}
用 lombok 后
@Builder
public class User {
private Long id;
private String name;
private Integer age;
}
清晰明了,爽!
但是這里有個很嚴重的問題,就是不符合 java bean 的規范,java bean 要求有一個無參數的構造函數的。不符號 java bean 要求會有什么后果能?比如:json 字符不能反序列化成 java 對象。
解決方式是寫成這樣,要同時寫上 @AllArgsConstructor 和 @NoArgsConstructor 才行
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
}
@Singular
Singular 要和 Builder 一起使用的,會對 List、Set 等集合類生出、處理 addOne、addAll、clear 方法
比如源碼是這樣的
@Builder
public class User {
private Long id;
private String name;
private Integer age;
private @Singular Set<String> Girlfriends;
}
生成的代碼 User 的靜態內部類 UserBuilder 會在增添
public User.UserBuilder Girlfriend(String Girlfriend) {
if (this.Girlfriends == null) {
this.Girlfriends = new ArrayList();
}
this.Girlfriends.add(Girlfriend);
return this;
}
public User.UserBuilder Girlfriends(Collection<? extends String> Girlfriends) {
if (this.Girlfriends == null) {
this.Girlfriends = new ArrayList();
}
this.Girlfriends.addAll(Girlfriends);
return this;
}
public User.UserBuilder clearGirlfriends() {
if (this.Girlfriends != null) {
this.Girlfriends.clear();
}
return this;
}
@Cleanup
處理煩人的資源釋放的神奇手段
在 java 7 之前的資源釋放是使用 try-catch-finally 的方式處理的,繁瑣而容易出錯
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(new File("a.txt"));
fos = new FileOutputStream("b.txt");
byte[] buffer = new byte[1024];
int len;
while ( (len = fis.read(buffer)) != -1){
fos.write(buffer,0,len);
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
當然 java 7 之后是使用簡化了一下,實現了 Closeable
的類可以用 try-with-resource 自動關閉連接
public static void main(String[] args) {
try (
FileInputStream fis = new FileInputStream(new File("a.txt"));
FileOutputStream fos = new FileOutputStream("b.txt");){
byte[] buffer = new byte[1024];
int len;
while ( (len = fis.read(buffer)) != -1){
fos.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
而 lombok 則只需添加 @Cleanup 則可以完成釋放資源,但同樣需要類本身實現了 Closeable
接口
try {
@Cleanup FileInputStream fis = new FileInputStream(new File("a.txt"));
@Cleanup FileOutputStream fos = new FileOutputStream("b.txt");
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
日志 @Log4j2
java 有很多日志的框架,這里用就只以 Log4j2 為例了
@Log4j2
public class User {
}
會生成
public class User {
private static final Logger log = LogManager.getLogger(User.class);
}
@SneakyThrows
算是一個比較有爭議的,意思是近悄悄地拋出異常,要謹慎使用。check exception 會轉成 unchecked 的。
@SneakyThrows(FileNotFoundException.class)
public static void read() {
FileReader reader = new FileReader("sdf.txt");
}
調用的使用也不用再聲明throws FileNotFoundException
public static void main(String[] args) {
read();
}
@Delegate
利用 @Delegate 能非常方便地實現代理模式。下面用個場景介紹一下,比如:有台代理服務器,有台文件服務器,而我們只能通過代理服務去訪問文件服務器。
最終調用大概如此
public class Main {
public static void main(String[] args) throws IOException {
ProxyServer proxyServer = new ProxyServer();
proxyServer.loadFile("avatar.png");
}
}
假設獲取接口是這樣的
public interface Server {
byte[] loadFile(String fileName) throws IOException;
}
文件服務器的簡單實現是如此
public class FileServer implements Server {
@Override
public byte[] loadFile(String fileName) throws IOException {
return Files.readAllBytes(new File(fileName).toPath());
}
}
不用 lombok 時要這樣
public class ProxyServer implements Server{
FileServer realServer = new FileServer();
@Override
public byte[] loadFile(String fileName) throws IOException {
return realServer.loadFile(fileName);
}
}
代理服務器利用 @Delegate 即可,簡單快捷
public class ProxyServer implements Server{
@Delegate
FileServer realServer = new FileServer();
}
@Accessors
使用 @Accessors 生成 Setter 會是鏈式的
@Accessors(fluent = true)
@Setter
public class User {
private Long id;
private String name;
private Integer age;
public User() {
}
public User id(Long id) {
this.id = id;
return this;
}
public User name(String name) {
this.name = name;
return this;
}
public User age(Integer age) {
this.age = age;
return this;
}
}
它有三個選項:(以 name 成員變量為例)
- fluent: 默認值是 false。如果是 false ,setter 生成的函數是
String setName(String name)
; 如果是 true, setter 是生成的是User name(String name)
,返回的是 this,是種鏈式 Setter,同時 chain 會是 true。 - chain: 默認值是 false。如果是 true,setter 生成的函數是
User setName(String name)
。否則的是void setName(String name)
- prefix: 比如成員變量是 gk_name,加上注解 @Accessors(prefix = "gk_"),生成的 Getter 和 Setter 中則沒有 gk_ 前綴
@Wither
用於修飾成員變量,可以根據用於被修飾的成員變量創建一個對象。
@AllArgsConstructor
public class User {
private Long id;
@Wither private String name;
private Integer age;
}
生成后的代碼
public class User {
private Long id;
private String name;
private Integer age;
public User(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
public User withName(String name) {
return this.name == name ? this : new User(this.id, name, this.age);
}
}
原理
或者你會好奇,這么神奇的魔法原理是什么呢?函數是如何生成的?
想想編譯原理 ,想想 java 文件編譯成 class 文件的過程,
Java 源碼編譯由以下三個過程組成:
- 詞法分析、語法分,輸出結果是 符號表 和 AST 語法樹
- 注解處理
- 語義分析和生成 class 文件
那么 lombok 是在那里添加要插入的代碼呢?估計是注解處理處理部分吧。
要如何生成呢?猜一下,估計可能是在注解處理時期 javac 能調用一個借口處理注解,我們並可以從中獲取當 AST 樹,然后就可以根據我們的想法,直接修改語法樹了
在網上搜索的時候找到這樣一篇文章《Project Lombok: Creating Custom Transformations》 及 stackoverflow 的 問題正好能解決我們的疑惑
lombok 的執行過程如下圖
和我猜想的差不多
上面一個答案說根據 JSR269 提案的 process 的 javax.annotation.processing.AbstractProcessor api 可能弄出來,還有一個回答說 lombok 做的東西比 process 多,還有針對 eclipse、javac 處理的 handle。找了一下文章看,確實如此,不僅設計的接口好,而且有豐富的例子可以給你參考。有關 java ast 的 jcTree 是有文檔,但我只找到 api 文檔,沒什么例子。但我下面還是會用 a
目標:單例模式
關於單例模式,我在之前的博客中也探討過了。
我選取其中的一個例子吧。希望生成的結果是這樣的
public class SingletonRegistry {
private SingletonRegistry() {}
private static class SingletonRegistryHolder {
private static SingletonRegistry instance = new SingletonRegistry();
}
public static SingletonRegistry getInstance() {
return SingletonRegistryHolder.instance;
}
// other methods
}
而源碼只需這樣就可以了,
@Singleton
public class SingletonRegistry{}
創建注解
在 com.jojo.annotation.processor 目錄下創建注解,其中
@Target(ElementType.TYPE)
意思是說注解只能修飾類,不能修飾方法、變量等,@Retention(RetentionPolicy.SOURCE)
意思是說,注解保留范圍是源代碼,也就是在編譯之后就看不到了,在 class 文件看不到,運行的時候用反射拿也就拿不到了
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Singleton {
}
HelloWorld
萬物起頭 Hello World。我們要確認下,實現 AbstractProcessor 接口后。是如何編譯、調用、調試的。
在 com.jojo.annotation.processor 創建 SingletonProcessor 類
@SupportedAnnotationTypes("com.jojo.annotation.processor.Singleton")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SingletonProcessor extends AbstractProcessor {
Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager(); //編譯的時候用於輸出
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
set.forEach(element -> {
note("hello world");
});
return true ;
}
//簡單封裝的函數
private void note(String message){
this.messager.printMessage(Diagnostic.Kind.NOTE, message);
}
}
編譯運行 命令行方式
在 com.jojo 中創建用來測試的類 SingletonRegistry
@Singleton
public class SingletonRegistry {
}
編譯是個問題,怎么編譯呢?編譯 SingletonRegistry.java 的時候就要編譯器(javac)看到 SingletonProcessor 才能處理啊。。。也就是說編譯 SingletonRegistry.java 要 SingletonProcessor 已經編譯好了。
先用命令行處理一下
在 maven 項目的根目錄下 創建 compile.sh
#!/usr/bin/env bash
if [ -d target ]; then
rm -rf target;
fi
mkdir target
source=src/main/java/com/jojo
javac -cp $JAVA_HOME/lib/tools.jar ${source}/annotation/*.java ${source}/processor/*.java -d target
javac -cp target -processor com.jojo.processor.SingletonProcessor ${source}/SingletonRegistry.java -d target
在命令行中執行 sh compile.sh
即可看到輸出 Note: hello world
debug
因為真的不知道該如何 debug 。之前一直用上面的方式看輸出進行調試。后面在 stackoverflower 上找到一篇文章了發現自己傻逼了
我簡單描述一下吧。在 meavn 上添加 google 的 auto-service
依賴
1. 添加依賴
<dependencies>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc4</version>
</dependency>
</dependencies>
這個東東能夠在打包成 jar 時候自動生成 META-INF/services/javax.annotation.processing.Processor
文件內容如下
com.jojo.processor.SingletonProcessor
javac 會檢測 jar 中 javax.annotation.processing.Processor 文件,並將文件對應的類注冊為 processor。編譯的時候就會調用到。
2. 添加 auto-service 注解
在 SingletonProcessor 下添加注解 @AutoService(Processor.class)
@SupportedAnnotationTypes("com.jojo.annotation.processor.Singleton")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class SingletonProcessor extends AbstractProcessor {
//....
}
添加后會在 javax.annotation.processing.Processor 文件中寫入被注解的類路徑。
3. maven 編譯配置
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
4. 配置調試器
配置參數結果如下
5. 創建 src/test/java 下創建測試類
類是以 Test 結尾,並添加上要用的注解
@Singleton
public class SingletonRegistryTest {
}
6. 在要調試的地方打上斷點
7. 在終端中輸入 mvnDebug clean install
這樣會進入 meavn debug 模式。運行后會看到輸出
Preparing to execute Maven in debug mode
Listening for transport dt_socket at address: 8000
8. 在 idea 中點擊運行按鈕
看到輸出 Connected to the target VM, address: 'localhost:8000', transport: 'socket'
也就連接成功,這樣會停在你打斷點的地方了
SingletonProcessor
回歸正傳,要生成一個上面所言的單例模式要怎樣做呢?
- 獲取注解對應的類的 AST 樹
- 添加一個私有的無參數構造器
- 添加一個靜態內聯類,內聯類里面要添加一個成員 instance 並完成初始化
- 添加一個成員函數,然后 instance
看起來不難,但實際上還真的有點煩的,下面是代碼的實現
1. 添加依賴
獲取 AST 樹要用到 sdk 中的tools.jar,所以要進入依賴
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
2. 配置工具類
public class SingletonProcessor extends AbstractProcessor {
private Messager messager;
private JavacTrees trees;
private TreeMaker treeMaker;
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager(); //用於編譯時的輸出
this.trees = JavacTrees.instance(processingEnv); //AST 樹
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context); //封裝了定義方法、變量、類等等的方法
this.names = Names.instance(context); //用於創建標識符
}
//還有更多的函數
}
3. 獲取語法樹
public class SingletonProcessor extends AbstractProcessor {
//...省略上面的
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//獲取被 Singleton 注解標注的類的集合
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
set.forEach(element -> {
//獲取到對應的 AST 樹
JCTree jcTree = trees.getTree(element);
});
return true;
}
}
我打了個斷點,你可以看到 jcTree 的定義是怎樣的。我關注的地方是 defs, 現在可以 defs 只定義了一個構造函數、名字也獨特叫
4. 創建構造函數
這個函數的目的是去掉默認的公有的無參數的構造函數、添加一個私有的無參數構造函數
private void createPrivateConstructor(JCTree.JCClassDecl singletonClass) {
JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PRIVATE);
JCTree.JCBlock block = treeMaker.Block(0L, nil());
JCTree.JCMethodDecl constructor = treeMaker
.MethodDef(
modifiers, //修飾符
names.fromString("<init>"), //函數名
null, //方法返回的類型
nil(), //泛型參數
nil(), //參數
nil(), //throw
block, //函數代碼塊,這里是空代碼塊
null); //默認值
//去掉默認的構造函數
ListBuffer<JCTree> out = new ListBuffer<>();
for (JCTree tree : singletonClass.defs) {
if (isPublicDefaultConstructor(tree)) {//是否公有無參數的構造函數
continue;
}
out.add(tree);
}
out.add(constructor);
singletonClass.defs = out.toList();
}
private boolean isPublicDefaultConstructor(JCTree jcTree) {
if (jcTree.getKind() == Tree.Kind.METHOD) {
JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) jcTree;
if (isConstructor(jcMethodDecl)
&& isNoArgsMethod(jcMethodDecl)
&& isPublicMethod(jcMethodDecl)) {
return true;
}
}
return false;
}
private static boolean isConstructor(JCTree.JCMethodDecl jcMethodDecl) {
String name = jcMethodDecl.name.toString();
if ("<init>".equals(name)) {
return true;
}
return false;
}
private static boolean isNoArgsMethod(JCTree.JCMethodDecl jcMethodDecl) {
List<JCTree.JCVariableDecl> jcVariableDeclList = jcMethodDecl.getParameters();
if (jcVariableDeclList == null
|| jcVariableDeclList.size() == 0) {
return true;
}
return false;
}
private boolean isPublicMethod(JCTree.JCMethodDecl jcMethodDecl) {
JCTree.JCModifiers jcModifiers = jcMethodDecl.getModifiers();
Set<Modifier> modifiers = jcModifiers.getFlags();
if (modifiers.contains(Modifier.PUBLIC)) {
return true;
}
return false;
}
在 process 函數中處理構造函數
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
set.forEach(element -> {
JCTree jcTree = trees.getTree(element);
//修改 jcTree 的方式,可以修改類的定義
jcTree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
createPrivateConstructor(jcClassDecl);
}
});
});
return true ;
}
在終端中輸入 mvn clean install
,編譯成功后,你可以在 target/test-classes/ 中看到編譯后的 SingletonRegistryTest。
javap 反編譯看一下 javap -p SingletonRegistryTest.class
Compiled from "SingletonRegistryTest.java"
public class SingletonRegistryTest {
private SingletonRegistryTest();
}
5. 創建靜態內聯類
要達到的結果是這樣的
private static class SingletonRegistryHolder {
private static SingletonRegistry instance = new SingletonRegistry();
}
private JCTree.JCClassDecl createInnerClass(JCTree.JCClassDecl singletonClass) {
JCTree.JCClassDecl innerClass = treeMaker.ClassDef(
treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC),
names.fromString(singletonClass.name+"Holder"), //類名
nil(), //泛型參數
null, //extending
nil(), //implementing
nil() //類定義的詳細語句,包括字段,方法定義等
);
addInstanceVar(innerClass, singletonClass); //給類添加添加 instance變量
singletonClass.defs = singletonClass.defs.append(innerClass);
return innerClass;
}
private void addInstanceVar(JCTree.JCClassDecl innerClass, JCTree.JCClassDecl singletonClass) {
JCTree.JCIdent singletonClassType = treeMaker.Ident(singletonClass.name); //獲取注解的類型
//new SingletonRegistry() 的語句
JCTree.JCNewClass newKeyword = treeMaker.NewClass( null, //encl,enclosingExpression lambda 箭頭嗎?不太清楚
nil(), //參數類型列表
singletonClassType, //待創建對象的類型
nil(), //參數蕾西
null); //類定義
JCTree.JCModifiers fieldMod = treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL);
//定義變量
JCTree.JCVariableDecl instanceVar = treeMaker.VarDef(
fieldMod, //修飾符
names.fromString("instance"), //變量名
singletonClassType, //類型
newKeyword); //賦值語句
innerClass.defs = innerClass.defs.prepend(instanceVar);
}
6. 創建一個成員函數,返回內聯類中的 instance 變量
目標完成
public static SingletonRegistry getInstance() {
return SingletonRegistryHolder.instance;
}
private void createReturnInstance(JCTree.JCClassDecl singletonClass,JCTree.JCClassDecl innerClass){
JCTree.JCModifiers fieldMod = treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC);
JCTree.JCIdent singletonClassType = treeMaker.Ident(singletonClass.name);
//獲取 return 語句塊
JCTree.JCBlock body = addReturnBlock(innerClass);
//創建方法
JCTree.JCMethodDecl methodDec = treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC|Flags.STATIC),
this.names.fromString("getInstance"),
singletonClassType, nil(), nil(), nil(), body , null);
singletonClass.defs = singletonClass.defs.prepend(methodDec);
}
private JCTree.JCBlock addReturnBlock(JCTree.JCClassDecl holderInnerClass) {
JCTree.JCIdent holderInnerClassType = treeMaker.Ident(holderInnerClass.name);
JCTree.JCFieldAccess instanceVarAccess = treeMaker.Select(holderInnerClassType, names.fromString("instance")); //獲取內聯類中的靜態變量
JCTree.JCReturn returnValue = treeMaker.Return(instanceVarAccess);//創建 return 語句
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(returnValue);
return treeMaker.Block(0L, statements.toList());
}
最后的 processor 函數像是這樣的
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
set.forEach(element -> {
JCTree jcTree = trees.getTree(element);
jcTree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
createPrivateConstructor(jcClassDecl);
JCTree.JCClassDecl innerClass= createInnerClass(jcClassDecl);
createReturnInstance(jcClassDecl,innerClass);
}
});
});
return true;
}
完成了,mvn clean install
一下,使用 idea 查看對應的 .class 文件,生成的結果就好漂亮了。
public class SingletonRegistryTest {
public static SingletonRegistryTest getInstance() {
return SingletonRegistryTest.SingletonRegistryTestHolder.instance;
}
private SingletonRegistryTest() {
}
private static class SingletonRegistryTestHolder {
private static final SingletonRegistryTest instance = new SingletonRegistryTest();
private SingletonRegistryTestHolder() {
}
}
}
# 參考鏈接
- [lombok-custom-annotation](https://www.baeldung.com/lombok-custom-annotation)
- [java-annotation-processing-builder](https://www.baeldng.com/java-annotation-processing-builder)
- [lombok 指南](https://zhuanlan.zhihu.com/p/30318534)