【JSR269實戰】之編譯時操作AST,修改字節碼文件,以實現和lombok類似的功能

筆者日常 兄弟姐妹們,還是儘量少熬夜啊。我感覺我記性有所下降,難受。


需求說明(本文以實現此需求爲例進行說明)

  現在有一個需求,就是要給枚舉類生成一個內部類,這個內部類中以靜態常量的形式記錄外部枚舉類所有枚舉項的值,即:

  • 編譯前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/裏的文件並作相應解析。

測試一下

  1. 把上面編寫的項目install到本地倉庫。上面的項目的maven信息是這樣的:
    在這裏插入圖片描述

  2. 打開一個新的項目,在新項目的pom.xml中引入依賴。
    在這裏插入圖片描述

  3. 在新項目中,創建一個模型,並使用我們自定義的註解。
    在這裏插入圖片描述

  4. 編譯新項目,並查看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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章