java使用AbstractProcessor、编译时注解和JCTree实现编译时织入代码(类似lombok)并实现Debug自己的Processor和编译后的代码

Java架构师交流群:793825326

java版本:jdk1.8

IDE:idea2019

先说怎么用,现在我们想写一个注解HelloWorld,让所有使用了这个注解的类,在编译的时候都打印“Hello World!”,注意,是在编译的时候,不是运行的时候。那么该怎么做呢,这就要用到AbstractProcessor这个东西了。

1.先创建一个maven项目abstractprocessor,在这个项目下创建两个子module,processor和compiletest,如图:

2.在processor里面添加一个类MyProcessor:

import com.google.auto.service.AutoService;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

@SupportedAnnotationTypes("HelloWorld")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Hello World!");
        return false;
    }
}

其中AutoService是Google开发用来生成META-INF/services/javax.annotation.processing.Processor文件的,有了它,我们就不用自己手动添加这个文件了。要使用它,需要在pom里面添加依赖:

<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service</artifactId>
    <version>1.0-rc5</version>
</dependency>

3.然后写一个注解HelloWorld:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.SOURCE)
public @interface HelloWorld {
}

至此,processor项目目录如下:

4.使用maven生成一下,这样,我们的HelloWorld注解就实现了,现在就是如何使用它了,很简单,像使用普通的注解一样。在compiletest这个项目里面添加引用:

<dependency>
    <groupId>AbstractProcessorDemo</groupId>
    <artifactId>processor</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

5.在compiletest的启动类上添加注解HelloWorld:

@HelloWorld
public class app {
    public static void main(String[] args) {
        System.out.println("HelloWorld");
    }
}

6.点击生成compiletest,不需要运行起来,就可以看到编译信息里面打印出了Hello World!:

这里需要注意一点,HelloWorld注解所在的项目要和使用它的项目分开,就像我们在这里的例子的做法一样。这里面我们可以利用这个功能,在一个项目编译期间打印出一些编译信息。

前面我们实现了在编译期间打印信息,现在我们实现在编译期间,修改源码,让你看到的源码,和最终生成的class文件里面的代码不一样。就像之前我们说过的一个插件lombok的做法一样,不知道lombok的话可以看下我之前的一篇博文简单了解下lombok插件的使用和原理分析_dap769815768的博客-CSDN博客_lombok插件原理

接下来要实现的功能是这样的,实现一个HelloWorld注解,所有的加了这个注解的方法内部,在编译时都会自动加一句代码:

System.out.println("Hello, world!!!");

下面我讲一下具体实现方法:

1.在一个单独的Module(processor)里面创建一个注解HelloWorld:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface HelloWorld {
}

2.创建一个MyProcessor继承自AbstractProcessor:

import com.google.auto.service.AutoService;
import com.sun.tools.javac.model.JavacElements;
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.util.Context;
import com.sun.tools.javac.util.List;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;

@SupportedAnnotationTypes("HelloWorld")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        final Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        final JavacElements elementUtils = (JavacElements) processingEnv.getElementUtils();
        final TreeMaker treeMaker = TreeMaker.instance(context);
        Set<? extends Element> elements = roundEnv.getRootElements();

        for (Element element : roundEnv.getElementsAnnotatedWith(HelloWorld.class)) {
            JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) elementUtils.getTree(element);

            treeMaker.pos = jcMethodDecl.pos;
            jcMethodDecl.body = treeMaker.Block(0, List.of(
                    treeMaker.Exec(
                            treeMaker.Apply(
                                    List.<JCTree.JCExpression>nil(),
                                    treeMaker.Select(
                                            treeMaker.Select(
                                                    treeMaker.Ident(
                                                            elementUtils.getName("System")
                                                    ),
                                                    elementUtils.getName("out")
                                            ),
                                            elementUtils.getName("println")
                                    ),
                                    List.<JCTree.JCExpression>of(
                                            treeMaker.Literal("Hello, world!!!")
                                    )
                            )
                    ),
                    jcMethodDecl.body
            ));
        }
        return false;
    }
}

这样,我们的HelloWorld注解插件就写好了,由于是maven项目,这里面引用了com.sun.tools的东西,所以,需要在maven的pom文件里面加上:

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

