Android APT(Java注解應(yīng)用)

關(guān)于Java注解

一.APT技術(shù)

APT(Annotation Process Tool),注解處理器。用來在編譯時(shí)掃描和處理注解驶睦,按照一定的規(guī)則,生成相應(yīng)的java文件匿醒。

Android 目前比較流行的Dagger2, ButterKnife, EventBus3都采用了APT技術(shù)场航。

二.APT使用

1. AbstractProcesser類

Java語言自帶的Override等注解,虛擬機(jī)會默認(rèn)處理廉羔。
對于自定義注解溉痢,需要我們自己處理。java提供一個AbstractProcesser.java類憋他,我們需要繼承該類孩饼,實(shí)現(xiàn)自動的注解處理器,來處理自定義注解

public abstract class AbstractProcessor implements Processor {
  
    protected ProcessingEnvironment processingEnv;
    private boolean initialized = false;

    /**
     * 構(gòu)造器
     */
    protected AbstractProcessor() {}

    /**
     * 指定可以被處理器識別的選項(xiàng)
     *
     * 默認(rèn)實(shí)現(xiàn):
     * 如果處理器類使用{@SupportedOptions}注解举瑰,則返回一個不可修改的字符串set集 
     * 合捣辆,包含注解設(shè)置的屬性值。如果未對類進(jìn)行注解此迅,則返回空集合   
     */
    public Set<String> getSupportedOptions() {
        SupportedOptions so = this.getClass().getAnnotation(SupportedOptions.class);
        if  (so == null)
            return Collections.emptySet();
        else
            return arrayToSet(so.value());
    }

    /**
     * 指定此處理器支持的注解類型
     * 
     * 默認(rèn)實(shí)現(xiàn):
     * 如果此處理器類使用了{(lán)@SupportedAnnotationTypes}注解汽畴,則返回一個不可修改的 
     * set字符串集合,包含注解設(shè)置的屬性值耸序。如果未對類進(jìn)行注釋忍些,則返回空集合
     */
    public Set<String> getSupportedAnnotationTypes() {
            SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class);
            if  (sat == null) {
                if (isInitialized())
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                             "No SupportedAnnotationTypes annotation " +
                                                             "found on " + this.getClass().getName() +
                                                             ", returning an empty set.");
                return Collections.emptySet();
            }
            else
                return arrayToSet(sat.value());
        }

    /**
     * 如果處理器類使用{@SupportedSourceVersion}注解,則返回注釋中設(shè)置的版本值坎怪。
     * 如果沒有對類進(jìn)行注解罢坝,則返回Java 6.0
     *
     * 指定此處理器支持的最新Java版本,通常返回SourceVersion.latestSupported()
     */
    public SourceVersion getSupportedSourceVersion() {
        SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
        SourceVersion sv = null;
        if (ssv == null) {
            sv = SourceVersion.RELEASE_6;
            if (isInitialized())
                processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                         "No SupportedSourceVersion annotation " +
                                                         "found on " + this.getClass().getName() +
                                                         ", returning " + sv + ".");
        } else
            sv = ssv.value();
        return sv;
    }


    /**
     * 初始化方法 搅窿,只能初始化一次嘁酿,否則會拋出IllegalStateException異常
     *
     *一般在這里獲取我們需要的工具類
     * @param processingEnv 提供工具類Elements, Types和Filer
     */
    public synchronized void init(ProcessingEnvironment processingEnv) {
        if (initialized)
            throw new IllegalStateException("Cannot call init more than once.");
        Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment");

        this.processingEnv = processingEnv;
        initialized = true;
    }

    /**
     * 注解處理方法隙券,可以在這里寫掃描、評估和處理注解的代碼闹司,生成Java文件
     */
    public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);

    /**
     * Returns an empty iterable of completions.
     */
    public Iterable<? extends Completion> getCompletions(Element element,
                                                         AnnotationMirror annotation,
                                                         ExecutableElement member,
                                                         String userText) {
        return Collections.emptyList();
    }

    protected synchronized boolean isInitialized() {
        return initialized;
    }

    private static Set<String> arrayToSet(String[] array) {
        assert array != null;
        Set<String> set = new HashSet<String>(array.length);
        for (String s : array)
            set.add(s);
        return Collections.unmodifiableSet(set);
    }
}

2. 自定義注解

Java注解
這里自定義一個BindView娱仔,應(yīng)用場景類似于ButterKnife框架的findView功能,用來找到View對象
注解定義如下:

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

3. 自定義注解處理器

這里需要注意需要創(chuàng)建Java library Module游桩,apply plugin: ‘java-library’不然找不到AbstractProcessor 類
依賴:

 implementation project(':annotation')

 implementation 'com.google.auto.service:auto-service:1.0-rc4'
 implementation 'com.squareup:javapoet:1.11.1'
  • :annotation:自定義注解的Module
  • auto-service依賴庫:
    為什么引入auto-service
    在使用注解處理器需要先聲明牲迫,步驟:
    1、需要在 processors 庫的 main 目錄下新建 resources 資源文件夾借卧;
    2盹憎、在 resources文件夾下建立 META-INF/services 目錄文件夾;
    3铐刘、在 META-INF/services 目錄文件夾下創(chuàng)建 javax.annotation.processing.Processor 文件陪每;
    4、在 javax.annotation.processing.Processor 文件寫入注解處理器的全稱镰吵,包括包路徑奶稠;)
    引入auto-service庫,使用@AutoService注解聲明就可以自動完成APT的聲明步驟捡遍。
  • javapoet square依賴庫:提供的生成Java文件開源庫 github地址 中文教程

