Android編譯時注解,和重復代碼Say No!

寫在前面:

越來越多的Android框架都使用了注解來實現,如有名ButterKnife、Dagger2都是用編譯時注解來生成代碼,好處是比反射效率更高,穩定性、可讀性也更好。既然注解這么好用,那么就非常有必要對其進行了解、學習和應用。

學習注解過程中,查找了很多人分享的文章,非常感謝這些無私分享的人。其中參考了比較多的是這篇文章,本文中的例子也是參考該文章,并結合自己對注解的理解,重新寫了本文中的Demo,加入更詳細的注釋。

本文是本人在學習注解時,對注解的理解和一些基礎知識的記錄所寫,僅僅作為入門,分享給需要的小伙伴們??赡艽嬖谝恍┦杪┖湾e誤,歡迎指正~

一、Java注解基礎:

在Java中,一個自定義的注解看起來是類似下面這樣子的:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Factory {
    String value() default "";
}

該注解用于編譯時使用,生命周期由@Retention指定,@Taget表示該注解的使用范圍,這里用于注解類、接口、枚舉。

那么,@Retention和@Target是什么東東?

元注解:

元注解的作用就是負責注解其他非元注解。Java5.0定義了4個標準的meta-annotation類型,它們被用來提供對其它 Annotation類型作說明。

Java5.0定義的元注解:

  • @Target
  • @Retention
  • @Documented
  • @Inherited
1. @Target
作用:用于描述注解的使用范圍(即:被描述的注解可以用在什么地方)

取值(ElementType)有:

  • CONSTRUCTOR:用于描述構造器
  • FIELD:用于描述域
  • LOCAL_VARIABLE:用于描述局部變量
  • METHOD:用于描述方法
  • PACKAGE:用于描述包
  • PARAMETER:用于描述參數
  • TYPE:用于描述類、接口(包括注解類型) 或enum聲明
2. @Retention
作用:表示需要在什么級別保存該注釋信息,用于描述注解的生命周期(即:被描述的注解在什么范圍內有效)

取值(RetentionPoicy)有:

  • SOURCE:在源文件中有效(即源文件保留,只在源文件中,如@Override)
  • CLASS:在class文件中有效(即class保留,可在編譯時獲取,本文主講內容)
  • RUNTIME:在運行時有效(即運行時保留,可在運行是通過反射獲?。?/li>
3.@Documented:
@Documented用于描述其它類型的annotation應該被作為被標注的程序成員的公共API,  
因此可以被例如javadoc此類的工具文檔化。Documented是一個標記注解,沒有成員。
4.@Inherited:
@Inherited 元注解是一個標記注解,@Inherited闡述了某個被標注的類型是被繼承的。  
如果一個使用了@Inherited修飾的annotation類型被用于一個class,則這個annotation將被用于該class的子類。  
使用Inherited聲明出來的注解,只有在類上使用時才會有效,對方法,屬性等其他無效。

自定義注解

格式:public @interface 注解名 {定義體}
注解參數的可支持數據類型:
  1. 所有基本數據類型(int,float,boolean,byte,double,char,long,short)
  2. String類型
  3. Class類型
  4. enum類型
  5. Annotation類型
  6. 以上所有類型的數組

參數職能用public或默認(default)修飾

如果只有一個參數成員,最好把參數名稱設為"value",后加小括號,即value()

二、在Android中應用編譯時注解,自動生成工廠代碼

首先以工廠模式為例,看看在工廠模式中存在的問題。本例假設為水果工廠。

1.通常,在工廠模式中,我們會定義一個工廠生產接口方法:

public interface IFruit {
    void produce();
}

2.接著,定義具體的工廠生產線類:

public class Apple implements IFruit {
    @Override
    public void produce() {
        Log.d("AnnotationDemo", "生產蘋果");
    }
}

public class Pear implements IFruit {
    @Override
    public void produce() {
        Log.d("AnnotationDemo", "生產梨子");
    }
}

3.然后,定義生產工廠類:

public class FruitFactory {
    public static IFruit create(int id) {
        if (1 == id) {
            return new Apple();
        }
        if (2 == id) {
            return new Pear();
        }
        
        return null;
    }
}

4.最后,使用工廠:

public void produceFruit() {
    FruitFactory.create(1).produce();
    FruitFactory.create(2).produce();
}
  • 存在問題:

    在以上例子中,每次新增生產線的時候,都需要先定義一個生產線,然后在FruitFactory的create方法中新增判斷,返回新的生產線類,并且每次添加的代碼都是非常相似重復的。

    為此,“懶惰”的我們肯定會想,是否有方法可以做到:只要我定義好一個生產線類后,無需手動地在工廠類中添加,就馬上可以使用?

    答案是肯定的,Java的注解處理器(AbstractProcessor)就可以幫助我們實現以上需求。

接下來,我們就一步步來實現這個可以讓我們懶出新境界的功能:

1. 新建Android工程和Java Module

注意:由于Android默認不支持部分javax包的內容,所以我們需要將注解解析相關的類放到Java Module中才能調用到。

  • 建立好Android工程 AnnotationDemo
  • 新建annotator Module :Filw -> New -> New Module -> Java Library 并命名為annotator

2. 配置APT(Annotation Processor Tool)工具。

由于android-apt已經不再維護,并且Android官方在Gradle2.2以上已經提供了另一個工具annotationProcessor替代了原來的android-apt,所以我們直接使用annotationProcessor。
Gradle2.2以下版本配置請看最后。

在app的build.gradle中添加如下依賴:

dependencies {
    ......
    
    compile project(':annotator')
    annotationProcessor project(':annotator')
}

3. 碼注解處理器

以上配置完成后,就可以開始碼注解處理器了。

1)首先,自定義一個注解,用于標識生產線類,該注解包含兩個參數:

  • 一個生產線類id數組ids,可多個id對應一個類
  • 另一個是該生產類的接口父類,用于標識生產線類的接口父類