这样,在使用maven打包的时候,才不会报错。该module的目录如下:

3.使用maven打包好,写一个测试项目compiletest测试一下是否达到了预期,在pom里面添加引用:

<dependency>
    <groupId>AbstractProcessorDemo</groupId>
    <artifactId>processor</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

4.在main方法上加上HelloWorld注解:

public class app {
    @HelloWorld
    public static void main(String[] args) {
    }
}

最终的目录结构:

理论来讲,如果我们写的代码没问题,这个main方法编译后应该在方法体内多一句代码:System.out.println("Hello, world!!!");

5.使用maven打包一下测试项目,然后用反编译工具看一下结果,这里我用的反编译工具是jd-gui,打开后看到的源码如下:

运行一下测试程序,看到控制台打印结果:

也就是说,我们成功修改了编译后的字节码文件。下面,针对所涉及的技术点,具体讲解一下。

这里涉及到了javac API的东西,这块的东西文档不是很多,网上的资料也有限,所以我个人也不是很熟悉,这里只针对例子中用到的东西,简单解释一下。重点看一下MyProcessor这个类:

1.首先MyProcessor继承自AbstractProcessor这个抽象类,并且重写了process,所有的处理操作,基本都是在这个方法里面。另外这个类的两个注解

@SupportedAnnotationTypes("HelloWorld")
@SupportedSourceVersion(SourceVersion.RELEASE_7)

分别表示了这个类支持的注解类型和支持的版本。

2.在process方法里面,有一个参数roundEnv,根据这个参数,可以获取到相关的语法树的对象,比如这段代码

Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
JavacElements elementUtils = (JavacElements) processingEnv.getElementUtils();
TreeMaker treeMaker = TreeMaker.instance(context);

3.然后遍历所有的注解了HelloWorld的元素,获取到元素里面的方法

JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) elementUtils.getTree(element);

4.jcMethodDecl.body即为方法体,利用treemaker的Block方法获取到一个新方法体,将原来的替换掉。就达到了修改方法体的目的了。这里的Block方法有两个参数,重点要关注的是第二个参数,也就是具体的方法体内容。它是一个List类型的参数,List里面每一个元素就代表一个语句块,比如,例子中有两块语句,第一块是我们织入的代码块:System.out.println("Hello, world!!!");用treeMaker.Exec()来实现,第二块是原来的代码块:jcMethodDecl.body。这块的代码是用户原本的代码,我们直接放进来就行。这个List是有顺序的,谁的顺序在前,谁最终生成的代码块就在前,比如这里我们织入的代码在原来的代码块之前,所以最终生成System.out.println("Hello, world!!!");语句就在该方法的第一行位置。

5.重点关注的应该是treeMaker.Exec()这个方法,这个方法帮助我们最终生成了System.out.println("Hello, world!!!");这条语句,它的参数是treeMaker.Apply这个方法的返回结果,这个方法的第二个参数,也就是最终实现了输出System.out.println("Hello, world!!!")的东西。

6.我们知道System.out.println的完整写法是java.lang.System.out.println,如果我们想输出这个完整写法该怎么写呢,参考如下代码:

@SupportedAnnotationTypes("HelloWorld")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        JavacElements elementUtils = (JavacElements) processingEnv.getElementUtils();
        TreeMaker treeMaker = TreeMaker.instance(context);
        for (Element element : roundEnv.getElementsAnnotatedWith(HelloWorld.class)) {
            JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) elementUtils.getTree(element);

            treeMaker.pos = jcMethodDecl.pos;
            jcMethodDecl.body = treeMaker.Block(0, List.of(
                    treeMaker.Exec(
                            treeMaker.Apply(
                                    List.<JCTree.JCExpression>nil(),
                                    treeMaker.Select(
                                            treeMaker.Select(
                                                    treeMaker.Ident(
                                                            elementUtils.getName("System")
                                                    ),
                                                    elementUtils.getName("out")
                                            ),
                                            elementUtils.getName("println")
                                    ),
                                    List.<JCTree.JCExpression>of(
                                            treeMaker.Literal("Hello, world!!!")
                                    )
                            )
                    ),
                    jcMethodDecl.body,
                    treeMaker.Exec(
                            treeMaker.Apply(
                                    List.<JCTree.JCExpression>nil(),
                                    treeMaker.Select(
                                            treeMaker.Select(
                                                    treeMaker.Select(
                                                            treeMaker.Select(
                                                                    treeMaker.Ident(elementUtils.getName("java")),
                                                                    elementUtils.getName("lang")
                                                            ),
                                                            elementUtils.getName("System")
                                                    ),
                                                    elementUtils.getName("out")
                                            ),
                                            elementUtils.getName("println")
                                    ),
                                    List.<JCTree.JCExpression>of(
                                            treeMaker.Literal("Hello, world!!!")
                                    )
                            )
                    )
            ));
        }
        return false;
    }
}

