• 【JSR269实战】之编译时操作AST,修改字节码文件,以实现和lombok类似的功能


    参考:https://blog.csdn.net/justry_deng/article/details/106176181

    maven编译不成功。
    笔者日常****: 兄弟姐妹们,还是尽量少熬夜啊。我感觉我记性有所下降,难受。


    需求说明(本文以实现此需求为例进行说明)

    现在有一个需求,就是要给枚举类生成一个内部类,这个内部类中以静态常量的形式记录外部枚举类所有枚举项的值,即:

    • 编译前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>` 
    
    ![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
    
    *   1
    *   2
    *   3
    *   4
    *   5
    *   6
    *   7
    *   8
    *   9
    *   10
    *   11
    *   12
    *   13
    *   14
    *   15
    *   16
    *   17
    *   18
    *   19
    *   20
    *   21
    *   22
    *   23
    *   24
    *   25
    *   26
    *   27
    *   28
    *   29
    *   30
    *   31
    *   32
    *   33
    *   34
    *   35
    *   36
    *   37
    *   38
    *   39
    *   40
    *   41
    *   42
    *   43
    *   44
    *   45
    *   46
    *   47
    *   48
    *   49
    *   50
    *   51
    *   52
    *   53
    *   54
    *   55
    *   56
    *   57
    *   58
    *   59
    *   60
    *   61
    *   62
    *   63
    *   64
    *   65
    *   66
    *   67
    *   68
    *   69
    *   70
    *   71
    *   72
    *   73
    *   74
    *   75
    *   76
    *   77
    *   78
    *   79
    *   80
    *   81
    *   82
    *   83
    *   84
    *   85
    *   86
    *   87
    *   88
    *   89
    *   90
    *   91
    *   92
    *   93
    *   94
    *   95
    *   96
    *   97
    *   98
    *   99
    *   100
    *   101
    *   102
    *   103
    *   104
    *   105
    *   106
    *   107
    *   108
    *   109
    *   110
    *   111
    
    
    

    第二步:自定义编译时注解。

    `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";
    }` 
    
    ![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
    
    *   1
    *   2
    *   3
    *   4
    *   5
    *   6
    *   7
    *   8
    *   9
    *   10
    *   11
    *   12
    *   13
    *   14
    *   15
    *   16
    *   17
    *   18
    
    
    

    第三步:编写处理器。

    `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

  • 相关阅读:
    bzoj1648
    bzoj3404
    bzoj1650
    bzoj1625
    bzoj1606
    bzoj1464
    bzoj1572
    bzoj1617
    bzoj1092
    bzoj1091
  • 原文地址:https://www.cnblogs.com/kuangke/p/16880670.html
Copyright © 2020-2023  润新知