深入理解Java注解——编译时注解实战
Posted yuxiyu!
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解Java注解——编译时注解实战相关的知识,希望对你有一定的参考价值。
在前面两篇博客中我记录了Java注解的一些知识点,以及如何使用JavaPoet生成Java源码,本篇主要记录的是使用Java编译时注解完成一个类似于ButterKnife的android View注入功能,通过注解即可完成View的ID绑定,不再显式调用findViewById方法。如果对Java注解还不太熟悉,可以参考我前面两篇博文:深入理解Java注解(一)——注解基础 | 深入理解Java注解(二)——JavaPoet使用
开始
本篇博客的代码还是基于深入理解Java注解(一)——注解基础 这篇博文中最后一部分“编译时注解”的代码,由于在深入理解Java注解(一)——注解基础 这篇博文中没有记录编译时注解的详细用法,仅仅是实现了一个空的注解处理器,它并未实现任何功能,故本篇中会实现一个类似ButterKnife的Android View注入框架,其使用方法如下:
-
在Activity中定义View并使用
@MyAnnotation
注解。public class MainActivity extends AppCompatActivity { @MyAnnotation(R.id.text_view) TextView textView; @MyAnnotation(R.id.image_view) ImageView imageView; ... }
-
build项目后,会生成与
MainActivity
对应的MainActivityViewInjector
类,在MainActivity
的onCreate
中调用MainActivityViewInjector.inject(this);
即可完成View和ID的绑定。@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MainActivityViewInjector.inject(this); textView.setText("Hello Injector!"); imageView.setImageResource(R.drawable.ic_launcher_background); textView.setOnClickListener(v -> toOtherActivity()); }
-
每个
Activity
都会生成对应的XXXViewInjector
类,比如OtherActivity
会生成OtherActivityViewInjector
类。
实现步骤
定义注解
在深入理解Java注解(一)——注解基础 这篇博文中已经定义了@MyAnnotation
注解,由于我们需要将注解作用在Android的View上,且绑定其ID,ID为整型,故需要修改注解代码如下:
// 该注解作用在类的成员变量上
@Target(ElementType.FIELD)
// 该注解的保留策略为保留到字节码阶段
@Retention(RetentionPolicy.CLASS)
public @interface MyAnnotation {
int value(); // 对应View的ID
}
实现注解处理器
在深入理解Java注解(一)——注解基础 这篇博文中虽然定义了MyProcessor
注解处理器,但并未有具体实现,这里为了实现本篇开头说的View注入功能,需要实现MyProcessor
类的process
方法,先放上注解处理器的全部代码:
package com.example.processor;
import com.example.annotation.MyAnnotation;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.Diagnostic;
public class MyProcessor extends AbstractProcessor {
// 打印日志用
private Messager messager;
// 操作文件用
private Filer filer;
// 针对MainActivity会生成MainActivityViewInjector类
private static final String CLS_SUFFIX = "ViewInjector";
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
filer = processingEnv.getFiler();
}
// 必须复写该方法,否则注解处理器不知道处理哪个注解
@Override
public Set<String> getSupportedAnnotationTypes() {
// 返回支持的注解类型
Set<String> types = new HashSet<>();
types.add(MyAnnotation.class.getCanonicalName());
return types;
}
@Override
public SourceVersion getSupportedSourceVersion() {
// 返回支持的源代码版本
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 注解处理主要在该方法中
// elementsAnnotatedWith集合为所有被MyAnnotation注解的元素(Element represents a program element such as a package, class, or method)
Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MyAnnotation.class);
if (elementsAnnotatedWith.isEmpty()) {
// 集合为空,不用继续处理
return false;
}
// map中的key代表某个类的全路径,value存放这个类中所有被注解标记了的元素
Map<String, List<VariableElement>> map = new HashMap<>();
for (Element e : elementsAnnotatedWith) {
// kind表示元素类型
ElementKind kind = e.getKind();
// ElementKind.FIELD表示元素是一个字段
if (kind == ElementKind.FIELD) {
// VariableElement表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
VariableElement variableElement = (VariableElement) e;
String clsName = getFullClassName(variableElement);
if (!map.containsKey(clsName)) {
map.put(clsName, new ArrayList<VariableElement>());
}
map.get(clsName).add(variableElement);
messager.printMessage(Diagnostic.Kind.NOTE, "add element " + e + " in class " + clsName);
}
}
if (!map.isEmpty()) {
Map<String, JavaFile> stringListMap = generateJavaCode(map);
if (!stringListMap.isEmpty()) {
// 将所有类对应的新生成的Java源码写入到文件
Iterator<String> iterator = stringListMap.keySet().iterator();
while (iterator.hasNext()) {
String clsName = iterator.next();
JavaFile javaFile = stringListMap.get(clsName);
try {
// 将生成的Java源码文件写入到filter,写入成功就会在项目主module的build/generated/ap_generated_sources/目录下生成源码文件
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return true;
}
// 使用JavaPoet生成新的Java代码
private Map<String, JavaFile> generateJavaCode(Map<String, List<VariableElement>> map) {
Iterator<String> iterator = map.keySet().iterator();
Map<String, JavaFile> resultMap = new HashMap<>();
while (iterator.hasNext()) {
// 类名
String clsName = iterator.next();
// 类中被注解的字段集合
List<VariableElement> list = map.get(clsName);
// 包名
String pkgName = getPackageName(list.get(0));
// 构造一个静态的inject方法,在其中完成View的绑定
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("inject")
// 修饰器为public static
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
// 给方法添加参数,参数名为activity,参数类型是Activity
.addParameter(ClassName.get(pkgName, clsName), "activity");
// 遍历类中所有被注解标记的元素
for (VariableElement e : list) {
// 拿到被注解标记的字段名称
String fieldName = e.getSimpleName().toString();
// 拿到注解中的值(即View的ID值)
int value = e.getAnnotation(MyAnnotation.class).value();
// 给上面构造的inject方法添加View赋值代码(activity.textView = activity.findViewById(xxx))
methodSpecBuilder.addStatement("activity.$L = activity.findViewById($L)", fieldName, value);
}
// 每个类都生成对应的XXXViewInjector类
String generatedClsName = getSimpleClassName(list.get(0)) + CLS_SUFFIX;
// 构造XXXViewInjector类
TypeSpec typeSpec = TypeSpec.classBuilder(generatedClsName)
// 类的修饰器为public
.addModifiers(Modifier.PUBLIC)
// 给这个类添加方法
.addMethod(methodSpecBuilder.build())
.build();
// JavaFile表示一个Java源码文件
JavaFile javaFile = JavaFile.builder(pkgName, typeSpec).build();
resultMap.put(generatedClsName, javaFile);
}
return resultMap;
}
// 获取元素的类名(不包含包名)
private String getSimpleClassName(VariableElement element) {
return ((TypeElement) element.getEnclosingElement()).getSimpleName().toString();
}
// 获取元素所在的类名全路径(包括包名,比如com.example.testapt.MainActivity)
private String getFullClassName(VariableElement element) {
String packageName = getPackageName(element);
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
String className = typeElement.getSimpleName().toString();
return packageName + "." + className;
}
// 获取元素所在的包名
private String getPackageName(VariableElement variableElement) {
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
return processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();
}
}
代码有点长,但是基本上每一行都做了注释,下面针对以上代码中的process
方法做一些说明:
Element
是一个接口,它是程序元素的一个抽象,可以表示程序中的包、类、方法、字段等。它的源码中注释如下:Represents a program element such as a package, class, or method.
- 有很多类实现了
Element
接口,主要有如下几个:
类 | 说明 |
---|---|
ExecutableElement | 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素。 |
PackageElement | 表示一个包程序元素。提供对有关包及其成员的信息的访问。 |
TypeElement | 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。 |
TypeParameterElement | 表示一般类、接口、方法或构造方法元素的形式类型参数。 |
VariableElement | 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。 |
- 使用
getKind()
方法判断某个元素属于哪个类,比如e.getKind() == ElementKind.FIELD
判断元素是否是一个字段。 generateJavaCode
方法中主要是使用JavaPoet
生成了与某个Activity对应的XXXViewInjector
类,如果对JavaPoet
不熟悉,可以看下我的上一篇博文深入理解Java注解(二)——JavaPoet使用
验证
为了验证注解处理器是否正常工作,我们在AndroidStudio中点击菜单栏的Make Project
图标,可以看到在Build
视图中输出一些日志,如下图红色字体所示:
另外,在项目主module(我这里是app module)下的build/generated/ap_generated_sources/
目录下,会生成对应的XXXViewInjector
文件,如下图:
文件源码如下:
package com.example.testapt;
public class MainActivityViewInjector {
public static void inject(com.example.testapt.MainActivity activity) {
activity.textView = activity.findViewById(2131231104);
activity.imageView = activity.findViewById(2131230909);
}
}
将项目跑在模拟器上,可以看到View注入能正常工作:
源码
本篇博客的源码放在GitHub上:https://github.com/yubo725/test-apt/tree/v0.2
参考
以上是关于深入理解Java注解——编译时注解实战的主要内容,如果未能解决你的问题,请参考以下文章