![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiYWan5iZ2QjMxQGMkFGN5UTZ1MGZ5E2Y2MGN1QTMwAjYmJGOx8CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.gif)
Java 是一門"繁瑣"的語言,使用 Lombok 可以顯著地減少樣闆代碼。比如使用
@Getter
注解可以為你的私有屬性建立 get 方法。
源代碼
@Getter private int age = 10;
生成後代碼
private int age = 10;
public int getAge() {
return age;
}
Lombok 自身已經擁有許多非常實用的注解,例如
@Getter
/
@Value
/
@Data
/
@Builder
等等。但你可能也想定義自己的注解來減少重複代碼,本文将講解如何實作這一目标。
Lombok是如何實作代碼注入的?
在使用 javac 編譯器時(netbeans,maven,gradle),Lombok 會以 annotation processor 方式運作。 Javac 會以 SPI 方式加載所有 jar 包中
META-INF/services/javax.annotation.processing.Processor
檔案所列舉的類,并以 annotation processor 的方式運作它。對于 Lombok,這個類是
lombok.launch.AnnotationProcessorHider$AnnotationProcessor
,當它被 javac 加載建立後,會執行
init
方法,在這個方法中會啟動一個特殊的類加載器
ShadowClassLoader
,加載同 jar 包下所有以
.SCL.lombok
結尾的類(Lombok 為了對 IDE 隐藏這些類,是以不是通常地以 .class 結尾)。其中就包含各式各樣的
handler
。每個
handler
申明并處理一種注解,比如
@Getter
對應
HandleGetter
。
委派給
handler
時,Lombok Annotation Processor 會提供一個被注解節點的Abstract Syntax Tree (AST)節點對象,它可能是一個方法、屬性或類。在
handler
中 可以對這個 AST 進行修改,之後編譯器将從被修改後的 AST 生成位元組碼。
下面我們以
@KLog
為例,說明如何編寫
Handler
。假設我們希望實作這樣的效果:
源代碼
@KLog public class Foo { }
生成後代碼
public class Foo {
private static final com.example.log.KLogger log = com.example.log.KLoggerFactory.getLogger(Foo.class);
}
KLog 可能是我們的日志類,在通用日志類的基礎上做了一些擴充。 使用
@KLog
可以避免因複制粘貼代碼導緻入參錯誤,也有利于統一命名。為了實作這個注解,我們需要實作:
- 建立 Javac Handler
- 建立 Eclipse Handler
- 建立 lombok-intellij-plugin Handler
前期準備:Fork Lombok 工程
我們需要先 fork Lombok 工程,項目中添加 Handler。前面談到因為 shadow loader類加載的原因,在另外的工程中建立 Handler 将變得非常困難, lombok作者推薦直接fork lombok工程定制自己的
lombok.jar
。
~ git clone https://github.com/rzwitserloot/lombok.git
需要注意的是,lombok 需要使用 JDK9 以上版本進行編譯,確定系統路徑配置了正确的 JAVA_HOME 路徑,然後執行
ant maven
将建構可以用于安裝本地倉庫的 jar 包。 可以運作以下指令将建構的 jar 包安裝到本地倉庫進行工程間共享:
~ mvn install:install-file -Dfile=dist/lombok-{lombok-version}.jar -DpomFile=build/mavenPublish/pom.xml
建立@KLog
package lombok.extern.klog;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE) // 1
@Target(ElementType.TYPE)
public @interface KLog {
String topic() default "";
}
這個注解隻用編譯階段,是以使用 RetentionPolicy.SOURCE 就可以
建立 Javac Handler
建立注解後,我們需要再實作一個 Handler 來處理被注解标注了的對象(
Foo
)。 我們将建立一個屬性的 AST 節點,然後插入到
Foo
類對應的節點。
/**
* Handles the {@link lombok.extern.klog.KLog} annotation for javac.
*/
@ProviderFor(JavacAnnotationHandler.class) // 1
public class HandleKLog extends JavacAnnotationHandler {
private static final String LOG_FIELD_NAME = "log";
@Override
public void handle(final AnnotationValues annotation, final JCTree.JCAnnotation ast, final JavacNode annotationNode) {
JavacNode typeNode = annotationNode.up(); // 2
if (!checkFieldInject(annotationNode, typeNode)) {
return;
}
JCTree.JCVariableDecl fieldDecl = createField(annotation, annotationNode, typeNode);
injectFieldAndMarkGenerated(typeNode, fieldDecl); // 3
}
}
- lombok 使用 SPI 方式發現 Handler,這裡 mangosdk 的注解
會為我們生成對應 services 檔案;@ProviderFor(JavacAnnotationHandler.class)
-
是Foo
的上層節點;@KLog
- 将屬性插入到注解所應用的節點,即
。Foo
上述代碼先檢查是否可以插入屬性,然後建立屬性并插入到
Foo
節點。為什麼需要檢查? 因為如果已經存在同名的屬性或者注解所應用的類不是一個
class
就無法插入。
private boolean checkFieldInject(final JavacNode annotationNode, final JavacNode typeNode) {
if (typeNode.getKind() != AST.Kind.TYPE) {
annotationNode.addError("@KLog is legal only on types.");
return false;
}
if ((((JCTree.JCClassDecl)typeNode.get()).mods.flags & Flags.INTERFACE) != 0) {
annotationNode.addError("@KLog is legal only on classes and enums.");
return false;
}
if (fieldExists(LOG_FIELD_NAME, typeNode) != JavacHandlerUtil.MemberExistsResult.NOT_EXISTS) {
annotationNode.addWarning("Field '" + LOG_FIELD_NAME + "' already exists.");
return false;
}
return true;
}
接着我們實作屬性的建立(createField)。我們需要建立屬性的 AST 節點,AST 樹的結構像下面這樣:
具體到我們需要生成的實際代碼則是這樣:
建立屬性的代碼較為複雜,涉及到許多 AST 包相關的操作,需要熟悉相關 API 的含義。建立
log
屬性的代碼如下:
private JCTree.JCVariableDecl createField(final AnnotationValues annotation, final JavacNode annotationNode, final JavacNode typeNode) {
JavacTreeMaker maker = typeNode.getTreeMaker();
Name name = ((JCTree.JCClassDecl) typeNode.get()).name;
JCTree.JCFieldAccess loggingType = maker.Select(maker.Ident(name), typeNode.toName("class"));
JCTree.JCExpression loggerType = chainDotsString(typeNode, "com.example.log.KLogger");
JCTree.JCExpression factoryMethod = chainDotsString(typeNode, "com.example.log.KLoggerFactory.getLogger");
JCTree.JCExpression loggerName;
String topic = annotation.getInstance().topic();
if (topic == null || topic.trim().length() == 0) { // 1
loggerName = loggingType;
} else {
loggerName = maker.Literal(topic);
}
JCTree.JCMethodInvocation factoryMethodCall = maker.Apply(List.nil(), factoryMethod, loggerName != null ? List.of(loggerName) : List.nil());
return recursiveSetGeneratedBy(maker.VarDef(
maker.Modifiers(Flags.PRIVATE | Flags.FINAL | Flags.STATIC ),
typeNode.toName(LOG_FIELD_NAME), loggerType, factoryMethodCall), annotationNode.get(), typeNode.getContext());
}
如果指定了
KLog(topic)
就使用
KLoggerFactory.getLogger(topic)
,否則使用
KLoggerFactory.getLogger(topic)
。
添加了 Javac Handler 之後我們就可以在 maven 中使用
@KLog
了,但還無法用于Eclipse/ejc,我們需要繼續添加 Eclipse Handler。
建立Eclipse Handler
package lombok.eclipse.handlers;
import lombok.core.AST;
import lombok.core.AnnotationValues;
import lombok.eclipse.EclipseAnnotationHandler;
import lombok.eclipse.EclipseNode;
import lombok.extern.klog.KLog;
import org.eclipse.jdt.internal.compiler.ast.*;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.mangosdk.spi.ProviderFor;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import static lombok.eclipse.Eclipse.fromQualifiedName;
import static lombok.eclipse.handlers.EclipseHandlerUtil.*;
/**
* Handles the {@link KLog} annotation for Eclipse.
*/
@ProviderFor(EclipseAnnotationHandler.class)
public class HandleKLog extends EclipseAnnotationHandler {
private static final String LOG_FIELD_NAME = "log";
@Override
public void handle(final AnnotationValues annotation, final Annotation source, final EclipseNode annotationNode) {
EclipseNode owner = annotationNode.up();
if (owner.getKind() != AST.Kind.TYPE) {
return;
}
TypeDeclaration typeDecl = null;
if (owner.get() instanceof TypeDeclaration) typeDecl = (TypeDeclaration) owner.get();
int modifiers = typeDecl == null ? 0 : typeDecl.modifiers;
boolean notAClass = (modifiers &
(ClassFileConstants.AccInterface | ClassFileConstants.AccAnnotation)) != 0;
if (typeDecl == null || notAClass) {
annotationNode.addError("@KLog is legal only on classes and enums.");
return;
}
if (fieldExists(LOG_FIELD_NAME, owner) != EclipseHandlerUtil.MemberExistsResult.NOT_EXISTS) {
annotationNode.addWarning("Field '" + LOG_FIELD_NAME + "' already exists.");
return;
}
ClassLiteralAccess loggingType = selfType(owner, source);
FieldDeclaration fieldDeclaration = createField(source, loggingType, annotation.getInstance().topic());
fieldDeclaration.traverse(new SetGeneratedByVisitor(source), typeDecl.staticInitializerScope);
injectField(owner, fieldDeclaration);
owner.rebuild();
}
private static ClassLiteralAccess selfType(EclipseNode type, Annotation source) {
int pS = source.sourceStart, pE = source.sourceEnd;
long p = (long) pS << 32 | pE;
TypeDeclaration typeDeclaration = (TypeDeclaration) type.get();
TypeReference typeReference = new SingleTypeReference(typeDeclaration.name, p);
setGeneratedBy(typeReference, source);
ClassLiteralAccess result = new ClassLiteralAccess(source.sourceEnd, typeReference);
setGeneratedBy(result, source);
return result;
}
private static FieldDeclaration createField(Annotation source, ClassLiteralAccess loggingType, String loggerTopic) {
int pS = source.sourceStart, pE = source.sourceEnd;
long p = (long) pS << 32 | pE;
// private static final com.example.log.KLogger log = com.example.log.KLoggerFactory.getLogger(Foo.class);
FieldDeclaration fieldDecl = new FieldDeclaration(LOG_FIELD_NAME.toCharArray(), 0, -1);
setGeneratedBy(fieldDecl, source);
fieldDecl.declarationSourceEnd = -1;
fieldDecl.modifiers = Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL;
fieldDecl.type = createTypeReference("com.example.log.KLog", source);
MessageSend factoryMethodCall = new MessageSend();
setGeneratedBy(factoryMethodCall, source);
factoryMethodCall.receiver = createNameReference("com.example.log.KLoggerFactory", source);
factoryMethodCall.selector = "getLogger".toCharArray();
Expression parameter = null;
if (loggerTopic == null || loggerTopic.trim().length() == 0) {
TypeReference copy = copyType(loggingType.type, source);
parameter = new ClassLiteralAccess(source.sourceEnd, copy);
setGeneratedBy(parameter, source);
} else {
parameter = new StringLiteral(loggerTopic.toCharArray(), pS, pE, 0);
}
factoryMethodCall.arguments = new Expression[]{parameter};
factoryMethodCall.nameSourcePosition = p;
factoryMethodCall.sourceStart = pS;
factoryMethodCall.sourceEnd = factoryMethodCall.statementEnd = pE;
fieldDecl.initialization = factoryMethodCall;
return fieldDecl;
}
public static TypeReference createTypeReference(String typeName, Annotation source) {
int pS = source.sourceStart, pE = source.sourceEnd;
long p = (long) pS << 32 | pE;
TypeReference typeReference;
if (typeName.contains(".")) {
char[][] typeNameTokens = fromQualifiedName(typeName);
long[] pos = new long[typeNameTokens.length];
Arrays.fill(pos, p);
typeReference = new QualifiedTypeReference(typeNameTokens, pos);
} else {
typeReference = null;
}
setGeneratedBy(typeReference, source);
return typeReference;
}
}
Eclipse Handler 的代碼比 Javac Handler 複雜不少,因為 Eclipse 的 AST 不如 Javac 簡潔。 代碼中建立的節點都需要關聯上源碼的行數,如果生成的代碼出錯,Eclipse 可以正确定位到
@KLog
。
在 Lombok 工程目錄下執行
ant maven
會生成 dist/lombok.jar 檔案,輕按兩下運作這個 jar 打開 eclipse installer 視窗。 選擇你所使用的 Eclipse,重新開機 Eclipse 并重新建構工程就可以使用新添加的注解了。
建立lombok-intellij-plugin Handler
對于 Intellij IDEA 的使用者,還需要在 lombok-intellij-plugin 插件中添加額外的實作。插件的實作和 lombok 實作互相獨立,無法複用。
package de.plushnikov.intellij.plugin.processor.clazz.log;
import lombok.extern.klog.KLog;
public class KLogProcessor extends AbstractLogProcessor {
private static final String LOGGER_TYPE = "com.example.log.KLog";
private static final String LOGGER_CATEGORY = "%s.class";
private static final String LOGGER_INITIALIZER = "com.example.log.KLoggerFactory(%s)";
public KLogProcessor() {
super(KLog.class, LOGGER_TYPE, LOGGER_INITIALIZER, LOGGER_CATEGORY);
}
}
<?xml version="1.0" encoding="UTF-8"?>https://github.com/mplushnikov/lombok-intellij-plugin">
public class LombokLoggerHandler extends BaseLombokHandler {
protected void processClass(@NotNull PsiClass psiClass) {
final Collection logProcessors = Arrays.asList(
new CommonsLogProcessor(), new JBossLogProcessor(),
new Log4jProcessor(), new Log4j2Processor(), new LogProcessor(),
new Slf4jProcessor(), new XSlf4jProcessor(), new FloggerProcessor(), new KLogProcessor());
// ...
}
}
插件編譯執行
./gradlew build
,在 build/distributions 目錄下會生成 lombok-plugin-{version}.zip 檔案。 在 IntelliJ 中選擇 Preferences > Plugins > Install Plugin from disk 安裝之前建構得到的檔案,重新開機 IntelliJ。
總結
本文以
@KLog
注解為例,講述了如何實作 Javac/Eclipse/Intellij 的 Lombok Handler,不同編譯器的文法樹結構不同,是以需要分别實作。 Eclipse Handler 的實作較為繁瑣,如果團隊成員沒有使用 Eclipse 的也可以略去不實作。
通過上面的例子,你可以定義自己的注解及 Handler。複雜的代碼生成會涉及更多的 AST 操作,你可以參考 Lombok 已有的例子了解這些 API 的用法。為了清楚地展示 AST 的構造,log 屬性的建立沒有使用 Lombok 通用的日志處理類 HandleLog, Lombok 的 @Slf4j/@Log4j/@Log 等都是通過它實作,使用它實作 @KLog 會更為簡單。
Lombok 的本質是通過修改 AST 文法樹進而影響到最後的位元組碼生成,普通的 Java Annotation Processor 隻能建立新的類而不能修改既有類,這使得 Lombok 尤為強大、無可替代。但同樣的,這種方式依賴于特定編譯器的文法樹結構,需要對編譯器文法樹相關類較為熟悉才能實作。這些結構也不屬于 Java 标準,随時可能發生變化。
Happy coding!
目前100000+人已關注加入我們