Java Annotation 简析(二)

上一篇文章解析了java Annotation的概念和基本用法,再简单回顾下注解的作用:
a.生成文档。这是最常见的,也是java 最早提供的注解。常用的有@see @param @return 等
b. 标记,用于告诉编译器一些信息,比如 @Override等,
c. 编译时动态处理,如动态生成代码
d. 运行时动态处理,如得到注解信息

上篇文章演示了处理运行时动态处理注解,这篇文件讲解注解的编译时动态处理机制,还是实现类似ButterKnife的功能,用注解代替findViewById()和setOnClickListener。

注解处理器

要使用编译时处理注解功能,首先的在android studio中添加注解处理器:android-apt
官方地址: https://bitbucket.org/hvisser/android-apt

Android Studio原本是不支持注解处理器的, 但是用这个插件后, 我们就可以使用注解处理器了, 这个插件可以自动的帮你为生成的代码创建目录, 让生成的代码编译到APK里面去, 而且它还可以让最终编译出来的APK里面不包含注解处理器本身的代码, 因为这部分代码只是编译的时候需要用来生成代码, 最终运行的时候是不需要的。

使用这个插件很简单, 首先在你项目顶层的build.gradle文件中添加依赖项, 如下:

java
dependencies {
classpath ‘com.android.tools.build:gradle:2.0.0’
classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’
}

然后在app的build.gradle里面添加插件的引用以及需要依赖哪些库, 如下:

apply plugin: ‘com.android.application’
apply plugin: ‘com.neenbedankt.android-apt’
….
….
dependencies {

compile project(‘:bindannotation’)
apt project(‘:viewbindcompiler’)
compile project(‘:viewinject’)
}

注意上面同时添加了bindannotation,viewbindcompiler,viewinject 三个自定义模块:

  • bindannotation : 这是个Java Library,,主要来定义注解。
  • viewbindcompiler: 必须是java Library, 用于处理上面bindannotation定义的注解。
  • viewinject:这个是Android Library,被android其他模块调用实现View的Bind。

代码结构如下:
这里写图片描述

使用注解

下面就来介绍怎么使用注解生成代码

定义bindannotation注解

1.定义ViewBind注解,处理findViewById方法:

@Documented
@Target(ElementType.FIELD) //用于类属性成员上
@Retention(RetentionPolicy.CLASS) //编译时注解
@Inherited
public @interface ViewBind {
    int value() default 0;
}

2.定义BindClick注解,处理View onClickListener方法:

@Documented
@Target(ElementType.METHOD)//用于类方法上
@Retention(RetentionPolicy.CLASS)//编译时注解
@Inherited
public @interface BindClick {
    int id() default 0;
}

定义注解产生代码调用者

在viewinject模块里先定义一个接口:

public interface ViewBinder<T> {
    void viewBind(T target);
}

为什么要先定义这个接口呢,这个接口就是用于规范编译时自动产生的代码,后续我们编译时自动生成的代码都继承这个接口,并在viewBind方法里自动生成findViewById,setOnClickListener等代码,这样我们其他地方就可以方便统一调用这些自动生成的代码。

定义一个ViewBind类,调用自动生成的代码:

public class ViewBind {
    private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";//生成类的后缀 以后会用反射去取

