1. 程式人生 > >深入理解Java註解型別

深入理解Java註解型別

Java註解在實際應用中很廣泛,目前很多主流的框架也採用了註解來提高效率,其實註解就是Java程式碼中的一個標記,也可以將它理解為物件,它有自己的相關屬性和值,只是不實現相關方法而已。下面我們通過一個例子來分析一下註解。

public class Test {

    //新增自定義註解
    @FunAnno(name="我是方法a")
    public void fun_a(){
        LogUtils.d("執行方法a");
    }

    //新增java內建的註解
    @Deprecated
    @SuppressWarnings("uncheck")
    public void fun_b(){

    }


    /**
     * 定義一個註解
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FunAnno{
        String name() default "";
    }
}

上面程式碼中我們自定義了一個註解@FunAnno,註解用@interface來宣告,@Target(ElementType.METHOD)代表此註解應用在方法上,@Retention(RetentionPolicy.RUNTIME)代表此註解的生命週期儲存到執行時,String name() default ""用來宣告一個String型別的註解元素,我們可以在使用註解的時候對其進行賦值,當然在宣告的時候要用default定義其初始值。然後我們在fun_a方法中使用了@FunAnno註解。@Deprecated和@SuppressWarnings(“uncheck”)都是Java內建的註解,下面我們將會介紹它們的意義。

一:註解的語法

在上面例子中,我們定義註解的時候用到了@Target和@Retention註解,其實這兩個是Java提供的元註解,所謂的元註解就是標記其他註解的註解,我們常見的元註解有:@Target、@Retention、@Documented、@Inherited、@Repeatable(java8新增)。

1. @Target

用來約束註解應用的地方(比如方法、類、欄位等等),其註解元素接收一個列舉陣列ElementType[],代表註解應用範圍

//Target註解原始碼
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

//value範圍
public enum ElementType {

   /**標明該註解可以用於類、介面(包括註解型別)或enum宣告*/
    TYPE,

    /** 標明該註解可以用於欄位(域)宣告,包括enum例項 */
    FIELD,

    /** 標明該註解可以用於方法宣告 */
    METHOD,

    /** 標明該註解可以用於引數宣告 */
    PARAMETER,

    /** 標明註解可以用於建構函式宣告 */
    CONSTRUCTOR,

    /** 標明註解可以用於區域性變數宣告 */
    LOCAL_VARIABLE,

    /** 標明註解可以用於註解宣告(應用於另一個註解上)*/
    ANNOTATION_TYPE,

    /** 標明註解可以用於包宣告 */
    PACKAGE,

    /**
     * 標明註解可以用於型別引數宣告(1.8新加入)
     */
    TYPE_PARAMETER,

    /**
     * 型別使用宣告(1.8新加入)
     */
    TYPE_USE

}

當Target未表明任何value時,代表此註解可以應用到任何元素上,還可以採用陣列的方式表明value值,例如:@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})表明註解可以應用的範圍是{}內的所有型別。

[email protected]

該註解用來約束註解的生命週期,其接收三個值,分別是SOURCE(原始碼級別)、CLASS(類檔案級別)、RUNTIME(執行時級別),詳解如下:

  • SOURCE:該型別的註解資訊只會保留在原始碼裡,原始碼經過編譯後,註解資訊會被丟棄,不會保留在編譯好的class檔案裡。

  • CLASS:註解在class檔案中可用,但會被VM丟棄(該型別的註解資訊會保留在原始碼裡和class檔案裡,在執行的時候,不會載入到虛擬機器中),請注意,當註解未定義Retention值時,預設值是CLASS,如Java內建註解@SuppressWarnning等。

  • RUNTIME:註解資訊將在執行期(JVM)也保留,因此可以通過反射機制讀取註解的資訊(原始碼、class檔案和執行的時候都有註解的資訊)。

[email protected]

@Documented 可以讓被修飾的註解上傳javadoc

[email protected]

可以讓註解被繼承,這裡繼承的意思是通過使用@Inherited,可以讓子類Class物件使用getAnnotations()獲取父類被@Inherited修飾的註解,如下:

public class InheritedTest {


    /**
     * 被Inherited修飾的註解
     */
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface AnnoA {

    }

