注解生成器(一)拼接方式
2019-07-11 本文已影响6人
程序员阿兵
介绍
APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。
作用
使用APT的优点就是方便、简单,可以少些很多重复的代码。
用过ButterKnife、Dagger、EventBus等注解框架的同学就能感受到,利用这些框架可以少些很多代码,只要写一些注解就可以了。
其实,他们不过是通过注解,生成了一些代码。通过对APT的学习,你就会发现,他们很强~~~
实现
- 目标
实现根据注解动态创建指定的自定义类 - 创建项目
创建Android Module命名为app 依赖 annotation 和 compiler
创建Java library Module命名为 annotation
创建Java library Module命名为 compiler依赖 annotation
结构如下

功能主要分为两个部分
- annotation:自定义注解
- compiler:注解处理器,根据annotation中的注解,在编译期生成指定的XActivity$$Activity.java代码
模拟生成指定类的样子
/**
* 模拟APT生成后的文件样子
*/
public class XActivity$$ARouter {
public static Class<?> findTargetClass(String path) {
if (path.equals("/app/MainActivity")) {
return MainActivity.class;
}
return null;
}
}
1.代码实现
- 自定义注解
/**
* <strong>Activity使用的布局文件注解</strong>
* <ul>
* <li>@Target(ElementType.TYPE) // 接口、类、枚举、注解</li>
* <li>@Target(ElementType.FIELD) // 属性、枚举的常量</li>
* <li>@Target(ElementType.METHOD) // 方法</li>
* <li>@Target(ElementType.PARAMETER) // 方法参数</li>
* <li>@Target(ElementType.CONSTRUCTOR) // 构造函数</li>
* <li>@Target(ElementType.LOCAL_VARIABLE)// 局部变量</li>
* <li>@Target(ElementType.ANNOTATION_TYPE)// 该注解使用在另一个注解上</li>
* <li>@Target(ElementType.PACKAGE) // 包</li>
* <li>@Retention(RetentionPolicy.RUNTIME) <br>注解会在class字节码文件中存在,jvm加载时可以通过反射获取到该注解的内容</li>
* </ul>
*
* 生命周期:SOURCE < CLASS < RUNTIME
* 1、一般如果需要在运行时去动态获取注解信息,用RUNTIME注解
* 2、要在编译时进行一些预处理操作,如ButterKnife,用CLASS注解。注解会在class文件中存在,但是在运行时会被丢弃
* 3、做一些检查性的操作,如@Override,用SOURCE源码注解。注解仅存在源码级别,在编译的时候丢弃该注解
*/
@Target(ElementType.TYPE) // 该注解作用在类之上
@Retention(RetentionPolicy.CLASS) // 要在编译时进行一些预处理操作。注解会在class文件中存在
public @interface ARouter {
// 详细路由路径(必填),如:"/app/MainActivity"
String path();
// 路由组名(选填,如果开发者不填写,可以从path中截取出来)
String group() default "";
}
- compiler(注解处理器)
- 依赖 annotation 工程 指定注解依赖库,当前我的gradle版本5.1.1
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// 注册注解,并对其生成META-INF的配置信息,rc2在gradle5.0后有坑
// As-3.2.1 + gradle4.10.1-all + auto-service:1.0-rc2
// implementation 'com.google.auto.service:auto-service:1.0-rc2'
// As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
// 引入annotation,让注解处理器-处理注解
implementation project(':annotation')
}
// java控制台输出中文乱码
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
// jdk编译版本1.7
sourceCompatibility = "7"
targetCompatibility = "7"
2.代码实现
- 创建 ARouterProcessor 注解生成器类继承AbstractProcessor,编码此类1句话:细心再细心,出了问题debug真的不好调试
@AutoService(Processor.class)
// 允许/支持的注解类型,让注解处理器处理(新增annotation module)
@SupportedAnnotationTypes({"com.netease.annotation.ARouter"})
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions("content")
public class ARouterProcessor extends AbstractProcessor {
}
- 实现init:初始化。可以得到ProcessingEnviroment,ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
// 该方法主要用于一些初始化的操作,通过该方法的参数ProcessingEnvironment可以获取一些列有用的工具类
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
// 父类受保护属性,可以直接拿来使用。
// 其实就是init方法的参数ProcessingEnvironment
// processingEnv.getMessager(); //参考源码64行
elementUtils = processingEnvironment.getElementUtils();
messager = processingEnvironment.getMessager();
filer = processingEnvironment.getFiler();
// 通过ProcessingEnvironment去获取build.gradle传过来的参数
String content = processingEnvironment.getOptions().get("content");
// 有坑:Diagnostic.Kind.ERROR,异常会自动结束,不像安卓中Log.e那么好使
messager.printMessage(Diagnostic.Kind.NOTE, content);
}
- 实现 process 方法,此方法相当于main函数,开始处理注解,注解处理器的核心方法,处理具体的注解,生成Java文件
/**
* 相当于main函数,开始处理注解
* 注解处理器的核心方法,处理具体的注解,生成Java文件
*
* @param set 使用了支持处理注解的节点集合(类 上面写了注解)
* @param roundEnvironment 当前或是之前的运行环境,可以通过该对象查找找到的注解。
* @return true 表示后续处理器不会再处理(已经处理完成)
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (set.isEmpty()) return false;
// 获取所有带ARouter注解的 类节点
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
// 遍历所有类节点
for (Element element : elements) {
// 通过类节点获取包节点(全路径:com.netease.xxx)
String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
// 获取简单类名
String className = element.getSimpleName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "被注解的类有:" + className);
// 最终想生成的类文件名
String finalClassName = className + "$$ARouter";
// 公开课写法,也是EventBus写法(https://github.com/greenrobot/EventBus)
try {
// 创建一个新的源文件(Class),并返回一个对象以允许写入它
JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + finalClassName);
// 定义Writer对象,开启写入
Writer writer = sourceFile.openWriter();
// 设置包名
writer.write("package " + packageName + ";\n");
writer.write("public class " + finalClassName + " {\n");
writer.write("public static Class<?> findTargetClass(String path) {\n");
// 获取类之上@ARouter注解的path值
ARouter aRouter = element.getAnnotation(ARouter.class);
writer.write("if (path.equals(\"" + aRouter.path() + "\")) {\n");
writer.write("return " + className + ".class;\n}\n");
writer.write("return null;\n");
writer.write("}\n}");
// 最后结束别忘了
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
上面的主要逻辑是:获取根据对应作用在类上的注解的集合,
遍历所有的注解类,拿到当前注解类上的全类包名packageName 以及类名className,然后在当前包名下创建自定义类名 ,通过JavaFileObject 和Writer 去拼接自己想要实现的自定义类的格式 。
2.在对应的类上添加注解
- MainActivity
@ARouter(path = "/app/MainActivity")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
- OrderActivity
@ARouter(path = "/app/OrderActivity")
public class OrderActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
- PersonalActivity
@ARouter(path = "/app/PersonalActivity")
public class PersonalActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
3.添加注解后去编译
编译完会发现在generated 文件目录下会生成三个注解类

点击进去会发现和模拟的一样:
public class MainActivity$$ARouter {
public static Class<?> findTargetClass(String path) {
if (path.equals("/app/MainActivity")) {
return MainActivity.class;
}
return null;
}
}
- 通过生成类可以去获取调用:
@ARouter(path = "/app/MainActivity")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void jump(View view) {
Intent intent = new Intent(this, OrderActivity$$ARouter.findTargetClass("/app/OrderActivity"));
startActivity(intent);
}
}
@ARouter(path = "/app/OrderActivity")
public class OrderActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void jump(View view) {
Intent intent = new Intent(this, PersonalActivity$$ARouter.findTargetClass("/app/PersonalActivity"));
startActivity(intent);
}
}
最终可以实现页面的跳转,但是对于上面的注解生成器拼接指定类的方式 squre公司提供了更好的方式 JavaPot 模版的形式去实现更加便捷: