天天看點

Java 注解

Java的注解是個很神奇的東西,它既可以幫你生成代碼,又可以結合反射來在運作時獲得注解辨別的對象,進行邏輯處理,它能幫助我們完成很多很多不可能完成的任務,這回我們就來一起來了解下它。

一、什麼可以被注解修飾

Java中的類、方法、變量、參數、包都可以被注解,在java8中注解可以被運用到任何地方。比如:

myString = (@NonNull String) str;
class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }
new @Interned MyObject();      

需要注意的是,類型注解隻是文法而不是語義,并不會影響java的編譯時間,加載時間,以及運作時間。在Java8沒有普及的情況下,本文僅僅讨論在jdk1.7中可被用于實踐的注解方案。

二、注解的類型

2.1 引子

我們先從我們最熟悉的@Override說起

/**
 * Annotation type used to mark methods that override a method declaration in a
 * superclass. Compilers produce an error if a method annotated with @Override
 * does not actually override a method in a superclass.
 *
 * @since 1.5
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}      

我們注意到了@Target和@Retention這兩個注解,這兩個家夥就是專門用來修飾注解的注解,看起來吊吊的,但在實際開發中我們都不會去用到他們,是以我們不是很熟悉。但今天我們已經開始學習注解了,姑且就和他們打個招呼吧,先照貓畫虎寫一個自己的注解,注解的名字叫做classInfo:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ClassInfo {

 String value() default "default";

}      

先不管這個注解有什麼用,我們就看這個注解類的辨別,其實上面兩行辨別就是起一個說明作用,和他們一樣的還有@Document等。

@Documented 是否會儲存到 Javadoc 文檔中

@Retention 保留時間,可選值 SOURCE(源碼時),CLASS(編譯時),RUNTIME(運作時),預設為 CLASS。

如果值為 SOURCE 大都為 Mark Annotation,這類 Annotation 大都用來校驗,比如 Override, Deprecated, SuppressWarnings

@Target 來指定這個注解可以修飾哪些元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等,未标注則表示可修飾所有類型

@Inherited 是否可以被繼承,預設為 false

2.2 詳細說明

我們來詳細說說看:

@Documented 這個東西如果加在了注解上面,就會在生成java doc時有相關注解的文檔,在小項目開發過程中,這個注解意義不大,可以忽略。

@Retention 它裡面的值都是以RetentionPolicy開頭的,來看看源碼是怎麼寫的:

public enum RetentionPolicy {
    /**
     * Annotation is only available in the source code.
     */
    SOURCE,
    /**
     * Annotation is available in the source code and in the class file, but not
     * at runtime. This is the default policy.
     */
    CLASS,
    /**
     * Annotation is available in the source code, the class file and is
     * available at runtime.
     */
    RUNTIME
}      
  • 如果是SOURCE,注解保留範圍為源代碼,在編譯時将會被編譯器丢棄。這類 Annotation 大都用來校驗,比如 Override, Deprecated, SuppressWarnings。
  • 如果是CLASS,這個注解保留範圍是源代碼和類檔案中,但并非作用于運作時,是以JVM不會識别此。如果你在自定義注解時,不寫@Retention,預設就是CLASS的。這類的注解和SOURCE的注解都可以配合AbstractProcessor進行使用,用于在編譯時進行自動處理一些事物或者生成一些檔案。
  • 如果是RUNTIME,這個注解的保留範圍是源代碼、類檔案和運作時,這類的注解一般會和反射配合使用。可以在運作時通過反射檢視被這個注解辨別的方法,然後得到被辨別的元素,接着進行處理。
       final Method[] allMethods = clazz.getDeclaredMethods();
            for (Method method : allMethods) {
                // 根據注解來解析函數
                Subscriber annotation = method.getAnnotation(Subscriber.class);
       }      

上面這段代碼會周遊這個類中被辨別了@Subscriber的方法。但如果我們把@Subscriber設定為@Retention(RetentionPolicy.CLASS),這時這個注解就不會被保留到運作時的代碼中了,是以我們用反射就擷取不到,就會報出如下錯誤。

Java 注解

@Target 指定注解可以被辨別于哪種Java元素上,指定類型(ElementType)如下:

  • ElementType.ANNOTATION_TYPE

     注釋類型聲明。
  • ElementType.CONSTRUCTOR

     構造方法聲明。
  • ElementType.FIELD

     字段聲明(包括枚舉常量)。
  • ElementType.LOCAL_VARIABLE

     局部變量聲明。
  • ElementType.METHOD

     方法聲明。
  • ElementType.PACKAGE

     包聲明。
  • ElementType.PARAMETER

     參數聲明。
  • ElementType.TYPE

     類、接口(包括注釋類型)或枚舉聲明。