    /**
     * 沒有Inherited修飾的註解
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface AnnoB {

    }

    @AnnoA
    class A{
    }

    class B extends A{

    }

    @AnnoB
    class C{

    }

    class D extends C{

    }

    public  void test(){
//        A instanceA=new A();
//        D instanceD=new D();

        LogUtils.d("使用了Inherited註解的情況:"+ Arrays.toString(B.class.getAnnotations()));
        LogUtils.d("沒有使用了Inherited註解的情況:"+ Arrays.toString(D.class.getAnnotations()));
    }

}

執行結果如下:

上面我們講解了註解的基本語法結構和常用的元註解分析,下面我們看看註解的元素值及其資料結構型別

二:註解元素及其資料型別

上面我們看到在用@FunAnno註解的時候,傳進了一個值name=“我是方法a”,這個name就是註解元素,其資料型別為String。我們在定義註解的時候,常常都會包含一些元素及其值,方便註解處理器使用。如下:

/**
 * 使用註解標記Person類,給註解元素賦值
 */
@Person.PersonInfo(name = "王大錘",age = 18,male = true)
public class Person {


    /**
     * 定義一個註解,包含有三個註解元素,分別代表名字、年齡、性別
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface PersonInfo{
        String name() default "";
        int age() default -1;
        boolean male() default false;
    }

    /**
     * 註解元素的簡化使用方式,只定義value()即可
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Age{
        int value() default -1;
    }


    /**
     * 註解的簡化使用
     */
    @Age(20)
    class Lily{
        
    }
}

上面程式碼我們展示了註解元素的定義及其使用,注意簡化方式的使用,如果簡化方式中不止一個元素的話, 還是要使用value=“”的方式去賦值元素的。下面是註解元素支援的資料型別:

  • 所有基本型別(int,float,boolean,byte,double,char,long,short)

  • String

  • Class

  • enum

  • Annotation 註解巢狀

  • 上述型別的陣列

下面我們用例子來展示所有註解元素資料型別的使用:

public class AnnoElement {

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface AnnoRef{
        int value() default -1;
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface AnnoElements{

        //定義性別列舉
        enum Gender{MALE,FEMALE};

        //宣告列舉
        Gender gender() default Gender.FEMALE;

        //宣告string
        String name() default "";

        //宣告int
        int age() default -1;

        //宣告class型別
        Class<?> Person() default Void.class;

        //註解巢狀
        AnnoRef ref() default @AnnoRef;

        //陣列型別
        String[] strs() default {""};
    }


    /**
     * 註解元素的使用
     */
    @AnnoElements(gender = AnnoElements.Gender.MALE,name = "java",
            age = 100,Person = Person.class,ref = @AnnoRef(10),strs = {"a","b"})
    class Test{
    }
}

註解元素對預設值有嚴格的要求,物件不能用null表示,所以一般我們都用一些沒有意義的值來表示預設值。像這些註解元素及其對應的值,我們的註解處理器是可以獲取的,從而可以作為我們後面相關處理的依據。

 

三:Java內建註解

Java內建註解主要有三個,我們分別來看一下:

  • @Override:用於標明此方法覆蓋了父類的方法,原始碼如下
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
  • Deprecated:用於標明已經過時的方法或類,原始碼如下
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
  • @SuppressWarnnings:用於有選擇的關閉編譯器對類、方法、成員變數、變數初始化的警告,其實現原始碼如下:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

其中value值接收如下:

deprecation:使用了不贊成使用的類或方法時的警告;
unchecked:執行了未檢查的轉換時的警告,例如當使用集合時沒有用泛型 (Generics) 來指定集合儲存的型別; 
fallthrough:當 Switch 程式塊直接通往下一種情況而沒有 Break 時的警告;
path:在類路徑、原始檔路徑等中有不存在的路徑時的警告; 
serial:當在可序列化的類上缺少 serialVersionUID 定義時的警告; 
finally:任何 finally 子句不能正常完成時的警告; 
all:關於以上所有情況的警告。
 

四:註解與反射的關係

在反射包的相關類中實現了都實現了AnnotatedElement介面,通過此介面我們可以利用反射技術獲得對應類的相關注解資訊,反射包的Constructor類、Field類、Method類、Package類和Class類都實現了AnnotatedElement介面,下面我們來看看AnnotatedElement中的相關方法:

方法名稱 返回值 說明
getAnnotation(Class<A> annotationClass) <A extends Annotation> 如果此元素存在指定型別的註解,那麼返回這些註解,否則返回null
getAnnotations() Annotation[] 返回此元素的所有註解,包括從父類繼承的
isAnnotationPresent(Class<? extends Annotation> annotationClass) boolean 如果指定型別的註解存在次元素上,則返回true,否則返回false

getDeclaredAnnotations()

Annotation[] 返回次元素上的所有註解,不包括從父類繼承的註解

案例演示如下:

public class AnnoElementTest{

