Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据。
Java语言中的类、方法、变量、参数和包等都可以被标注。Java标注和Javadoc不同,标注有自反性。在编译器生成类文件时,标注可以被嵌入到字节码中,由Java虚拟机执行时获取到标注。
根据元注解 @Retention
指定值的不同,注解可分为 SOURCE
、 CLASS
和 RUNTIME
三种类型。当被声明为SOURCE时,注解仅仅在源码级别被保留,编译时被丢弃;声明为CLASS时,注解会由编译器记录在class文件内,但在运行时会被忽略,默认的Retention级别即为CLASS;声明为RUNTIME时,注解将被保留到运行时,可通过反射在运行时获取到。
下面我们针对CLASS级别的注解,介绍在编译期处理注解的方法。
注解处理器( 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
注解的处理器。
此处我们将注解解析器作为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方法中完成对注解的解析和代码生成操作:
@Override public 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,触发注解处理器处理逻辑即可实现断点调试。
大概的内容就是这些,剩下的就是一些小细节了,需要自己领会233