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​(类 a) 返回使用给定注解类型注解的元素。
Set<? extends Element> getElementsAnnotatedWith​(TypeElement a) 返回使用给定注解类型注解的元素。

有关RoundEnvironment中方法的介绍,我们在下一篇博文中再做进一步介绍。

小结

本文所介绍的内容不多,主要包括AbstractProcessor在编译过程中的处理时机,以及AbstractProcessor中的几个方法,可能更多的还是源码的分析与介绍。有关注解进阶的文章大概可以梳理三篇博文,后面会继续介绍如何在AndroidStudio开发工具中定义配置注解,如何在注解处理器中拿到所定义的注解类型,如何区分不同的注解类型,如哪些注解是类或者接口,哪些是方法,哪些是参数等等。最后,会通过一个简单的示例,演示如何使用编译时注解实现类似ButterKnife的功能。

参考资料

关于注解你所需要知道的一切

注解处理,RoundEnvironment.processingOver()

Java 《注解篇》 编译时注解

评论

您确定要删除吗?删除之后不可恢复