    public static void init(Activity activity){
        String bindClassName = activity.getClass().getName() + BINDING_CLASS_SUFFIX;
        try {
            Class bindClass = Class.forName(bindClassName);
            ViewBinder viewBind = (ViewBinder) bindClass.newInstance();
            viewBind.viewBind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

有了ViewBind类后,我们就只需在activity中,调用ViewBind.init(activity)后,就可以完成view的bind工作了。注意,后面编译时生成的类的类名,都是按使用注解的类的类名+$$ViewBinder。

比如MainActivity.class,会生成MainActivity$$ViewBinder.class 文件。

定义注解的处理器

注解处理器最核心的就是要有一个Processor, 它继承自AbstractProcessor,编译器在编译时自动查找所有继承自 AbstractProcessor 的类,然后调用他们的 process 方法去处理。

先在viewbindcompiler模块里添加所需的依赖:

apply plugin: 'java'

sourceCompatibility = 1.7
targetCompatibility = 1.7

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':bindannotation')
}

auto-service:使用它就不需要把processor在META-INF配置了,编译时配置的Processor能够自动被发现并处理。
javapoet:辅助生成代码,能够更简单的生成.java源文件

定义一个ViewBindProcessor类:

@AutoService(Processor.class)
public class ViewBindProcessor extends AbstractProcessor {
    private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";//生成类的后缀 以后会用反射去取
    private static final ClassName VIEW_BINDER = ClassName.get("com.nick.study.viewinject", "ViewBinder");
    private Elements elementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ViewBind.class.getCanonicalName());
        types.add(BindClick.class.getCanonicalName());
        return types;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Map<String, ViewBindInfo> targetClassMap = new LinkedHashMap<>();//定义一个按类名为key值,保存其类下所有使用了注解的元素
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(ViewBind.class); //取出所有使用ViewBind注解的元素
        // Element表示一个程序元素,比如包、类或者方法。
        for (Element element : set) {
            if (element.getKind() != ElementKind.FIELD) { //如果此元素的不是类成员,则抛出异常
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support class field");
                break;
            }
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String packageName = getPackageName(enclosingElement); //得到此元素所在的包名
            String className = getClassName(enclosingElement,packageName); //得到此元素所在类的类名
            String classFullPath = packageName+"."+className;  //完整类名

            ViewBindInfo viewBindInfo = targetClassMap.get(classFullPath);
            if(viewBindInfo == null){
                viewBindInfo = new ViewBindInfo();
                targetClassMap.put(classFullPath,viewBindInfo); //保存这个类的注解信息
            }
            viewBindInfo.packageName = packageName;
            viewBindInfo.className = className;
            viewBindInfo.viewBindElementList.add(element); //所有的viewBind元素保存在一个链表里
            viewBindInfo.typeClassName =  ClassName.bestGuess(getClassName(enclosingElement, packageName));//此ViewBind注解所在的类名类
        }
        Set<? extends Element> clickSet = roundEnv.getElementsAnnotatedWith(BindClick.class); //处理BindClick注解如上面一致
        for (Element element : clickSet) {
            if (element.getKind() != ElementKind.METHOD) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support class method");
                break;
            }
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String packageName = getPackageName(enclosingElement);
            String className = getClassName(enclosingElement,packageName);
            String classFullPath = packageName+"."+className;
            processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,"process: "+classFullPath);
            ViewBindInfo viewBindInfo = targetClassMap.get(classFullPath);
            if(viewBindInfo == null){
                viewBindInfo = new ViewBindInfo();
                targetClassMap.put(classFullPath,viewBindInfo);
            }
            viewBindInfo.packageName = packageName;
            viewBindInfo.className = className;
            viewBindInfo.viewClickElementList.add(element);
            viewBindInfo.typeClassName =  ClassName.bestGuess(getClassName(enclosingElement, packageName));
        }
        buildViewBindClass(targetClassMap); //生成相应的java文件
        return false;
    }

    private void buildViewBindClass( Map<String, ViewBindInfo> targetClassMap ){
        if(targetClassMap.size() == 0){
            return;
        }
        for (Map.Entry<String, ViewBindInfo> item : targetClassMap.entrySet()) {
            String newClassName = item.getValue().className+BINDING_CLASS_SUFFIX; //java文件名
            String packageName = item.getValue().packageName;
            ClassName typeClassName = item.getValue().typeClassName;
            String methodName = "viewBind";  //方法名,跟自定义的ViewBinder接口中一致
            MethodSpec.Builder viewBindMethodBuilder = MethodSpec.methodBuilder(methodName)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.VOID)  //返回值void
                    .addAnnotation(Override.class)
                    .addParameter(typeClassName, "target", Modifier.FINAL); //添加一个参数,类型为typeClassName,形参名为target

            for(Element element : item.getValue().viewBindElementList){
                int id = element.getAnnotation(ViewBind.class).value();
                ClassName viewClass = ClassName.bestGuess(element.asType().toString());
                //生成findViewById方法
                viewBindMethodBuilder.addStatement("target.$L=($T)target.findViewById($L)",element.getSimpleName().toString(),viewClass, id);
            }

            if(item.getValue().viewClickElementList.size() > 0){
                viewBindMethodBuilder.addStatement("$T listener", ClassTypeUtils.ANDROID_ON_CLICK_LISTENER);
            }
            for(Element element : item.getValue().viewClickElementList){
                int id = element.getAnnotation(BindClick.class).id();
                // declare OnClickListener anonymous class
                TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                        .addSuperinterface(ClassTypeUtils.ANDROID_ON_CLICK_LISTENER)
                        .addMethod(MethodSpec.methodBuilder("onClick")
                                .addAnnotation(Override.class)
                                .addModifiers(Modifier.PUBLIC)
                                .returns(TypeName.VOID)
                                .addParameter(ClassTypeUtils.ANDROID_VIEW, "view")
                                .addStatement("target.$N()",((ExecutableElement)element).getSimpleName())
                                .build())
                        .build();
                viewBindMethodBuilder.addStatement("listener = $L ", listener);
                // set listeners
                viewBindMethodBuilder.addStatement("target.findViewById($L).setOnClickListener(listener)", id);
            }

            MethodSpec viewBindMethod = viewBindMethodBuilder.build();
            TypeSpec viewBind = TypeSpec.classBuilder(newClassName) //生成viewBind方法
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)  //方法的修饰限定词
                    .addTypeVariable(TypeVariableName.get("T", typeClassName))
                    .addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, typeClassName))
                    .addMethod(viewBindMethod) //增加方法里面的语句
                    .build();
            //生成java文件
            JavaFile javaFile = JavaFile.builder(packageName, viewBind).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }

    private static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
    }

    //代表某个使用了ViewBind或BindClick注解类中的element注解信息
    private class ViewBindInfo{
        String packageName;
        String className;
        ClassName typeClassName; //JavaPoet类,代表某个类名
        List<Element> viewBindElementList = new ArrayList<>(); //当前类下的所有ViewBind注解元素
        List<Element> viewClickElementList = new ArrayList<>();//当前类下的所有ViewClick注解元素
    }
}

