简介Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据,下面我们就来聊聊关于java中的编译工具和执行工具?接下来我们就一起去了解一下吧!

java中的编译工具和执行工具(实现一个简单的Java编译时注解处理器)

java中的编译工具和执行工具

简介

Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据。

Java语言中的类、方法、变量、参数和包等都可以被标注。Java标注和Javadoc不同,标注有自反性。在编译器生成类文件时,标注可以被嵌入到字节码中,由Java虚拟机执行时获取到标注。

根据元注解@Retention指定值的不同,注解可分为SOURCEClassRUNTIME三种类型。当被声明为SOURCE时,注解仅仅在源码级别被保留,编译时被丢弃;声明为CLASS时,注解会由编译器记录在class文件内,但在运行时会被忽略,默认的Retention级别即为CLASS;声明为RUNTIME时,注解将被保留到运行时,可通过反射在运行时获取到。

下面我们针对CLASS级别的注解,介绍在编译期处理注解的方法。

APT

注解处理器(Annotation Processing Tool)是javac内置的工具,用于在编译时期扫描和处理注解信息。从JDK 6开始,apt暴露了可用的API。一个特定的处理器接收一个Java源代码或已编译的字节码作为输入,然后输出一些文件(通常是.java文件)。这就意味着你可以使用apt动态生成代码逻辑,需要注意的是apt仅可以生成新的Java类而不能对已存在的Java类进行修改。所有生成的Java类将和其他源代码一起被javac编译。

定义和使用注解

举个栗子,此处我们定义一个用于标注Field的注解Meta,包含两个参数repeat和id,在编译阶段我们将通过处理这一注解,给被标注的Field赋值,如repeat为2,id为Aa,则被标注的Field会被赋值为"AaAa"。

@Retention(RetentionPolicy.CLASS)@Target(ElementType.FIELD)public @interface Meta { int repeat() default 0; String id() default "";

}

在Field上使用注解

@Meta(repeat = 3, id = "^_^")

public String test;

处理注解

下面我们基于Android Studio编写一个处理上文中定义的Meta注解的处理器。

创建Module

此处我们将注解解析器作为Android Project中的一个module来开发,新建一个Module,类型选择Java Library。

创建处理器

注解需要通过注解处理器进行处理,所有的注解处理器都实现了Processor接口,一般我们选择继承AbstractProcessor来创建自定义注解处理器。

继承AbstractProcessor,实现public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)方法。方法参数中annotations包含了该处理器声明支持并已经在源码中使用了的注解,roundEnv则包含了注解处理的上下文环境。 此方法返回true时,表示此注解已经被处理完毕,返回false时将会交给其他处理器继续处理。

声明支持的注解类型和源码版本

覆盖getSupportedSourceVersion方法,返回处理器支持的源码版本,一般直接返回SourceVersion.latestSupported()即可。

覆盖getSupportedAnnotationTypes方法,返回处理器想要处理的注解类型,此处需返回一个包含了所有注解完全限定名的集合。

在Java 7及以上,可以使用类注解@SupportedAnnotationTypes@SupportedSourceVersion替代上面的方法进行声明。

声明注解处理器

注解处理器在使用前需要先向JVM注册,在module的META-INF目录下新建services目录,并创建一个名为javax.annotation.processing.Processor的文件,在此文件内逐行声明注解处理器。同样地,此处需要声明的也是处理器类的完全限定名。

另一个简便的方法是使用Google提供的auto-services库,在build.gradle中引入com.google.auto.service:auto-service:1.0-rc2,并在处理器类上添加注解@AutoService(Processor.class),auto-services也是一个注解处理器,会在编译时为该module生成声明文件。

解析注解

首选我们定义一个接口来规范生成的类:

public interface Actor { void action();

}

再定义一个类结构来描述我们生成的Java类:

public class TargetGen<T extends Target> implements Actor{ protected T target; public TargetGen(T obj) { this.target = obj;

} @Override

public void action() { //赋值操作

}

}

如果我们有一个类A,其中的Field f包含了Meta注解,我们会为其生成一个AGen类,并在action方法中完成对f的赋值操作。

