Annotation系列之编译时注解使用

上一篇文章《Annotation系列之运行时注解使用》中讲述了如何在程序运行时获取并处理运行时注解,但很多时候在运行时处理注解并不是很好的选择,尤其当程序中需要使用大量反射才能达到我们的目的时,这时编译时注解也许是非常好的选择。编译时注解(RetentionPolicy.CLASS)会被编译器保留,也就是说在编译时我们就可以处理注解,那么编译时怎么能够处理注解呢?答案就是注解处理器(Annotation Processor)。

我们可以参照《使用注解处理器生成代码》中对注解处理器的介绍:

注解处理器在 Java 5 引入, 但那时并没有标准化的 API 可用, 需通过 apt(Annotation Processing
Tool)结合 Mirror API(com.sun.mirror)来实现. Java 6 开始, 注解处理器被标准化, 定义在 JSR
269 标准中, 在标准库中提供了 API, apt 也被集成到 javac 工具中。

以及《Java注解处理器》中对注解处理器的功能描述:

一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这具体的含义什么呢?你可以生成Java代码!这些生成的Java代码是在生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。

注解处理器的强大功能造福了Java开发者,像butterknifeEventBus3等Android界比较有名的开源项目都有用到编译时注解,并为我们提供了非常方便实用的功能。

接下来我们同样仿照butterknife,新建一个项目,介绍使用编译时注解实现View注入及click事件设置的大致流程。

1. 创建项目结构

  一般编译时注解库会有多个module,为了简单起见,我们的示例中只以Activity为处理目标,这里先对示例项目结构做个简要说明,主要有以下模块:
annotation模块:类型为Java Library,存放定义的编译时注解
processor模块:类型为Java Library,存放注解处理器相关类,依赖annotation模块
api 模块:类型为Android Library,依赖annotation模块,存放将Activity与注解处理器生成的注入处理类关联的api类
app模块:依赖processor模块和api 模块,作为示例程序入口及演示

2. 定义注解

  在annotation模块中需要创建如下两个注解:

定义绑定View的注解,有个int型值对应View的id

1
2
3
4
5
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}

定义绑定View的Click方法的注解,同样有个int型值对应View的id

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface BindClick {
int value();
}

3. 定义注解处理器

  在processor模块我们需要对上面定义的注解进行信息收集及java文件生成工作,我们的目标是能够根据一个Activity中的@BindView@BindClick注解,生成对应的一个工具类,以ReceiverActivity(简单的显示登录信息)为例,我们的目标是利用注解处理器自动生成ReceiverActivity_InjectUtil.java文件,这样后面我们只需要在ReceiverActivity中想办法调用到ReceiverActivity_InjectUtil .inject(...)方法即可进行View绑定及Click事件设置,我们为每个使用注解的类生成的目标代码示例如下:

1
2
3
4
5
6
7
8
9
10
public final class ReceiverActivity_InjectUtil {
public static void inject(final ReceiverActivity activity) {
activity.mUserNameTextView = (android.widget.TextView)activity.findViewById(2131427417);
activity.mPasswordTextView = (android.widget.TextView)activity.findViewById(2131427418);
activity.findViewById(2131427419).setOnClickListener(new android.view.View.OnClickListener(){
public void onClick(android.view.View view) {
activity.onFinishClicked();
}});
}
}

1. 定义注解信息类ActivityAnnotatedInfo.java

  一个ActivityAnnotatedInfo类对应一个Activity类文件,用于记录Activity中的注解信息并提供生成对应Java文件对象的模板,ActivityAnnotatedInfo类示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116

public class ActivityAnnotatedInfo {
/**
* 生成的java文件后缀名,如ReceiverActivity生成的对应java文件为ReceiverActivity_InjectUtil.java
*/
private static final String SUFFIX = "_InjectUtil";

//存放Activity中一个id对应的注解信息
private final Map<Integer, ActivityAnnotatedInfo.IdAnnotatedInfo> viewIdMap = new LinkedHashMap<>();
private final String classPackage;//Activity类所在包的包名
private final String className;//Activity类的名称

ActivityAnnotatedInfo(String classPackage, String className) {
this.classPackage = classPackage;
this.className = className;
}

void addBindField(int id, String name, String type) {
getTargetIdAnnotatedInfo(id).field = new ActivityAnnotatedInfo.AnnotatedField(name, type);
}

void addBindMethod(int id, String name, String parameterType) {
getTargetIdAnnotatedInfo(id).method = new ActivityAnnotatedInfo.AnnotatedMethod(name, parameterType);
}

private ActivityAnnotatedInfo.IdAnnotatedInfo getTargetIdAnnotatedInfo(int id) {
ActivityAnnotatedInfo.IdAnnotatedInfo info = viewIdMap.get(id);
if (info == null) {
info = new ActivityAnnotatedInfo.IdAnnotatedInfo(id);
viewIdMap.put(id, info);
}
return info;
}

String getActivityName() {
return classPackage + "." + className;
}

/**
* 使用JavaPoet创建Java文件
*/
JavaFile createBinderClassFile() {
final String acName = getActivityName();
ClassName targetActivityName = ClassName.get(classPackage, className);
//方法名(inject())
MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(targetActivityName, "activity",Modifier.FINAL);
for (Map.Entry<Integer, ActivityAnnotatedInfo.IdAnnotatedInfo> entry : viewIdMap.entrySet()) {
......
//遍历处理每个View,生成处理代码
}
MethodSpec injectMethod = injectMethodBuilder.build();
//类名为:Activity名+$$InjectUtil.java
TypeSpec binderClass = TypeSpec.classBuilder(className + SUFFIX)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(injectMethod)
.build();
//构建java文件
return JavaFile.builder(classPackage, binderClass)
.addFileComment("auto generate InjectUtil class response to : $S ", acName)
.build();
}

/**
* 一个R.id.xxx(View)相关的注解信息,一个id只能被注解到一个变量和一个方法中
*/
private static class IdAnnotatedInfo {
final int id;
ActivityAnnotatedInfo.AnnotatedField field;
ActivityAnnotatedInfo.AnnotatedMethod method;

IdAnnotatedInfo(int id) {
this.id = id;
}
}

/**
* 被注解的View成员变量信息
*/
private static class AnnotatedField {
/**
* 变量名称,如 mPasswordTextView
*/
final String name;
/**
* View类型,如 TextView
*/
final String type;//变量

AnnotatedField(String name, String type) {
this.name = name;
this.type = type;
}
}

/**
* 被注解的Click方法信息
*/
private static class AnnotatedMethod {
/**
* 方法名称
*/
final String name;
/**
* 参数类型,必须为View
*/
final String parameterType;

AnnotatedMethod(String name, String parameterType){
this.name = name;
this.parameterType = parameterType;
}
}
}

  代码中使用到了JavaPoet用来构建java文件,当然我们也可以采用完全拼接的方式实现,这里不做过多介绍。下面我们还会用到AutoService,AutoService注解处理器是Google提供的,用于自动生成SPI(Service Provider Interface)标准中约定的文件结构:META-INF/services/配置文件,这里的配置文件是指以服务接口命名的配置文件,文件中记录了服务接口具体的实现类;这里要自动生成的文件名为javax.annotation.processing.Processor,表明提供的是一个注解处理器服务,文件中只有一行代码:com.example.runtime_processor.BindAnnotationProcessor,记录了Processor服务的一个具体实现类是BindAnnotationProcessor,这样才能被注解处理工具(Annotation Processing Tool )使用。使用AutoService省去了我们手动去创建SPI所约定的文件。processor模块中build.gradle文件如下:

1
2
3
4
5
6
7
8
9
10
11
apply plugin: 'java'

dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile project(':clazz_annotation')
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.8.0'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

