apt你真的了解吗?解析javac源码apt执行原理(代码片段)

冬天的毛毛雨 冬天的毛毛雨     2023-01-07     248

关键词:

前言

最近又到了面试季,大家的技术都在提升,如果自己还是原地踏步,工作10年还是在用着刚工作1-2年的技术在应对现在的开发,所以很多同学的感受是:面试一年比一年“难”。在和一些同学的交流中,感觉很多同学的技术并不扎实。对于很多技术听说过,也大致写过Demo,就认为自己懂了。比如这次和大家分享的APT,小公司不会问,大公司要问就不再是怎么使用这么简单了。

但是在网上搜索了一个圈都没发现有针对APT原理分析的文章,所以本篇文件我们就根据javac源码彻底搞清楚APT的执行与设计。

阅读前提


1、了解APT是什么?
2、基于APT能够做什么?(应用场景)
3、怎么使用(编写自己的)APT程序。

解决问题

1、APT原理是什么,怎么被执行起来的?
2、APT中process方法到底执行几次?为什么这么设计?
3、APT中process方法boolean返回值返回true或者false有什么影响?

APT原理

大家在Android Studio上开发,可以创建一个java模块来实现APT,在这个模块中写一个类继承AbstractProcessor,同时还要进行注册,注册可以采用两种方式:

1、手动
在src/main目录下,创建resources/META-INF/services/javax.annotation.processing.Processor 文件,在文件里写上APT的实现类(AbstractProcessor子类)全限定名。

2、自动
在APT模块中引入Google的AutoService,使用@AutoService(Processor.class)注解声明APT的实现类(AbstractProcessor子类), AutoService本质上也是利用APT技术来自动创建了第一种方式的注册文件。

大家请注意,@AutoService传的是Processor.class,而手动创建的方式写的文件名其实就是Processor类的全限定名。

其实javac认定的注解处理器是实现了 Processor接口的类,我们一般继承的AbstractProcessor就是实现了Processor接口。

在完成了APT程序的实现以及注册之后,接下来我们可以直接利用Gradle的依赖配置组:annotationProcessor引入我们的APT模块。也可以将APT模块打包出单独的Jar包程序,利用javac -processorpath xxx.jar(APT) 对指定的java源文件进行编译。

其实到这里,APT怎么被执行起来的已经很明显了。APT程序就是Javac的小插件,由javac在编译时候根据条件调起! 具体的执行过程可以结合javac源码进一步了解。

Javac执行追溯

javac本身也是一个计算机程序,当需要编译java源代码时就需要执行程序。而javac程序的main函数定义在com/sun/tools/javac/Main.java中:

public class Main public static void main(String[] args) throws Exception 
        System.exit(compile(args));
    public static int compile(String[] args) 
        com.sun.tools.javac.main.Main compiler =
            new com.sun.tools.javac.main.Main("javac");
        return compiler.compile(args).exitCode;
    
​
​
    public static int compile(String[] args, PrintWriter out) 
        com.sun.tools.javac.main.Main compiler =
            new com.sun.tools.javac.main.Main("javac", out);
        return compiler.compile(args).exitCode;
    

可以看到,当执行javac程序将会执行上面的main方法,而main方法会调用到compile方法,在compile方法中又会创建com.sun.tools.javac.main.Main并执行其compile方法。

打开com/sun/tools/javac/main/Main.java文件,其compile实现为:

public Result compile(String[] args) 
        Context context = new Context();
        JavacFileManager.preRegister(context); 
    // 调用两参的重载compile方法
    Result result = compile(args, context);
    if (fileManager instanceof JavacFileManager) 
        ((JavacFileManager)fileManager).close();
    
    return result;
public Result compile(String[] args, Context context) 
        // 最后一个参数:processors 本次编译要执行的注解处理器集合 直接置为null
        return compile(args, context, List.<JavaFileObject>nil(), null);

​
​
public Result compile(String[] args,
                       Context context,
                       List<JavaFileObject> fileObjects,
                       Iterable<? extends Processor> processors)
        return compile(args,  null, context, fileObjects, processors);

public Result compile(String[] args,
                          String[] classNames,
                          Context context,
                          List<JavaFileObject> fileObjects,
                          Iterable<? extends Processor> processors)
    //......
​
    comp = JavaCompiler.instance(context);
    //......
    comp.compile(fileObjects,
                         classnames.toList(),
                         processors);//......