这里我在原本的方法体前后各插入了一句打印"Hello, world!!!"的代码,区别就是一个用完整写法,一个是不完整的,它最终生成的代码如下:

其源码为:

这里之所以最终生成的两句完全一样,是因为编译器编译之后会把java.lang给省略掉,这个是编译器优化所致。比对这两个写法的区别你会发现,这里面用到了两个方法,一个是treeMaker.Select(生成具体的方法),一个是treeMaker.Literal(方法的参数)。

7.treeMaker.Select里面套了很多层,对比两种写法的区别,你也能明白,这是为了写出多级方法的做法,多级方法的第一级以treeMaker.Ident开始,然后一层套一层,直到整个方法结束。

8.process方法最终返回的是false,这个返回值涉及到编译器的处理逻辑,返回true表示已经处理,返回false表示未被处理,也就是说后续还会有操作。这里用true合理,还是false合理,由于我目前对于便编译器的编译逻辑还不清楚,所以目前还没法给出确切的答案,当然,在此例子中,true和false看起来都没什么问题。

那么现在还剩下一个问题,如何debug,这里面的debug包括debug自己的processor和debug编译后的代码。

1.debug自己的processor方法,这里讲的方法是基于idea18的方法

a)idea右上角,Edit Configurations

b)点击左上角的+号,选择Remote

c)随便起一个名字,比如ProcessorDebug,如图,点击确定。

d)进入Terminal界面,输入mvnDebug clean install,回车,不出意外的话,会出现如图的提示

f)process里面打上断点,然后点击debug按钮,便命中了断点

2.Debug编译后的代码,使用maven,点击install之后,在target文件里面找到编译好的class文件app.class。

这里由于我之前创建的项目没有包,所以我改了一下,把包加了进去,和之前例子里面有些不同。打开这个app.class,看到的文件如下,这个是增强后的代码:

现在你只是能够看到增强后的代码,并没有办法调试它,虽然你可以在代码上打断点,但是这个断点并没有什么用处。如何debug它,我目前还不知道有什么方法,能想到的替代方法就是把这部分代码先拿出来替换掉源码,利用源码debug,debug通了,再还原回去。

换句话说,使用自动代码修改源码,大多数场景下,我们实现的都是很简单的代码,比如lombok给我们增强的代码都是很简单的,是没有必要debug的,如果你真的碰见需要debug的场景,那么你首先要考虑的是这个场景是否适合用AbstractProcessor来解决问题,如果确定适合又必须单步调试,那么就先按照我说的笨方法来解决吧。

至于其他的没有说到的部分,有兴趣的话可以自行在网上找些资料研究一下。后续针对这块,我还会深入探究。

补充

有很多人跟我反馈无法按照我的步骤实现代码织入,这里我把示例代码放上:GitHub - DaiAnpeng/AbstractProcessorDemo

我后期可能还会继续完善这个项目,争取对javac api做一次封装,让代码织入相对容易些,如果您有时间精力,欢迎您在我的基础上补充,在该项目上重新创建一个module就可以了。

我分析了一下无法成功织入代码的原因,大概可能是因为以下几个原因:

1.services文件没有正常生成,这个是关键,在打包的时候,看一下这个文件有没有正常生成

在我提供的示例代码中,生成的内容如下:

2.SupportedAnnotationTypes没有写完整的包名,我讲解的时候,项目里面没有使用包名,所以代码里面没有写包名,但是你创建的项目可能存在包名,这个时候你就需要写全了,参考我后来提供的示例代码:

  • 12
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 29
    评论
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值