lombok是一款能夠在java代碼編譯階段改變代碼的插件欺殿。比如生成setter和getter方法再沧,生成log類變量等饶囚,能夠簡(jiǎn)化一些特定的模版式代碼军援。本文將以實(shí)現(xiàn)一個(gè)基于特定注解生成日志代碼的方式腿短,簡(jiǎn)單介紹在lombok基礎(chǔ)上自定義擴(kuò)展的方式屏箍。
1、實(shí)現(xiàn)功能
基于自定義注解橘忱,將下面的代碼塊1變成代碼塊2赴魁,自動(dòng)生成日志代碼:
//代碼塊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、環(huán)境準(zhǔn)備
首先搭建lombok工程钝诚,git地址:https://github.com/rzwitserloot/lombok颖御,并安裝ant環(huán)境,lombok需要使用ant編譯凝颇,并下載openjdk(可選)潘拱,使用openjdk有助于理解javac的源碼,因?yàn)槟J(rèn)jkd是沒(méi)有javac的源碼的祈噪。
關(guān)于這些環(huán)境泽铛,自己想辦法百度去搞定吧!
3辑鲤、核心實(shí)現(xiàn)
自定義注解:
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";
}
這個(gè)注解是用來(lái)作用在方法上盔腔,來(lái)表示需要在方法第一行增加代碼,log方法傳入的參數(shù)月褥;
自定義注解解析:
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();
// 遍歷類弛随,尋找是否有l(wèi)og類變量
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;
}
}
}
// 沒(méi)有l(wèi)og類變量,則直接返回
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的參數(shù)表達(dá)式
*/
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插件在編譯時(shí)遇到LogBefore時(shí)會(huì)掉調(diào)用這個(gè)handler來(lái)處理舀透。這里使用javac的一些相關(guān)方法,比較難理解决左,會(huì)將我們的java代碼抽象成一顆語(yǔ)法樹(shù)愕够,我們?cè)谶@顆樹(shù)上進(jìn)行相關(guān)的處理動(dòng)作,關(guān)于javac的文檔比較少佛猛,好在其方法名都比較直接惑芭,基本可以通過(guò)方法名來(lái)理解其作用,使用openjdk在使用的時(shí)候會(huì)更舒服一些继找。
4遂跟、編譯使用
lombok jar 包生成:
在lombok代碼當(dāng)前目錄使用ant打包,當(dāng)前目錄cmd下輸入ant回車就可以了,jar包會(huì)出現(xiàn)在dist目錄下幻锁。然后用產(chǎn)生的jar包替換掉當(dāng)前maven倉(cāng)庫(kù)中的jar包凯亮。
自定義注解使用
書(shū)寫(xiě)代碼如下:
@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");
}
}
總結(jié):
1、難點(diǎn)在于handler的邏輯處理哄尔,里面的javac的api不太容易掌握假消,容易出錯(cuò),本文也只是簡(jiǎn)單實(shí)現(xiàn)岭接,可能還有很多潛在的情況沒(méi)有考慮到置谦。
2、基于這種在javac期間改變代碼的方式亿傅,可以在模版代碼比較多的時(shí)候考慮使用。我后面還寫(xiě)了使用打樁的方式瘟栖,在代碼里放入特定的空方法葵擎,再編譯期間動(dòng)態(tài)替換的代碼,也是一種減少模版代碼的方式半哟。