轉載:自定義註解

轉載:https://juejin.im/entry/577142c3a633bd006435eea4html

什麼是註解

先來看看Java文檔中的定義java

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.android

註解是一種元數據, 能夠添加到java代碼中. 類、方法、變量、參數、包均可以被註解,註解對註解的代碼沒有直接影響.nginx

首先, 明確一點: 註解並無什麼魔法, 之因此產生做用, 是對其解析後作了相應的處理. 註解僅僅只是個標記罷了.git

定義註解用的關鍵字是@interfacegithub

元註解

java內置的註解有Override, Deprecated, SuppressWarnings等, 做用相信你們都知道.bash

如今查看Override註解的源碼app

@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }

發現Override註解上面有兩個註解, 這就是元註解. 元註解就是用來定義註解的註解.其做用就是定義註解的做用範圍, 使用在什麼元素上等等, 下面來詳細介紹.框架

元註解共有四種@Retention, @Target, @Inherited, @Documentedless

  • @Retention 保留的範圍,默認值爲CLASS. 可選值有三種

    • SOURCE, 只在源碼中可用
    • CLASS, 在源碼和字節碼中可用
    • RUNTIME, 在源碼,字節碼,運行時都可用
  • @Target 能夠用來修飾哪些程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER等,未標註則表示可修飾全部

  • @Inherited 是否能夠被繼承,默認爲false

  • @Documented 是否會保存到 Javadoc 文檔中

其中, @Retention是定義保留策略, 直接決定了咱們用何種方式解析. SOUCE級別的註解是用來標記的, 好比Override, SuppressWarnings. 咱們真正使用的類型是CLASS(編譯時)和RUNTIME(運行時)

自定義註解

舉個栗子, 結合例子講解

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface TestAnnotation { String value(); String[] value2() default "value2"; }

元註解的的意義參考上面的講解, 再也不重複, 這裏看註解值的寫法:

類型 參數名() default 默認值;

其中默認值是可選的, 能夠定義, 也能夠不定義.

處理運行時註解

Retention的值爲RUNTIME時, 註解會保留到運行時, 所以使用反射來解析註解.

使用的註解就是上一步的@TestAnnotation, 解析示例以下:

public class Demo { @TestAnnotation("Hello Annotation!") private String testAnnotation; public static void main(String[] args) { try { // 獲取要解析的類 Class cls = Class.forName("myAnnotation.Demo"); // 拿到全部Field Field[] declaredFields = cls.getDeclaredFields(); for(Field field : declaredFields){ // 獲取Field上的註解 TestAnnotation annotation = field.getAnnotation(TestAnnotation.class); if(annotation != null){ // 獲取註解值 String value = annotation.value(); System.out.println(value); } } } catch (ClassNotFoundException e) { e.printStackTrace(); } } }

此處只演示瞭解析成員變量上的註解, 其餘類型與此相似.

解析編譯時註解

解析編譯時註解須要繼承AbstractProcessor類, 實現其抽象方法

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)

該方法返回ture表示該註解已經被處理, 後續不會再有其餘處理器處理; 返回false表示仍可被其餘處理器處理.

處理示例:

// 指定要解析的註解 @SupportedAnnotationTypes("myAnnotation.TestAnnotation") // 指定JDK版本 @SupportedSourceVersion(SourceVersion.RELEASE_7) public class MyAnnotationProcesser extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (TypeElement te : annotations) { for (Element element : roundEnv.getElementsAnnotatedWith(te)) { TestAnnotation testAnnotation = element.getAnnotation(TestAnnotation.class); // do something } } return true; } }

這裏先大體介紹是怎麼個套路, 接下來講具體實踐過程.

Android中使用編譯時註解

註解是個什麼東西咱們已經知道了, 也知道了如何解析註解. 咱們下一步的目標是如ButterKnife通常自動生成代碼.

接下來的操做基於InteliJ IDEA(開發註解及其解析類, 打出jar包)和Android Studio(實測使用狀況)

note: AS的Android開發環境中沒有AbstractProcessor類, 而我新建了Java Module後遇到了各類各樣的花式錯誤(後面的報錯之路會敘述), 無奈只能在IDEA中開發並打出jar包

開發註解庫

在IDEA中新建java項目, 並開啓maven支持. 若是新建項目的頁面沒有maven選項, 建好項目後右鍵項目目錄->"Add Framwork Support...", 選擇maven.

自定義編譯時註解

@Retention(RetentionPolicy.CLASS) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) public @interface TestAnnotation { String value() default "Hello Annotation"; }

解析編譯時註解

// 支持的註解類型, 此處要填寫全類名 @SupportedAnnotationTypes("myannotation.TestAnnotation") // JDK版本, 我用的是java7 @SupportedSourceVersion(SourceVersion.RELEASE_7) public class MyAnnotationProcessor extends AbstractProcessor { // 類名的前綴後綴 public static final String SUFFIX = "AutoGenerate"; public static final String PREFIX = "My_"; @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (TypeElement te : annotations) { for (Element e : roundEnv.getElementsAnnotatedWith(te)) { // 準備在gradle的控制檯打印信息 Messager messager = processingEnv.getMessager(); // 打印 messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.toString()); messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.getSimpleName()); messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.getEnclosingElement().toString()); // 獲取註解 TestAnnotation annotation = e.getAnnotation(TestAnnotation.class); // 獲取元素名並將其首字母大寫 String name = e.getSimpleName().toString(); char c = Character.toUpperCase(name.charAt(0)); name = String.valueOf(c+name.substring(1)); // 包裹註解元素的元素, 也就是其父元素, 好比註解了成員變量或者成員函數, 其上層就是該類 Element enclosingElement = e.getEnclosingElement(); // 獲取父元素的全類名, 用來生成包名 String enclosingQualifiedName; if(enclosingElement instanceof PackageElement){ enclosingQualifiedName = ((PackageElement)enclosingElement).getQualifiedName().toString(); }else { enclosingQualifiedName = ((TypeElement)enclosingElement).getQualifiedName().toString(); } try { // 生成的包名 String genaratePackageName = enclosingQualifiedName.substring(0, enclosingQualifiedName.lastIndexOf('.')); // 生成的類名 String genarateClassName = PREFIX + enclosingElement.getSimpleName() + SUFFIX; // 建立Java文件 JavaFileObject f = processingEnv.getFiler().createSourceFile(genarateClassName); // 在控制檯輸出文件路徑 messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + f.toUri()); Writer w = f.openWriter(); try { PrintWriter pw = new PrintWriter(w); pw.println("package " + genaratePackageName + ";"); pw.println("\npublic class " + genarateClassName + " { "); pw.println("\n /** 打印值 */"); pw.println(" public static void print" + name + "() {"); pw.println(" // 註解的父元素: " + enclosingElement.toString()); pw.println(" System.out.println(\"代碼生成的路徑: "+f.toUri()+"\");"); pw.println(" System.out.println(\"註解的元素: "+e.toString()+"\");"); pw.println(" System.out.println(\"註解的值: "+annotation.value()+"\");"); pw.println(" }"); pw.println("}"); pw.flush(); } finally { w.close(); } } catch (IOException x) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, x.toString()); } } } return true; } }

看似代碼很長, 其實很好理解. 只作了兩件事, 1.解析註解並獲取須要的值 2.使用JavaFileObject類生成java代碼.

向JVM聲明解析器

咱們的解析器雖然定義好了, 可是jvm並不知道, 也不會調用, 所以咱們須要聲明.

如圖所示

在java的同級目錄新建resources目錄, 新建META-INF/services/javax.annotation.processing.Processor文件, 文件中填寫你自定義的Processor全類名

而後打出jar包以待使用(打包方式自行百度)

Android中使用

使用apt插件

項目根目錄gradle中buildscriptdependencies添加

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

module目錄的gradle中, 添加

apply plugin: 'android-apt'

代碼中調用

將以前打出的jar包導入項目中, 在MainActivity中寫個測試方法

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); test(); } @TestAnnotation("hehe") public void test(){ }