這個注解辨別僅僅做個辨別,沒有任何代碼邏輯,它的目的是避免使用者随便辨別注解,進而造成處理注解時出現錯誤。

三、自定義編譯時注解

3.1 編譯時注解

所謂編譯時注解就是在你寫代碼時,就能産生作用的注解,一旦程式運作成apk,你的注解就沒用了,是以它的生命周期在于你寫代碼到編譯的過程之間。我們先來看看一個Android特有的注解方式,這種注解方式屬于特殊的編譯時注解。

   public static final int VANILLA = 0;
    public static final int CHOCOLATE = 1;
    public static final int STRAWBERRY = 2;
 
    @IntDef({VANILLA, CHOCOLATE, STRAWBERRY})
    public @interface Flavour {
    }      

首先我們定義了三個常量,然後定義了一個注解 @Flavour,在這個注解上用@IntDef辨別了這個注解的作用。說明用@Flavour辨別的變量,必須是0,1,2這三個int類型值,是不是很像枚舉類型呢?其實它就是為了替代枚舉而出現的(Android中枚舉的效率稍低)。在使用的時候,我們隻需要像如下辨別,編譯器就會自動進行判斷,進而提升代碼品質。

  @Flavour
    public int getFlavour() {
        return flavour;
    }
 
    public void setFlavour(@Flavour int flavour) {
        this.flavour = flavour;
    }      

如果在使用的時候傳入了錯誤的值(不是0,1,2),編譯器自動會提示警告:

Java 注解

好了,上面僅僅是小試牛刀,現在我們開始真正寫一個編譯時注解。

我希望有個注解可以幫助我們自動生成網絡請求的代碼,之前我們的網絡請求代碼是這樣的:

public Observable post041(String create_time, String user_name) {
        HashMap<String, String> map = new HashMap<>();
        map.put("create_time", create_time);
        map.put("user_name", user_name);
        map.put("name", "kale");
        map.put("user", "aaaa3");

        return (Observable) mHttpRequest.doPost("http://image.baidu.com", map, null);
}      

這種代碼就是模闆式代碼,注解最适合幹掉這樣的代碼了。高興之餘,先分析下需求,拆分可變部分和不可變部分是主要需求,我們的可變部分在于定義url,請求的參數,其中包含預設的請求參數還有從外部傳入的請求參數,還有進行json解析的model類、是get請求還是post請求。

分析完畢,分分鐘定義一個注解:

@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface HttpPost {

    String url();

    Class<?> model() default HttpProcessor.class;
}      

這個注解中包含兩個方法,一個是url,這個是必須傳入的(使用者不寫就會報錯)。如果你這個請求沒有解析的model,那麼就不用傳入model對象,是以這裡給一個預設的model對象。這個注解我希望出現在java doc裡面,是以加上了@documented。這個注解辨別的是java中的method,是以寫了method,而且我希望它僅僅是在編譯時有效,是以用了source。

現在定義好了,開始使用:

@HttpPost(url =  "http://image.baidu.com?user=aaaa3&name=kale", model = String.class)
 Observable post041(String create_time, String user_name);      

将url寫入注解中,并且定義好model(如果不需要json解析,可以不定義),如果是必須傳的參數,就和url寫到一起,把需要從外部得到的注解寫到方法的參數中。現在兩行代碼寫了一個網絡請求,是不是很簡單呢?現在api、請求方法體、解析model的聚合度變得很高了。注意哦,現在調用這個方法其實根本不起作用,因為我們還沒有去解析這個注解呢,下面來說說怎麼解析。

3.2 建立解析編譯時注解類

首先在as中建立一個java的lib,然後在這個lib中開始寫解析類。我建立了HttpProcessor這個類,這個類繼承了AbstractProcessor這個類,它會強制你實作process這個方法,這樣HttpProcessor就有了解析注解的能力了。

public class HttpProcessor extends AbstractProcessor{

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}      

接着,我在頭部定義一些配置代碼:

