澳门新葡萄京官网注册 6

澳门新葡萄京官网注册如何构建编译时注解解析框架

APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的
Java 文件,减少手动的代码输入。现在有很多主流库都用上了 APT,比如
Dagger2, ButterKnife, EventBus3 等,我们要紧跟潮流,与时俱进呐! (ง
•̀_•́)ง

最近学习了编译时注解框架的制作,写了一个小项目。阅读本文前希望大家有关于注解的相关知识。

前言

在前面的文章中,咱们学习了Java类加载、Java反射、Java注解,那现在咱们就可以利用所学搞点事情了,所谓学以致用,方为正途。

如果想直接阅读源码,请点这里Github

下面通过一个简单的 View 注入项目 ViewFinder 来介绍 APT
相关内容,简单实现了类似于ButterKnife 中的两种注解 @BindView 和 @OnClick 。

本文介绍一个简单的编译时注解小项目的制作过程。项目地址:https://github.com/wuapnjie/EasyShortcuts,我选择了Android
API 25的新功能App
Shortcut,使用注解来快速制作一个Shortcut。为什么选择Shortcut呢,因为我觉得很多应用只需要使用到静态加载的Shortcut就好了,而对于静态加载的Shortcut要写一个比较长的Xml配置文件,我觉得特别麻烦。

铺垫

在开始搞事情前,咱们还需要了解以下几个物件:

  • Annotation Processor: 注解处理器
  • JavaPoet:Java源码文件生成者
  • 澳门新葡萄京官网注册,javax.lang.model.element:用于解析程序中的元素,例如:包、类、方法、变量

项目地址:

先来看一下我们实现的效果。

Annotation Processor

注解处理器是在编译时用来扫描和处理注解的工具。你可以注册自己感兴趣的注解,程式编译时会将添加注解的元素,交由注册它的注解处理器来处理。

那咱们如何实现一个自己的注解处理器?

  1. 继承AbstractProcessor
  2. 覆盖getSupportedAnnotationTypes()
  3. 覆盖getSupportedSourceVersion()
  4. 覆盖process()

AbstractProcessor:抽象注释处理器,为大多数自定义注释处理器的超类。

getSupportedAnnotationTypes():这里注册你感兴趣的注解。它的返回一个字符串的Set,包含注解类型的合法全称。

getSupportedSourceVersion():指定使用的Java版本。通常这里返回SourceVersion.latestSupported()。

process(Set<? extends TypeElement> set, RoundEnvironment
roundEnvironment):注解处理器的核心方法,在这里进行注解扫描、评估和处理,以及生成Java文件。

生成Java文件,就交由JavaPoet来完成

大概项目结构如下:

Java代码

JavaPoet

JavaPoet是一个用来生成
.java源文件的工具(由Square提供)。

咱们来讲一下JavaPoet里面常用的几个类:

  • TypeSpec:表示一个类、接口或者枚举声明
  • MethodSpec:表示一个构造函数或方法声明
  • FieldSpec:表示一个成员变量、字段声明
  • JavaFile:生成java文件

下面通过一个实例来说明具体使用方式:

private void generateHelloWorld() throws IOException {
        MethodSpec mainMethod = MethodSpec.methodBuilder("main")
                .addModifiers(new Modifier[]{Modifier.PUBLIC, Modifier.STATIC})
                .addParameter(String[].class, "args")
                .addStatement("System.out.println("Hello World")")
                .build();

        FieldSpec androidVersion = FieldSpec.builder(String.class, "androidVer")
                .addModifiers(new Modifier[]{Modifier.PRIVATE})
                .initializer("$S", "Lollipop")
                .build();

        TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(new Modifier[]{ Modifier.FINAL, Modifier.PUBLIC})
                .addMethod(mainMethod)
                .addField(androidVersion)
                .build();

        JavaFile javaFile = JavaFile.builder("com.hys.test", typeSpec).build();
        javaFile.writeTo(System.out);
    }

执行函数,结果如下:

package com.hys.test;

import java.lang.String;

public class HelloWorld {
    private String androidVer = "Lollipop";

    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

这里$S占位符,JavaPoet占位符如下:

  • $S:字符串类型占位符
  • $T:类型占位符
  • $N:名称占位符(方法名或者变量名等)
  • $L:字面常量

这里只是投石问路,关于JavaPoet更多API使用,请参见其文档

  • viewFinder-annotation – 注解相关模块
  • viewFinder-compiler – 注解处理器模块
  • viewfinder – API 相关模块
  • sample – 示例 Demo 模块
@ShortcutApplication
@AppShortcut(resId = R.mipmap.ic_launcher,
        description = "First Shortcut")
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //API 调用
        ShortcutCreator.create(this);
    }
}