@Retention(RetentionPolicy.CLASS) //該注解只保留到編譯時
@Target(ElementType.TYPE) //該注解只作用與類、接口、枚舉
public @interface Factory {
    /**
     * 工廠對應的ID,可以多個ID對應一個生產線類
     */
    int[] ids();

    /**
     * 生產接口類
     */
    Class superClass();
}

2)使用以上注解標記生產線類

@Factory(ids = {1}, superClass = IFruit.class)
public class Apple implements IFruit {
    @Override
    public void produce() {
        Log.d("AnnotationDemo", "生成蘋果");
    }
}

@Factory(ids = {2,3}, superClass = IFruit.class)
public class Pear implements IFruit {
    @Override
    public void produce() {
        Log.d("AnnotationDemo", "生成梨子");
    }
}

以上Pear類上,我們使用了Factory注解標記,其中參數ids有兩個id,即使用2或者3都可以獲取到Pear;superClass為生產接口類。

3)編寫注解解析器

  • i. 首先,定義一個注解屬性類,用于保存獲取到的每個生產線類相關的屬性
public class FactoryAnnotatedCls {
    private TypeElement mAnnotatedClsElement; //被注解類元素

    private String mSupperClsQualifiedName; //被注解的類的父類的完全限定名稱(即類的絕對路徑)

    private String mSupperClsSimpleName; //被注解類的父類類名

    private int[] mIds; //被注解的類的對應的ID數組


    public FactoryAnnotatedCls(TypeElement classElement) throws ProcessingException {
        this.mAnnotatedClsElement = classElement;
        Factory annotation = classElement.getAnnotation(Factory.class);
        mIds = annotation.ids();
        try {
            //直接獲取Factory中的supperClass參數的類名和完全限定名字,如果是源碼上的注解,會拋異常
            mSupperClsSimpleName = annotation.superClass().getSimpleName();
            mSupperClsQualifiedName = annotation.superClass().getCanonicalName();
        } catch (MirroredTypeException mte) {
            //如果獲取異常,通過mte可以獲取到上面無法解析的superClass元素
            DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
            TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
            mSupperClsQualifiedName = classTypeElement.getQualifiedName().toString();
            mSupperClsSimpleName = classTypeElement.getSimpleName().toString();
        }

        if (mIds == null || mIds.length == 0) { //判斷是否存在ID,不存在則拋出異常
            throw new ProcessingException(classElement,
                    "id() in @%s for class %s is null or empty! that's not allowed",
                    Factory.class.getSimpleName(), classElement.getQualifiedName().toString());
        }

        if (mSupperClsSimpleName == null || mSupperClsSimpleName == "") { //判斷是否存在父類接口,不存在拋出異常
            throw new ProcessingException(classElement,
                    "superClass() in @%s for class %s is null or empty! that's not allowed",
                    Factory.class.getSimpleName(), classElement.getQualifiedName().toString());
        }
    }

    public int[] getIds() {
        return mIds;
    }

