Java注解进阶一
引言
在ServiceLoader一文中,可以了解到ServiceLoader解耦了服务的具体实现和使用,使得程序的扩展性大大增强,甚至可插拔。但是,在项目中如果每次使用SPI接口都需要手动在相对应的文件中注册一下,是不是有点违背程序员的宗旨,能代码实现的功能尽量不手动去实现。相信许多熟悉J2EE的开发人员,应该都在XML文件中配置过IOC或者AOP相关类,而目前的常规开发中一般使用注解代替了XML。其实,生成ServiceLoader相对应的配置文件也有一个注解库AutoService,它是Google提供的一个可以通过注解自动注册SPI接口的库。
本文的重点并不在AutoService注解类的如何使用上面,而是通过分析AutoService源码,去了解Java中注解的另一个方面:编译时注解。平常接触比较多的可能都是运行时注解,不过,不管是编译时注解还是运行时注解,两者在注解的定义方式上面是一致的,不同的是它们对于注解的处理方式上。
目前比较流行的编译时注解库有Butterknife和Lombok等,Butterknife是一款Android终端开发库,而Lombok更常见于服务端开发。Butterknife是在编译时生成Java源代码文件,而Lombok则会直接作用于class字节码文件。
AbstractProcessor的发现过程
在进一步了解编译时注解之前,先了解一下注解在编译过程中处理时机。一般如果不了解Java代码的调用栈时,常常采用抛出异常的方式查看代码的调用栈。有一点需要说明一下,采用抛出异常方式时并不建议使用IDE开发工具,比如eclipse、IntelliJ IDEA或者Android Studio,因为这些开发工具都有一套自己的编译库,比如eclipse采用的是eclipse插件编译库,AndroidStudio中可能使用的是gradle编译,此时打印的调用栈可能并不是我们需要的。本文示例中我们使用最原始的javac命令方式编译。
如果使用注解开发一套通用第三方库,在定义了相关注解名之后,还需要借助一个注解处理器来解释处理注解。编译时注解也不例外,不过编译时注解有一个专门的类AbstractProcessor用于处理注解,AbstractProcessor类是一个实现了Processor接口的抽象类。
// 编译处理器类 D:\com\example\event\processor>javac -encoding UTF-8 D:\com\example\event\processor\MyAnnotationProcessor.java // 编译Java文件 D:\com\example\event\processor>javac -encoding UTF-8 -processor MyAnnotationProcessor D:\com\example\event\MainTest.java
有关编译时详细信息, 可以参看以下堆栈。
java.lang.NoClassDefFoundError: MyAnnotationProcessor (wrong name: com/example/event/processor/MyAnnotationProcessor) at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) at java.net.URLClassLoader.access$100(URLClassLoader.java:73) at java.net.URLClassLoader$1.run(URLClassLoader.java:368) at java.net.URLClassLoader$1.run(URLClassLoader.java:362) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:361) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator.hasNext(JavacProcessingEnvironment.java:409) at com.sun.tools.javac.processing.JavacProcessingEnvironment$DiscoveredProcessors$ProcessorStateIterator.hasNext(JavacProcessingEnvironment.java:609) at com.sun.tools.javac.processing.JavacProcessingEnvironment.atLeastOneProcessor(JavacProcessingEnvironment.java:447) at com.sun.tools.javac.main.JavaCompiler.initProcessAnnotations(JavaCompiler.java:1050) at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:852) at com.sun.tools.javac.main.Main.compile(Main.java:523) at com.sun.tools.javac.main.Main.compile(Main.java:381) at com.sun.tools.javac.main.Main.compile(Main.java:370) at com.sun.tools.javac.main.Main.compile(Main.java:361) at com.sun.tools.javac.Main.compile(Main.java:56) at com.sun.tools.javac.Main.main(Main.java:42)
其实编译过程主要涉及两个类JavaCompiler和JavacProcessingEnvironment,ClassLoader已经是加载class文件了,这里仅简单分析下JavaCompiler和JavacProcessingEnvironment部分关键代码,有兴趣的开发人员可以在网上查看更多源码。
如果查看Butterknife的源码,我们会发现在ButterKnifeProcessor类上面使用了AutoService注解,AutoService库是为了解决手动注册ServiceLoader的问题。这里可以推测,在注解处理类Processor的处理过程中应该也借助了ServiceLoader,既然可以查看JavaCompiler和JavacProcessingEnvironment源码,那么很容易通过源码可以验证推测的正确性。
在JavacProcessingEnvironment类中,有如下方法:
private void initProcessorIterator(Context context, Iterable<? extends Processor> processors) { // ... String processorNames = options.get("-processor"); JavaFileManager fileManager = context.get(JavaFileManager.class); // If processorpath is not explicitly set, use the classpath. processorClassLoader = fileManager.hasLocation(ANNOTATION_PROCESSOR_PATH) ? fileManager.getClassLoader(ANNOTATION_PROCESSOR_PATH) : fileManager.getClassLoader(CLASS_PATH); /* * If the "-processor" option is used, search the appropriate * path for the named class. Otherwise, use a service * provider mechanism to create the processor iterator. */ if (processorNames != null) { processorIterator = new NameProcessIterator(processorNames, processorClassLoader, log); } else { processorIterator = new ServiceIterator(processorClassLoader, log); } // ... }
options.get("-processor")
这个方法很容易理解,使用javac命令方式编译时option选项有-processor
参数,则使用
NameProcessIterator迭代器。本文示例就是使用的javac方式编译,所以走得是这一条流程,NameProcessIterator再往里查看源代码,可以发现接下来就是使用loadClass()方式生成Processor实例了。
然后看下ServiceIterator类,如下是ServiceIterator构造方法。
ServiceIterator(ClassLoader classLoader, Log log) { // ... Class<?> loaderClass; String loadMethodName; loaderClass = Class.forName("java.util.ServiceLoader"); loadMethodName = "load"; Method loadMethod = loaderClass.getMethod(loadMethodName, Object result = loadMethod.invoke(null,Processor.class,classLoader); Method m = loaderClass.getMethod("iterator"); result = m.invoke(result); // serviceLoader.iterator(); this.iterator = (Iterator<?>) result; // ... }
在ServiceIterator的构造方法中,通过反射动态调用了java.util.ServiceLoader对应的load方法,通过语句Object result = loadMethod.invoke(null, Processor.class, classLoader)
实现,其中,传入了接口参数Processor.class,最终完成了基于ServiceLoader的服务动态发现过程。
至此,编译时注解处理器的发现过程,流程上已经相对清晰,如果是javac命令,则直接通过option参数传入,否则通过反射使用ServiceLoader的SPI方式。
AbstractProcessor执行过程
先介绍AbstractProcessor类的几个主要方法,一般涉及到如下三个主要方法:
public Set<String> getSupportedAnnotationTypes() public SourceVersion getSupportedSourceVersion() public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
getSupportedAnnotationTypes()
可以通过getSupportedAnnotationTypes()方法返回所有可支持的注解类型,如果没有任何类型,一般返回一个空的集合。
除了可以使用getSupportedAnnotationTypes()方法之外,还可以通过SupportedAnnotationTypes注解返回所有可支持的注解类型。一般情况下方法或者注解在使用时二选一即可,但是,如果支持的注解类型比较多,建议使用方法的方式,这样代码结构上面更清晰,直接返回一个不可修改的集合。其实使用注解,最终也是返回了一个不可修改的集合。
public Set<String> getSupportedAnnotationTypes() { SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class); if (sat == null) { if (isInitialized()) processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "No SupportedAnnotationTypes annotation " + "found on " + this.getClass().getName() + ", returning an empty set."); return Collections.emptySet(); } else return arrayToSet(sat.value()); } private static Set<String> arrayToSet(String[] array) { assert array != null; Set<String> set = new HashSet<String>(array.length); for (String s : array) set.add(s); return Collections.unmodifiableSet(set); }
getSupportedSourceVersion()
该方法用于返回Java编程语言的源版本。同getSupportedAnnotationTypes()方法类型,getSupportedSourceVersion()也有注解支持类似功能,通过SupportedSourceVersion就可以返回Java的源版本,如果注解中没有标明支持的源版本,一般返回SourceVersion.RELEASE_6。
getSupportedSourceVersion()在注解处理器中所起的作用不大,一般如果返回的源版本小于当前编译器的源版本,在编译时会输出一些告警信息。
private void checkSourceVersionCompatibility(Source source, Log log) { SourceVersion procSourceVersion = processor.getSupportedSourceVersion(); if (procSourceVersion.compareTo(Source.toSourceVersion(source)) < 0 ) { log.warning("proc.processor.incompatible.source.version", procSourceVersion, processor.getClass().getName(), source.name); } }
process()方法
process()方法可以说是AbstractProcessor核心方法,对注解的解释处理一般都会在该方法中进行。
public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
process()方法是一个boolean类型的方法,如果返回true,则表明这些注解已声明并且不要求后续Processor处理它们;如果返回false,则表明这些注解未声明并且可能要求后续Processor处理它们。Processor可能总是返回相同的boolean值,或者可能基于所选择的标准而返回不同的结果。
annotations入参是集合类型,这里的annotations其实就是getSupportedAnnotationTypes返回的值,即所有支持的注解类型。
roundEnv入参类型是RoundEnvironment,RoundEnvironment在整个注解的处理过程中扮演着十分重要的角色,根据类的命名也可以知道大致的意义,它实际上保存了注解处理器在某一轮中处理注解的相关信息,这里为什么说是某一轮呢?因为在整个注解处理器执行过程中,process()方法会被调用多次。
官方的java文档定义的解释如下:
注解处理发生在一系列轮次中。在每轮中,一个解释器可能会被要求解释源码中注解的一个子集,并且类文件的产生是在优先的轮次中。输入到首轮的解释是工具运行的初始输入; 这些初始输入可被视为第0轮。
RoundEnvironment
RoundEnvironment可以简单理解为是注解处理过程中某一轮的环境变量。
变量和类型 | 方法 | 描述 |
---|---|---|
boolean | errorRaised() | 如果在前一轮处理中引发错误,则返回true;否则返回false。 |
boolean | processingOver() | 如果此轮生成的类型不受后续轮注解处理的影响,则返回true;否则返回false。 |
Set<? extends Element> | getElementsAnnotatedWith(类 extends Annotation> a) | 返回使用给定注解类型注解的元素。 |
Set<? extends Element> | getElementsAnnotatedWith(TypeElement a) | 返回使用给定注解类型注解的元素。 |
有关RoundEnvironment中方法的介绍,我们在下一篇博文中再做进一步介绍。
小结
本文所介绍的内容不多,主要包括AbstractProcessor在编译过程中的处理时机,以及AbstractProcessor中的几个方法,可能更多的还是源码的分析与介绍。有关注解进阶的文章大概可以梳理三篇博文,后面会继续介绍如何在AndroidStudio开发工具中定义配置注解,如何在注解处理器中拿到所定义的注解类型,如何区分不同的注解类型,如哪些注解是类或者接口,哪些是方法,哪些是参数等等。最后,会通过一个简单的示例,演示如何使用编译时注解实现类似ButterKnife的功能。