前言

Lombok的出現幫助開發人員在開發工程中消除了大部分冗餘程式碼:繁瑣的get、set方法甚至建造者模式。

Lombok的實現方式是什麼呢?

新建一個測試類使用Lombok的Getter和Setter註解,通過IDEA進行編譯

import lombok.Getter;
import lombok.Setter; @Getter
@Setter
public class UserInfo {
private String userId; private String userName;
}

開啟編譯後生成的UserInfo.class檔案



發現已經生成了get、set方法,由此可以推斷出Lombok是在編譯期為程式碼進行了增強,那麼在編譯期進行增強是如何實現的?

編譯階段

在JDK6提出並通過了JSR-269提案,提案通過了一組被稱為“插入式註解處理器”的標準API,可以提前至編譯期對程式碼中的特定註解進行處理, 從而影響到編譯器的工作過程。

對於底層的一些實現,普遍會認為實現是像虛擬機器一樣使用C++實現,對於Java程式設計師來說並不是特別友好。但是Javac編譯器是使用Java實現的更容易上手。

Javac的編譯過程大致分為幾步

  1. 準備過程:初始化插入式註解處理器
  2. 解析與填充符號表過程 詞法、語法分析構建抽象語法樹(AST)
  3. 插入式註解處理器的註解處理過程
  4. 分析與位元組碼生成過程

    語法樹變動後會再次解析與填充符號表,語法樹沒有變動時編譯器就不會再對原始碼字元流操作,而是基於抽象語法樹



    綜上所述想實現Lombok的效果只需要遵守JSR-269在編譯期對AST進行操作即可實現

    當然不止有Lombok通過這種方式實現,例如FindBug、MapStruct等也通過這種方式實現

實現

JCTree

JCTree是AST元素的基類,想實現效果只需要新增JCTree節點即可

JCTree是一個抽象類部分實現



從類名可以猜到是什麼節點使用的這裡挑幾個常用的解釋

JCStatement 宣告語法樹節點

JCBlock 語法塊

JCReturn:return語句語法樹節點

JCClassDecl:類定義語法樹節點

JCVariableDecl:欄位/變數定義語法樹節點

JCMethodDecl:方法定義語法樹節點

JCModifiers:訪問標誌語法樹節點

JCExpression:表示式語法樹節點,常見的子類如下

JCAssign:賦值語句語法樹節點

JCIdent:識別符號語法樹節點 例如this

TreeMaker

主要用於生成語法樹節點

程式碼

首先需要註解類,標明作用的範圍和作用的時期,兩個類分別對應Lombok的Getter、Setter

@Target({ElementType.TYPE}) //加在類上的註解
@Retention(RetentionPolicy.SOURCE) //作用於編譯期
public @interface Getter {
}
@Target({ElementType.TYPE}) //加在類上的註解
@Retention(RetentionPolicy.SOURCE) //作用於編譯期
public @interface Setter {
}

新建抽象註解處理器需要繼承AbstractProcessor,這裡使用模板方法模式

public abstract class MyAbstractProcessor extends AbstractProcessor {
//語法樹
protected JavacTrees trees; //構建語法樹節點
protected TreeMaker treeMaker; //建立識別符號的物件
protected Names names; @Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
} @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//獲取註解標識的類
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(getAnnotation());
//拿到語法樹
set.stream().map(element -> trees.getTree(element)).forEach(jcTree -> jcTree.accept(new TreeTranslator() {
//拿到類定義
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
//拿到所有成員變數
for (JCTree tree : jcClassDecl.defs) {
if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
}
} jcVariableDeclList.forEach(jcVariableDecl -> {
jcClassDecl.defs = jcClassDecl.defs.prepend(makeMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
}));
return true;
} /**
* 建立方法
* @param jcVariableDecl
* @return
*/
public abstract JCTree.JCMethodDecl makeMethodDecl(JCTree.JCVariableDecl jcVariableDecl); /**
* 獲取何種註解
* @return
*/
public abstract Class<? extends Annotation> getAnnotation();
}

用於處理Setter註解 繼承MyAbstractProcessor

@SupportedAnnotationTypes("com.ingxx.processor.Setter") //註解處理器作用於哪個註解 也可以重寫getSupportedAnnotationTypes
@SupportedSourceVersion(SourceVersion.RELEASE_8) //可以處理什麼版本 也可以重寫getSupportedSourceVersion
public class SetterProcessor extends MyAbstractProcessor { @Override
public JCTree.JCMethodDecl makeMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
//生成函式體 this.name = name;
statements.append(treeMaker.Exec(treeMaker.Assign(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()),treeMaker.Ident(jcVariableDecl.getName()))));
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
//生成方法
return treeMaker.MethodDef(
treeMaker.Modifiers(Flags.PUBLIC), //訪問標誌
getNewMethodName(jcVariableDecl.getName()), //名字
treeMaker.TypeIdent(TypeTag.VOID), //返回型別
List.nil(), //泛型形參列表
List.of(getParameters(jcVariableDecl)), //引數列表
List.nil(), //異常列表
body, //方法體
null //預設方法(可能是interface中的那個default)
);
} @Override
public Class<? extends Annotation> getAnnotation() {
return Setter.class;
} private Name getNewMethodName(Name name) {
String fieldName = name.toString();
return names.fromString("set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1, name.length()));
} private JCTree.JCVariableDecl getParameters(JCTree.JCVariableDecl prototypeJCVariable) {
return treeMaker.VarDef(
treeMaker.Modifiers(Flags.PARAMETER), //訪問標誌
prototypeJCVariable.name, //名字
prototypeJCVariable.vartype, //型別
null //初始化語句
);
}
}

用於處理Getter註解 繼承MyAbstractProcessor

@SupportedAnnotationTypes("com.ingxx.processor.Getter") //註解處理器作用於哪個註解 也可以重寫getSupportedAnnotationTypes
@SupportedSourceVersion(SourceVersion.RELEASE_8) //可以處理什麼版本 也可以重寫getSupportedSourceVersion
public class GetterProcessor extends MyAbstractProcessor { @Override
public JCTree.JCMethodDecl makeMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
//生成函式體 return this.欄位名
statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
//生成方法
return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), jcVariableDecl.vartype, List.nil(), List.nil(), List.nil(), body, null);
} @Override
public Class<? extends Annotation> getAnnotation() {
return Getter.class;
} private Name getNewMethodName(Name name) {
String fieldName = name.toString();
return names.fromString("get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1, name.length()));
}
}

編譯現有類

javac -cp $JAVA_HOME/lib/tools.jar *.java -d .

新建一個測試類 IDEA中由於找不到get set方法會報錯,可以忽略

@Getter
@Setter
public class UserInfo {
private String userId; private String userName; public static void main(String[] args) {
UserInfo userInfo = new UserInfo();
userInfo.setUserId("001");
userInfo.setUserName("得物");
System.out.println("id = "+userInfo.getUserId()+" name = "+userInfo.getUserName());
}
}

接著編譯

//多個處理器用逗號分隔
javac -processor com.ingxx.processor.GetterProcessor,com.ingxx.processor.SetterProcessor UserInfo.java -d .

檢視編譯後的檔案發現已經生成了get、set方法