具体的编译是通过JavaCompiler#compile完成。这个方法的第三个参数即为要执行的注解处理器集合,根据执行流程追溯,此处直接传递的为null。进一步进入com/sun/tools/javac/main/JavaCompiler.java

public void compile(List sourceFileObjects,
List classnames,
Iterable<? extends Processor> processors)
//......
//初始化
initProcessAnnotations(processors);
//执行注解处理器
delegateCompiler =
            processAnnotations(
                enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                classnames);
//......

初始化

终于在JavaCompiler#compile方法中找到了javac执行过程中对APT的处理。首先initProcessAnnotations方法实现了对APT的初始化。根据源码流程可知此时,该方法参数为要执行的注解处理器集合,当前其实被设置为null。

那initProcessAnnotations方法中会怎么初始化我们的APT程序呢?实际上,在一开始我们说APT程序就是Javac的小插件,由javac在编译时候根据条件调起! 那么既然javac要调起APT中AbstractProcessor的process方法,而process方法是实例方法,自然需要先实现对APT中的AbstractProcessor(Processor接口)实现类class对象的加载。

而这个实现类由我们编写,javac如何得知要加载的APT实现类的类名呢?

结合到文章最开头处APT的注册,相信基础扎实的同学马上就能够想到:Java SPI机制。实际上,javac就是利用ServiceLoader加载注册文件,从而得到了APT实现类的类名!

很多同学听到AutoService就只能想到APT,这是片面的,实际上AutoService就是利用APT技术完成对Java SPI机制配置文件的自动生成。
ServiceLoader源码非常简单,Java与Android的实现也没有差异,可以自行阅读。

public void initProcessAnnotations(Iterable<? extends Processor> processors) 
      //......
      procEnvImpl = JavacProcessingEnvironment.instance(context);
      procEnvImpl.setProcessors(processors); 
      //......

进入com/sun/tools/javac/processing/JavacProcessingEnvironment.java文件:

public void setProcessors(Iterable<? extends Processor> processors) 
        Assert.checkNull(discoveredProcs);
        initProcessorIterator(context, processors);

private void initProcessorIterator(Context context, Iterable<? extends Processor> processors) 
        Log log = Log.instance(context);
        //要执行的注解处理器集合
        Iterator<? extends Processor> processorIterator;
        //....
        // ServiceIterator 使用SPI机制(ServiceLoader)加载注册文件并创建APT实现类实例对象
        processorIterator = new ServiceIterator(processorClassLoader, log);
        //....
        discoveredProcs = new DiscoveredProcessors(processorIterator);

执行注解处理器

回到JavaCompiler#compile,在通过initProcessAnnotations初始化注解处理器后,接着执行processAnnotations实现对注解的处理。

public JavaCompiler processAnnotations(List<JCCompilationUnit> roots,
                                           List<String> classnames) 
    //......
    JavaCompiler c = procEnvImpl.doProcessing(context, roots, classSymbols, pckSymbols,
                        deferredDiagnosticHandler);
    //......

进入com/sun/tools/javac/processing/JavacProcessingEnvironment.java文件:

 public JavaCompiler doProcessing(Context context,
                                     List<JCCompilationUnit> roots,
                                     List<ClassSymbol> classSymbols,
                                     Iterable<? extends PackageSymbol> pckSymbols,
                                     Log.DeferredDiagnosticHandler deferredDiagnosticHandler) 
    Round round = new Round(context, roots, classSymbols, deferredDiagnosticHandler);
    boolean errorStatus;
    boolean moreToDo;
    do 
        // 第一次执行apt
        round.run(false, false);
        errorStatus = round.unrecoverableError();
        moreToDo = moreToDo(); //执行apt后是否还需要再次执行
        round = round.next(
                    new LinkedHashSet<JavaFileObject>(filer.getGeneratedSourceFileObjects()),
                    new LinkedHashMap<String,JavaFileObject>(filer.getGeneratedClasses()));
        if (round.unrecoverableError())
            errorStatus = true; while (moreToDo && !errorStatus);// 最后一次执行apt
    round.run(true, errorStatus);
    //......

此处代码包含了本文需要解决的第2、3个文件。注解处理器的执行是由javac调起我们APT实现类的process方法,而这个方法就是在round.run中调起的。

第一次执行
第一次执行process方法是在do-while中调起round.run(false, false)完成。

