一小時搞明白注解處理器(Annotation Processor Tool)

Java中的注解是個很神奇的東西,還不了解的可以看下一小時搞明白自定義注解(Annotation)?,F(xiàn)在很多Android的庫都用使用注解實現(xiàn)的,比如ButterKnife,我們不防也來學習一下,學完注解處理器,我們嘗試寫一個簡單的類似ButterKnife的東西來綁定控件。

什么是注解處理器?

注解處理器是(Annotation Processor)是javac的一個工具,用來在編譯 時掃描和編譯和處理注解(Annotation)。你可以自己定義注解和注解處理器去搞一些事情。一個注解處理器它以Java代碼或者(編譯過的字節(jié)碼)作為輸入,生成文件(通常是java文件)。這些生成的java文件不能修改,并且會同其手動編寫的java代碼一樣會被javac編譯??吹竭@里加上之前理解,應該明白大概的過程了,就是把標記了注解的類,變量等作為輸入內(nèi)容,經(jīng)過注解處理器處理,生成想要生成的java代碼。

處理器AbstractProcessor

處理器的寫法有固定的套路,繼承AbsstractProcessor。如下:

public class MyProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv){
        super.init(processingEnv);
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return null;
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return true;
    }
}
  • init(ProcessingEnvironment processingEnv) 被注解處理工具調(diào)用,參數(shù)ProcessingEnvironment 提供了Element,F(xiàn)iler,Messager等工具
  • getSupportedAnnotationTypes() 指定注解處理器是注冊給那一個注解的,它是一個字符串的集合,意味著可以支持多個類型的注解,并且字符串是合法全名。
  • getSupportedSourceVersion 指定Java版本
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 這個也是最主要的,在這里掃描和處理你的注解并生成Java代碼,信息都在參數(shù)RoundEnvironment 里了,后面會介紹。

在Java7 中還可以使用

@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
   // 合法注解全名的集合
 })

代替 getSupportedSourceVersion() 和 getSupportedAnnotationType() ,沒毛病,還可以在注解處理離器中使用注解。

注冊注解處理器

打包注解處理器的時候需要一個特殊的文件 javax.annotation.processing.Processor 在 META-INF/services 路徑下

--myprcessor.jar
----com
------example
--------MyProcessor.class
----META-INF
------services
--------javax.annotation.processing.Processor

打包進javax.annotation.processing.Processor的內(nèi)容是處理器的合法全稱,多個處理器之間換行。

com.example.myprocess.MyProcessorA
com.example.myprocess.MyProcessorB

google提供了一個注冊處理器的庫

compile 'com.google.auto.service:auto-service:1.0-rc2'

一個注解搞定:

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
      ...
}

讀到這里ButterKnife用到的知識點我們都已經(jīng)了解了
1.自定義注解
2.用注解處理器解析注解
3.解析完成后生成Java文件
BufferKnife使用:

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.rxjava_demo)
    Button mRxJavaDemo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        mRxJavaDemo.setText("Text");
    }

}

然后我們編譯一下,打開路徑:/app/build/intermediates/classes/release/com/ming/rxdemo/MainActivity$$ViewBinder.class
這就是我們生成的Java文件,可以看到Button已經(jīng)在bind里面初始化了。

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {
    public MainActivity$$ViewBinder() {
    }

    public void bind(Finder finder, T target, Object source) {
        View view = (View)finder.findRequiredView(source, 2131492944, "field \'mRxJavaDemo\'");
        target.mRxJavaDemo = (Button)finder.castView(view, 2131492944, "field \'mRxJavaDemo\'");
    }

    public void unbind(T target) {
        target.mRxJavaDemo = null;
    }
}

接下來我們創(chuàng)建一個項目,寫一個簡單的用注解綁定控件的例子

項目結構

--apt-demo
----bindview-annotation(Java Library)
----bindview-api(Android Library)
----bindview-compiler(Java Library)
----app(Android App)
  • bindview-annotation 注解聲明
  • bindview-api 調(diào)用Android SDK API
  • bindview-compiler 注解處理器相關
  • app 測試App