自定義注解處理器
一般注解處理器邏輯:
1. 遍歷得到源碼中,需要解析的元素列表竹握。
2. 判斷元素是否可見和符合要求画株。
3. 組織數(shù)據(jù)結(jié)構(gòu)得到輸出類參數(shù)。
4. 輸入生成java文件啦辐。
5. 錯誤處理谓传。

自定義APT,需要了解一下Java Element

Processor處理過程中芹关,會掃描全部Java源碼续挟,代碼的每一個部分都是一個特定類型的Element,它們像是XML一層的層級機(jī)構(gòu)侥衬。

自定義BindView注解處理器:BinderProcessor類

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

    private Elements mElementUtils;
    private HashMap<String, BinderClassCreator> mCreatorMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //processingEnvironment.getElementUtils(); 處理Element的工具類诗祸,用于獲取程序的元素,例如包轴总、類直颅、方法。
        //processingEnvironment.getTypeUtils(); 處理TypeMirror的工具類怀樟,用于取類信息
        //processingEnvironment.getFiler(); 文件工具
        //processingEnvironment.getMessager(); 錯誤處理工具
        mElementUtils = processingEnv.getElementUtils();
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //大部分class而已getName功偿、getCanonicalNam這兩個方法沒有什么不同的。
        //但是對于array或內(nèi)部類等就不一樣了往堡。
        //getName返回的是[[Ljava.lang.String之類的表現(xiàn)形式械荷,
        //getCanonicalName返回的就是跟我們聲明類似的形式共耍。
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
        //因?yàn)榧嫒莸脑颍貏e是針對Android平臺吨瞎,建議使用重載getSupportedAnnotationTypes()方法替代默認(rèn)使用注解實(shí)現(xiàn)
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //掃描整個工程   找出含有BindView注解的元素
        Set<? extends Element> elements =
                roundEnvironment.getElementsAnnotatedWith(BindView.class);
        //遍歷元素
        for (Element element : elements) {
            //BindView限定了只能屬性使用痹兜,這里強(qiáng)轉(zhuǎn)為VariableElement
            VariableElement variableElement = (VariableElement) element;
            //返回此元素直接封裝(非嚴(yán)格意義上)的元素。
            //類或接口被認(rèn)為用于封裝它直接聲明的字段关拒、方法佃蚜、構(gòu)造方法和成員類型
            //這里就是獲取封裝屬性元素的類元素
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            //獲取簡單類名
            String fullClassName = classElement.getQualifiedName().toString();
            BinderClassCreator creator = mCreatorMap.get(fullClassName);
            if (creator == null) {
                creator = new BinderClassCreator(mElementUtils.getPackageOf(classElement),
                        classElement);
                mCreatorMap.put(fullClassName, creator);

            }
            //獲取元素注解
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            //注解值
            int id = bindAnnotation.value();
            creator.putElement(id, variableElement);
        }
        for (String key : mCreatorMap.keySet()) {
            BinderClassCreator binderClassCreator = mCreatorMap.get(key);
            //通過javapoet構(gòu)建生成Java類文件
            JavaFile javaFile = JavaFile.builder(binderClassCreator.getPackageName(),
                    binderClassCreator.generateJavaCode()).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return false;
    }
}

BinderClassCreator類 用于生成Java類代碼,具體代碼如下:

public class BinderClassCreator {

    public static final String ParamName = "view";

    private TypeElement mTypeElement;
    private String mPackageName;
    private String mBinderClassName;
    private Map<Integer, VariableElement> mVariableElements = new HashMap<>();

    /**
     * @param packageElement 包元素
     * @param classElement   類元素
     */
    public BinderClassCreator(PackageElement packageElement, TypeElement classElement) {
        this.mTypeElement = classElement;
        mPackageName = packageElement.getQualifiedName().toString();
        mBinderClassName = classElement.getSimpleName().toString() + "_ViewBinding";
    }

    public void putElement(int id, VariableElement variableElement) {
        mVariableElements.put(id, variableElement);
    }

    public TypeSpec generateJavaCode() {
        return TypeSpec.classBuilder(mBinderClassName)
                //public 修飾類
                .addModifiers(Modifier.PUBLIC)
                //添加類的方法
                .addMethod(generateMethod())
                //構(gòu)建Java類
                .build();

    }

    private MethodSpec generateMethod() {
        //獲取所有注解的類的類名
        ClassName className = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
        //構(gòu)建方法--方法名
        return MethodSpec.methodBuilder("bindView")
                //public方法
                .addModifiers(Modifier.PUBLIC)
                //返回void
                .returns(void.class)
                //方法傳參(參數(shù)全類名着绊,參數(shù)名)
                .addParameter(className, ParamName)
                //方法代碼
                .addCode(generateMethodCode())
                .build();
    }

    private String generateMethodCode() {
        StringBuilder code = new StringBuilder();
        for (int id : mVariableElements.keySet()) {
            VariableElement variableElement = mVariableElements.get(id);
            //使用注解的屬性的名稱
            String name = variableElement.getSimpleName().toString();
            //使用注解的屬性的類型
            String type = variableElement.asType().toString();
            //view.name = (type)view.findViewById(id)
            String findViewCode = ParamName + "." + name + "=(" + type + ")" + ParamName +
                    ".findViewById(" + id + ");\n";
            code.append(findViewCode);

        }
        return code.toString();
    }

    public String getPackageName() {
        return mPackageName;
    }
}

4. APT生成代碼使用

注解處理器完成了谐算,怎么使用呢?需要通過反射來調(diào)用注解處理器生成代碼
通過上面的注解處理器可知道归露,注解生成的類包名和使用注解的類的包名一致洲脂,類名為使用注解的類名__ViewBinding,方法名為bindView剧包,傳參為使用注解的類對象恐锦。所以可以如下通過反射調(diào)用

BinderViewTools類

public class BinderViewTools {
    public static void init(Activity activity) {
        Class clazz = activity.getClass();
        try {
            Class<?> bindClass = Class.forName(clazz.getName() + "_ViewBinding");
            Method bind = bindClass.getMethod("bindView", clazz);
            bind.invoke(bindClass.newInstance(),activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5. APP Module中使用注解

依賴:

  • gradle>=2.2 使用annotationProcessor替代android-apt依賴注解處理器并進(jìn)行工作
    (關(guān)于annotationProcessor替代android-apt:能夠輔助 Android Studio 在項(xiàng)目的對應(yīng)目錄中存放注解處理器在編譯期間生成的文件,且打包時(shí)不會包含處理器代碼)
    在Moudle的gradle中配置
annotationProcessor project(':apt_lib')
implementation project(':annotation')
  • gradle<2.2
    在project的gradle和Moudle的gradle中分別做如下配置
buildscript {
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'  
    }
}
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
    apt project(':apt_lib')
}

在Activity使用注解
MainActivity類

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.textView)
    TextView mTextView;

    @BindView(R.id.imageView)
    ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BinderViewTools.init(this);
        mTextView.setVisibility(View.VISIBLE);
        mImageView.setVisibility(View.VISIBLE);
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="獲取View成功"
        android:visibility="gone"/>

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/textView"
        android:layout_centerInParent="true"
        android:src="@mipmap/ic_launcher"
        android:visibility="gone"/>
</RelativeLayout>

兩個View在XML中默認(rèn)隱藏疆液,使用BindView注解一铅,然后調(diào)用BinderViewTools.init(this);方法調(diào)用注解處理器生成的代碼來獲取View,最后設(shè)置View顯示

注解處理器生成代碼 MainActivity_ViewBinding類

public class MainActivity_ViewBinding {
  public void bindView(MainActivity view) {
    view.mTextView=(android.widget.TextView)view.findViewById(2131165295);
    view.mImageView=(android.widget.ImageView)view.findViewById(2131165240);
  }
}

運(yùn)行效果


可以看到注解處理器正常生成代碼堕油,并且最終通過反射成功調(diào)用潘飘,View對象獲取成功。

參考文章
1.[Android] APT
2.Android注解快速入門和實(shí)用解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掉缺,一起剝皮案震驚了整個濱河市卜录,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌眶明,老刑警劉巖艰毒,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異搜囱,居然都是意外死亡丑瞧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門犬辰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗦篱,“玉大人,你說我怎么就攤上這事幌缝【拇伲” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長浴栽。 經(jīng)常有香客問我荒叼,道長,這世上最難降的妖魔是什么典鸡? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任被廓,我火速辦了婚禮,結(jié)果婚禮上萝玷,老公的妹妹穿的比我還像新娘嫁乘。我一直安慰自己,他們只是感情好球碉,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布蜓斧。 她就那樣靜靜地躺著,像睡著了一般睁冬。 火紅的嫁衣襯著肌膚如雪挎春。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天豆拨,我揣著相機(jī)與錄音直奋,去河邊找鬼。 笑死施禾,一個胖子當(dāng)著我的面吹牛脚线,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弥搞,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼殉挽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拓巧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤一死,失蹤者是張志新(化名)和其女友劉穎肛度,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體投慈,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡承耿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伪煤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片加袋。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖抱既,靈堂內(nèi)的尸體忽然破棺而出职烧,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布蚀之,位于F島的核電站蝗敢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏足删。R本人自食惡果不足惜寿谴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望失受。 院中可真熱鬧讶泰,春花似錦、人聲如沸拂到。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谆焊。三九已至惠桃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間辖试,已是汗流浹背辜王。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留罐孝,地道東北人呐馆。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像莲兢,于是被迫代替她去往敵國和親汹来。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容