在process方法中完成对注解的解析和代码生成操作:

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { /*roundEnv.getRootElements()会返回工程中所有的Class

在实际应用中需要对各个Class先做过滤以提高效率,避免对每个Class的内容都进行扫描*/

for (Element e : roundEnv.getRootElements()) {

List<String> statements = new ArrayList<>(); /*遍历Class内所有元素*/

for (Element el : e.getEnclosedElements()) { /*只处理包含了注解并被修饰为public的Field*/

if (el.getKind().isField() && el.getAnnotation(Meta.class) != null && el.getModifiers().contains(Modifier.PUBLIC)) { /*获取注解信息,生成代码片段*/

Meta meta = el.getAnnotation(Meta.class); int repeat = meta.repeat(); String seed = meta.id(); String result = ""; for (int i = 0; i < repeat; i ) {

result = seed;

}

statements.add("\t\ttarget." el.getSimpleName() " = \"" result "\";");

}

} if (statements.size() == 0) { return true;

} String enclosingName; if (e instanceof PackageElement) {

enclosingName = ((PackageElement) e).getQualifiedName().toString();

} else {

enclosingName = ((TypeElement) e).getQualifiedName().toString();

} /*获取生成类的类名和package*/

String pkgName = enclosingName.substring(0, enclosingName.lastIndexOf('.')); String clsName = e.getSimpleName() "Gen"; log(pkgName "," clsName); /*创建文件,写入代码内容*/

try {

JavaFileObject f = processingEnv.getFiler().createSourceFile(clsName); log(f.toUri().toString());

Writer writer = f.openWriter();

PrintWriter printWriter = new PrintWriter(writer);

printWriter.println("//Auto generated code, do not modify it!");

printWriter.println("package " pkgName ";");

printWriter.println("\nimport com.moxun.Actor;\n");

printWriter.println("public class " clsName "<T extends " e.getSimpleName() "> implements Actor{");

printWriter.println("\tprotected T target;");

printWriter.println("\n\tpublic " clsName "(T obj) {");

printWriter.println("\t\tthis.target = obj;");

printWriter.println("\t}\n");

printWriter.println("\t@Override");

printWriter.println("\tpublic void action() {"); for (String statement : statements) {

printWriter.println(statement);

}

printWriter.println("\t}");

printWriter.println("}");

printWriter.flush();

printWriter.close();

writer.close();

} catch (IOException e1) {

e1.printStackTrace();

}

} return true;

}

在目标module的dependencies中加入处理器模块的依赖,clean并rebuild工程,源代码就能被自定义的注解处理器处理并将产出的类生成到build/intermediates/classes目录下。由于一个Android Gradle插件的issue,直到插件版本2.2.0-alpha4,产出的class仍会被放到此目录下。intermediates目录下的源文件不会被IDE索引,所以给生成代码的调试带来一些不便,不过这并不影响后续的编译过程。在未来的版本中,该issue可能会被修正,产物会被输出到正确的地方也就是build/generated/source/apt目录下。

在运行时使用生成的类

在运行时可以使用反射来访问生成的类,此处定义了一个简单的帮助类来实例化生成的类并给目标Field赋值:

public class MetaLoader { public static void load(Object obj) { String fullName = obj.getClass().getCanonicalName(); String pkgName = fullName.substring(0, fullName.lastIndexOf('.')); String clsName = pkgName "." obj.getClass().getSimpleName() "Gen"; try {

Class<Actor> clazz = (Class<Actor>) Class.forName(clsName);

Constructor<Actor> constructor = clazz.getConstructor(obj.getClass());

Actor actor = constructor.newInstance(obj);

actor.action();

} catch (Exception e) {

e.printStackTrace();

}

}

}

在目标类初始化的时候调用MetaLoader.load,传入目标类的实例,便可完成对Field的赋值操作。

在打包过程中排除处理器

由于在前面引入了auto-service库,最终打包apk的时候会报错Duplicate files copied in APK META-INF/services/javax.annotation.processing.Processor,而该文件在运行时又是不需要的,所以可以在packagingOptions中排除这个文件以规避该错误:

packagingOptions { exclude 'META-INF/services/javax.annotation.processing.Processor'}

然而这并不是彻底的解决方案,如上所述,注解处理器在运行时是完全无用的,能否让其仅存在于编译期而不打包进最终产物内呢?答案是肯定的。

在工程的build.gradle内添加插件:

dependencies { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

//etc……}

在module的build.gradle内应用插件:

apply plugin: 'com.neenbedankt.android-apt'

应用插件后,dependencies会新增一个新的依赖方法apt,修改依赖声明为:

dependencies { compile fileTree(dir: 'libs', include: ['*.jar'])

apt project(':processor') //etc……}

如此声明后处理器module内的类将不会被打包到最终的产物中,有利于缩小产物体积。

调试注解处理器

在Android Studio中添加新的Run/Debug Configurations,类型选择Remote;

在工程的gradle.properties中添加

org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

选中上面定义的Configuration,点击Debug按钮等待目标进程attach;

在注解处理器逻辑内设置断点,选择Rebuild Project,触发注解处理器处理逻辑即可实现断点调试。

更多深度技术内容,请关注云栖社区yunqiinsight。

,