1.在 bindview-annotation 下創(chuàng)建一個@BindView注解,該注解返回一個值,整型,名字為value,用來表示控件ID。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    /**
     * 用來裝id
     *
     * @return
     */
    int value();
}

2.在 bindview-compiler 中創(chuàng)建注解處理器 BindViewProcessor 并注冊,做基本的初始化工作。

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    /**
     * 文件相關的輔助類
     */
    private Filer mFiler;
    /**
     * 元素相關的輔助類
     */
    private Elements mElementUtils;
    /**
     * 日志相關的輔助類
     */
    private Messager mMessager;
    /**
     * 解析的目標注解集合
     */
    private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>();

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());//返回該注解處理器支持的注解集合
        return types;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

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

是不是注意到了里面有個Map容器,而且類型是AnnotatedClass,這是干啥的呢?這個很好理解,我們在解析XML,解析Json的時候數(shù)據(jù)解析完之后是不是要以對象的形式表示出來,這里也一樣,@BindView用來標記類成員,一個類下可以有多個成員,好比一個Activity中可以有多個控件,一個容器下有多個控件等。如下:

package com.mingwei.myprocess.model;

import com.mingwei.myprocess.TypeUtil;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.util.ArrayList;
import java.util.List;

import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;

/**
 * Created by mingwei on 12/10/16.
 * CSDN:    http://blog.csdn.net/u013045971
 * Github:  https://github.com/gumingwei
 */
public class AnnotatedClass {
    /**
     * 類名
     */
    public TypeElement mClassElement;
    /**
     * 成員變量集合
     */
    public List<BindViewField> mFiled;
    /**
     * 元素輔助類
     */
    public Elements mElementUtils;

    public AnnotatedClass(TypeElement classElement, Elements elementUtils) {
        this.mClassElement = classElement;
        this.mElementUtils = elementUtils;
        this.mFiled = new ArrayList<>();
    }
    /**
     * 獲取當前這個類的全名
     */
    public String getFullClassName() {
        return mClassElement.getQualifiedName().toString();
    }
    /**
     * 添加一個成員
     */
    public void addField(BindViewField field) {
        mFiled.add(field);
    }
    /**
     * 輸出Java
     */
    public JavaFile generateFinder() {
        return null;
    }
    /**
     * 包名
     */
    public String getPackageName(TypeElement type) {
        return mElementUtils.getPackageOf(type).getQualifiedName().toString();
    }
    /**
     * 類名
     */
    private static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
    }
}

成員用BindViewField表示,沒什么復雜的邏輯,在構造函數(shù)判斷類型和初始化,簡單的get函數(shù)

package com.mingwei.myprocess.model;

import com.mingwe.myanno.BindView;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;

/**
 * Created by mingwei on 12/10/16.
 * CSDN:    http://blog.csdn.net/u013045971
 * Github:  https://github.com/gumingwei
 * 被BindView注解標記的字段的模型類
 */
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 field can be annotated with @%s",
                    BindView.class.getSimpleName()));
        }
        mFieldElement = (VariableElement) element;
        //獲取注解和值
        BindView bindView = mFieldElement.getAnnotation(BindView.class);
        mResId = bindView.value();
        if (mResId < 0) {
            throw new IllegalArgumentException(String.format("value() in %s for field % is not valid",
                    BindView.class.getSimpleName(), mFieldElement.getSimpleName()));
        }
    }

    public Name getFieldName() {
        return mFieldElement.getSimpleName();
    }

    public int getResId() {
        return mResId;
    }

    public TypeMirror getFieldType() {
        return mFieldElement.asType();
    }
}

這里看到了很多的Element,在Xml解析時候就有Element這個概念。在Java源文件中同樣有Element概念:

package com.example;        // PackageElement

public class MyClass {      // TypeElement

    private int a;          // VariableElement

    private Foo other;      // VariableElement

    public Foo () {}        // ExecuteableElement

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

    }
}

接下來就是在處理器的process中解析注解了
每次解析前都要清空,因為process方法可能不止走一次。
拿到注解模型之后遍歷調(diào)用生成Java代碼

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mAnnotatedClassMap.clear();
        try {
            processBindView(roundEnv);
        } catch (IllegalArgumentException e) {
            error(e.getMessage());
            return true;
        }

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