@SupportedAnnotationTypes({"kale.net.http.annotation.HttpPost"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class HttpProcessor extends AbstractProcessor {      

上面兩行代碼定義了這個類能處理的注解類,并且辨別了基于的java版本。寫完了之後千萬不要忘記了把這個注解處理類注冊到項目中,注冊的方法就是在resource/META-INF/services中建立一個javax.annotation.processing.Processor檔案,在裡面寫上這個注解處理類的全名。如果你有多個注解處理類,請用回車分割。

Java 注解

3.2 解析注解

我們為了友善首先在init時定義好一個工具類,以後會用到。

  private Elements elementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }      

然後,在process方法中開始處理傳入的注解對象。需要注意的是,這個process方法會被調用多次,調用次數取決于你這個注解處理類能處理的注解個數。

@SupportedAnnotationTypes({"kale.net.http.annotation.HttpPost"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class HttpProcessor01 extends AbstractProcessor{

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 傳入目前注解處理器可以處理的注解元素
        for (TypeElement te : annotations) {
            // 找到被辨別了可處理的注解的元素
            for (Element e : roundEnv.getElementsAnnotatedWith(te)) {
                if (e.getKind() == ElementKind.INTERFACE) {
                    // 如果是接口
                    TypeElement ele = (TypeElement) e;
                    // ……
                    
                    
                } else if (e.getKind() == ElementKind.METHOD) {
                    // 如果是方法
                    ExecutableElement method = (ExecutableElement) e;
                    // ……
                   
                }
            }
        }
        return true;
    }
}      

代碼有些複雜,我分布講解:

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 傳入目前注解處理器可以處理的注解元素
        for (TypeElement te : annotations) {
            // 找到被辨別了可處理的注解的元素
            for (Element e : roundEnv.getElementsAnnotatedWith(te)) {      

在這個for循環中,我們可以利用 e.getKind() 這個方法來判斷注解辨別的是什麼對象,如果是方法就采用方法的處理邏輯,如果辨別的是類就采用類的處理邏輯,如果是接口就用接口的,根據需要進行處理即可。

           if (e.getKind() == ElementKind.METHOD) {
                    // 如果是方法
                    ExecutableElement method = (ExecutableElement) e;
                    if (method.getAnnotation(HttpPost.class) != null) {
                        handlerHttp(mStringBuilder, e, method, true);
                   }
                }      

進入if塊後,我首先将e進行了強制轉換,為啥要強制轉換呢,因為e是辨別被@httpPost辨別的元素對象,但目前程式不知道它是什麼類型的。我們通過之前的判斷,知道它現在是方法對象,是以在這裡就強轉了。那麼如果是接口改強轉成什麼呢?如果是類應該強轉什麼呢?來看下面的說明:

package com.example;    // PackageElement

public class Foo {        // TypeElement

    private int a;      // VariableElement
    private Foo other;  // VariableElement

    public Foo () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
              ) {}
}      

如果注解辨別到了類中,就強轉為TypeElement;

如果辨別的是變量,就強轉VariableElement;

如果辨別内部類或者方法,強轉ExecuteableElement;

如果辨別方法中的參數,就強轉為TypeElement。

說明:這裡的每個強轉後的對象都有自己好用的api,我就不詳細說明了,大家可以在用的時候進行測試。

現在我們必須換個角度來看源代碼,它隻是結構化的文本,他不是可運作的。你可以想象它就像你将要去解析的XML檔案一樣(或者是編譯器中抽象的文法樹)。就像XML解釋器一樣,有一些類似DOM的元素。你可以從一個元素導航到它的父或者子元素上。

舉例來說,假如你有一個代表

public class Foo

類的

TypeElement

元素,你可以周遊它的孩子,如下:

TypeElement fooClass = ... ;  
for (Element e : fooClass.getEnclosedElements()){ // iterate over children  
    Element parent = e.getEnclosingElement();  // parent == fooClass
}      

正如你所見,Element代表的是源代碼。

TypeElement

代表的是源代碼中的類型元素,例如類。然而,

TypeElement

并不包含類本身的資訊。你可以從

TypeElement

中擷取類的名字,但是你擷取不到類的資訊,例如它的父類。這種資訊需要通過

TypeMirror

擷取。你可以通過調用

elements.asType()

擷取元素的

TypeMirror

好,現在我們回過頭來擴充上面的那段代碼:

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 傳入目前注解處理器可以處理的注解元素
        for (TypeElement te : annotations) {
            // 找到被辨別了可處理的注解的元素
            for (Element e : roundEnv.getElementsAnnotatedWith(te)) {
                if (e.getKind() == ElementKind.INTERFACE) {
                    // 如果是接口
                    TypeElement ele = (TypeElement) e;
                    if (ele.getAnnotation(ApiInterface.class) != null) {
                        String interFaceName = ele.getQualifiedName().toString();
                        mStringBuilder = createClsBlock(interFaceName, mStringBuilder);
                    } else {
                        fatalError("Should use " + ApiInterface.class.getName());
                    }
                } else if (e.getKind() == ElementKind.METHOD) {
                    // 如果是方法
                    ExecutableElement method = (ExecutableElement) e;
                    if (method.getAnnotation(HttpPost.class) != null) {
                        handlerHttp(mStringBuilder, e, method, true);
                    } else {
                        handlerHttp(mStringBuilder, e, method, false);
                    }
                }
            }
        }
        mStringBuilder.append("\n}");
        createClassFile(PACKAGE_NAME, CLASS_NAME, mStringBuilder.toString());
        return true;
    }      

如果是被辨別為@HttpPost的方法體,那麼就開始進入handlerHttp(…)中了。這個方法的代碼和注解其實沒啥關系了,就是做些字元串的拼接,拼接完畢後生成一個類檔案。

public void handlerHttp(StringBuilder sb, Element ele, ExecutableElement method, boolean isPost) {
        String url;
        String modelName;
        if (isPost) {
            HttpPost httpPost = ele.getAnnotation(HttpPost.class);
            url = httpPost.url();
            try {
                modelName = httpPost.model().getName();
            } catch (MirroredTypeException ex) {
                modelName = ex.getTypeMirror().toString();
            }
        } else {
            HttpGet httpGet = ele.getAnnotation(HttpGet.class);
            url = httpGet.url();
            try {
                modelName = httpGet.model().getName();
            } catch (MirroredTypeException ex) {
                modelName = ex.getTypeMirror().toString();
            }
        }

        if (url.equals("")) {
            fatalError("Url is null");
            return;
        }
        log("Working on method: " + method.getSimpleName());
        Map<String, String> defaultParams = UrlUtil.getParams(url);
        List<String> customParams = getCustomParams(method);
        if (modelName.equals(HttpProcessor.class.getName())) {
            modelName = null;
        }

        if (modelName != null && modelName.contains("<any?>")) {
            modelName = modelName.replace("<any?>", UrlUtil.url2packageName(url));
        }
        url = UrlUtil.getRealUrl(url);
        if (isPost) {
            sb.append(createPostMethodBlock(method.getSimpleName().toString(), url, defaultParams, customParams, modelName));
        } else {
            sb.append(createGetMethodBlock(method.getSimpleName().toString(), url, defaultParams, customParams, modelName));
        }
        log("Parse method: " + method.getSimpleName() + " completed");
    }      

3.3 生成檔案的方法

還記得我們在init中産生的工具類麼,現在我們需要靠它來生成檔案了。傳入類包名、類名和類内部的資訊就行。

private void createClassFile(String PACKAGE_NAME, String clsName, String content) {
        //PackageElement pkgElement = elementUtils.getPackageElement("");
        TypeElement pkgElement = elementUtils.getTypeElement(PACKAGE_NAME);

        OutputStreamWriter osw = null;
        try {
            JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(PACKAGE_NAME + "." + clsName, pkgElement);
            OutputStream os = fileObject.openOutputStream();
            osw = new OutputStreamWriter(os, Charset.forName("UTF-8"));
            osw.write(content, 0, content.length());

        } catch (IOException e) {
            e.printStackTrace();
            //fatalError(e.getMessage());
        } finally {
            try {
                if (osw != null) {
                    osw.flush();
                    osw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
                fatalError(e.getMessage());
            }
        }
    }      

3.4 打log的方法

注解中的log有個自己的api,封裝一下就是這樣了:

   private void log(String msg) {
        if (processingEnv.getOptions().containsKey("debug")) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, TAG + msg);
        }
    }

    private void fatalError(String msg) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, TAG + " FATAL ERROR: " + msg);
    }      