    @AnnoElementTest.AnnoA
    public static class A{

    }

    //B 繼承A
    @AnnoElementTest.AnnoB
    public static class B extends A{

    }

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface AnnoA {

    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface AnnoB {

    }

    public static void test(){
        B instanceB=new B();
        Class<?> classB=instanceB.getClass();

        //根據指定註解型別獲得註解
        AnnoB annoB=classB.getAnnotation(AnnoB.class);
        LogUtils.d("根據指定註解獲取註解:"+annoB);

        //獲取此元素上的所有註解,包括從父類繼承的
        Annotation[] annotations=classB.getAnnotations();
        LogUtils.d("獲取所有註解包括繼承的:"+ Arrays.toString(annotations));

        //獲取此元素上所有註解,不包括繼承的
        Annotation[] annotations1=classB.getDeclaredAnnotations();
        LogUtils.d("獲取所有註解不包括繼承的:"+Arrays.toString(annotations1));

        //判斷註解AnnoA是否在次元素上
        boolean is=classB.isAnnotationPresent(AnnoB.class);
        LogUtils.d("是否="+is);
    }
}

執行結果如下:

在這裡要獲取到父類繼承的註解,此註解上必須有@Inherited標記,否則獲取不到。

 

五:執行時註解和編譯時註解

執行時註解,在執行時拿到類的Class物件,然後遍歷其方法、變數,判斷有無註解,然後做一些操作。

編譯時註解,在java的編譯階段,根據註解標識,動態生成一些類或xml檔案,在執行時期,這些註解是沒有的,我們是依靠動態生成的一些類做操作,由於沒有反射, 效率和直接呼叫方法沒什麼區別。

所以我們可以看到,編譯時註解的效率要比執行時註解高,很多框架如ButterKnife、EventBus都是用的編譯時註解。下面我們通過例子去看看,執行時註解和編譯時註解是怎麼的一個實現過程。

1.執行時註解的處理

我們以通過執行時註解去建立一個數據庫表為例展開, 先來看看註解的定義,如下:

/**
 * Created by XR_liu on 2018/11/22.
 * 執行時註解
 */
public class RAnnotation {


    /**
     * 約束註解
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Contrains{

        //是否為主鍵
        boolean primaryKey() default false;

        //是否允許為null
        boolean allowNull() default false;

        //是否唯一
        boolean isUnique() default false;
    }

    /**
     * 實體類註解
     */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Entity{
        String tableName() default "";
    }

    /**
     * 整型欄位
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FeildInteger{
        //對應的欄位名
        String name() default "";
        //巢狀註解
        Contrains contrain() default @Contrains;
    }

    /**
     * 字串欄位
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FeildString{

        String name() default "";

        int varchar() default 30;

        Contrains contrain() default @Contrains;
    }

}

我們定義的上面幾個註解,都是為建立一個數據庫表服務的, 注意生命週期都應該標註為RUNTIME,只有這樣我們才能在執行時通過反射獲取相關資訊。下面通過定義一個Person類來看註解的使用:

/**
 * Created by XR_liu on 2018/11/22.
 */
@RAnnotation.Entity
public class Person {

    //主鍵
    @RAnnotation.FeildString(contrain = @RAnnotation.Contrains(primaryKey = true))
    private String id;

    @RAnnotation.FeildString
    private String name;

    @RAnnotation.FeildInteger
    private int age;