    public String getSupperClsQualifiedName() {
        return mSupperClsQualifiedName;
    }

    public String getSupperClsSimpleName() {
        return mSupperClsSimpleName;
    }

    public TypeElement getAnnotatedClsElement() {
        return mAnnotatedClsElement;
    }
}

其中,有個類為TypeElement,該類繼承Element。程序編譯時,IDE掃描文件所有的屬性都可以被看作元素。繼承自Element的子類共有四個,分別為:

  • TypeElement (類屬性元素,對應一個類)
  • PackageElement (包元素,對應一個包)
  • VariableElement (變量元素,對應變量)
  • ExecuteableElement (方法元素,對應函數方法)

在這里,定義的注解目標是Type,因此為TypeElement。FactoryAnnotatedCls類將被Factory注解的類中的必要屬性都保存下來,用于后面生成代碼。

  • ii. 接下來,是解析注解代碼的關鍵類:注解處理器

所有在編譯時處理注解的程序,都需要定義一個注解處理器,繼承自AbstractProcessor。

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

    private Types mTypeUtil;
    private Elements mElementUtil;
    private Filer mFiler;
    private Messager mMessager;

    private FactoryCodeBuilder mFactoryCodeBuilder = new FactoryCodeBuilder();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mTypeUtil = processingEnvironment.getTypeUtils();
        mElementUtil = processingEnvironment.getElementUtils();
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
    }
    
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Factory.class.getCanonicalName());
        return annotations;
    }

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

    ......
}

其中,

getSupportedAnnotationTypes()配置需要處理的注解,這里只處理@Factory注解;
getSupportedSourceVersion()配置支持的Java版本

init()方法中,獲取了幾個即將用到的工具:
mTypeUtil--主要用于獲取類
mElementUtil--主要用于解析各種元素
mFiler--用于寫文件,生成代碼
mMessager--用于在控制臺輸出信息

另外,在第一個行代碼中,有一個注解AutoService(Processor.class)。這個注解的作用是可以自動生成javax.annotation.processing.Processor文件。該文件位于"build/classes/main/com/META-INF/services/"中。

文件中只有一句話,配置了注解處理器的完全限定名。

com.factorybuilder.FactoryProcesser

當然,需要在annotator Module的build.gradle添加依賴才能使用AutoService注解。

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

注:只有在該文件配置了的注解處理器,在編譯時才會被調用。

完成以上配置后,就可以進入注解的解析和處理了。在編譯時,編譯器將自動調用注解處理器的process方法。如下:

@AutoService(Processor.class)
public class FactoryProcesser extends AbstractProcessor {
    ......
    
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(Factory.class)) { //遍歷所有被Factory注解的元素
                if (annotatedElement.getKind() != ElementKind.CLASS) { //判斷是否為類,如果不是class,拋出異常
                    error(annotatedElement,
                          String.format("Only class can be annotated with @%s",
                                Factory.class.getSimpleName()));
                }

                TypeElement typeElement = (TypeElement) annotatedElement; //將元素轉換為TypeElement(因為在上面的代碼中,已經判斷了元素為class類型)
                FactoryAnnotatedCls annotatedCls = new FactoryAnnotatedCls(typeElement); //接著將該元素保存到先前定義的類中
                supperClsPath = annotatedCls.getSupperClsQualifiedName().toString(); //獲取元素的父類路徑(在這里為IFruit)

                checkValidClass(annotatedCls);//檢查元素是否符合規則

                mFactoryCodeBuilder.add(annotatedCls); //將元素壓入列表中,等待最后用于生成工廠代碼
        }
    
        if (supperClsPath != null && !supperClsPath.equals("")) { //檢查是否有父類路徑
            mFactoryCodeBuilder
            .setSupperClsName(supperClsPath)
            .generateCode(mMessager, mElementUtil, mFiler); //開始生成代碼
        }
    
        return true; //return true表示處理完畢
    }
}

在process方法中,
首先,遍歷了所有被Factory標記的元素;
然后,對每一個元素進行檢查,如果為class類型,并且符合指定的規則,統統壓入FactoryCodeBuilder的列表中;
最后,如果所有的元素都符合規則,調用factoryCodeBuilderd的generateCode生成代碼。

  • iii. 最后,來看看FacrotyCodeBuilder都做了些什么
public class FactoryCodeBuilder {

    private static final String SUFFIX = "Factory";

    private String mSupperClsName;