使用注解

下面我们就可以开始在activity中使用了:

public class MainActivity extends AppCompatActivity {

    @ViewBind(R.id.user_tv)
    TextView mTextView;
    @ViewBind(R.id.user_tv2)
    TextView mTextView2;

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

        ViewBind.init(this);

        mTextView.setText("hahaha!!!! annotation success!!!!");
        mTextView2.setText("hahaha!!!! annotation success!!!!");
    }

    @BindClick(id = R.id.user_tv)
    public void onClick(){
        Toast.makeText(this,"hahaha!!!! annotation click success!!!!",Toast.LENGTH_SHORT).show();
    }

}

此时编译下项目,就会发现在app的build\generated\source\apt\目录下生成一个MainActivity$$ViewBinder.java文件,这就是apt编译时自动帮我们生成的,其代码如下:

public final class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<MainActivity> {
  @Override
  public void viewBind(final MainActivity target) {
    target.mTextView=(TextView)target.findViewById(2131492944);
    target.mTextView2=(TextView)target.findViewById(2131492945);
    View.OnClickListener listener;
    listener = new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        target.onClick();
      }
    } ;
    target.findViewById(2131492944).setOnClickListener(listener);
  }
}

这就跟我们手动代码一样,运行下,看看效果:
这里写图片描述

JavaPoet

上面用到的JavaPoet 是一个用来生成 Java源文件的Java API。

javapoet 常用的API

大多数JavaPoet的API使用的是简单的不可变的Java对象。通过建造者模式,链式方法,可变参数使得API比较友好。JavaPoet提供了(TypeSpec)用于创建类或者接口,(FieldSpec)用来创建字段,(MethodSpec)用来创建方法和构造函数,(ParameterSpec)用来创建参数,(AnnotationSpec)用于创建注解。

还可以通过字符串来构建代码块:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addCode(""
        + "int total = 0;\n"
        + "for (int i = 0; i < 10; i++) {\n"
        + "  total += i;\n"
        + "}\n")
    .build();

生成的代码如下:

void main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total += i;
  }
}

addStatement(): 方法会给代码语句加上分号和换行
beginControlFlow() + endControlFlow() 需要一起使用,会提供换行符和缩进。
addCode() 以字符串的形式添加代码内
constructorBuilder() 生成构造器函数
addAnnotation 添加注解
addSuperinterface 给类添加实现的接口
superclass 给类添加继承的父类
ClassName.bestGuess(“类全名称”) 返回ClassName对象,这里的类全名称表示的类必须要存在,会自动导入相应的包
ClassName.get(“包名”,”类名”) 返回ClassName对象,不检查该类是否存在
TypeSpec.interfaceBuilder(“HelloWorld”)生成一个HelloWorld接口addTypeVariable(TypeVariableName.get(“T”, typeClassName))
会给生成的类加上泛型

javaPoet占位符

$L:代表的是字面量

$S:Strings

$N:Names(我们自己生成的方法名或者变量名等等)

$T: Types 这里的$T,在生成的源代码里面,也会自动导入你的类。

更多精彩Android技术可以关注我们的微信公众号,扫一扫下方的二维码或搜索关注公共号: Android老鸟


这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值