    //允許為null
    @RAnnotation.FeildString(contrain = @RAnnotation.Contrains(allowNull = true))
    private String address;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

下面我們就開始自己編寫一個註解處理器,關鍵邏輯是通過Class物件去獲取有註解的欄位,然後再獲取對應註解的相關資訊,然後就可以根據這些資訊去拼接一個SQL語句了,如下:

/**
     * 通過一個實體類返回一個建立資料庫表的SQL語句
     *
     * @return
     */
    public static String createTableSql(Class<?> cl) {
        String sql = null;
        //獲取class物件
        Class<?> clazz = cl;

        RAnnotation.Entity entity = clazz.getAnnotation(RAnnotation.Entity.class);

        String tabelName; //資料庫名稱
        if (entity == null || entity.tableName() == "") {
            tabelName = clazz.getSimpleName().toUpperCase();
        }
        tabelName = entity.tableName();

        //所有欄位名的集合
        List<String> columNames = new ArrayList<>();

        //通過反射獲取class的所有欄位
        for (Field field : clazz.getDeclaredFields()) {
            String columName = null;
            //獲取欄位上的所有註解
            Annotation[] annotations = field.getDeclaredAnnotations();
            if (annotations.length < 1) {
                continue; //不屬於表的欄位
            }
            //整型欄位
            if (annotations[0] instanceof RAnnotation.FeildInteger) {
                RAnnotation.FeildInteger feildInteger = (RAnnotation.FeildInteger) annotations[0];
                //給欄位名賦值
                columName = "".equals(feildInteger.name()) ? field.getName().toUpperCase() : feildInteger.name();
                //儲存構建SQL語句片段
                columNames.add(columName + " INT" + getContrains(feildInteger.contrain()));
            }

            //string欄位
            if (annotations[0] instanceof RAnnotation.FeildString) {
                RAnnotation.FeildString feildString = (RAnnotation.FeildString) annotations[0];
                columName = "".equals(feildString.name()) ? field.getName().toUpperCase() : feildString.name();
                columNames.add(columName + " VARCHAR(" + feildString.varchar() + ")" + getContrains(feildString.contrain()));
            }

            //構建資料庫表語句
            StringBuilder createSql = new StringBuilder("CREATE TABLE " + tabelName + "(");

            for (String colum : columNames) {
                createSql.append("\n " + colum + ",");
            }
            sql = createSql.substring(0, createSql.length() - 1) + ")";

        }
        return sql;

我們來看一下執行結果:

到這裡成功地返回了一個正確的SQL語句!附上原始碼:https://github.com/jiusetian/AndroidStudyData/tree/master/app/src/main/java/com/androidstudydata/annotation

上面是執行時註解的例子,其中關鍵的是利用Class物件去尋找註解的相關資訊,下面我們來看看編譯時註解又是怎麼回事的。

2.編譯時註解

編譯時註解主要利用的註解處理器(Annotation Processor)是javac內建的一個用於編譯時掃描和處理註解的工具,就是在原始碼的編譯階段,通過註解處理器,我們可以獲得檔案中註解的相關內容。常見的用途是,我們在獲得註解相關資料之後,通過去生成有規律的程式碼,解決程式設計過程中的重複工作,大大提高了效率,比如ButterKnife、Dagger2等框架。

下面我們模仿butterknife的實現原理,自己去實現一個類似功能的簡單例子,首先我們在註解實現的Java Module的Gradle檔案中新增如下配置:

    compile 'com.google.auto.service:auto-service:1.0-rc2'//谷歌的幫助我們快速實現註解處理器
    compile 'com.squareup:javapoet:1.7.0'//用來生成java檔案的

定義一個註解@BindView,如下:

/**
 * Created by XR_liu on 2018/11/24.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value() default -1;
}

當然註解的生命週期是編譯階段,適用範圍為欄位。定義一個注入view例項公共介面,如下

/**
 * Created by XR_liu on 2018/11/24.
 */
public interface ViewInject<T> {
    //T是指使用註解的類,viewOwner是註解view的持有者
    void inject(T t, Object viewOwner);
}

下面我們來看看怎麼去實現這個註解處理器的,這個是關鍵,先看看程式碼:

/**
 * Created by XR_liu on 2018/11/24.
 */
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor{

    private Elements elementUtils;
    private Map<String, CodeCreater> codeCreaterMap = new HashMap<String, CodeCreater>();


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

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv)
    {
        super.init(processingEnv);

        //Elements mElementUtils;跟元素相關的輔助類,幫助我們去獲取一些元素相關的資訊。
        elementUtils = processingEnv.getElementUtils();
    }


    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        codeCreaterMap.clear();

        //element所代表的元素只在編譯期可見,用於儲存元素在編譯期的各種狀態,而Type所代表的元素是執行期可見,用於儲存元素在編譯期的各種狀態
        //通過roundEnv.getElementsAnnotatedWith拿到我們通過@BindView註解的元素,這裡返回值,按照我們的預期應該是VariableElement集合,因為我們用於成員變數上。
        Set<? extends Element> elesWithBind = roundEnv.getElementsAnnotatedWith(BindView.class); //所有被Bind註解標識的元素,此時的Element是一個通用介面
        //一、收集資訊

        //接下來for迴圈我們的元素,首先檢查型別是否是VariableElement(代表變數、欄位)
        for (Element element : elesWithBind)
        {
            //檢查element型別
            checkAnnotationValid(element, BindView.class);
            //field type
            VariableElement variableElement = (VariableElement) element; //因為上面檢查過了,所以這裡強轉
            //class type,拿到對應的類元素
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            //full class name,類的全路徑名
            String fqClassName = classElement.getQualifiedName().toString();

            //如果沒有生成才會去生成一個新的,。
            CodeCreater codeCreater = codeCreaterMap.get(fqClassName);
            if (codeCreater == null)
            {
                //如果對應的類還沒有生成代理類,才去建立一個並且儲存
                codeCreater = new CodeCreater(elementUtils, classElement);
                codeCreaterMap.put(fqClassName, codeCreater);
            }
            //接下來,會將與該類對應的且被@BindView宣告的VariableElement加入到codeCreater中去,key為我們宣告時填寫的id,即View的id
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class); //獲取對應的註解物件
            int id = bindAnnotation.value();
            //如果這個欄位屬於某個類,這裡就會儲存到同一個codeCreater中,因為不同類的時候,這裡的codeCreater是不同的
            codeCreater.injectVariables.put(id , variableElement);
        }