javax.lang.model.element

Element
用于 Java 的模型元素的接口。

  • ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素
  • PackageElement:表示一个包程序元素
  • TypeElement:表示一个类或接口程序元素
  • TypeParameterElement:表示类、接口、方法或构造方法元素的形式类型参数
  • VariableElement:表示一个字段、enum
    常量、方法或构造方法参数、局部变量或异常参数

通过Element的getModifiers()获得元素的修饰符

Modifier
表示程序元素(如类、方法或字段)上的修饰符。
以下是常用修饰符:

  • ABSTRACT:修饰符 abstract
  • FINAL:修饰符 final
  • NATIVE:修饰符 native
  • PRIVATE:修饰符 private
  • PROTECTED:修饰符 protected
  • PUBLIC:修饰符 public
  • STATIC:修饰符 static
  • SYNCHRONIZED:修饰符 synchronized

通过Element的asType()获得元素的类型

TypeMirror
表示 Java
编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和
null 类型。

通过TypeMirror的getKind()类型的种类

TypeKind
表示类型的种类。

以下是常用的类型:

  • ARRAY:数组类型
  • BOOLEAN:基本类型 boolean
  • BYTE:基本类型 byte
  • CHAR:基本类型 char
  • DECLARED:类或接口类型
  • DOUBLE:基本类型 double
  • ERROR:无法解析的类或接口类型。
  • EXECUTABLE:方法、构造方法或初始化程序
  • FLOAT:基本类型 float
  • INT:基本类型 int
  • LONG:基本类型 long
  • NONE:在实际类型不适合的地方使用的伪类型
  • NULL:null 类型
  • PACKAGE:对应于包元素的伪类型
  • SHORT:基本类型 short
  • TYPEVAR:类型变量
  • VOID:对应于关键字 void 的伪类型

获取元素的父元素
通过Element的getEnclosingElement返回元素的父元素。

获取元素上的注解
通过Element的getAnnotation(Class<A>
annotationType)获得元素上的注解。

了解了上述内容,下面咱们开始搞事情

实现目标

在通常的 Android
项目中,会写大量的界面,那么就会经常重复地写一些代码,比如:

TextView text = (TextView) findViewById(R.id.tv);
text.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // on click
    }
});

天天写这么冗长又无脑的代码,还能不能愉快地玩耍啦。所以,我打算通过 ViewFinder 这个项目替代这重复的工作,只需要简单地标注上注解即可。通过控件
id
进行注解,并且@OnClick 可以对多个控件注解同一个方法。就像下面这样子咯:

@BindView(R.id.tv) TextView mTextView;
@OnClick({R.id.tv, R.id.btn})
public void onSomethingClick() {
    // on click
}

效果图:

创建注解处理器

1.Android Studio的File->New->New module,如下图:

澳门新葡萄京官网注册 1

2.在弹出的Create New Module对话框中选择Java
Library,命名为MockButterknife-complier,如下图:

澳门新葡萄京官网注册 2

3.创建注解处理器类,继承AbstractProcessor,覆盖getSupportedAnnotationTypes()、getSupportedSourceVersion()、process()三个方法,如下图:

澳门新葡萄京官网注册 3

4.注册注解处理器,在项目下创建resources->META-INF->Services目录,在Services目录下创建javax.annotation.processing.Processor文件,如下图:

澳门新葡萄京官网注册 4

5.编辑javax.annotation.processing.Processor文件,添加注解处理器类,如下图:

澳门新葡萄京官网注册 5

6.配置注解处理器,添加JavaPoet,如下:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.squareup:javapoet:1.8.0'
}

7.创建自定义注解,咱们在这里创建两个注解:

  • BindView注解

package com.hys.mockbutterknife.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}
  • OnClick注解

package com.hys.mockbutterknife.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
    int[] value();

8.注册自定义注解到注解处理器,在AnnotationProcessor添加如下代码:

private Set<Class<? extends Annotation>> getSupportedAnnotations(){
        Set<Class<? extends Annotation>> supportedAnnotations = new LinkedHashSet<>();
        supportedAnnotations.add(BindView.class);
        supportedAnnotations.add(OnClick.class);
        return supportedAnnotations;
    }

在getSupportedAnnotationTypes()方法中调用getSupportedAnnotations(),即将自定义注解注册到注解处理器,代码如下:

 @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedAnnotationTypes = new LinkedHashSet<>();

        Iterator ite = getSupportedAnnotations().iterator();
        while (ite.hasNext()){
            Class annotation = (Class<? extends Annotation>)ite.next();
            supportedAnnotationTypes.add(annotation.getCanonicalName());
        }

        return supportedAnnotationTypes;
    }

