一 前言
前面两篇文章,注解处理器,理解注解,对注解有了一个初步认识,第二篇文章末尾也提到了,注解不是代码的一部分,当开发者使用了Annotation注解以后,注解不会自己起作用,必须提供相应的代码来处理这些信息。
这篇文章,我们就写一个简单的注解处理器,作用是类似于ButterKnife查找id。
源码传送门
二 项目结构
整个项目采用如下所示的结构:
- BindViewAnnotation,Java Library,存放我们定义的注解。
- bindviewapi,Android Library,声明注解框架使用的api,本例子中,我们要实现的是查找view控件,并将控件和xml中的绑定。
- BindViewCompiler,Java Library,注解处理器,根据注解生成处理打代码。
新建这几个Library的过程不再陈述,特别注意的是,建BindViewCompiler Java Library时,在build.gradle下要加入以下代码:
// 用于生成Java文件的库
implementation 'com.squareup:javapoet:1.11.1'
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
这些代码后面会提及。
三 正式开始吧
(一)在BindViewAnnotation新建注解
前面说过了,我们要实现的功能是查找xml文件的id功能,注解歹意如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
使用注解,在Activity中使用注解,在xml中定义一个button,
<Button
android:id="@+id/btn_bind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="BindButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
在Activity中使用我们定义的注解。
@BindView(R.id.btn_bind)
public Button mButton;
前面说了,注解不会自动起作用,如果我们直接运行代码,会直接报错的,提示Button没有定义,所以我们要写代码来处理这个注解的信息。
(二) 声明注解框架用到的api
① 定义一个绑定注解的接口
public interface IViewBind<T> {
void bind(T t);
}
② 向外提供的绑定方法,这里使用静态方法来管理。
public class ViewBinder {
public static void bind(Activity activity){
try {
// 1
Class clazz=Class.forName(activity.getClass().getCanonicalName()+"$$ViewBinder");
// 2
IViewBind<Activity> iViewBinder= (IViewBind<Activity>) clazz.newInstance();
// 3
iViewBinder.bind(activity);
} catch (ClassNotFoundException e) {
Log.d("hbj--exp",e.getException()+"");
Log.d("hbj--cause",e.getCause()+"");
Log.d("hbj--mess",e.getMessage()+"");
Log.d("hbj--class",e.getClass()+"");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
// 1 处获取生成的类的名字,生成类的规则代码在后面写,生成的规则是 类名$$ViewBinder,例如在MainActivity中需要使用,生成的文件名字就是MainActivity$$ViewBinder.java。
// 2 获取生成的类的实例。
// 3 完成绑定。
可能对第二条和第三条不是很好理解,现在贴出生成的java文件的源码,结合生成的文件,应该就好理解了吧。
public class MainActivity$$ViewBinder< T extends MainActivity> implements IViewBind<T> {
@Override
public void bind(T activity) {
activity.mButton=activity.findViewById(2131165250);
}
}
(三) 根据注解生成代码
现在只剩根据注解生成代码。
创建一个自定义的Annotation Processor,继承自AbstractProcessor。
// 1
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
// 2
@Override
public synchronized void init(ProcessingEnvironment env){
}
// 3
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment roundEnv) { }
// 4
@Override
public Set<String> getSupportedAnnotationTypes() {
}
// 5
@Override
public SourceVersion getSupportedSourceVersion() {
}
}
// 1处 @AutoService(Processor.class),向javac注册我们自定义的注解处理器, 这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。
AutoService主要是生成 META-INF/services/javax.annotation.processing.Processor文件的。如果不加上这个注解的话,需要通过以下方法进行手动配置进行手动注册:
1.在main下创建resources目录,然后创建META-INF/services 目录,最后在此目录下创建文件:javax.annotation.processing.Processor,目录如下,
文件里的内容是一些列的自定义注解处理器完整有效的类名集合,以换行符切割,这里就自定义了一个注解处理器,
注:但是有可能会出现使用@AutoService()无法动态生成入口文件的,这个问题可以如下解决:
这个要从google auto service 和META_INF,谷歌的 auto service 也是一种 Annotation Processor,它能自动生成 META-INF 目录以及相关文件,避免手工创建该文件,手工创建的方法,上文有,手工创建有可能失误。使用 auto service 中的 @AutoService(Processor.class) 注解修饰 Annotation Processor 类就可以在编译过程中自动生成文件。
如果要进入的话,还要注意要引入两个配置:
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
这两个重复写的原因是:annotationProcessor这个是新版 gradle 提供的 java plugin 内置的配置项,在gradle 5.+ 中将 Annotation Processor 从编译期 classpath 中去除了,javac 也就无法发现 Annotation Processor。此处如果按照 gradle 4.+ 的写法,只写一个 implementation 是无法使用 auto service 的 Annotation Processor 的。必须要使用 annotationProcessor 来配置 Annotation Processor 使其生效。
// 2处 init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。
// 3处 public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env)这相当于每个处理器的主函数main()。 在这里写扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让查询出包含特定注解的被注解元素。
// 4处 getSupportedAnnotationTypes(),这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,在这里定义你的注解处理器注册到哪些注解上。
// 5处 getSupportedSourceVersion();用来指定你使用的Java版本。
下面给出BindViewProcessor的完整代码:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
// 文件相关辅助类
private Filer mFiler;
// 日志相关辅助类
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations=new LinkedHashSet<>();
annotations.add(BindView.class.getCanonicalName());
return annotations;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
Map<TypeElement,ArrayList<BindViewInfo>> bindViewMap=new HashMap<>();
for (Element element : elements) {
// 因为BindView只作用于Filed,判断注解是否是属性,不是的话直接结束
if (element.getKind() != ElementKind.FIELD) {
mMessager.printMessage(Diagnostic.Kind.ERROR, element.getSimpleName().toString() + " is not filed,can not use @BindView");
return false;
}
// 获取注解元数据
int id = element.getAnnotation(BindView.class).value();
// 获取属性的类
TypeElement typeElement= (TypeElement) element.getEnclosingElement();
if (!bindViewMap.containsKey(typeElement)){
bindViewMap.put(typeElement,new ArrayList<BindViewInfo>());
}
ArrayList<BindViewInfo> bindViewInfos=bindViewMap.get(typeElement);
// 添加list
bindViewInfos.add(new BindViewInfo(id,element.getSimpleName().toString()));
}
produceClass(bindViewMap);
return true;
}
private void produceClass(Map<TypeElement,ArrayList<BindViewInfo>> hasMap){
if (hasMap == null || hasMap.isEmpty()){
return;
}
Set<TypeElement> typeElements=hasMap.keySet();
for (TypeElement typeElement:typeElements){
produceJavaClass(typeElement,hasMap.get(typeElement));
}
}
/**
* 产生Java文件
* @param typeElement
* @param bindViewInfos
*/
private void produceJavaClass(TypeElement typeElement, List<BindViewInfo> bindViewInfos){
try {
StringBuffer stringBuffer=new StringBuffer();
stringBuffer.append("package ");
stringBuffer.append(getPackageName(typeElement.getQualifiedName().toString())+";\n");
stringBuffer.append("import com.jackson.bindviewapi.IViewBind;\n");
stringBuffer.append("public class "+typeElement.getSimpleName()+"$$ViewBinder< T extends "+typeElement.getSimpleName()+"> implements IViewBind<T> {\n");
stringBuffer.append("@Override\n");
stringBuffer.append("public void bind(T activity) {\n");
for (BindViewInfo bindViewInfo:bindViewInfos){
stringBuffer.append("activity."+bindViewInfo.name+"=activity.findViewById("+bindViewInfo.id+");\n");
}
stringBuffer.append("}\n}");
JavaFileObject javaFileObject=mFiler.createSourceFile(typeElement.getQualifiedName().toString()+"$$ViewBinder");
Writer writer=javaFileObject.openWriter();
writer.write(stringBuffer.toString());
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private String getPackageName(String className){
if (className==null || className.equals("")){
return "";
}
return className.substring(0,className.lastIndexOf("."));
}
}
重新编译项目,在app/build/source/apt/debug/com.jackson.annotationdemo下就会找到生成的文件,如下图
生成文件的代码,前面已经给出,可以对照着生成java文件的代码,来看BindViewProcessor生成java文件的代码规则。
(四) 使用
在MainActivity中使用,代码如下:
@BindView(R.id.btn_bind)
public Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBinder.bind(this);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this,"注解测试",Toast.LENGTH_SHORT).show();
}
});
}
可以看到,在MainActivity中,并没有mButton的findViewById()来初始化,而是通过注解完成,代码运行正常。
源码传送门