lombok是一款能夠在java代碼編譯階段改變代碼的插件。比如生成setter和getter方法,生成log類變量等,能夠簡化一些特定的模版式代碼。本文將以實現一個基於特定注解生成日志代碼的方式,簡單介紹在lombok基礎上自定義擴展的方式。
1、實現功能
基於自定義注解,將下面的代碼塊1變成代碼塊2,自動生成日志代碼:
//代碼塊1 static void m1(Map<String, String> req) { System.out.println("m1 running"); }
//代碼塊2 static void m1(Map<String, String> req) { log.info("Application.m1 req:{}", JSON.toJSONString(req)); System.out.println("m1 running"); }
2、環境准備
首先搭建lombok工程,git地址:https://github.com/rzwitserloot/lombok,並安裝ant環境,lombok需要使用ant編譯,並下載openjdk(可選),使用openjdk有助於理解javac的源碼,因為默認jkd是沒有javac的源碼的。
關於這些環境,自己想辦法百度去搞定吧!
3、核心實現
自定義注解:
package lombok.extern.youzan; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * * Date: 2018/5/26 * @author xuzhiyi */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) public @interface LogBefore { String level() default "info"; }
這個注解是用來作用在方法上,來表示需要在方法第一行增加代碼,log方法傳入的參數;
自定義注解解析:
package lombok.javac.handlers; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.ListBuffer; import com.sun.tools.javac.util.Name; import org.mangosdk.spi.ProviderFor; import java.util.logging.Logger; import lombok.core.AST; import lombok.core.AnnotationValues; import lombok.core.HandlerPriority; import lombok.extern.youzan.LogBefore; import lombok.javac.JavacAnnotationHandler; import lombok.javac.JavacNode; import lombok.javac.JavacTreeMaker; /** * Date: 2018/5/26 * * @author xuzhiyi */ @ProviderFor(JavacAnnotationHandler.class) @HandlerPriority(20) public class HandleLogBefore extends JavacAnnotationHandler<LogBefore> { private static Logger logger = Logger.getLogger(HandleLogBefore.class.getName()); @Override public void handle(AnnotationValues<LogBefore> annotation, JCTree.JCAnnotation ast, JavacNode annotationNode) { JavacNode methodNode = annotationNode.up(); switch (methodNode.getKind()) { case METHOD: JCTree.JCMethodDecl methodDecl = (JCTree.JCMethodDecl) methodNode.get(); String methodName = methodDecl.getName().toString(); String logLevel = annotation.getInstance().level(); if (logLevel == null) { logLevel = "info"; } String logFieldName = "log"; String logMethodName = logFieldName + "." + logLevel; String className = null; String logTypeName = null; Name logVarName = null; JavacNode typeNode = methodNode.up(); if (AST.Kind.TYPE == typeNode.getKind()) { JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) typeNode.get(); className = classDecl.getSimpleName().toString(); // 遍歷類,尋找是否有log類變量 for (JCTree def : classDecl.defs) { if (def instanceof JCTree.JCVariableDecl) { JCTree.JCVariableDecl variableDecl = (JCTree.JCVariableDecl) def; if (variableDecl.name.toString().equals(logFieldName)) { logVarName = variableDecl.name; logTypeName = variableDecl.getType().toString(); break; } } } // 沒有log類變量,則直接返回 if (logVarName == null) { return; } } JCTree.JCBlock block = methodDecl.getBody(); List<JCTree.JCStatement> statements = block.stats; JavacTreeMaker maker = annotationNode.getTreeMaker(); JCTree.JCExpression logMethod = JavacHandlerUtil.chainDotsString(typeNode, logMethodName); JCTree.JCExpression logType = JavacHandlerUtil.chainDotsString(typeNode, logTypeName); List<JCTree.JCVariableDecl> parameters = methodDecl.getParameters(); JCTree.JCExpression apply = maker.Apply(List.<JCTree.JCExpression>of(logType), logMethod, generateLogArgs(parameters, className, methodName, maker, typeNode)); ListBuffer<JCTree.JCStatement> listBuffer = new ListBuffer<JCTree.JCStatement>(); listBuffer.append(maker.Exec(apply)); for (JCTree.JCStatement stat : statements) { listBuffer.append(stat); } methodDecl.body.stats = listBuffer.toList(); annotationNode.getAst().setChanged(); break; default: annotationNode.addError("@LogBefore is legal only on types."); break; } } /** * 生成log的參數表達式 */ public static List<JCTree.JCExpression> generateLogArgs(List<JCTree.JCVariableDecl> parameters, String className, String methodName, JavacTreeMaker maker, JavacNode typeNode) { JCTree.JCExpression[] argsArray = new JCTree.JCExpression[parameters.size() + 1]; StringBuilder stringBuilder = new StringBuilder(className).append(".").append(methodName); if (parameters.size() > 0) { stringBuilder.append(" "); for (JCTree.JCVariableDecl variableDecl : parameters) { stringBuilder.append(variableDecl.getName()).append(":{},"); } stringBuilder.deleteCharAt(stringBuilder.length() - 1); } else { stringBuilder.append(" begin"); } argsArray[0] = maker.Literal(stringBuilder.toString()); JCTree.JCExpression jsonStringMethod = JavacHandlerUtil.chainDotsString(typeNode, "com.alibaba.fastjson.JSON.toJSONString"); for (int i = 0; i < parameters.size(); i++) { argsArray[i + 1] = maker.Apply(List.<JCTree.JCExpression>nil(), jsonStringMethod, List.<JCTree.JCExpression>of(maker.Ident(parameters.get(i)))); } return List.<JCTree.JCExpression>from(argsArray); } }
這是自定義的處理LogBefore的handler,lombok插件在編譯時遇到LogBefore時會掉調用這個handler來處理。這里使用javac的一些相關方法,比較難理解,會將我們的java代碼抽象成一顆語法樹,我們在這顆樹上進行相關的處理動作,關於javac的文檔比較少,好在其方法名都比較直接,基本可以通過方法名來理解其作用,使用openjdk在使用的時候會更舒服一些。
4、編譯使用
lombok jar 包生成:
在lombok代碼當前目錄使用ant打包,當前目錄cmd下輸入ant回車就可以了,jar包會出現在dist目錄下。然后用產生的jar包替換掉當前maven倉庫中的jar包。
自定義注解使用
書寫代碼如下:
@Slf4j public class Application { @LogBefore static void m1(Map<String, String> req) { System.out.println("m1 running"); } }
使用mvn clean package打包編譯后代碼如下:
public class Application { private static final Logger log = LoggerFactory.getLogger(Application.class); public Application() { } static void m1(Map<String, String> req) { log.info("Application.m1 req:{}", JSON.toJSONString(req)); System.out.println("m1 running"); } }
總結:
1、難點在於handler的邏輯處理,里面的javac的api不太容易掌握,容易出錯,本文也只是簡單實現,可能還有很多潛在的情況沒有考慮到。
2、基於這種在javac期間改變代碼的方式,可以在模版代碼比較多的時候考慮使用。我后面還寫了使用打樁的方式,在代碼里放入特定的空方法,再編譯期間動態替換的代碼,也是一種減少模版代碼的方式。