9.上面咱们已经注册了自定义注解,接下来应该处理这些注解(啰嗦,不处理,注册它们做啥?!)

后面以BindView为例

查找添加注解的元素

Iterator ite = env.getElementsAnnotatedWith(BindView.class).iterator();

验证元素合法性

  • 验证元素是否可以访问

private boolean isInaccessible(Element element, String targetThing, Class<? extends Annotation> annotationClass) {

        TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
        //检查元素的访问修饰符
        Set<Modifier> modifiers = element.getModifiers();
        if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC)) {
            this.error(element, "@%s %s must not be private or static. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
            return true;
        }

        //检查元素的父元素
        if (enclosingElement.getKind() != ElementKind.CLASS) {
            this.error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
            return true;
        }

        //检查父元素的访问修饰符
        if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) {
            this.error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
            return true;
        }

        return false;
    }
  • 验证元素所在包的合法性

private boolean isInWrongPackage(Element element, Class<? extends Annotation> annotationClass) {

        TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
        String qualifiedName = enclosingElement.getQualifiedName().toString();
        //元素的父元素(即元素所在的类)不能在android的系统包中
        if (qualifiedName.startsWith("android.")) {
            this.error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", annotationClass.getSimpleName(), qualifiedName);
            return true;
        } 
        ////元素的父元素不能在java的资源包中
        else if (qualifiedName.startsWith("java.")) {
            this.error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", annotationClass.getSimpleName(), qualifiedName);
            return true;
        }

        return false;
    }
  • 验证元素类型的合法性

/*
* 递归验证
* 以TextView为例:isSubtypeOfType(typeMirror, "android.view.View")
*/
public static boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) {
        // 类型相同
        if (isTypeEqual(typeMirror, otherType))
            return true;

        if (typeMirror.getKind() != TypeKind.DECLARED)
            return false;

        DeclaredType declaredType = (DeclaredType)typeMirror;
        List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
        if (typeArguments.size() > 0) {
            StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
            typeString.append('<');

            for(int i = 0; i < typeArguments.size(); ++i) {
                if (i > 0) {
                    typeString.append(',');
                }

                typeString.append('?');
            }

            typeString.append('>');
            if (typeString.toString().equals(otherType)) {
                return true;
            }
        }

        Element element = declaredType.asElement();
        if (!(element instanceof TypeElement)) {
            return false;
        } else {
            TypeElement typeElement = (TypeElement)element;
            // 获取元素的父类
            TypeMirror superType = typeElement.getSuperclass();
            // 检查父类的类型
            if (isSubtypeOfType(superType, otherType)) {
                return true;
            } else {                
                Iterator var7 = typeElement.getInterfaces().iterator();

                TypeMirror interfaceType;
                do {
                    if (!var7.hasNext()) {
                        return false;
                    }

                    interfaceType = (TypeMirror)var7.next();
                } while(!isSubtypeOfType(interfaceType, otherType));

                return true;
            }
        }

    }

生成Java源文件

  • 生成类

private TypeSpec createTypeSpec(){
        // 生成新类名,原类名+ _ViewBinding
        String className = this.encloseingElement.getSimpleName().toString() + "_ViewBinding";
        // 获取父元素的类型全称
        TypeName targetTypeName = TypeName.get(this.encloseingElement.asType());

        // 创建类构建器
        TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
                .addModifiers(new Modifier[]{Modifier.PUBLIC}) // 添加public修饰符
                .addField(targetTypeName, "target", new Modifier[]{Modifier.PRIVATE}); // 添加成员变量target

        classBuilder.addFields(createFieldForListener());

        if(isActivity()){
            classBuilder.addMethod(createConstructorForActivity());
        } else if(isView()){
            classBuilder.addMethod(createConstructorForView());
        } else if(isDialog()){
            classBuilder.addMethod(createConstructorForDialog());
        }

        // 默认类构造器
        classBuilder.addMethod(createBindConstructor());
        // 生成类
        return classBuilder.build();
    }
  • 生成JavaFile对象

public JavaFile brewJava() {
        String packageName = MoreElements.getPackage(this.encloseingElement).getQualifiedName().toString();
        return JavaFile.builder(packageName, createTypeSpec()).build();
    }
  • 生成Java源文件

...
JavaFile javaFile = bindSet.brewJava();

try{
      javaFile.writeTo(this.processingEnv.getFiler());
}catch (IOException ex){
     this.error(typeElement, "Unable to write binding for type %s: %s", typeElement, ex.getMessage());
}
...