processBindView 和 getAnnotatedClass

    /**
     * 遍歷目標RoundEnviroment
     * @param roundEnv
     */
    private void processBindView(RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField field = new BindViewField(element);
            annotatedClass.addField(field);
        }
    }
    /**
     * 如果在map中存在就直接用,不存在就new出來放在map里
     * @param element
     */
    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
        String fullClassName = encloseElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(encloseElement, mElementUtils);
            mAnnotatedClassMap.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }

3.在生成Java之前 我們要在bindview-api 中創(chuàng)建一些類,配合 bindview-compiler 一起使用。
你在使用Butterknife的時候不是要在onCreate里掉用一下BindView.bind(this)嗎,那這個玩意是干什么呢。試想一下,前面做的一大堆工作是為了生成自動綁定控件的Java代碼,如果生成的Java代碼不能和你要使用的地方關聯(lián)起來,那也是沒有用的,可以把BindView.bind(this)理解為調(diào)用了你生成的Java代碼,而生成了代碼中完成了一些控件的初始化工作,自然你的控件就變得可用了。
接口:Finder 定義findView方法
實現(xiàn)類:ActivityFinder Activity中使用,ViewFinder View中使用
接口:Injector inject方法將來是要創(chuàng)建在生成的Java文件中,用該方法中傳遞過來的參數(shù)進行控件的初始化。
輔助類:ViewInjector 調(diào)用和傳遞參數(shù)
這個代碼我就不貼了,就一點點內(nèi)容,一看就明白了。
4.在AnnotatedClass中生成Java代碼
生成代碼使用了一個很好用的庫 Javapoet 。類,方法,都可以使用構建器構建出來,很好上手,再也不用拼接字符串了。哈哈哈哈~

public JavaFile generateFinder() {
        //構建方法
        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)//添加描述
                .addAnnotation(Override.class)//添加注解
                .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)//添加參數(shù)
                .addParameter(TypeName.OBJECT, "source")//添加參數(shù)
                .addParameter(TypeUtil.FINDER, "finder");//添加參數(shù)

        for (BindViewField field : mFiled) {
            //添加一行
            injectMethodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName()
                    , ClassName.get(field.getFieldType()), field.getResId());
        }

        String packageName = getPackageName(mClassElement);
        String className = getClassName(mClassElement, packageName);
        ClassName bindClassName = ClassName.get(packageName, className);
        //構建類
        TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")//類名
                .addModifiers(Modifier.PUBLIC)//添加描述
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(mClassElement.asType())))//添加接口(類/接口,范型)
                .addMethod(injectMethodBuilder.build())//添加方法
                .build();

        return JavaFile.builder(packageName, finderClass).build();
    }

    public String getPackageName(TypeElement type) {
        return mElementUtils.getPackageOf(type).getQualifiedName().toString();
    }

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

可以在代碼里System.out調(diào)試注解處理器的代碼。
還要注意的一點,項目之間的相互引用。
bindview-complier 引用 bindview-annotation
app 引用了剩下的三個module,在引用 bindview-complier 的時候用的apt的方式

apt project(':bindview-compiler')

就寫到這里吧,Demo 放在 Github上了

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,355評論 25 708
  • 什么是注解(Annotation):Annotation(注解)就是Java提供了一種元程序中的元素關聯(lián)任何信息和...
    九尾喵的薛定諤閱讀 3,428評論 0 2
  • 未來的你很遙遠, 我越來越不等待奢求。 我本來就是沒有過去可言的人。 我在這里, 沒有現(xiàn)在。 我不可能遇見你, 我...
    加加丁的書閱讀 184評論 0 0
  • 校園內(nèi)七點零八分的早晨,校園內(nèi)已有瑯瑯書聲,同學們都坐在各自的座位上,眾生百態(tài),有精神抖擻聲音鏗鏘有力的,當然也有...
    爾幼閱讀 321評論 0 0
  • 【一小朵兒】20170714學習力踐行D57 今天我和爸爸都發(fā)高燒,下班回到家我們都攤床上,可憐的寶寶想讓我們陪她...
    佛鈴花海閱讀 125評論 0 0

友情鏈接更多精彩內(nèi)容