    private Map<String, FactoryAnnotatedCls> mAnnotatedClasses = new LinkedHashMap<>();

    public void add(FactoryAnnotatedCls annotatedCls) {
        if (mAnnotatedClasses.get(annotatedCls.getAnnotatedClsElement().getQualifiedName().toString()) != null)
            return ;

        mAnnotatedClasses.put(
                annotatedCls.getAnnotatedClsElement().getQualifiedName().toString(),
                annotatedCls);
    }

    public void clear() {
        mAnnotatedClasses.clear();
    }
    
    ......
}

代碼生成器中定義了一個哈希列表,用于保存所有遍歷到的符合規則的元素。

public class FactoryCodeBuilder {

    ......


    public FactoryCodeBuilder setSupperClsName(String supperClsName) {
        mSupperClsName = supperClsName; //設置上產線接口父類的路徑
        return this;
    }

    public void generateCode(Messager messager, Elements elementUtils, Filer filer) throws IOException {
        TypeElement superClassName = elementUtils.getTypeElement(mSupperClsName); //通過Elements工具獲取父類元素
        String factoryClassName = superClassName.getSimpleName() + SUFFIX; //然后設置即將生成的工廠類的名字(在這里為IFruitFactory)
        PackageElement pkg = elementUtils.getPackageOf(superClassName); //通過Elements工具,獲取父類所在包名路徑(在這里為annotation.demo.factorys)
        String packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName().toString(); //獲取即將生成的工廠類的包名

        TypeSpec typeSpec = TypeSpec
                .classBuilder(factoryClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(newCreateMethod(elementUtils, superClassName))
                .addMethod(newCompareIdMethod())
                .build();

        // Write file
        JavaFile.builder(packageName, typeSpec).build().writeTo(filer);
    }
    
    ......
}

在generateCode方法中,獲取了生產線父類的名稱和包名,以及為即將生成的工廠類設置了包名和類名。

然后借助了一個非常厲害的工具JavaPoet。這個工具是由square公司提供的,用于優雅地生成Java代碼,如其名字“會寫Java的詩人”。
在annotator build.gradle中添加依賴:

compile 'com.squareup:javapoet:1.7.0'

簡單介紹一下JavaPoet的用法:

  • TypeSpec用于創建類、接口或者枚舉
    調用classBuilder設置類名;
    調用addModifiers可以設置類的屬性類型,public static final等,可以同時添加多個屬性
    調用addMethod可以在類中添加一個函數方法
  • JavaFile將創建的類寫入文件中
  • MethodSpec接下來即將用到的,用于創建函數方法,其使用參考下面代碼注釋

更詳細用法請自行google,有很多的文章可以查閱。

本例中,給工廠類生成了兩個方法分別為

public static IFruit create(int id)
private static compareId(int[] ids, id)

具體代碼如下:

public class FactoryCodeBuilder {

    ......
    
    private MethodSpec newCreateMethod(Elements elementUtils, TypeElement superClassName) {

        MethodSpec.Builder method =
                MethodSpec.methodBuilder("create") //設置方法名字
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC) //設置方法類型為public static
                .addParameter(int.class, "id") //設置參數int id
                .returns(TypeName.get(superClassName.asType())); //設置返回IFruit

        method.beginControlFlow("if (id < 0)") //beginControlFlow與endControlFlow要成對調用
                .addStatement("throw new IllegalArgumentException($S)", "id is less then 0!")
                .endControlFlow();

        for (FactoryAnnotatedCls annotatedCls : mAnnotatedClasses.values()) { //遍歷所有保存起來的被注解的生產線類
            String packName = elementUtils
                    .getPackageOf(annotatedCls.getAnnotatedClsElement())
                    .getQualifiedName().toString(); //獲取生產線類的包名全路徑
            String clsName = annotatedCls.getAnnotatedClsElement().getSimpleName().toString(); //獲取生產線類名字
            ClassName cls = ClassName.get(packName, clsName); //組裝成一個ClassName

            //將該生產線類的所有id組成數組
            int[] ids = annotatedCls.getIds();
            String allId = "{";
            for (int id : ids) {
                allId = allId + (allId.equals("{")? "":",") + id;
            }
            allId+="}";

            method.beginControlFlow("if (compareId(new int[]$L, id))", allId) //開始一個控制流,判斷該生產線類是否包含了指定的id
                    .addStatement("return new $T()", cls)   // $T 替換類名,可以自動import對應的類。還有以下占位符:
                                                            // $N 用于方法名或者變量名替換,也可用于類名,但是不會自動生成import;
                                                            // $L 字面量替換,如上面if中allId的值替換;;
                                                            // $S 為替換成String
                    .endControlFlow();
        }