void run(boolean lastRound, boolean errorStatus) 
    try 
        if (lastRound) 
            filer.setLastRound(true);
            Set<Element> emptyRootElements = Collections.emptySet(); // immutable
            RoundEnvironment renv = new JavacRoundEnvironment(true,
                            errorStatus,
                            emptyRootElements,
                            JavacProcessingEnvironment.this);
             //只有最后一次执行此处
            discoveredProcs.iterator().runContributingProcs(renv);
         else 
            //不是最后一次执行此处
            discoverAndRunProcs(context, annotationsPresent, topLevelClasses, packageInfoFiles);
        
         catch (Throwable t) 
             //.......
         finally 
            //.......
        

如果我们的APT实现类将会被javac调起process方法,它的原型是:
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
在编译过程中第一次由discoverAndRunProcs调起:

 private void discoverAndRunProcs(Context context,
                                     Set<TypeElement> annotationsPresent,
                                     List<ClassSymbol> topLevelClasses,
                                     List<PackageSymbol> packageInfoFiles) 
        //......
        //调用APT实现类的process方法的参数
        RoundEnvironment renv = new JavacRoundEnvironment(false,
                                                          false,
                                                          rootElements,
                                                          JavacProcessingEnvironment.this);while(unmatchedAnnotations.size() > 0 && psi.hasNext() ) 
            ProcessorState ps = psi.next();
            Set<String>  matchedNames = new HashSet<String>();
            Set<TypeElement> typeElements = new LinkedHashSet<TypeElement>();for (Map.Entry<String, TypeElement> entry: unmatchedAnnotations.entrySet()) 
                String unmatchedAnnotationName = entry.getKey();
                //匹配apt实现类支持的注解
                if (ps.annotationSupported(unmatchedAnnotationName) ) 
                    matchedNames.add(unmatchedAnnotationName);
                    //调用APT实现类的process方法的参数
                    TypeElement te = entry.getValue();
                    if (te != null)
                        typeElements.add(te);
                
            if (matchedNames.size() > 0 || ps.contributed) 
                //执行注解处理器
                boolean processingResult = callProcessor(ps.processor, typeElements, renv);
                /**
                 * TODO 问题3 
                 * APT实现类返回值为ture,删除它能处理的注解信息,
                 * 这样其他需要处理相同注解的注解处理器就得不到执行了
                 */
                if (processingResult) 
                    // unmatchedAnnotations : 所有的注解集合
                    // matchedNames:匹配此注解处理器的注解
                    unmatchedAnnotations.keySet().removeAll(matchedNames);
                
        
    //......
private boolean callProcessor(Processor proc,Set<? extends TypeElement> tes,
                              RoundEnvironment renv) 
    return proc.process(tes, renv);
        

process方法返回值

其实到现在,我们已经看到文章开头的第三个问题的答案。

在javac执行时可以指定多个APT程序(-processorpath 指定的jar包),一个APT程序可以包含多个APT实现类,所以javac会将指定的多个APT程序中的所有注册的APT实现类加载并实例化,使用迭代器Iterator装载。

在discoverAndRunProcs中对所有要执行的APT实现类进行迭代,依次执行APT实现类的process方法,顺序由注册顺序决定。

但是执行APT实现类的前提是:有APT实现类声明的支持处理的注解信息。而若先注册的APT实现类其process方法返回true,则会在执行结束此APT实现类后,通过
unmatchedAnnotations.keySet().removeAll(matchedNames);将其能处理的注解信息删除。这样后注册的APT实现类将会因为没有匹配处理的注解而得不到执行。

比如AProcessor声明处理@Test注解,而BProcessor也声明处理@Test注解,而AProcessor先于BProcessor注册,AProcessor的process方法返回ture。此时BProcessor不会执行。

第2-N次执行
回到JavacProcessingEnvironment#doProcessing

public JavaCompiler doProcessing(Context context,
                                     List<JCCompilationUnit> roots,
                                     List<ClassSymbol> classSymbols,
                                     Iterable<? extends PackageSymbol> pckSymbols,
                                     Log.DeferredDiagnosticHandler deferredDiagnosticHandler) 
    Round round = new Round(context, roots, classSymbols, deferredDiagnosticHandler);
    boolean errorStatus;
    boolean moreToDo;
    do 
        // 第一次执行apt
        round.run(false, false);
        errorStatus = round.unrecoverableError();
        moreToDo = moreToDo(); //执行apt后是否还需要再次执行
        round = round.next(
                    new LinkedHashSet<JavaFileObject>(filer.getGeneratedSourceFileObjects()),
                    new LinkedHashMap<String,JavaFileObject>(filer.getGeneratedClasses()));
        if (round.unrecoverableError())
            errorStatus = true; while (moreToDo && !errorStatus);// 最后一次执行apt
    round.run(true, errorStatus);
    //......