        //二、生成代理類
        for (String key : codeCreaterMap.keySet())
        {
            CodeCreater codeCreater = codeCreaterMap.get(key);
            try
            {
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
                        codeCreater.getCreaterClassFullName(),
                        codeCreater.getTypeElement());
                Writer writer = jfo.openWriter();
                //寫入自動生成的程式碼
                writer.write(codeCreater.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e)
            {
                error(codeCreater.getTypeElement(),
                        "Unable to write injector for type %s: %s",
                        codeCreater.getTypeElement(), e.getMessage());
            }

        }
        return true;
    }

    //要field欄位和非private修飾才有效
    private boolean checkAnnotationValid(Element annotatedElement, Class clazz)
    {
        if (annotatedElement.getKind() != ElementKind.FIELD)
        {
            error(annotatedElement, "%s must be declared on field.", clazz.getSimpleName());
            return false;
        }
        //是否為私有的欄位
        if (annotatedElement.getModifiers().contains(PRIVATE))
        {
            error(annotatedElement, "%s() must can not be private.", annotatedElement.getSimpleName());
            return false;
        }

        return true;
    }

    private void error(Element element, String message, Object... args)
    {
        if (args.length > 0)
        {
            message = String.format(message, args);
        }
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message, element);
    }
}

上面通過繼承系統的AbstractProcessor來自己實現註解處理器,我們主要重寫process方法,上面涉及到了一個很重要的介面Element(元素),這個介面有幾個實現,代表欄位元素、類元素、方法元素等等。就是通過這些元素,我們才能獲得元素對應的註解資訊。其中CodeCreater類是一個程式碼生成器,通過它我們可以為每一個使用註解的activity或view自動生成一個對應的ViewInject類,這個類就是給被BindView註解標識的元素賦值的。我們來看看程式碼:

public class CodeCreater {
    private String packageName;
    private String createrClassName;
    private TypeElement typeElement;

    //儲存findViewById要用到的id值和對應的變數元素
    public Map<Integer, VariableElement> injectVariables = new HashMap<>();

    public static final String SUFFIX = "ViewInject";

    public CodeCreater(Elements elementUtils, TypeElement classElement) {
        this.typeElement = classElement;
        //獲取註解元素類的全包名
        PackageElement packageElement = elementUtils.getPackageOf(classElement);
        String packageName = packageElement.getQualifiedName().toString();
        //classname
        String className = classElement.getQualifiedName().toString().substring(packageName.length() + 1);
        this.packageName = packageName;
        this.createrClassName = className + "$$" +SUFFIX;
    }


    public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("/* Generated code. Do not modify!*/\n");
        builder.append("package ").append(packageName).append(";\n\n"); //包路徑
        builder.append("import com.lib_java.compileAnnotation.*;\n"); //匯入包路徑
        builder.append('\n');

