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<lombok.extern.klog.KLog> { private static final String LOG_FIELD_NAME = "log"; @Override public void handle(final AnnotationValues<KLog> 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 的注解
@ProviderFor(JavacAnnotationHandler.class)
會為我們生成對應 services 文件; 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<KLog> 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.<JCTree.JCExpression>nil(), factoryMethod, loggerName != null ? List.of(loggerName) : List.<JCTree.JCExpression>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<KLog> { private static final String LOG_FIELD_NAME = "log"; @Override public void handle(final AnnotationValues<KLog> 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"?> <idea-plugin url="https://github.com/mplushnikov/lombok-intellij-plugin"> <extensions defaultExtensionNs="Lombook Plugin"> <processor implementation="de.plushnikov.intellij.plugin.processor.clazz.log.KLogProcessor"/> </extensions> </idea-plugin>
public class LombokLoggerHandler extends BaseLombokHandler { protected void processClass(@NotNull PsiClass psiClass) { final Collection<AbstractLogProcessor> 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!