        method.addStatement("throw new IllegalArgumentException($S + id)", "Unknown id = ");

        return method.build();
    }

    private MethodSpec newCompareIdMethod() {
        MethodSpec.Builder builder = MethodSpec.methodBuilder("compareId") //設置函數方法名字
                .addModifiers(Modifier.PRIVATE, Modifier.STATIC) //設置方法類型為private static
                .addParameter(int[].class, "ids") //設置參數int[] ids
                .addParameter(int.class, "id") //設置參數int id
                .returns(TypeName.BOOLEAN); //設置返回類型

        builder.beginControlFlow("for (int i : ids)") //開始一個控制流
                .beginControlFlow("if (i == id)") //在以上for循環中加入一個if控制流
                .addStatement("return true") //添加一行代碼,最后會自動添加分號";"
                .endControlFlow() //結束一個控制流,add和end要成對調用。這里對應if的控制流
                .endControlFlow() //結束for控制流
                .addStatement("return false"); //按添加返回

        return builder.build();
    }
}

以上代碼創建了兩個方法,一個對外的create方法和內部使用的compareId方法。

  • 在newCreateMethod中,首先創建了create(int id)方法,然后在里面用for循環遍歷所有的生產線類,并生成了對應的判斷和返回,最終生成類似如下代碼:
public static IFruit create(int id) {
    if(compareId(new int[]{1},id)) {
        return new Apple();
    }
    if(compareId(new int[]{2,3},id)) {
        return new Pear();
    }
}
  • 在newCompareIdMethod中,生成了compareId方法,并生了判斷輸入id與生產線ID匹配的方法,生成類似如下代碼:
private static boolean compareId(int[] ids, int id) {
    for (int i : ids) {
      if (i == id) {
        return true;
      }
    }
    return false;
  }

至此,一個自動生成工廠類的注解工具就封裝完成了。當然,在執行process過程中,還會對元素做一些判斷,具體就不做介紹了,需要可以直接看源碼。

如何使用該工具呢?如新增一個Orange生產線類型。

在app Mudule中的新建Orange如下:

@Facroty(ids = {5}, superClass = IFruit.class)
public class Orange implement IFruit {
    @Override
    public void produce () {
        Log.d("AnnotationDemo", "生成橙子");
    }
}

Build一下工程,就可以直接使用了,簡直不能再爽,哈哈哈~

private void produceFruit() {
    IFruitFactory.create(5).produce();
}

最后,看下自動生成的工廠類,跟手寫的基本是一樣的(該類位于app/build/generated/source/apt/debug/接口父類包名):

package annotation.demo.factorys;

public class IFruitFactory {
  public static IFruit create(int id) {
    if (id < 0) {
      throw new IllegalArgumentException("id is less then 0!");
    }
    if (compareId(new int[]{1}, id)) {
      return new Apple();
    }
    if (compareId(new int[]{4,5}, id)) {
      return new Orange();
    }
    if (compareId(new int[]{2,3}, id)) {
      return new Pear();
    }
    if (compareId(new int[]{6}, id)) {
      return new Persimmon();
    }
    throw new IllegalArgumentException("Unknown id = " + id);
  }

  private static boolean compareId(int[] ids, int id) {
    for (int i : ids) {
      if (i == id) {
        return true;
      }
    }
    return false;
  }
}

以上代碼中為了方便講解省略了一些判斷和異常處理,具體可以查看源碼。

------------------------------------==正文End : ) 我是分割線==----------------------------------

gradle2.2以下版本配置

  • 由于Android不完全支持Java8,可能會導致編譯報錯,所以設置Java版本為Java7。

    1)在app的build.gradle的android標簽中添加如下配置

    compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_7
       targetCompatibility JavaVersion.VERSION_1_7
    }
    

    2)在annotator的build.gradle中配置

    sourceCompatibility = 1.7
    targetCompatibility = 1.7
    
  • 配置APT

    1)在項目的build.gradle dependencies添加apt插件:

    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        // apt
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
    

    2)在app build.gradle最上面添加

    apply plugin: 'com.neenbedankt.android-apt'
    
  • 配置annotator build.gradle依賴在dependencies中添加依賴

    dependencies {
        ......
        compile project(':annotator')
        apt project(':annotator')
    }
    
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容