        //類的宣告並且繼承ViewInject<T>介面,其中T是使用註解的那個類
        builder.append("public class ").append(createrClassName).append(" implements " + CodeCreater.SUFFIX +
                "<" + typeElement.getQualifiedName() + ">");
        builder.append(" {\n\n");

        generateMethods(builder);
        builder.append('\n');

        builder.append("}\n");
        return builder.toString();

    }


    private void generateMethods(StringBuilder builder) {

        //參加inject方法,實際上是實現ViewInject介面的方法
        builder.append("   @Override\n ");
        builder.append("  public void inject(" + typeElement.getQualifiedName() + " master, Object viewOwner ) {\n");

        for (int id : injectVariables.keySet()) {
            VariableElement element = injectVariables.get(id); //id對應的變數
            String name = element.getSimpleName().toString(); //變數名
            String type = element.asType().toString(); //變數型別
            //master代表使用註解的那個類的全限定名,所以master.name代表被註解的那個變數,其實也就是對應的activity物件
            builder.append("   if(viewOwner instanceof android.app.Activity){\n"); //如果master是activity
            builder.append("      master." + name).append(" = ");
            builder.append("(" + type + ")(((android.app.Activity)viewOwner).findViewById( " + id + "));\n");
            builder.append("\n   }else{\n");
            builder.append("      master." + name).append(" = ");
            builder.append("(" + type + ")(((android.view.View)viewOwner).findViewById( " + id + "));\n"); //master是View物件
            builder.append("\n    }\n");

        }
        builder.append("  }\n");


    }

    public String getCreaterClassFullName() {
        return packageName + "." + createrClassName;
    }

    public TypeElement getTypeElement() {
        return typeElement;
    }


}

我們可以看到,主要作用就是通過字串的拼接去生成一個類,這個類的名字為使用者的類名+“$$ViewInject”。什麼註解處理器也完成了,下面我們就暴露一個使用介面ViewInjector類,用這個類來完成最後的繫結工作,如下:

public class ViewInjector
{
    private static final String SUFFIX = "$$ViewInject";

    public static void injectView(Activity activity)
    {
        //找到自動生成的代理類
        ViewInject proxyActivity = findProxyActivity(activity);
        proxyActivity.inject(activity, activity); //執行注入方法,兩個引數都是對應的activity物件
    }

    //object是持有被註解欄位的物件,view是指我們要findViewById那個view物件, 也可以是activity物件
    public static void injectView(Object object, View view)
    {
        ViewInject proxyActivity = findProxyActivity(object);
        proxyActivity.inject(object, view);
    }

    private static ViewInject findProxyActivity(Object activity)
    {
        try
        {
            Class clazz = activity.getClass();
            Class injectorClazz = Class.forName(clazz.getName() + SUFFIX);
            return (ViewInject) injectorClazz.newInstance();
        } catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        } catch (InstantiationException e)
        {
            e.printStackTrace();
        } catch (IllegalAccessException e)
        {
            e.printStackTrace();
        }
        throw new RuntimeException(String.format("can not find %s , something when compiler.", activity.getClass().getSimpleName() + SUFFIX));
    }
}

下面我們來測試一下


public class MainActivity extends AppCompatActivity {

    @BindView(R.id.bindView)
    Button BindBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjector.injectView(this); //呼叫注入
        BindBtn.setText("成功綁定了view");

    }

最後測試是成功通過的,最後我們來看看自動生成的那個MainActivity$$ViewInject是怎麼樣的,如下

/* Generated code. Do not modify!*/
package com.androidstudydata;

import com.lib_java.compileAnnotation.*;

public class MainActivity$$ViewInject implements ViewInject<com.androidstudydata.MainActivity> {

   @Override
   public void inject(com.androidstudydata.MainActivity master, Object viewOwner ) {
   if(viewOwner instanceof android.app.Activity){
      master.BindBtn = (android.widget.Button)(((android.app.Activity)viewOwner).findViewById( 2131230762));

   }else{
      master.BindBtn = (android.widget.Button)(((android.view.View)viewOwner).findViewById( 2131230762));

    }
  }

}

可以看到,其實到最後還是通過findViewById方法去給view賦值的,只是我們通過編譯時的註解處理器,將這些程式碼自動生成了,這樣就節省了我們很多時間,提高了效率。

好了,關於註解的知識就講到這裡了!附上原始碼:

https://github.com/jiusetian/AndroidStudyData/tree/master/lib-java/src/main/java/com/lib_java/compileAnnotation