定义注解

创建 module viewFinder-annotation ,类型为 Java
Library,定义项目所需要的注解。

在 ViewFinder 中需要两个注解 @BindView 和 @OnClick 。实现如下:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
    int[] value();
}

@BindView 需要对成员变量进行注解,并且接收一个 int
类型的参数; @OnClick 需要对方法进行注解,接收一组 int
类型参数,相当于给一组 View 指定点击响应事件。

澳门新葡萄京官网注册 6

创建API

注解处理器搞好了,还需要给用户提供API,用户才能使用。

咱们创建一个新的Module,Android Studio的File->New->New
module,选择Android Library,命名为Mockbutterknife-source

这个Module主要使用反射技术,动态的创建并调用上文中生成的类(下文中称为绑定类)。

  • 编写API接口(其中之一)

    @UiThread
    public static void bind(Activity target) {
        View sourceView = target.getWindow().getDecorView();
        createBinding(target, sourceView);
    }
  • 动态创建绑定类,调用其构造器方法

private static void createBinding(Object target, View source) {
        Class<?> targetClass = target.getClass();
        // 查找targetClass名称+_ViewBinding的class文件,加载并返回构造器
        Constructor constructor = findBindConstructorForClass(targetClass);

        if (constructor == null) {
            return ;
        }

        try {
            constructor.newInstance(target, source);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            if (cause instanceof Error) {
                throw (Error) cause;
            }
            throw new RuntimeException("Unable to create binding instance.", cause);
        }
    }

编写 API

创建 module viewfinder,类型为 Android Library。在这个 module
中去定义 API,也就是去确定让别人如何来使用我们这个项目。

首先需要一个 API 主入口,提供静态方法直接调用,就比如这样:

ViewFinder.inject(this);

同时,需要为不同的目标(比如 Activity、Fragment 和 View
等)提供重载的注入方法,最终都调用 inject() 方法。其中有三个参数:

  • host 表示注解 View 变量所在的类,也就是注解类
  • source 表示查找 View 的地方,Activity & View
    自身就可以查找,Fragment 需要在自己的 itemView 中查找
  • provider 是一个接口,定义了不同对象(比如 Activity、View
    等)如何去查找目标 View,项目中分别为 Activity、View
    实现了 Provider 接口。

    public class ViewFinder {

    private static final ActivityProvider PROVIDER_ACTIVITY = new ActivityProvider();
    private static final ViewProvider PROVIDER_VIEW = new ViewProvider();
    
    public static void inject(Activity activity) {
        inject(activity, activity, PROVIDER_ACTIVITY);
    }
    public static void inject(View view) {
        // for view
        inject(view, view);
    }
    public static void inject(Object host, View view) {
        // for fragment
        inject(host, view, PROVIDER_VIEW);
    }
    public static void inject(Object host, Object source, Provider provider) {
        // how to implement ?
    }
    

    }

那么 inject() 方法中都写一些什么呢?

首先我们需要一个接口 Finder,然后为每一个注解类都生成一个对应的内部类并且实现这个接口,然后实现具体的注入逻辑。在 inject() 方法中首先找到调用者对应的 Finder 实现类,然后调用其内部的具体逻辑来达到注入的目的。

接口 Finder 设计如下 :

public interface Finder<T> {
    void inject(T host, Object source, Provider provider);
}

举个例子,为 MainActivity 生成 MainActivity$$Finder,对其注解的 View
进行初始化和设置点击事件,这就跟我们平常所写的重复代码基本相同。

public class MainActivity$$Finder implements Finder<MainActivity> {
    @Override
    public void inject(final MainActivity host, Object source, Provider provider) {
        host.mTextView = (TextView) (provider.findView(source, 2131427414));
        host.mButton = (Button) (provider.findView(source, 2131427413));
        host.mEditText = (EditText) (provider.findView(source, 2131427412));
        View.OnClickListener listener;
        listener = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                host.onButtonClick();
            }
        };
        provider.findView(source, 2131427413).setOnClickListener(listener);
        listener = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                host.onTextClick();
            }
        };
        provider.findView(source, 2131427414).setOnClickListener(listener);
    }
}

好了,所有注解类都有了一个名为 xx$$Finder 的内部类。我们首先通过注解类的类名,得到其对应内部类的
Class 对象,然后实例化拿到具体对象,调用注入方法。

public class ViewFinder {

    // same as above

    private static final Map<String, Finder> FINDER_MAP = new HashMap<>();