round.run(false, false);是在do-while循环中被调用,这也是为什么本节小标题为:第2-N次执行。执行多轮的条件为:moreToDo && !errorStatus。

第二个条件是执行APT实现类时未产生异常,而第一个条件:moreTodo

private boolean moreToDo() 
    return filer.newFiles();

public boolean newFiles() 
    return (!generatedSourceNames.isEmpty())
            || (!generatedClasses.isEmpty());

如果熟悉APT的同学,应该清楚,一般的我们利用APT实现在编译阶段生成新的Java类:
//在apt中生成 Test.java

JavaFileObject sourceFile = processingEnv.getFiler()
                        .createSourceFile("com.xx.Test");
OutputStream os = sourceFile.openOutputStream();
os.write("package com.xx;\\n  public class Test".getBytes());
os.close();

JavaPoet框架实际上就是封装了这些API。所以学习技术还是应该掌握本质与原理,否则学习其他相关联或者类似技术时,只能从头开始,很吃力。这也是所谓面试八股文的意义,掌握和没掌握,确实有差别!

在执行到os.close()时就会执行一次generatedSourceNames.add(typeName)。

也就是说APT执行多次的条件是:在APT执行是生成了一个java源文件(或者class文件)都会导致APT再执行一次,这次执行只会处理新生成的类:

//只处理新生成类的round
round = round.next( new LinkedHashSet<JavaFileObject>(filer.getGeneratedSourceFileObjects()),
                    new LinkedHashMap<String,JavaFileObject>(filer.getGeneratedClasses()));
//执行apt
round.run(false, false);

而如果第二轮执行又新生成了类,就会执行第三轮、第四轮…,直到不再产生新的.java(或.class)

最后一次执行

不产生新的类文件时会退出do-While循环,此时会执行一次round.run(true, errorStatus);

void run(boolean lastRound, boolean errorStatus) 
    try 
        if (lastRound) 
            filer.setLastRound(true);
            Set<Element> emptyRootElements = Collections.emptySet(); // immutable
            RoundEnvironment renv = new JavacRoundEnvironment(true,
                            errorStatus,
                            emptyRootElements,
                            JavacProcessingEnvironment.this);
             //只有最后一次执行此处
            discoveredProcs.iterator().runContributingProcs(renv);
         else 
            //不是最后一次执行

解析javac源码apt执行原理(代码片段)

阅读前提1、了解APT是什么?2、基于APT能够做什么?(应用场景)3、怎么编写自己的APT程序。解决问题1、APT原理是什么,怎么被执行起来的?2、APT中process方法到底执行几次?为什么这么设计?3、A... 查看详情

想了解apt与加密勒索软件?那这篇文章你绝不能错过

目前全球APT攻击趋势如何?针对APT攻击,企业应如何防护?针对最普通的APT攻击方式加密勒索软件,现今有何对策?带着这些疑问,记者采访到APT攻击方面的安全专家,来自亚信安全的APT治理战略及网关产品线总监白日和产品管理部... 查看详情

你真的了解“对象解构赋值”吗?关于对象解构的全面解析✌

前言大家好,我是 CoderBin。本文将给大家分享JavaScript中,有关对象解构赋值的那些代码技巧,希望能给大家带来帮助,谢谢。如果文中有不对、疑惑的地方,欢迎在评论区留言指正 查看详情

android-asm字节码插桩与apt原理补充

...T原理补充技术点APT补充1.策略模式2.SPI机制分析3.通过javac源码分析APT执行原理ASM1.逆波兰表达式2.java文件转换class文件基本规则3.ASM框架完成字节码插桩APT在java文件编译成class文件的过程中,apt可以监视在这个过程中的注解... 查看详情

android-asm字节码插桩与apt原理补充

...T原理补充技术点APT补充1.策略模式2.SPI机制分析3.通过javac源码分析APT执行原理ASM1.逆波兰表达式2.java文件转换class文件基本规则3.ASM框架完成字节码插桩APT在java文件编译成class文件的过程中,apt可以监视在这个过程中的注解... 查看详情

java示例代码_在apt/javac命令行上指定一个AbstractProcessor

java示例代码_在apt/javac命令行上指定一个AbstractProcessor 查看详情

