二. 重識Java之夯實註解

jasonhww發表於2018-12-21

不忘初心 砥礪前行, Tomorrow Is Another Day !

相關文章

引言

本文概要:

  1. 註解基礎知識
  2. 註解的定義及使用

一. 註解的基礎知識

註解分類:標準註解、元註解.

1.1 標準註解

  • @Override : 標記覆蓋超類的方法
  • @Deprecated : 標記棄用
  • @SuppressWarnings : 取消警告
  • @SafeVarargs : 宣告在使用可變長度引數的方法,與泛型類一起使用不會出現型別安全問題

1.2 元註解

簡稱註解的註解,從而建立新的註解.

  • @Target : 註解所修飾的型別
  • @Inherited : 註解可以被繼承
  • @Documented : 標記註解應該被javaDoc工具記錄
  • @Retention : 註解的保留策略

這裡主要介紹@Target和@Retention兩個元註解.

@Target

修飾型別從列舉類ElementType取值

public enum ElementType {
    /** 類,介面(包含註解型別)*/
    TYPE,

    /** 成員變數*/
    FIELD,

    /** 方法*/
    METHOD,

    /** 方法引數或構造方法引數 */
    PARAMETER,

    /** 構造方法  */
    CONSTRUCTOR,

    /** 區域性變數 */
    LOCAL_VARIABLE,

    /** 註解型別,可參考元註解Retention宣告  */
    ANNOTATION_TYPE,

    /** 包  */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

複製程式碼
@Retention

保留策略從列舉類RetentionPolicy中取值

public enum RetentionPolicy {
    /**
     * 原始碼級java檔案
     */
    SOURCE,

    /**
     * 編譯時class檔案  This is the default behavior.
     */
    CLASS,

    /**
     * 
     * 執行時,載入到jvm虛擬機器中.
     * 通過反射獲取該註解資訊
     */
    RUNTIME
}
複製程式碼

二.註解的定義及使用

  • 定義註解 - 反射機制解析註解 - 使用註解
  • 定義註解 - AbstractProcessor(註解解析器)解析註解 - 使用註解

2.1 執行時註解示例

//定義註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)//執行時註解
public @interface Name {
    //定義成員變數,可以設定預設值
    String value() default "我是預設名";
}

//使用註解
public class UseAnnotation {

    @Name(value = "小學僧")
    public String getName() {
        return "什麼都沒有";
    }

    @Name
    public String getFinalName() {
        return "什麼都沒有";
    }
}

//解析註解
public class Main {

    public static void main(String[] args) {
        //通過反射處理
        Method[] methods = UseAnnotation.class.getDeclaredMethods();
        for (Method method : methods) {
            Name name = method.getAnnotation(Name.class);
            System.out.println("註解的值:"+name.value());
        }
    }
}

//輸出結果
註解的值:小學僧
註解的值:我是預設名
複製程式碼

定義了一個註解,並且分別在getName與getFinalName方法上使用;由於第二個方法上沒有設定value,所以在反射呼叫時輸出的是預設名.

2.2 編譯時註解示例

瞭解編譯時註解,需要先了解下Element相關的知識.接下來看Element.

2.2.1 Element

Element位於javax.lang.model.element包下,一個Element代表一個程式元素.
對應關係如下:

package annotationDemo.compile; //PackageElement包

public class TestElement {//TypeElement類
    private int value;//VariableElement變數

    public int getValue() {//ExecutableElement方法
        return value;
    }
}

複製程式碼

Element的中重要API;

  • getEnclosingElement : 獲取一個元素的外部元素.

    • 比如上述成員變數value,以及getValue方法,它們對應VariableElement、ExecutableElement,那麼獲取到的外部元素則是TestElement類對應的元素.
  • getEnclosedElement : 獲取一個元素的內部元素.