    public static void inject(Object host, Object source, Provider provider) {
        String className = host.getClass().getName();
        try {
            Finder finder = FINDER_MAP.get(className);
            if (finder == null) {
                Class<?> finderClass = Class.forName(className + "$$Finder");
                finder = (Finder) finderClass.newInstance();
                FINDER_MAP.put(className, finder);
            }
            finder.inject(host, source, provider);
        } catch (Exception e) {
            throw new RuntimeException("Unable to inject for " + className, e);
        }
    }
}

另外代码中使用到了一点反射,所以为了提高效率,避免每次注入的时候都去找 Finder 对象,这里用一个
Map 将第一次找到的对象缓存起来,后面用的时候直接从 Map 里面取。

到此,API
模块的设计基本搞定了,接下来就是去通过注解处理器为每一个注解类生成 Finder内部类。

APT简单介绍

APT,全称Annotation Processing
Tool,它用来在编译时处理源代码中的注解信息,我们可以根据注解来生成一些Java文件,防止编写冗余的代码,比如ButterKnife项目,正是利用了APT工具,帮助我们少写了许多重复冗余的代码。本文中,通过注解来少写一些配置文件。

在APP中使用

  • 配置APP,在build.gradle中添加如下内容:

dependencies {
    ...
    annotationProcessor project(':MockButterknife-complier') 
    implementation project(path: ':MockButterknife-complier')
    implementation project(path: ':Mockbutterknife-source')
  • 为Activity添加自定义注解

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_click)
    TextView tvClick;
    @BindView(R.id.tv_dont_click)
    TextView tvDontClcik;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MockButterKnife.bind(this);

        initData();
    }

   ...

    @OnClick(value = {R.id.tv_click, R.id.tv_dont_click})
    public void onClick(View view){

        if(view.getId() == R.id.tv_click)
            new AboutDialog().show(this.getSupportFragmentManager());
        else if(view.getId() == R.id.tv_dont_click)
            Toast.makeText(this, getString(R.string.main_toast), Toast.LENGTH_SHORT).show();
    }
}
  • 生成的class文件

package com.hys.annotationprocessortest;

import android.support.annotation.UiThread;
import android.view.View;
import android.widget.TextView;

public class MainActivity_ViewBinding {
  private MainActivity target;

  private View view2131165309;

  private View view2131165310;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;
    this.target.tvClick = (TextView)source.findViewById(2131165309);
    this.target.tvDontClcik = (TextView)source.findViewById(2131165310);
    this.view2131165309 = source.findViewById(2131165309);
    this.view2131165309.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
           target.onClick(v);
       }
    });
    this.view2131165310 = source.findViewById(2131165310);
    this.view2131165310.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
           target.onClick(v);
       }
    });
  }
}

好了,关于如何构建编译时注解解析框架,就先讲到这,上述项目的具体代码在Github,感谢你耐心的阅读。


我是青岚之峰,如果读完后觉的有所收获,欢迎点赞加关注

创建注解处理器

创建 module viewFinder-compiler,类型为 Java
Library,实现一个注解处理器。

这个模块需要添加一些依赖:

compile project(':viewfinder-annotation')
compile 'com.squareup:javapoet:1.7.0'
compile 'com.google.auto.service:auto-service:1.0-rc2'
  • 因为要用到前面定义的注解,当然要依赖 viewFinder-annotation
  • javapoet 是方块公司出的又一个好用到爆炸的裤子,提供了各种 API
    让你用各种姿势去生成 Java 代码文件,避免了徒手拼接字符串的尴尬。
  • auto-service 是 Google
    家的裤子,主要用于注解 Processor,对其生成 META-INF 配置信息。

下面就来创建我们的处理器 ViewFinderProcessor

@AutoService(Processor.class)
public class ViewFinderProcesser extends AbstractProcessor {

    /**
     * 使用 Google 的 auto-service 库可以自动生成 META-INF/services/javax.annotation.processing.Processor 文件
     */

