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),這時這個注解就不會被保留到運行時的代碼中了,因此我們用反射就獲取不到,就會報出如下錯誤。
@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),編譯器自動會提示警告:
好了,上面僅僅是小試牛刀,現在我們開始真正寫一個編譯時注解。
我希望有個注解可以幫助我們自動生成網絡請求的代碼,之前我們的網絡請求代碼是這樣的:
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文件,在里面寫上這個注解處理類的全名。如果你有多個注解處理類,請用回車分割。
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