自定义运行时注解、编译时注解[ButterKnife原理探析]
程序员文章站
2022-06-16 17:25:49
...
本篇博文针对具备注解基础的读者,主要讲解如何进行自定义注解。关于注解的具体基础知识点,网上这方面的学习资料非常多,可自行学习。
注解目前在主流的框架,比如Android中的Glide、Retrofit;Java Web方向的Spring等都有大量的使用。在给开发者带来巨大方便的同时,作为开发者有必要了解学习注解及其自定义,甚至可以自定义自己的注解库呦。
以下分析均在Android Studio中进行;
1、基础
上图来源于网络。感谢。上图是对Java注解知识点的总结,已经十分的全面了。如果你只是想自定义一个运行时的注解及处理器,上述知识点已经足够。但是如果你想要自定义一个编译型注解,那你还需要学习更多东西,比如AbstractProcessor、APT、JavaPoet等等,稍后会介绍。
2、区别
运行时注解与编译时注解的区别是什么呢?
a)保留阶段不同。运行时注解保留到运行时,可在运行时访问。而编译时注解保留到编译时,运行时无法访问。
b)原理不同。运行时注解是Java反射机制,而编译时注解通过APT、AbstractProcessor。
c)性能不同。运行时注解由于使用Java反射,因此对性能上有影响。编译时注解对性能没影响。这也是为什么ButterKnife从运行时切换到了编译时的原因。
d)产物不同。运行时注解只需自定义注解处理器即可,不会产生其他文件。而编译时注解通常会产生新的Java源文件。
3、运行时注解
相对于编译时注解,运行时注解要简单的多。运行时注解的自定义只有两个步骤:
自定义注解 + 注解处理器(会用到java反射机制);
3.1、自定义运行时注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimeBind {
int value();
}
RetentionPolicy.RUNTIME:表明这是一个运行时的注解,如果是编译时注解,则应该是RetentionPolicy.CLASS。ElementType.FIELD:表明该注解是用于域的。
3.2、自定义注解处理器
public class RuntimeAnnotationProcessor {
public static void bind2(Activity activity){
if (activity==null) return;
Field[] fields = activity.getClass().getDeclaredFields();
if (fields == null || fields.length == 0) return;
for(int i=0;i<fields.length;i++){
if (fields[i].isAnnotationPresent(RuntimeBind.class)){
int resId = fields[i].getAnnotation(RuntimeBind.class).value();
fields[i].setAccessible(true);
try {
fields[i].set(activity,activity.findViewById(resId));
fields[i].setAccessible(false);
} catch (IllegalAccessException e) {
e.printStackTrace();
fields[i].setAccessible(false);
}
}
}
}
}
可以看到,代码量还是很小的。首先,我们通过反射获取到当前Activity都有哪些字段,然后再判断每个字段是不是在使用RuntimeBind注解,如果使用了,则获取该注解的值,同时将该注解的值赋值给对应的字段就可以了。3.3、使用运行时注解处理器
public class MainActivity extends AppCompatActivity {
@RuntimeBind(R.id.tv_runtime)
TextView tv_runtime;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RuntimeAnnotationProcessor.bind2(this);
tv_runtime.setText("运行时注解成功");
}
}
在使用的时候,只需要调用RuntimeAnnotationProcessor.bind2(..)即可完成繁琐的findViewById工作了。是不是很方便。但是,如果存在大量的通过运行时注解,则在一定程度上会影响程序的性能的。因此,编译时注解的优势就发挥出来了。4、编译时注解
编译时注解总体结构:编译时注解 + 注解处理器(基于AbstractProcessor) + APT + JavaPoet(自定义Java源文件会用到) + auto-service(处理器注册);
4.1、基础准备
a)什么是APT?
APT(Annotation Processing Tool)是javac内置的工具,用于在编译时期扫描和处理注解信息。它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码(调用注解处理器的Process方法生成源文件)。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
b)APT插件(工具)
android-apt:由一位开发者自己开发的apt框架,源代码托管在这里,随着Android Gradle 插件 2.2 版本的发布,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt ,自此android-apt 作者在官网发表声明最新的Android Gradle插件现在已经支持annotationProcessor,并警告和或阻止android-apt ,并推荐大家使用 Android 官方插件annotationProcessor。
annotationProcessor:是google开发的内置框架,不需要引入,可以直接在build.gradle文件中使用。
c)JavaPoet:该库是由Android大神ButterKnife作者完成,目的是简化我们书写自动生成Java代码的工作量。https://github.com/square/javapoet
d)auto-service:编译时注解处理器需要注册到JVM中。在以前我们需要手动的配置,具体如下:在Java Library项目中,在resources资源文件夹下创建META-INF.services,然后在该路径下创建名为javax.annotation.processing.Processor的文件,在该文件中配置(处理器的完整路径,每行一个)需要启用的注解处理器。现在使用这个库,一个@autoservice注解就完成了注册,不要太方便。
4.2、编译时注解流程
这个生成的Java源文件是干什么的呢?其实就是替完成findViewById、OnClick等工作部分的代码,这也是编译时注解的目标,即免去开发者书写这些繁琐且无用功的代码。这些代码会在后面被调用,我们熟悉的ButterKnife.bind(this),其实主要就是调用了这部分代码。
4.3、编译时注解
这是我在本地写的一个关于编译时注解的项目,后面会给出项目链接。该项目分为四个模块:
annotation模块:Java Library,包含了我们所有的自定义注解。
annotation_processor:Java Library,包含了所有的自定义注解处理器。
annotation_api:Android Library,该模块是暴露给开发者的,由开发者调用暴露的Api完成绑定。
app:主项目。
如上四个模块的依赖关系是:
annotation_processor依赖annotation;
annotation_api依赖annotation;
app依赖annotation_api,同时还会在app的build.gradle中,添加apt project(':annotation_processor');
在这四个模块中,annotation_processor与annotation的流程在4.2小节中已经很清楚了。annotation_api起到了桥梁的作用,annotation_api的主要工作如下:
1、寻找Java源文件。Java源文件由annotation_processor生成,我们可以根据传入的参数,比如Activity,寻找annotation_processor生成的Java源文件。注意Java源文件的命名有一定的规则,例如,后缀依据编写者的爱好会有所不同。之所以存在这样的命名规则,是方便我们可以按照这种规则快速的找到Java源文件。
2、通过接口调用绑定方法。annotation_api与生成的Java源文件之间是通过接口来完成调用的。
public interface IInject<T> {
void inject(T t);
}
因此,我们生成的java源文件也必须实现这个接口。4.3.1、自定义编译时注解【annotation模块】
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Bind {
int value();
}
注意:一定要是RetentionPolicy,CLASS才是编译时注解。4.3.2、自定义注解处理器【annotation_processor】
编译时的注解处理器需要继承AbstractProcessor,并重写其process方法即可。有的同学可能会说,应该还有getSupportedAnnotationTypes、getSupportedSourceVersion方法。没错,是需要重写这两个方法,但是由于我们使用了auto_service库,因此,两个注解就搞定了,不用再去麻烦的重写这两个方法了。
方法 | 作用 |
---|---|
init(ProcessingEnvironment processingEnv) | 该方法有注解处理器自动调用,其中ProcessingEnvironment类提供了很多有用的工具类:Filter,Types,Elements,Messager等 |
getSupportedAnnotationTypes() | 该方法返回字符串的集合表示该处理器用于处理那些注解 |
getSupportedSourceVersion() |
该方法用来指定支持的java版本,一般来说我们都是支持到最新版本,因此直接返回SourceVersion.latestSupported() 即可 |
process(Set annotations, RoundEnvironment roundEnv) | 该方法是注解处理器处理注解的主要地方,我们需要在这里写扫描和处理注解的代码,以及最终生成的java文件。其中需要深入的是RoundEnvironment类,该用于查找出程序元素上使用的注解 |
1、Element
- VariableElement //一般代表成员变量
- ExecutableElement //一般代表类中的方法
- TypeElement //一般代表代表类
- PackageElement //一般代表Package
- VariableElement //一般代表成员变量
- ExecutableElement //一般代表类中的方法
- TypeElement //一般代表代表类
- PackageElement //一般代表Package
2、RoundEnvironment
public interface RoundEnvironment {
boolean processingOver();
//上一轮注解处理器是否产生错误
boolean errorRaised();
//返回上一轮注解处理器生成的根元素
Set<? extends Element> getRootElements();
//返回包含指定注解类型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(TypeElement a);
//返回包含指定注解类型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}
3、ProcessingEnvironment
public interface ProcessingEnvironment {
Map<String,String> getOptions();
//Messager用来报告错误,警告和其他提示信息
Messager getMessager();
//Filter用来创建新的源文件,class文件以及辅助文件
Filer getFiler();
//Elements中包含用于操作Element的工具方法
Elements getElementUtils();
//Types中包含用于操作TypeMirror的工具方法
Types getTypeUtils();
SourceVersion getSourceVersion();
Locale getLocale();
}
好了,材料准备好了,可以开始做饭了。@SupportedAnnotationTypes("com.example.Bind")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(Processor.class)
public class BindAnnotationProcessor extends AbstractProcessor {
private Elements elementsUtils;
private Filer fileUtil;
private Map<String, AnnotationInfo> annotationInfoMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementsUtils = processingEnv.getElementUtils();
fileUtil = processingEnv.getFiler();
annotationInfoMap = new HashMap<>();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> eleSet = roundEnv.getElementsAnnotatedWith(Bind.class);
for (Element ele : eleSet) {
if (!checkIsField(ele)) {
System.out.println("non filed is error inject ");
continue;
}
if (checkIsPrivate(ele)) {
System.out.println("filed cant not be private ");
continue;
}
VariableElement variableElement = (VariableElement) ele;
// full class name
String className = ((TypeElement) variableElement.getEnclosingElement()).getQualifiedName().toString();
AnnotationInfo annotationInfo = new AnnotationInfo(elementsUtils.getPackageOf(variableElement), (TypeElement) variableElement.getEnclosingElement());
int value = variableElement.getAnnotation(Bind.class).value();
annotationInfo.bindData.put(value, variableElement);
annotationInfoMap.put(className, annotationInfo);
}
for (String key : annotationInfoMap.keySet()) {
AnnotationInfo annotationInfo = annotationInfoMap.get(key);
JavaFileObject sourceFile = null;
try {
sourceFile = fileUtil.createSourceFile(annotationInfo.classEle.getSimpleName().toString() + "$$Injector");
Writer writer = sourceFile.openWriter();
writer.write(generateJavaSourceFile(annotationInfo));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
private boolean checkIsPrivate(Element ele) {
if (ele.getModifiers().contains(Modifier.PRIVATE))
return true;
return false;
}
private boolean checkIsField(Element ele) {
if (ele.getKind().isField())
return true;
return false;
}
public String generateJavaSourceFile(AnnotationInfo annotationInfo) {
StringBuilder sb = new StringBuilder();
sb.append("//auto generate do not modify\n");
sb.append("package " + annotationInfo.packageEle.getQualifiedName().toString() + ";\n")
.append("import android.app.Activity;\n")
.append("import com.example.annotation_api.IInject;\n")
.append("public class " + annotationInfo.classEle.getSimpleName().toString() + "$$Injector")
.append(" implements IInject<" + annotationInfo.classEle.getQualifiedName() + "> {")
.append("@Override\n")
.append("public void inject(" + annotationInfo.classEle.getQualifiedName() + " activity) {");
for (int key : annotationInfo.bindData.keySet()) {
Element ele = annotationInfo.bindData.get(key);
sb.append("activity." + ele.getSimpleName() + " = (" + ele.asType() + ")activity.findViewById(" + key + ");\n");
}
sb.append("}}\n");
return sb.toString();
}
}
@SupportedAnnotationTypes("com.example.Bind"):声明支持Bind注解;@SupportedSourceVersion(SourceVersion.RELEASE_7):升值直接的JDK版本;
@AutoService(Processor.class):注册处理器至JVM;
主要的工作都是由process方法完成的,APT根据注解生成代码其实就是调用注解处理器的process方法完成的,生成的Java类会放在app/build/generated/source/apt/debug中。因为我们需要在app中使用生成的Java源文件,以完成绑定工作。所以annotation_processor生成的Java源文件必须在app模块中。这也是为什么在app的build.gradle中,添加apt project(':annotation_processor');
我们获取使用了Bind注解的所有Element,然后遍历Element的集合,并提取注解的信息保存到AnnotationInfo中。
在注解处理器中,我们定义了一个Map<String, AnnotationInfo> annotationInfoMap;其中key为使用该注解的类的全路径名称,value为该类的所有注解信息。例如:MainActivity,则key为packageName.MainActivity,而Value则为一个AnnotationInfo实例,该实例保存了MainActivity中所有的注解信息。下面我们先看下这个我们自定义的AnnotationInfo:
public class AnnotationInfo {
public Map<Integer,Element> bindData;
public PackageElement packageEle;
public TypeElement classEle;
public AnnotationInfo(PackageElement packageName,TypeElement className){
this.packageEle = packageName;
this.classEle = className;
bindData = new HashMap<>();
}
}
Map<Integer,Element>:key为注解的值,比如R.id.tv_annotationtest;Value则为对应的元素,比如TextView tv_annotationtest;这样我们可以总结在process方法中,类与AnnotationInfo的关系:
MainActivity - 1:1 - AnnotationInfo - 1:N - 注解的值及被注解的元素;
生成的Java源文件(MainActivity$$Injector)的代码:
package com.example.xxx.myannotations;
import com.example.annotation_api.IInject;
public class MainActivity$$Injector implements IInject<com.example.xiuli.myannotations.MainActivity> {
@Override
public void inject(com.example.xiuli.myannotations.MainActivity activity) {
activity.tv_annotationtest = (android.widget.TextView)activity.findViewById(2131492944);
}
}
4.4、暴露Api供开发者使用【annotation_api】
在该模块,我们定义了一个Injector类,通过bind方法暴露给开发者:public class Injector {
private static String suffix = "$$Injector";
public static void bind(Activity activity){
if (activity==null)
return;
StringBuilder sb = new StringBuilder();
sb.append(activity.getClass().getCanonicalName());
sb.append(suffix);
//查找注解生成的Java文件是否存在
if(!findAnnotationGenerateJavaFile(sb.toString()))
return;
try {
//通过接口调用Java 类的绑定方法inject,将注解的值绑定到字段
IInject inject = (IInject) Class.forName(sb.toString()).newInstance();
inject.inject(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
private static boolean findAnnotationGenerateJavaFile(String fullName){
try {
Class.forName(fullName);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
Injector的主要作用就是根据既定的规则寻找已生成的Java源文件,然后加载并调用生成的Java类中的绑定方法。4.5、使用
public class MainActivity extends AppCompatActivity {
@Bind(R.id.tv_annotationtest)
TextView tv_annotationtest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Injector.bind(this);
tv_annotationtest.setText("编译型注解成功");
}
}
源码:https://github.com/sparkerandroid/MyAnnotations
感谢:
http://www.mamicode.com/info-detail-1743070.html
https://yq.aliyun.com/articles/59493#
https://juejin.im/entry/585fe4e61b69e600562147fa/view
http://blog.csdn.net/lmj623565791/article/details/51931859
上一篇: 自定义注解 - 实体转Excel