2. 创建注解处理器BindAnnotationProcessor.java

  所有的注解处理器都要继承AbstractProcessor类,并复写 process(...) 方法,通常我们会复写4个方法来实现一个完整的注解处理器功能,BindAnnotationProcessor.java示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {
/**
* 用来处理Element(表示程序元素(如包、类或方法),每个元素代表静态的、语言级别的结构(而不是虚拟机的运行时结构))的工具类
*/
private Elements elementUtils;
/**
* 用来处理TypeMirror(Java编程语言中的类)的工具类
*/
private Types typeUtils;
/**
* 用于创建文件
*/
private Filer filer;


/**
* 每一个注解处理器类都必须有一个空的构造函数。
* 然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。
*
* @param processingEnvironment ProcessingEnvironment提供很多有用的工具类,如Elements, Types和Filer。
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//在这里进行工具的初始化
elementUtils = processingEnvironment.getElementUtils();
typeUtils = processingEnvironment.getTypeUtils();
filer = processingEnvironment.getFiler();
}

/**
* 这相当于每个处理器的主函数main()。
* 在这里进行注解的收集,以及Java文件的创建工作。
* 通过输入参数RoundEnvironment可以查询出包含特定注解的被注解元素
* @param set 请求被处理的注解类型
* @param roundEnvironment 当前和前一轮的信息环境
* @return 请求被处理的注解类型是否已被当前处理器声明并不需要后续Processor处理
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//收集所有Activity类文件中的注解信息
Map<TypeElement, ActivityAnnotatedInfo> targetClassMap = findAndParseTargets(roundEnvironment);

for (Map.Entry<TypeElement, ActivityAnnotatedInfo> entry : targetClassMap.entrySet()) {
TypeElement typeElement = entry.getKey();
ActivityAnnotatedInfo classBindInfo = entry.getValue();
// 为每个注解的Activity生成java文件
try {
JavaFile jfo = classBindInfo.createBinderClassFile();
jfo.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to generate InjectUtil for type %s: %s", typeElement, e.getMessage());
}
}
return true;
}

/**
* 这里你必须指定,这个注解处理器是注册给哪个注解的。
* 注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
* 换句话说,你在这里定义你的注解处理器注册到哪些注解上。
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
//表明该注解处理器只处理@BindView和@BindClick两个注解
Set<String> annotations = new LinkedHashSet<>();
annotations.add(BindView.class.getCanonicalName());
annotations.add(BindClick.class.getCanonicalName());
return annotations;
}

/**
* 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。
* 在Java 7中,你也可以使用下面两个注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion()
* 1、@SupportedSourceVersion(SourceVersion.latestSupported())
* 2、@SupportedAnnotationTypes({ //合法注解全名的集合 })
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}

}

4. 提供api

  前面已经定义了注解、并定义了能够生成注解对应工具类的注解处理器,我们还需要提供一个工具类来将使用注解的类与注解处理器生成的类进行关联,在api 模块创建BindInjector.java类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class BindInjector {
/**
* BindAnnotationProcessor生成的java文件后缀名
*/
private static final String SUFFIX = "_InjectUtil";

private static final String TAG = "BindInjector";
private static final Map<Class<?>, Method> INJECT_UTILS = new LinkedHashMap<Class<?>, Method>();
private static final Method NO_OP = null;

/**
* 为xxActivity找到并调用xxActivity_InjectUtil工具类的inject方法进行注入
*/
public static void inject(Activity activity) {
Class<?> targetClass = activity.getClass();
try {
Log.d(TAG, "Looking up view injector for " + targetClass.getName());
Method inject = findInjectUtilMethodForClass(targetClass);
if (inject != NO_OP) {
inject.invoke(null, activity);
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new UnableToInjectException("Unable to inject views for " + activity, e);
}
}

/**
* 查找cls对应的工具类方法并进行缓存
*/
private static Method findInjectUtilMethodForClass(Class<?> cls) throws NoSuchMethodException {
Method inject = INJECT_UTILS.get(cls);
if (inject != null) {
Log.d(TAG, "HIT: Cached in injector map.");
return inject;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return NO_OP;
}
try {
Class<?> injector = Class.forName(clsName + SUFFIX);
inject = injector.getMethod("inject", cls);
Log.d(TAG, "HIT: Class loaded injection class.");
} catch (ClassNotFoundException e) {
Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
inject = findInjectUtilMethodForClass(cls.getSuperclass());
}
INJECT_UTILS.put(cls, inject);
return inject;
}

private static class UnableToInjectException extends RuntimeException {
UnableToInjectException(String message, Throwable cause) {
super(message, cause);
}
}
}

5. 使用定义的注解、注解处理器及API

  在app模块我们需要使用注解处理器以及api,因此需要添加processor模块api模块的引用,但注解处理器只在编译处理期间需要用到,编译处理完后就没有实际作用了,在主项目直接引用processor模块的话就会产生冗余文件,为了处理这个问题我们需要引入android-apt插件,首先要在project目录的build.gradle中添加依赖,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
buildscript {
repositories {
jcenter()
mavenCentral()// add
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'

// The android-apt plugin assists in working with annotation processors in combination with Android Studio. It has two purposes:
//
// 1、Allow to configure a compile time only annotation processor as a dependency, not including the artifact in the final APK or library
// 2、Set up the source paths so that code that is generated from the annotation processor is correctly picked up by Android Studio
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'// add
}
}

接着我们需要在app模块的build.gradle中使用apt,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' // add
android {
......
}

dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:24.2.1'
//引用api模块
compile project(':api')

//compile project(':runtime_processor') 替换为下面,不会将processor代码打包到apk中,只在编译时使用
apt project(':processor')
}

添加好依赖之后我们就可以使用了,ReceiverActivity的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ReceiverActivity extends AppCompatActivity {

private static final String USER_NAME = "user_name";
private static final String PASSWORD = "password";

public static void start(Context context, String userName, String password) {
Intent intent = new Intent();
intent.setClass(context, ReceiverActivity.class);
intent.putExtra(USER_NAME, userName);
intent.putExtra(PASSWORD, password);
context.startActivity(intent);
}

@BindView(R.id.tv_user_name)
TextView mUserNameTextView;
@BindView(R.id.tv_password)
TextView mPasswordTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_receiver);
//使用自定义注入库
BindInjector.inject(this);
Intent intent = getIntent();
if (intent.getExtras() != null) {
mUserNameTextView.setText(getString(R.string.user_name_text, intent.getStringExtra(USER_NAME)));
mPasswordTextView.setText(getString(R.string.password_text, intent.getStringExtra(PASSWORD)));
}

}

@BindClick(R.id.btn_finish)
public void onFinishClicked() {
this.finish();
}

}

至此,我们已经介绍完了运行时注解的使用过程,因为涉及到的内容较多,这里只是尽可能简单的介绍了一个简化版butterknife的开发流程,细节性东西一带而过,Demo源码详见关于三种注解类型的示例Demo

文章目录
  1. 1. 创建项目结构
  2. 2. 定义注解
  3. 3. 定义注解处理器
    1. 1. 定义注解信息类ActivityAnnotatedInfo.java
    2. 2. 创建注解处理器BindAnnotationProcessor.java类
  4. 4. 提供api
  5. 5. 使用定义的注解、注解处理器及API
|