關于詳細的代碼可以參考:https://github.com/tianzhijiexian/HttpAnnotation

關于用編譯時注解寫工廠方法的代碼:http://www.codeceo.com/article/java-annotation-processor.html

類似的用注解寫網絡架構的文章:http://segmentfault.com/a/1190000002785541

四、運作時注解

有時候一些注解會配合反射進行調用,比如事件總線。

/**
 * 事件接收函數的注解類,運用在函數上
 *
 * @author mrsimple
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscriber {

    /**
     * 事件的tag,類似于BroadcastReceiver中的Action,事件的辨別符
     */
    String tag();

}      

這種注解必須把生命周期寫到Runtime,否則反射就擷取不到它了。寫好了後,就可以通過反射來得到它标記的元素,進而進行處理:

public void registerMethods(Object subscriber) {
        Class<?> clazz = subscriber.getClass();
        // 查找類中符合要求的注冊方法,直到Object類
        while (clazz != null && !isSystemCls(clazz.getName())) {
            final Method[] allMethods = clazz.getDeclaredMethods();
            for (Method method : allMethods) {
                // 根據注解來解析函數
                Subscriber annotation = method.getAnnotation(Subscriber.class);
                if (annotation != null) {
                    String tag = annotation.tag();
                    // 擷取方法的tag
                    if (!TextUtils.isEmpty(tag)) {
                        SubscriberBean bean = new SubscriberBean();
                        bean.setSubscriber(subscriber);
                        bean.setMethod(method);
                        if (subscriberMap.containsKey(tag)) {
                            // 如果已經有這個tag了,那麼說明已經有人注冊了,是以可以直接添加到注冊清單中
                            subscriberMap.get(tag).add(bean);
                        } else {
                            // 如果之前沒有這個tag,那麼建立新的注冊清單
                            List<SubscriberBean> list = new ArrayList<>();
                            list.add(bean);
                            subscriberMap.put(tag, list);
                  
                        }
                    }
                }
            } // end for
            // 擷取父類,以繼續查找父類中符合要求的方法
            clazz = clazz.getSuperclass();
        }
    }      

