apt 使用指南

     最近在看 <<Think in Java >> 已经进展到注解一张, 其中讲解到了 APT(Annotation Process Tool) 的使用. 因为当时是基于 JDK1.5 的, 基于 com.sun.mirror.* 包下的相关类, 而这些类在 JDK1.8 的时候就已经完全废弃掉了, 替换成 javax.annotation.processingjavax.land.model, 而且调试环境可以直接用 Android Studio. 所以, 就研究了下如使用新的 APT 工具.

知识准备

     这个不是指导手册, 而是总结性的文章, 一些比较常用的操作和知识点就不展开. 愉快的阅读这篇博客之前, 可能需要了解:

  • 注解的基本语法
  • Android Studio 的常用操作
  • gradle 的基本使用

     不过我觉得对于一个合格的 Android 开发者, 这些应该没有问题吧. 还有, 小生的开发环境是 AS3.0, 在之前的版本会有些不同. 如果小伙伴们还是用的老版本的开发工具, 是时候升级了.

编码 & 问题 & 操作.

  • 新建一个 Android Studio 工程.

     介个 so easy 吧…

  • 在项目中新建一个 Model, 暂且命名为 annotation, 并且让我们的 app Model 去依赖这个库.

     注意, 这个必须是一个 java library 而不能是 Android library, 如果是一个 Android library, 那么 javax.* 包中的一些东西就不能使用了.

  • 在 java library 中创建一个注解类.
1
2
3
4
5
6
7
8
9
10
package com.congguangzi.annotation;
// import is left out
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS) // 编译时的注解
public @interface MyAnnotation {
String value();
}
  • 用该注解去修饰 MainActivity 中的方法.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// package and import are left out
public class MainActivity extends AppCompatActivity {
@MyAnnotation("onCreate")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
// some other code
}
  • 在 java model 中创建一个类, 名字随意, 姑且命名为 MyProcessor, 继承自 AbstractProcessor.
1
2
3
4
5
6
7
8
9
10
11
// package and import are left out
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
// some other code
return false;
}
}
  • 在 java Model 的 src/main 目录下创建 resources 包.

     直接创建就可以, 不需要通过 Android Studio 的 Wizard.

  • resources 包中创建 META-INFO 包, 接着在 META-INFO 包中创建 services 包.

  • services 包中创建 javax.annotation.processing.Processor 文件.

  • javax.annotation.processing.Processor 文件中, 添加刚才创建的 MyProcessor 的完整路径.

     完整路径指的是相对路径, 对于当前测试项目而言, 为: com.congspark.annotation.MyProcessor

     项目目录的最终的截图如下(请忽略这屎绿色的背景…):



     前期的准备工作大致完成了, 现在我们向 MyProcessor 中添加写简单的功能, 仅仅是用来打印一些信息.

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
// omit the package and import...
@SupportedAnnotationTypes("com.congspark.annotation.MyAnnotation")
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
// 找出每一个使用 MyAnnotation 的元素
for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
// is a method
if (element.getKind() == ElementKind.METHOD) {
// divide line
System.out.println("----------------------------");
ExecutableElement executableElement = (ExecutableElement) element;
// 打印方法名.
System.out.println(executableElement.getSimpleName());
// 打印方法类型.
System.out.println(executableElement.getReturnType().toString());
// get parameters
List<? extends VariableElement> params = executableElement.getParameters();
// print parameters
for (VariableElement param : params) {
System.out.println(param.getSimpleName());
}
// 打印注解的值
System.out.println(executableElement.getAnnotation(MyAnnotation.class).value());
}
}
// maybe always false.
return false;
}
// @Override
// public Set<String> getSupportedAnnotationTypes() {
// HashSet<String> set = new HashSet<>();
// set.add("com.congspark.annotation.MyAnnotation");
// return set;
// }
//
// 与类注解 SupportedAnnotationTypes 具有相同的功能.
}

     NOTE, getSupportedAnnotationTypes() 方法与类注解 @SupportedAnnotationTypes 的功能相同, 保留其中之一即可.
     好了, 现在我们尝试着运行一下, 额, 不, 是编译一下程序. 使用 gradle 或者直接点击编译按钮都可以.

     然而, 居然报错了… Gradle Console 的输出结果如下.



     提示我们 Annotation processors must be explicitly declared now. 注解编译器必须明确的声明, 而且还给出了一个问题连接. 点击去看一下.



     很是感动的说, 谷歌居然给出的是中文的, 不过, 这个翻译的… 其实就是让我们添加对 APT 插件的依赖. 跟着指导方案, 我们在 app 下的 build.gradle 文件中添加:



     然后再次运行一下.



     然后我们看到, 相应的方法名和相关信息, 同时上面的 :app:compileDebugJavaWithJavac 也证实了, 确实实在编译时运行的 MyProcessor 中的 process() 方法.

