解析Javac源码APT执行原理
Posted 小陈乱敲代码
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解析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
//不是最后一次执行此处
discoverAndRunProcs(context, annotationsPresent, topLevelClasses, packageInfoFiles);
catch (Throwable t)
//.......
finally
//.......
此时,会通过discoveredProcs.iterator().runContributingProcs(renv);执行最后一轮APT。
public void runContributingProcs(RoundEnvironment re)
if (!onProcInterator)
// process方法第一个参数
Set<TypeElement> emptyTypeElements = Collections.emptySet();
while(innerIter.hasNext())
ProcessorState ps = innerIter.next();
//执行最后一轮
if (ps.contributed)
callProcessor(ps.processor, emptyTypeElements, re);
可以看到在执行最后一轮时,会构建一个空Set集合作为process方法的第一个参数。这么做的意义猜测是为了将工作和打扫分开。
第一轮执行:处理你的工作
第X轮执行: 处理你的工作
最后执行:在处理完工作之后,再通知一次,执行收尾。
如:
public class LanceProcessor extends AbstractProcessor
FileOutputStream fos;
int i = 0;
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
//最后一轮执行会为true
if (roundEnvironment.processingOver())
if (fos != null)
//最后一轮收尾,close
try
fos.close();
catch (IOException e)
e.printStackTrace();
else
if (fos == null)
// 只在第一轮执行初始化。
fos = new FileOutputStream(xxx);
fos.write(("第" + i + "次执行").getBytes());
i++;
//......
return true;
总结
Q:APT原理是什么,怎么被执行起来的?
A:在javac编译时,先通过SPI机制加载所有的APT实现类,并将解析到需要编译的java源文件中所有的注解信息,与APT声明的支持处理的注解信息进行匹配,若待编译的源文件中存在APT声明的注解,则调起APT实现类的process方法。
Q:APT中process方法到底执行几次?为什么这么设计?
A:最少执行两次,最多无数次。最后一次执行可以视为Finish结束通知,执行收尾工作。
Q:APT中process方法boolean返回值返回true或者false有什么影响?
A: true:声明的注解只能被当前APT处理;false:在当前APT处理完声明的注解后,仍能被其他APT处理。
以上是关于解析Javac源码APT执行原理的主要内容,如果未能解决你的问题,请参考以下文章