如果想用注解幹掉findviewById也是可以的。先定義一個注入的注解:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView 
{
  //id就是控件id,在某一個控件上使用注解标注其id
  int id() default -1;
}      

在activity中進行反射查找,找到了後利用注解自動調用findviewById即可:

public class MainActivity extends Activity 
{
  public static final String TAG=MainActivity;
  //标注TextView的id
  @InjectView(id=R.id.tv_img)
  private TextView mText;
   
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    try {
      autoInjectAllField(this);
    } catch (IllegalAccessException e) {
    } catch (IllegalArgumentException e) {
    }
     
    if(mText!=null)
      mText.setText(Hello Gavin);
  }
   
  public void autoInjectAllField(Activity activity) throws IllegalAccessException, IllegalArgumentException
  {
    //得到Activity對應的Class
    Class clazz=this.getClass();
    //得到該Activity的所有字段
    Field []fields=clazz.getDeclaredFields();
    Log.v(TAG, fields size-->+fields.length);
    for(Field field :fields)
    {
      //判斷字段是否标注InjectView
      if(field.isAnnotationPresent(InjectView.class))
      {
        Log.v(TAG, is injectView);
        //如果标注了,就獲得它的id
        InjectView inject=field.getAnnotation(InjectView.class);
        int id=inject.id();
        Log.v(TAG, id--->+id);
        if(id>0)
        {
          //反射通路私有成員,必須加上這句
          field.setAccessible(true);
          //然後對這個屬性指派
          field.set(activity, activity.findViewById(id));
        }
      }
    }
  }
 
}      

 如果你活學活用了這一特性,那麼你完全可以用它做任何事情。假如你厭倦了在Android程式中打出一個完整的靜态限定常量,比如:

public class CrimeActivity {
    public static final String ACTION_VIEW_CRIME = 
        “com.bignerdranch.android.criminalintent.CrimeActivity.ACTION_VIEW_CRIME”;
}      

你完全可以使用一個運作時注解來幫你做這些事情。首先,建立一個注解類:

@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.FIELD })
public @interface ServiceConstant { }      

一旦定義了注解,我們接着就要寫些代碼來尋找并自動填充帶注解的字段:

public static void populateConstants(Class<?> klass) {
    String packageName = klass.getPackage().getName();
    for (Field field : klass.getDeclaredFields()) {
        if (Modifier.isStatic(field.getModifiers()) && 
                field.isAnnotationPresent(ServiceConstant.class)) {
            String value = packageName + "." + field.getName();
            try {
                field.set(null, value);
                Log.i(TAG, "Setup service constant: " + value + "");
            } catch (IllegalAccessException iae) {
                Log.e(TAG, "Unable to setup constant for field " + 
                        field.getName() +
                        " in class " + klass.getName());
            }
        }
    }
}      

哈哈,現在我們就可以用注解自動指派常量了:

public class CrimeActivity {
    @ServiceConstant
    public static final String ACTION_VIEW_CRIME;

    static {
        ServiceUtils.populateConstants(CrimeActivity.class);
}      

得到className的小技巧:

try {
                modelName = httpPost.model().getName();
            } catch (MirroredTypeException ex) {
                modelName = ex.getTypeMirror().toString();
            }      

參考自:

http://www.trinea.cn/android/java-annotation-android-open-source-analysis/

http://blog.zenfery.cc/archives/78.html

http://www.race604.com/annotation-processing/

http://www.2cto.com/kf/201405/302998.html

http://objccn.io/issue-11-6/

http://www.codeceo.com/article/java-annotation-processor.html