Debian:了解 /etc/apt/sources.list

】Debian:了解/etc/apt/sources.list【英文标题】:Debian:understanding/etc/apt/sources.list【发布时间】:2015-02-0116:13:23【问题描述】:我是Debian世界的新手。我刚刚获得了一个私人虚拟服务器来托管我的网站,我目前正在学习如何正确保护... 查看详情

java.lang.IllegalAccessError:类 lombok.javac.apt.LombokProcessor 无法访问类 com.sun.tools.javac.processing

】java.lang.IllegalAccessError:类lombok.javac.apt.LombokProcessor无法访问类com.sun.tools.javac.processing.JavacProcessingEnvironment[重复]【英文标题】:java.lang.IllegalAccessError:classlombok.javac.apt.LombokProcessorcannotaccessclassc 查看详情

在命令行的类路径中包含 jars(javac 或 apt)

】在命令行的类路径中包含jars(javac或apt)【英文标题】:Includingjarsinclasspathoncommandline(javacorapt)【发布时间】:2011-01-0623:10:51【问题描述】:试图运行这个程序。我认为要设置我需要运行apt的所有Web服务。(虽然使用javac我有同... 查看详情

你真的了解asynctask吗?asynctask源码分析(代码片段)

转载请注明出处:http://blog.csdn.net/yianemail/article/details/516113261,概述AndroidUI是线程不安全的,如果想要在子线程很好的访问ui,就要借助Android中的异步消息处理机制http://blog.csdn.net/yianemail/article/details/5023 查看详情

Java 构建开始失败 - 致命错误编译:java.lang.IllegalAccessError: class lombok.javac.apt.LombokProcessor

】Java构建开始失败-致命错误编译:java.lang.IllegalAccessError:classlombok.javac.apt.LombokProcessor【英文标题】:Javabuildhasstartedfailing-Fatalerrorcompiling:java.lang.IllegalAccessError:classlombok.javac.apt.LombokProcessor【发布时间】:2021-06- 查看详情

多态你真的了解吗?

概念面向对象的三大特性之一“多态”。多态表示不同的对象可以执行相同的动作,但要通过他们自己的实现代码来执行。多态首先是建立在继承的基础上的,先有继承才能有多态。多态是指不同的子类在继承父类后分别都重写... 查看详情

apt更改为国内源

...内容,如版本选择的是Ubuntu18.04LTS,内容如下#默认注释了源码镜像以提高aptupdate速度,如有需要可自行取消注释debhttps://mirrors.tuna.tsinghua.edu.cn/ubuntu/bionicmainrestricteduniversemultiverse#deb-srchttps://mirrors.tuna.tsinghua.edu.cn/ubuntu/bionicmainrestri... 查看详情

apt-getinstall与makeinstall的区别linux

...装软件的命令,其本身是一个工具。makeinstall是用于编译源码,生成目标的命令,两者完全没有联系!来自:求助得到的回答 参考技术B首先你要先搞清这么个问题apt-getinstall这个是基于deb包的安装makeinstall用的是tar.gz安装这种安... 查看详情

涨知识:equals和==你真的了解吗?(代码片段)

基本概念==是运算符,比较的是两个变量是否相等;equals()是Object方法,用于比较两个对象是否相等看一下源码:publicbooleanequals(ObjectanObject)if(this==anObject)returntrue;if(anObjectinstanceofString)StringanotherString=(String)anObject;intn=va 查看详情

字节码插桩android打包流程|android中的字节码操作方式|aop面向切面编程|apt编译时技术

文章目录一、Android中的Java源码打包流程1、Java源码打包流程2、字符串常量池二、Android中的字节码操作方式一、Android中的Java源码打包流程Java程序在Java虚拟机执行前,需要先将Java源码通过javac编译成.class字节码文件,然后才能在虚... 查看详情

为啥centos装上以后,执行apt-get命令提示没有该命令

...知识建议参考《linux就该这样学》。遇到问题就想办法去了解问题,这样才能提高问题。linux学习是一个漫长的过程,加油!!! 参考技术Bapt-get是ubuntu的软件安装命令,centos用的是yum命令。 查看详情

你真的了解lateralviewexplode吗?--源码复盘(代码片段)

​这几天情绪真的很down啊,包括写这篇文章的时候,无论如何,希望大家看了这篇文章后,功力能再上涨一层~~用Lateralviewexplode这么久,竟然发现,不是很了解它?Lateralview与UDTF函数一起使用,UDTF... 查看详情