    private Filer mFiler; //文件相关的辅助类
    private Elements mElementUtils; //元素相关的辅助类
    private Messager mMessager; //日志相关的辅助类

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }

    /**
     * @return 指定哪些注解应该被注解处理器注册
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

    /**
     * @return 指定使用的 Java 版本。通常返回 SourceVersion.latestSupported()。
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // to process annotations 
        return false;
    }
}

用 @AutoService 来注解这个处理器,可以自动生成配置信息。

在 init() 可以初始化拿到一些实用的工具类。

在 getSupportedAnnotationTypes() 方法中返回所要处理的注解的集合。

在 getSupportedSourceVersion() 方法中返回 Java 版本。

这几个方法写法基本上都是固定的,重头戏在 process() 方法。

这里插播一下 Element 元素相关概念,后面会用到不少。

Element 元素,源代码中的每一部分都是一个特定的元素类型,分别代表了包、类、方法等等,具体看
Demo。

package com.example;

public class Foo { // TypeElement

    private int a; // VariableElement
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {
    }
}

这些 Element 元素,相当于 XML 中的 DOM
树,可以通过一个元素去访问它的父元素或者子元素。

element.getEnclosingElement();// 获取父元素
element.getEnclosedElements();// 获取子元素

注解处理器的整个处理过程跟普通的 Java
程序没什么区别,我们可以使用面向对象的思想和设计模式,将相关逻辑封装到
model
中,使得流程更清晰简洁。分别将注解的成员变量、点击方法和整个注解类封装成不同的
model。

public class BindViewField {
    private VariableElement mFieldElement;
    private int mResId;

    public BindViewField(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(
                String.format("Only fields can be annotated with @%s", BindView.class.getSimpleName()));
        }

        mFieldElement = (VariableElement) element;
        BindView bindView = mFieldElement.getAnnotation(BindView.class);
        mResId = bindView.value();
    }
    // some getter methods
}

主要就是在初始化时校验了一下元素类型,然后获取注解的值,在提供几个 get
方法。OnClickMethod 封装类似。

public class AnnotatedClass {

    public TypeElement mClassElement;
    public List<BindViewField> mFields;
    public List<OnClickMethod> mMethods;
    public Elements mElementUtils;

    // omit some easy methods 

    public JavaFile generateFinder() {

        // method inject(final T host, Object source, Provider provider)
        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
            .addModifiers(Modifier.PUBLIC)
            .addAnnotation(Override.class)
            .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
            .addParameter(TypeName.OBJECT, "source")
            .addParameter(TypeUtil.PROVIDER, "provider");

        for (BindViewField field : mFields) {
            // find views
            injectMethodBuilder.addStatement("host.$N = ($T)(provider.findView(source, $L))", field.getFieldName(),
                ClassName.get(field.getFieldType()), field.getResId());
        }

        if (mMethods.size() > 0) {
            injectMethodBuilder.addStatement("$T listener", TypeUtil.ANDROID_ON_CLICK_LISTENER);
        }
        for (OnClickMethod method : mMethods) {
            // declare OnClickListener anonymous class
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                .addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)
                .addMethod(MethodSpec.methodBuilder("onClick")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.VOID)
                    .addParameter(TypeUtil.ANDROID_VIEW, "view")
                    .addStatement("host.$N()", method.getMethodName())
                    .build())
                .build();
            injectMethodBuilder.addStatement("listener = $L ", listener);
            for (int id : method.ids) {
                // set listeners
                injectMethodBuilder.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
            }
        }
        // generate whole class 
        TypeSpec finderClass = TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Finder")
            .addModifiers(Modifier.PUBLIC)
            .addSuperinterface(ParameterizedTypeName.get(TypeUtil.FINDER, TypeName.get(mClassElement.asType())))
            .addMethod(injectMethodBuilder.build())
            .build();

        String packageName = mElementUtils.getPackageOf(mClassElement).getQualifiedName().toString();
        // generate file
        return JavaFile.builder(packageName, finderClass).build();
    }
}

AnnotatedClass 表示一个注解类,里面放了两个列表,分别装着注解的成员变量和方法。在generateFinder() 方法中,按照上一节设计的模板,利用 JavaPoet 的
API 生成代码。这部分没啥特别的姿势,照着 JavaPoet
文档 来就好了,文档写得很细致。

有很多地方需要用到对象的类型,普通类型可以用

ClassName get(String packageName, String simpleName, String... simpleNames)

传入包名、类名、内部类名,就可以拿到想要的类型了(可以参考
项目中TypeUtil 类)。

用到泛型的话,可以用

ParameterizedTypeName get(ClassName rawType, TypeName... typeArguments)

传入具体类和泛型类型就好了。

这些 model
都确定好了之后,process() 方法就很清爽啦。使用 RoundEnvironment 参数来查询被特定注解标注的元素,然后解析成具体的
model,最后生成代码输出到文件中。

@AutoService(Processor.class)
public class ViewFinderProcesser extends AbstractProcessor {

    private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>();

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // process() will be called several times
        mAnnotatedClassMap.clear();

        try {
            processBindView(roundEnv);
            processOnClick(roundEnv);
        } catch (IllegalArgumentException e) {
            error(e.getMessage());
            return true; // stop process
        }

        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            try {
                info("Generating file for %s", annotatedClass.getFullClassName());
                annotatedClass.generateFinder().writeTo(mFiler);
            } catch (IOException e) {
                error("Generate file failed, reason: %s", e.getMessage());
                return true;
            }
        }
        return true;
    }

    private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField field = new BindViewField(element);
            annotatedClass.addField(field);
        }
    }

    private void processOnClick(RoundEnvironment roundEnv) {
        // same as processBindView()
    }

    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement classElement = (TypeElement) element.getEnclosingElement();
        String fullClassName = classElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(classElement, mElementUtils);
            mAnnotatedClassMap.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }
}

首先解析注解元素,并放到对应的注解类对象中,最后调用方法生成文件。model
的代码中还会加入一些校验代码,来判断注解元素是否合理,数据是否正常,然后抛出异常,处理器接收到之后可以打印出错误提示,然后直接返回 true 来结束处理。

至此,注解处理器也基本完成了,具体细节参考项目代码。

项目结构

本项目共分为4个Module,两个Java Library module,一个Android Library
module和用于演示的Android Application module

  • easyshortcuts-api:Android Library module,用于供客户端的调用。
  • easyshortcuts-annotation:Java Library module,用于提供注解类。
  • easyshortcuts-compiler:Java Library
    module,用于编写处理注解并生成相关Processor的注解处理模块
  • 还有一个普通的应用模块

其中easyshortcuts-apieasyshortcuts-compiler模块依赖easyshortcuts-annotation模块。

实际项目使用

创建 module sample,普通的 Android
module,来演示 ViewFinder 的使用。

在整个项目下的 build.gradle 中添加

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

然后在 sample module 下的 build.gradle 中添加

apply plugin: 'com.neenbedankt.android-apt'

同时添加依赖:

compile project(':viewfinder-annotation')
compile project(':viewfinder')
apt project(':viewfinder-compiler')

然后随便创建个布局,随便添加几个控件,就能体验注解啦。

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv) TextView mTextView;
    @BindView(R.id.btn) Button mButton;
    @BindView(R.id.et) EditText mEditText;

    @OnClick(R.id.btn)
    public void onButtonClick() {
        Toast.makeText(this, "onButtonClick", Toast.LENGTH_SHORT).show();
    }

    @OnClick(R.id.tv)
    public void onTextClick() {
        Toast.makeText(this, "onTextClick", Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewFinder.inject(this);
    }
}

这个时候 build
一下项目,就能看到生成的 MainActivity$$Finder 类了,再运行项目就跑起来了。每次改变注解之后,build
一下项目就好啦。

all done ~

这个项目也就是个玩具级的 APT 项目,借此来学习如何编写 APT 项目。感觉 APT
项目更多地是考虑如何去设计架构,类之间如何调用,需要生成什么样的代码,提供怎样的
API 去调用。最后才是利用注解处理器去解析注解,然后用 JavaPoet
去生成具体的代码。

思路比实现更重要,设计比代码更巧妙。

注解模块的编写

搭好项目后,第一个动手编写的应该是easyshortcuts-annotation模块,通过查看Android
Developer官网上的Shortcut介绍后,发现通过Java代码,我们只可以通过ShortcutManager生成动态Shortcut,生成一个动态Shortcut的代码简单重复,每个Shortcut需要一个String类型的Id,图标的ResId,显示的文字,以及一个Intent的Action字段。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface AppShortcut {
    int resId();

    int rank() default 1;

    String description();

    String action() default Define.SHORTCUT_ACTION;
}

之后我用注解所在类的类名称作为Shortcut的Id。

这里,我还建了一个注解ShortcutApplication,是一个没有任何字段的注解,这个注解应该用在用户第一个打开的Activity。因为这里使用了动态加载的方式创建Shortcut,所以必须要执行代码才可以生成Shortcut,所以应该在Launcher
Activity使用。

参考

  • Annotation-Processing-Tool详解 (大力推荐)
  • Android
    如何编写基于编译时注解的项目
  • JavaPoet 文档
  • ButterKnife (代码结构设计很棒)

注解处理器的编写

确定了注解后,我们要编写相应的注解处理器来处理注解并生成相应的Java文件,这里easyshortcuts-compiler依赖了google的auto-service库和square的javapoet库。

compile "com.squareup:javapoet:$rootProject.ext.squareJavaPoetVersion"
compile "com.google.auto.service:auto-service:$rootProject.ext.googleAutoServiceVersion"

其中auto-service库可以很方便的帮助我们生成配置文件,javapoet库可以很方便的帮助我们自动生成Java代码。

新建一个继承自AbstractProcessorShortcutProcessor,下面是一个基本的Processor应有的要素,我们的重点在与process()方法。

//帮助我们生成配置文件的注解
@AutoService(Processor.class)
public class ShortcutsProcessor extends AbstractProcessor {
    private Filer mFiler;
    private Elements mElementUtils;
    private Messager mMessager;
    ……

    //在初始化时获得相关帮助对象
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();
        mMessager = processingEnvironment.getMessager();
    }

    //根据相应的注解进行处理
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        ……
        return true;
    }

    //返回要支持的注解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ShortcutApplication.class.getCanonicalName());
        types.add(AppShortcut.class.getCanonicalName());
        return types;
    }

    //返回Java语言的支持版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    //辅助的日志打印方法
    private void printNote(String message) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, message);
    }

    private void printError(String error) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, error);
    }
}

process()方法中,要获取所有有相关注解的Element,并获取每个注解中附带的字段,最后根据这些信息生成一个Java文件。为了更好的获取储存这些字段,我建立了一个model类Shortcut

public class Shortcut {
    private int mResId;
    private int mRank;
    private String mDescription;
    private String mAction;
    private TypeElement mTypeElement;

    public Shortcut(Element element) {
        mTypeElement = (TypeElement) element;
        AppShortcut appShortcut = mTypeElement.getAnnotation(AppShortcut.class);
        mResId = appShortcut.resId();
        mRank = appShortcut.rank();
        mDescription = appShortcut.description();
        mAction = appShortcut.action();
    }

    //相关的getXXX()方法
    ……

}

之后在process()方法中遍历所有带有相关注解的Element,并生成model对象

for (Element element : roundEnvironment.getElementsAnnotatedWith(AppShortcut.class)) {
     //检查注解所标注的元素是否为我们需要
     if (!isValid(element)) {
         return false;
     }
     //解析这个element并生成相应的Shortcut对象
     parseShortcut(element);
}

最后根据所有Shortcut对象生成Java文件

mShortcutClass.generateCode().writeTo(mFiler);

生成代码我使用Javapoet,可以很方便的生成代码,以下代码通过查看Javapoet的README就可以很快理解。

public JavaFile generateCode() {
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("create")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(CONTEXT, "context")
                .addStatement("$T shortcutManager = context.getSystemService($T.class)", SHORTCUT_MANAGER, SHORTCUT_MANAGER)
                .addStatement("$T.Builder builder",SHORTCUT_INFO)
                .addStatement("$T intent",INTENT);

        for (Shortcut shortcut : mShortcuts) {
            methodBuilder.
                    addStatement("builder = new $T.Builder(context,$S)", SHORTCUT_INFO, shortcut.getTypeElement().getSimpleName().toString())
                    .addStatement("intent = new $T(context, $T.class)", INTENT, TypeName.get(shortcut.getTypeElement().asType()))
                    .addStatement("intent.setAction($S)", shortcut.getAction())
                    .addStatement("builder.setIntent(intent)")
                    .addStatement("builder.setShortLabel($S)", shortcut.getDescription())
                    .addStatement("builder.setLongLabel($S)", shortcut.getDescription())
                    .addStatement("builder.setRank($L)", shortcut.getRank())
                    .addStatement("builder.setIcon($T.createWithResource(context, $L))", ICON, shortcut.getResId())
                    .addStatement("shortcutManager.addDynamicShortcuts(singletonList(builder.build()))");
        }

        TypeSpec shortcutClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + SUFFIX)
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(CREATOR)
                .addMethod(methodBuilder.build())
                .build();

        String packageName = mElementUtils.getPackageOf(mTypeElement).getQualifiedName().toString();

        return JavaFile
                .builder(packageName, shortcutClass)
                .addStaticImport(Collections.class, "singletonList")
                .build();
}

提供调用接口

写完了注解的解释器后,我们每次编译都生成了一个Java类文件,但是我们并没有调用它,我们要提供一个接口来调用,本项目中提供了这样一个静态方法

ShortcutCreator.create(this);

public class ShortcutCreator {
    public static void create(Context context) {
        try {
            Class<?> targetClass = context.getClass();
            Class<?> creatorClass = Class.forName(targetClass.getName() + "$$Shortcut");
            Creator creator = (Creator) creatorClass.newInstance();
            creator.create(context);
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

由于我们利用APT自动生成的Java类的类名称是知道的且提供了默认的无参数构造器,所以我们很容易生成一个对象,并调用其相关方法来生成相应的Shortcut。

总结

编译时注解可以大大加快我们的开发效率,希望大家可以多制作一些编译时注解的库来造福广大开发者,让大家少些许多简单重复的代码。最后附上源码地址:https://github.com/wuapnjie/EasyShortcuts,希望对大家有所帮助。

发表评论

电子邮件地址不会被公开。 必填项已用*标注