運行一遍項目以後, 代碼就會自動生成.

如下是生成的代碼, 在路徑yourmodule/build/generated/source/apt/debug/yourpackagename中:

public class My_MainActivityAutoGenerate { /** 打印值 */ public static void printTest() { // 註解的父元素: com.example.pan.androidtestdemo.MainActivity System.out.println("代碼生成的路徑: file:/Users/Pan/AndroidStudioProjects/AndroidTestDemo/app/build/generated/source/apt/debug/My_MainActivityAutoGenerate.java"); System.out.println("註解的元素: test()"); System.out.println("註解的值: hehe"); } }

而後在test方法中調用自動生成的方法

@TestAnnotation("hehe") public void test(){ My_MainActivityAutoGenerate.printTest(); }

會看到如下打印結果:

代碼生成的路徑: file:/Users/Pan/AndroidStudioProjects/AndroidTestDemo/app/build/generated/source/apt/debug/com/example/pan/androidtestdemo/MainActivityAutoGenerate.java 註解的元素: test() 註解的值: hehe

報錯之路

開始時, 我在Android Studio的Java Library中編寫解析類, 而後在Android Module依賴Java庫, 而後報下面這個錯誤

For more information see https://docs.gradle.org/current/userguide/build_environment.html Error:Error converting bytecode to dex: Cause: Dex cannot parse version 52 byte code. This is caused by library dependencies that have been compiled using Java 8 or above. If you are using the 'java' gradle plugin in a library submodule add targetCompatibility = '1.7' sourceCompatibility = '1.7' to that submodule's build.gradle file.

我tm原本就是Java8啊, 一番Google, 須要開啓手動開啓才能支持java8, 步驟以下:

android { compileSdkVersion 23 // 開啓Java8, buildTools版本必須24以上 buildToolsVersion "24" ... defaultConfig { ... // Java8須要jack工具鏈支持 jackOptions{ enabled true } } ... // 指定編譯版本 compileOptions{ targetCompatibility = '1.8' sourceCompatibility = '1.8' } }

然而...又報了這個錯誤

Error: Could not find the property 'options' on the task' : app: compileDebugJavaWithJack '.

來自JakeWharton大神的回覆, jack編譯器目前並不支持apt插件https://github.com/JakeWharton/butterknife/issues/571

摔! 不用java8報錯, 用了又尼瑪報. 自動生成代碼是必需要用apt插件的. 那就只能用java7在IDEA裏開發了.

時至今日(2016年06月23日), Google並無解決這個問題, 目前jack編譯器還處於預覽版, 相信之後會解決吧

總結

有了本文所述的註解知識, 對Dagger,ButterKnife等框架就不難理解了. 若是在時間精力容許的狀況下, 咱們也徹底能夠自定義個註解框架.

本文中自動生成代碼的部分十分簡單, 也隱含bug: 在for循環中建立了文件, 若是一個類中使用了兩次該註解, 第二次是沒法建立新文件的. 真正的實際項目中, 確定是將須要的信息保存起來, 以後統一建立java類.

相關文章
相關標籤/搜索