    • 同樣,TestElement類對應typeElement元素,那麼獲取到的是VariableElement和ExecutableElement的元素集合.

2.2.2 使用示例

建立一個java-library,命名為:annotations
定義兩個註解用來注入int和String型別資料.

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface injectInt {

}

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface injectString {

}
複製程式碼

再建立一個java-library,命名為:compiler
定義註解處理器,解析註解

//build.gradle檔案

dependencies {
    implementation project(':annotations')
    implementation 'com.squareup:javapoet:1.11.1'//java檔案生成工具
    implementation 'com.google.auto.service:auto-service:1.0-rc3'
    //註冊註解處理,自動在ressources生成META-INF/services/javax.annotation.processing.Processor的檔案.檔案內容為:自定義的註解處理器的類的全路徑.
}

複製程式碼
@SupportedAnnotationTypes({"com.cjy.lhk_annotations.mylhk,injectInt",
        "com.cjy.lhk_annotations.mylhk.injectString"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(Processor.class)
public class injectProcessor extends AbstractProcessor {

    private static final ClassName CONTEXT =
            ClassName.get("android.content", "Context");


    //待生成java檔案的的集合,key為被註解的類的類名,value為GenerateJavaFile物件
    private HashMap<String, GenerateJavaFile> mGenerateJavaFiles = new HashMap<>();


    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        for (TypeElement typeElement : set) {//遍歷所有註解類對應的TypeElement
            //獲取註解類對應被註解的對應元素
            //比如註解被用在某個成員變數上,那麼這個就是獲取成員變數對應的元素
            for (Element element : roundEnvironment.getElementsAnnotatedWith(typeElement)) {
                addElementToGenerateJavaFile(element);
            }
        }
        createJavaFile();
        return true;
    }

    /**
     * 解析Element,並新增一個註解元素到對應的GenerateJavaFile物件中
     * (收集資訊)
     *
     * @param element 註解元素
     */
    private void addElementToGenerateJavaFile(Element element) {
        //獲取element對應成員變數所在的類,即被註解的類
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();//獲取外部元素
        //System.out.println("---getQualifiedName =---" + typeElement.getQualifiedName());
        String[] split = typeElement.getQualifiedName().toString().split("\\.");
        String className = split[split.length - 1];

        //通過父類的processingEnv獲取報信者,用於在編譯過程中列印log
        Messager messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.NOTE, "add element to generate file " + className);

        //獲取被註解類對應的GenerateJavaFile物件,如果沒有,則建立
        GenerateJavaFile generateJavaFile = mGenerateJavaFiles.get(className);
        if (generateJavaFile == null) {
            GenerateJavaFile file = new GenerateJavaFile();
            //設定待生成java檔案的包名
            file.packageName = processingEnv.getElementUtils().getPackageOf(element).toString();
            //設定待生成java檔案的類名
            file.className = className + "_Inject";
            //初始化元素集合
            file.elements = new ArrayList<>();
            file.elements.add(element);
            //儲存被註解類所對應要生成java類的GenerateJavaFile物件
            mGenerateJavaFiles.put(className, file);
        } else {
            //將註解元素新增到有的generateJavaFile物件中
            generateJavaFile.elements.add(element);
        }
    }

    /**
     * 生成java檔案
     */
    private void createJavaFile() {
        //遍歷GenerateJavaFile集合
        for (String className : mGenerateJavaFiles.keySet()) {

            //獲取一個GenerateJavaFile物件
            GenerateJavaFile file = mGenerateJavaFiles.get(className);

            //構建一個構造方法,該構造方法帶有一個Context型別的引數
            MethodSpec.Builder builder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(CONTEXT, "context");

            //遍歷該類中需要處理的註解元素
            for (Element element : file.elements) {
                //如果註解的成員變數是一個int型別
                if (element.asType().toString().equals("int")) {
                    //在構造方法中新增一條語句
                    //例如:((MainActivity)context).one = context.getResources().getInteger(R.integer.one);
                    builder.addStatement("(($N)context).$N = context.getResources().getInteger(R.integer.$N)",
                            file.className.split("_")[0], element.getSimpleName(), element.getSimpleName());
                    //如果註解的是一個String型別
                } else if (element.asType().toString().equals("java.lang.String")) {
                    //在構造方法中新增一條語句
                    //例如:((MainActivity)context).hello = context.getResources().getString(R.string.hello);
                    builder.addStatement("(($N)context).$N = context.getResources().getString(R.string.$N)",
                            file.className.split("_")[0], element.getSimpleName(), element.getSimpleName());
                }
            }
            //構建一個類,新增一個上述的構造方法
            TypeSpec typeSpec = TypeSpec.classBuilder(file.className)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(builder.build())
                    .build();
            try {
                //輸出java檔案
                JavaFile javaFile = JavaFile.builder(file.packageName, typeSpec).build();
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 待生成的Java檔案資訊
     */
    private static class GenerateJavaFile {
        String packageName;//包名
        String className;//類名
        List<Element> elements;//程式元素集合
    }
}

複製程式碼

註解處理器大致工作流程:

  1. 收集資訊
    1. 遍歷所有註解類對應的TypeElement
      1. 通過TypeElement獲取被註解的元素(如類,成員變數,方法對應的元素物件)
      2. 解析Element(被註解對應元素)資訊,將被相關資訊新增到待生成的檔案物件中
  2. 生成java檔案
apt與annotationProcessor的作用

對於compiler專案,我們只需在編譯時使用,執行時無需載入到jvm虛擬機器中.所以採用apt的替代品annotationProcessor進行引入.

//android專案
dependencies {
    implementation project(':annotations')
    //對於android專案,由於外掛的預設支援可以直接使用此方式,無需應用apt外掛
    annotationProcessor (:'compiler')

}
複製程式碼

由於本人技術有限,如有錯誤的地方,麻煩大家給我提出來,本人不勝感激,大家一起學習進步.

相關文章