继续进阶

     现在我们回到 <<Think in Java >> 中, 书中用的是 JDK1.5. 而现在的已经更新到 JDK1.8 了, 书中使用的 api 已经废弃不用了, 现在我们用新的 api 和 Android Studio, 重写调试一下. 实现的的功能为, 将 Multiplier 中的非静态的公共方法提取出来, 生成 IMultipler.java 的接口文件.
     添加注解类.

1
2
3
4
5
6
7
// omit package and import...
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface ExtractInterface {
String value();
}

     具体的实体类. NOTE: 该实体类需要放在 app Model 中, 而不是 annotation Model 中, 否则无效.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// omit package and import...
@ExtractInterface("IMultiplier")
public class Multiplier {
public int multiply(int x, int y) {
int total = 0;
for (int i = 0; i < x; i++) {
total = add(total, y);
}
return total;
}
private int add(int x, int y) {
return x + y;
}
public static void main(String[] args) {
Multiplier m = new Multiplier();
System.out.println("11 * 16 = " + m.multiply(11, 16));
}
}

     然后是注解处理器.

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
// omit package and import...
@SupportedAnnotationTypes("com.congspark.annotation.ExtractInterface")
public class InterfaceExtractProcessor extends AbstractProcessor {
private ArrayList<ExecutableElement> interfaceMethods =
new ArrayList<>();
// 为了简化, 路径直接指定, 应该获取注解的路径和包名.
private String outPath = "annotation/src/main/java/com/congspark/annotation/";
// 为了简化, 路径直接指定.
private final String pack = "package com.congspark.annotation;";
private PrintWriter writer = null;
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(ExtractInterface.class)) {
if (element instanceof TypeElement) {
// need a type-element here
TypeElement typeElement = (TypeElement) element;
// make out put
ExtractInterface anno = typeElement.getAnnotation(ExtractInterface.class);
try {
File file = new File(outPath + anno.value() + ".java");
file.createNewFile();
writer = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(outPath + anno.value() + ".java"), "UTF-8"));
// put the package and interface name
writer.println(pack);
writer.println();
writer.println("public interface " + anno.value() + "{");
} catch (FileNotFoundException | UnsupportedEncodingException e) {
e.printStackTrace();
}
// find method
for (Element method : typeElement.getEnclosedElements()) {
ExecutableElement executableElement = (ExecutableElement) method;
// put PUBLIC and NOT STATIC methods in a list.
if (executableElement.getModifiers().contains(Modifier.PUBLIC) &&
!executableElement.getModifiers().contains(Modifier.STATIC) &&
!(executableElement.getKind() == ElementKind.CONSTRUCTOR)) {
interfaceMethods.add(executableElement);
}
}
}
}
try {
// put the methods in the interface and make a relative .java file
for (ExecutableElement method : interfaceMethods) {
writer.print(" ");
writer.print(method.getReturnType().toString() + " ");
writer.print(method.getSimpleName() + "(");
int i = 0;
for (VariableElement param : method.getParameters()) {
// parameter type and parameter name.
writer.print(param.asType() + " " + param.getSimpleName());
if (++i < method.getParameters().size()) {
writer.print(", ");
}
}
writer.println(");");
}
writer.println("}");
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}

     编译一下, 然后就可以看到 IMultipler 接口了.



     小小的总结一下, 这个仅仅是一个小小的示例, 算是辅助学习 <<Think in Java >> 吧. 项目中如果是生成 .java 文件, 可以考虑使用 JavaPoet 框架. 而且, 实际开发中, 应该引入访问者模式来解除耦合, 否则功能稍微多一点, 那么自己编写的注解处理器就会非常的复杂且难以维护.

~感谢捧场,您的支持将鼓励我继续创作~