筆者日常: 兄弟姐妹們,還是儘量少熬夜啊。我感覺我記性有所下降,難受。
需求說明(本文以實現此需求爲例進行說明)
:
現在有一個需求,就是要給枚舉類生成一個內部類,這個內部類中以靜態常量的形式記錄外部枚舉類所有枚舉項的值,即:
- 編譯前java文件是這樣的:
- (編譯時操作AST,)編譯後的class文件是這樣的:
編譯時操作AST,修改字節碼文件:
軟硬件環境說明: JDK1.8、Mavne3.6.3、IntelliJ IDEA。
項目整體(步驟)說明:
第一步:創建一個普通的maven項目,並在pom中引入相關依賴。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.pingan</groupId>
<artifactId>jsr269-custom-ast</artifactId>
<version>1.0.1</version>
<name>jsr269-custom-ast</name>
<description>基於JSR269, 面向AST,自定義實現一個編譯時修改字節碼的類。具體功能爲: generate inner-class containing outer-class's all
public-static-final parameters
</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
<build>
<!--
問: 爲什麼這裏需要先排除【META-INF/**/*】, 然後又用maven-resources-plugin插件
在prepare-package階段時引入?
答: 首先, 需要先明白META-INF下services目錄下javax.annotation.processing.Processor文件的
作用: java在編譯時,會先去資源目錄下讀取META-INF文件夾, 當發現META-INF/services/javax.annotation.processing.Processor
文件時, 會去找該文件(內容)裏指定的處理器(注: 這個處理器此時應當是一個已經編譯了的class文件),以
這個處理器來輔助執行此次的編譯。
簡單的說,就是: 要編譯這個項目,可以! 但是你得先提供對應處理器的class文件纔行。
但是呢, 這個處理器,正是我們此次需要編譯的java文件之一;也就是說它還沒有被編譯呢,還只是java文件,而不是class文件。
所以問題就產生與解決:
編譯器maven大哥: 要編譯, 可以啊!請提供Processor對應的class文件。
項目小弟: 大哥,我現在要編譯的對象就是Processor對應的java文件,還沒編譯呢,我怎麼提供class文件給您呢!
編譯器maven大哥: 我不管, 反正我檢測到了META-INF/services/javax.annotation.processing.Processor文件存在,
那你就必須得提供該文件(的文件內容裏)定義的Processor的class文件
項目小弟(思考中): emmmmmmmm~
項目大哥是因爲檢測到了META-INF/services/javax.annotation.processing.Processor文件,所以
才問我要Processor對應的class文件;那我可不可以在資源目錄下添加這個文件,
即:不要META-INF/services/javax.annotation.processing.Processor文件呢?
項目小弟(思考中): 如果我不要這個文件的話, 會有什麼影響呢?讓我想想:
比如說,如果不要這個文件後,我被打成了jar包abc.jar, 那麼別人在引入我(abc.jar)後, 使用了
進行我的編譯時操作AST的註解後,要想讓註解生效,就得在編譯時主動指定使用處理器了,就像這樣:
javac …… -processor com.pingan.mylombok.processor.EnumInnerConstantProcessor Test.java
, 這樣的話, 太麻煩了,使用者肯定要罵孃的。不能這麼搞, 所以在我提供出去的jar包中,一定要有
META-INF/services/javax.annotation.processing.Processor文件纔行,我可不喜歡被別人罵娘!
項目小弟(思考中): 哈!想到了!!!!!!!!
我完全可以在[編譯器maven大哥]進行META-INF/services/javax.annotation.processing.Processor文件
檢測之後,再把文件考過去嘛, 這樣一來, [編譯器maven大哥]既不會阻撓我編譯,編譯後打出來的jar裏又有
META-INF/services/javax.annotation.processing.Processor文件了, 我真是個天才。
-->
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>META-INF/**/*</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<!--
指定Java compiler編譯時,使用的source與target版本。
注: 不同的source、target版本,編譯後產生的Class版本號可能不同
-->
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>process-META</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>target/classes</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/resources/</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
第二步:自定義編譯時註解。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 生成一個公開的靜態內部類, 這個內部類中持有了外部類的public-static-final常量
*
* @author JustryDeng
* @date 2020/5/13 20:50:51
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface EnumInnerConstant {
/** 默認的內部類名 */
String innerClassName() default "JustryDeng";
}
第三步:編寫處理器。
import com.pingan.jsr269ast.annotation.EnumInnerConstant;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.tools.Diagnostic;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
* generate inner-class containing outer-class's all public-static-final parameters
*
* @author JustryDeng
* @date 2020/5/13 20:53:30
*/
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.pingan.jsr269ast.annotation.EnumInnerConstant")
public class EnumInnerConstantProcessor extends AbstractProcessor {
/** 消息記錄器 */
private Messager messager;
/** 可將Element轉換爲JCTree的工具。(注: 簡單的講,處理AST, 就是處理一個又一個CTree) */
private JavacTrees trees;
/** JCTree製作器 */
private TreeMaker treeMaker;
/** 名字處理器*/
private Names names;
/** 內部類類名校驗 */
private static final String INNER_CLASS_NAME_REGEX = "[A-Z][A-Za-z0-9]+";
private static final Pattern INNER_CLASS_NAME_PATTERN = Pattern.compile(INNER_CLASS_NAME_REGEX);
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
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) {
messager.printMessage(Diagnostic.Kind.NOTE, "roundEnv -> " + roundEnv);
// 獲取被@EnumInnerConstant註解標記的所有元素(可能是類、變量、方法等等)
Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(EnumInnerConstant.class);
elementSet.forEach(element -> {
/*
* 存儲參數名與參數值的map、存儲參數名與參數類型的map、一個輔助計數的map
* <p>
* 注: 照理來說,這裏是單線程的。但考慮到本人對AST的處理機制也不是很熟,爲
* 保證萬無一失,這裏直接用線程安全的類吧。
*/
Map<String, Object> paramsNameValueMap = new ConcurrentHashMap<>(8);
Map<String, JCTree.JCExpression> paramsNameTypeMap = new ConcurrentHashMap<>(8);
Map<String, AtomicInteger> paramIndexHelper = new ConcurrentHashMap<>(4);
// 獲取到註解信息
EnumInnerConstant annotation = element.getAnnotation(EnumInnerConstant.class);
String originInnerClassName = annotation.innerClassName();
// 內部類類名校驗
String innerClassName = checkInnerClassName(originInnerClassName);
// 將Element轉換爲JCTree
JCTree jcTree = trees.getTree(element);
String className = (((JCTree.JCClassDecl)jcTree).sym).type.toString();
String enumFlag = "enum";
if (!enumFlag.equalsIgnoreCase(jcTree.getKind().name())) {
// 爲保證錯誤信息能在各種情況下都能被看到, 這裏用多種方式記錄錯誤信息
String errorMessage = "@EnumInnerConstant only support enum-class, [" + className + "] is not supported";
System.err.println(errorMessage);
messager.printMessage(Diagnostic.Kind.ERROR, errorMessage);
throw new RuntimeException(errorMessage);
}
/*
* 通過JCTree.accept(JCTree.Visitor)訪問JCTree對象的內部信息。
*
* JCTree.Visitor有很多方法,我們可以通過重寫對應的方法,(從該方法的形參中)來獲取到我們想要的信息:
* 如: 重寫visitClassDef方法, 獲取到類的信息;
* 重寫visitMethodDef方法, 獲取到方法的信息;
* 重寫visitVarDef方法, 獲取到變量的信息;
* 重寫visitLabelled方法, 獲取到常量的信息;
* 重寫visitBlock方法, 獲取到方法體的信息;
* 重寫visitImport方法, 獲取到導包信息;
* 重寫visitForeachLoop方法, 獲取到for循環的信息;
* ......
*/
jcTree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
// 不要放在 jcClassDecl.defs = jcClassDecl.defs.append(a);之後,否者會遞歸
super.visitClassDef(jcClassDecl);
// 生成內部類, 並將內部類寫進去
JCTree.JCClassDecl innerClass = generateInnerClass(innerClassName, paramsNameValueMap, paramsNameTypeMap);
jcClassDecl.defs = jcClassDecl.defs.append(innerClass);
}
@Override
public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
boolean isEnumConstant = className.equals(jcVariableDecl.vartype.type.toString());
if (!isEnumConstant) {
super.visitVarDef(jcVariableDecl);
return;
}
Name name = jcVariableDecl.getName();
String paramName = name.toString();
/*
* 枚舉項本身也屬於變量, 每個枚舉項裏面,可能還有變量。 這裏繼
* 續JCTree.accept(JCTree.Visitor)進入,訪問這個枚舉項的內部信息。
*/
jcVariableDecl.accept(new TreeTranslator() {
@Override
public void visitLiteral(JCTree.JCLiteral jcLiteral) {
Object paramValue = jcLiteral.getValue();
if (paramValue == null) {
return;
}
TypeTag typetag = jcLiteral.typetag;
JCTree.JCExpression paramType;
if (isPrimitive(typetag)) {
// 如果是基本類型,那麼可以直接生成
paramType = treeMaker.TypeIdent(typetag);
} else if (paramValue instanceof String) {
// 如果不是基本類型,那麼需要拼接生成
paramType = generateJcExpression("java.lang.String");
} else {
return;
}
AtomicInteger atomicInteger = paramIndexHelper.get(paramName);
if (atomicInteger == null) {
atomicInteger = new AtomicInteger(0);
paramIndexHelper.put(paramName, atomicInteger);
}
int paramIndex = atomicInteger.getAndIncrement();
String key = paramName + "_" + paramIndex;
paramsNameTypeMap.put(key, paramType);
paramsNameValueMap.put(key, paramValue);
super.visitLiteral(jcLiteral);
}
});
super.visitVarDef(jcVariableDecl);
}
});
});
return false;
}
/**
* 內部類類名 合法性校驗
*
* @param innerClassName
* 內部類類名
* @return 校驗後的內部類類名
* @date 2020/5/17 13:45:27
*/
private String checkInnerClassName (String innerClassName) {
if (innerClassName == null || innerClassName.trim().length() == 0) {
// 爲保證錯誤信息能在各種情況下都能被看到, 這裏用多種方式記錄錯誤信息
String errorMessage = "@EnumInnerConstant. inner-class-name cannot be empty";
System.err.println(errorMessage);
messager.printMessage(Diagnostic.Kind.ERROR, errorMessage);
throw new RuntimeException(errorMessage);
}
innerClassName = innerClassName.trim();
if (!INNER_CLASS_NAME_PATTERN.matcher(innerClassName).matches()) {
// 爲保證錯誤信息能在各種情況下都能被看到, 這裏用多種方式記錄錯誤信息
String errorMessage = "@EnumInnerConstant. inner-class-name must match regex " + INNER_CLASS_NAME_REGEX;
System.err.println(errorMessage);
messager.printMessage(Diagnostic.Kind.ERROR, errorMessage);
throw new RuntimeException(errorMessage);
}
return innerClassName;
}
/**
* 判斷typeTag是否屬於基本類型
*
* @param typeTag
* typeTag
* @return 是否屬於基本類型
* @date 2020/5/17 13:10:54
*/
private boolean isPrimitive(TypeTag typeTag) {
if (typeTag == null) {
return false;
}
TypeKind typeKind;
try {
typeKind = typeTag.getPrimitiveTypeKind();
} catch (Throwable e) {
return false;
}
if (typeKind == null) {
return false;
}
return typeKind.isPrimitive();
}
/**
* 生成內部類
*
* @return 生成出來的內部類
* @date 2020/5/16 15:43:56
*/
private JCTree.JCClassDecl generateInnerClass(String innerClassName, Map<String, Object> paramsInfoMap,Map<String, JCTree.JCExpression> paramsNameTypeMap) {
JCTree.JCClassDecl jcClassDecl1 = treeMaker.ClassDef(
treeMaker.Modifiers(Flags.PUBLIC + Flags.STATIC),
names.fromString(innerClassName),
List.nil(),
null,
List.nil(),
List.nil());
List<JCTree.JCVariableDecl> collection = generateAllParameters(paramsInfoMap, paramsNameTypeMap);
collection.forEach(x -> jcClassDecl1.defs = jcClassDecl1.defs.append(x));
return jcClassDecl1;
}
/**
* 生成參數
*
* @param paramNameValueMap
* 參數名-參數值map
* @param paramsNameTypeMap
* 參數名-參數類型map
*
* @return 參數JCTree集合
* @date 2020/5/16 15:44:47
*/
private List<JCTree.JCVariableDecl> generateAllParameters(Map<String, Object> paramNameValueMap,
Map<String, JCTree.JCExpression> paramsNameTypeMap) {
List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
JCTree.JCVariableDecl statement;
if (paramNameValueMap != null && paramNameValueMap.size() != 0) {
for (Map.Entry<String, Object> entry : paramNameValueMap.entrySet()) {
// 定義變量
statement = treeMaker.VarDef(
// 訪問修飾符
treeMaker.Modifiers(Flags.PUBLIC + Flags.STATIC + Flags.FINAL),
// 參數名
names.fromString(entry.getKey()),
// 參數類型
paramsNameTypeMap.get(entry.getKey()),
// 參數值
treeMaker.Literal(entry.getValue()));
jcVariableDeclList = jcVariableDeclList.append(statement);
}
}
return jcVariableDeclList;
}
/**
* 根據全類名獲取JCTree.JCExpression
*
* 如: 類變量 public static final String ABC = "abc";中, String就需要
* 調用此方法generateJCExpression("java.lang.String")進行獲取。
* 追注: 其餘的複雜類型,也可以通過這種方式進行獲取。
* 追注: 對於基本數據類型,可以直接通過類TreeMaker.TypeIdent獲得,
* 如: treeMaker.TypeIdent(TypeTag.INT)可獲得int的JCTree.JCExpression
*
* @param fullNameOfTheClass
* 全類名
* @return 全類名對應的JCTree.JCExpression
* @date 2020/5/16 15:47:32
*/
private JCTree.JCExpression generateJcExpression(String fullNameOfTheClass) {
String[] fullNameOfTheClassArray = fullNameOfTheClass.split("\\.");
JCTree.JCExpression expr = treeMaker.Ident(names.fromString(fullNameOfTheClassArray[0]));
for (int i = 1; i < fullNameOfTheClassArray.length; i++) {
expr = treeMaker.Select(expr, names.fromString(fullNameOfTheClassArray[i]));
}
return expr;
}
}
第四步:配置META-INF/services/javax.annotation.processing.Processor文件,使編譯項目時觸發我們自定義的處理器。
提示: 容器啓動時,會掃描jar包下的META-INF/services/裏的文件並作相應解析。
測試一下:
-
把上面編寫的項目install到本地倉庫。上面的項目的maven信息是這樣的:
-
打開一個新的項目,在新項目的pom.xml中引入依賴。
-
在新項目中,創建一個模型,並使用我們自定義的註解。
-
編譯新項目,並查看class文件。
由此可見,編譯時操作AST,修改字節碼文件成功 !
調試說明:
方式一:debug調試
提示: 此方式需要一個提供Processor的項目,以及一個使用該Processor的項目。
- 第一步: 對使用Processor的項目進行
mvnDebug clean compile
。
注: 我這裏是直接在開發工具裏進行的mvn,你也可以在其它命令行窗口裏面進行mvn。 - 第二步: 在我們的Processor項目中,配置Remote調試。
方式二:輸出調試
提示: 此方式只需要一個Processor項目即可。
- 第一步: 先利用開發工具,對我們自定義的註解及處理器進行編譯。
- 第二步: 使用第一步編譯後的處理器協助編譯,編譯目標類。
- 第三步: 查看第二步的Messager輸出日誌,進行調試。
到此爲止,我們實現的功能就與lombok非常相近了,唯一差的一點是:lombok針對不同IDE有提供不同的插件,使得IDE能夠識別到lombok編譯後的內容 !針對IDE插件的開發,個人興趣不大,如果以後時間非常充足的話,我可能會花一點時間去搞一搞。
^_^ 如有不當之處,歡迎指正
^_^ 參考連接
https://blog.mythsman.com/p…bf/
https://www.jianshu.com…35a
https://blog.csdn.net/a_zhenzhen…063
https://blog.csdn.net/sunqu…274
^_^ 測試代碼託管鏈接
https://github.com/JustryDeng/CommonRep…
^_^ 本文已經被收錄進《程序員成長筆記(七)》,筆者JustryDeng