有時(shí)候轉(zhuǎn)過頭看回一些基礎(chǔ)知識(shí),才發(fā)現(xiàn)原來當(dāng)時(shí)候自己覺得很難的東西都是從基礎(chǔ)知識(shí)衍生而來的,突然會(huì)有點(diǎn)豁然開朗的感覺次员。譬如說我們今天要講的知識(shí)點(diǎn)———注解。
初識(shí)王带,無處不在的注解
從Java1.5就開始引入淑蔚,在注解中,我們很容易就看到了Java的理念辫秧,"Write Once,Run Anywhere"束倍。平時(shí)開發(fā)的時(shí)候我們看到最多的注解莫過于是Java三種內(nèi)建注解之一的@Override被丧。
@Override——當(dāng)我們想要復(fù)寫父類中的方法時(shí)盟戏,我們需要使用該注解去告知編譯器我們想要復(fù)寫這個(gè)方法。這樣一來當(dāng)父類中的方法移除或者發(fā)生更改時(shí)編譯器將提示錯(cuò)誤信息甥桂。
其實(shí)在Android開發(fā)中柿究,我們?cè)诤芏嗟谌綆熘袝?huì)經(jīng)常看到注解黄选,下面我就列舉介紹一下在《Android高級(jí)進(jìn)階》中看到的一些關(guān)于運(yùn)用到注解的例子:
1.標(biāo)準(zhǔn)注解:
Java API中默認(rèn)定義的注解我們稱之為標(biāo)準(zhǔn)注解蝇摸,他們定義在java.lang,java.lang.annotation和javax.annotation包中,按照不同場(chǎng)景分為三類:
- 編譯時(shí)相關(guān)注解:編譯相關(guān)的注解是給編譯器使用办陷,如
@Override貌夕、@Deprecated、SuppressWarnings民镜、@SafeVarags啡专、@Generated、@FunctionalInterface... - 資源相關(guān)注解:一般用在JavaEE領(lǐng)域制圈,Android開發(fā)中應(yīng)該不會(huì)使用到们童,如@PostConstruct、@PreDestroy鲸鹦、@Resource慧库、@Resources...
- 元注解:用來定義和實(shí)現(xiàn)注解的注解,如@Target馋嗜、@Retention齐板、@Documented、@Inherited、@Repeatable
2.Support Annotation Library:
Support Annotation Library是從Android Support Library19.1開始引入的一個(gè)全新的函數(shù)包甘磨,它包含了一系列有用的元注解听皿,用來幫助開發(fā)者在編譯期間發(fā)現(xiàn)可能存在的Bug。
- Nullness注解:如@Nullable宽档、@NonNull
- 資源類型注解:如@AnimatorRes尉姨、@AttrRes、@LayoutRes...
- 類型定義注解:如@IntDef...
- 線程注解:如@UiThread吗冤、@MainThread又厉、@WorkerThread、@BinderThread...
- RGB顏色值注解:如@ColorRes
- 值范圍注解:如@Size椎瘟、@IntRange覆致、@FloatRange...
- 權(quán)限注解:如@RequirdsPermission
- 重寫函數(shù)注解:如@CallSuper
- 返回值注解:如@CheckResult
- @VisibleForTesting
- @Keep
3.一些著名的第三方庫:
如Butterknife、Dagger2肺蔚、DBFlow煌妈、Retrofit、JUnit
以上都是總結(jié)了大部分在《Android高級(jí)進(jìn)階》中出現(xiàn)的注解的地方宣羊,很多注解沒一個(gè)個(gè)解釋璧诵,有興趣的同學(xué)可以自己去搜索一下自己想知道的注解的具體用途。我們可以看到注解無論在java和Android中都是使用很廣泛的仇冯,而且慢慢變得必不可少之宿。下面我們就進(jìn)入我們的主題,分別用兩種方式去自定義注解苛坚。
那么我們先列出一個(gè)簡單的題目比被,然后用兩種不同的方式去實(shí)現(xiàn):
題目:用注解實(shí)現(xiàn)兩數(shù)相加的運(yùn)算
一、運(yùn)行時(shí)自定義注解:
運(yùn)行時(shí)注解一般和反射機(jī)制配合使用泼舱,相比編譯時(shí)注解性能比較低等缀,但靈活性好,實(shí)現(xiàn)起來比較簡單娇昙,所以我們先來用這個(gè)去實(shí)現(xiàn)尺迂。
1. 我們先去創(chuàng)建文件和寫一個(gè)注解
/* 用來指明注解的訪問范圍
* 1.源碼級(jí)注解SOURCE,該類型的注解信息會(huì)留在.java源碼中涯贞,
* 源碼編譯后枪狂,注解信息會(huì)被丟棄,不會(huì)保留在編譯好的.class文件中宋渔;
* 2.編譯時(shí)注解CLASS州疾,注解信息會(huì)保留在.java源碼里和.class文件中,
* 在執(zhí)行的時(shí)候皇拣,會(huì)被Java虛擬機(jī)丟棄不回家再到虛擬機(jī)中严蓖;
* 3.運(yùn)行時(shí)注解RUNTIME薄嫡,java源碼里,.class文件中和Java虛擬機(jī)在運(yùn)行期也保留注解信息颗胡,
* 可通過反射讀取
*/
@Retention(RUNTIME)
//是一個(gè)ElementType類型的數(shù)組毫深,用來指定注解所使用的對(duì)象范圍
@Target(value = FIELD)
public @interface Add {
float ele1() default 0f;
float ele2() default 0f;
}
可以看到,因?yàn)槭沁\(yùn)行時(shí)注解毒姨,所以我們定義了@Retention是Runtime哑蔫,定義了ele1,ele2兩個(gè)看上去像函數(shù)的變量(在注解里這樣寫算是變量而不是方法或函數(shù))
2. 下面我們使用反射去告訴這個(gè)注解你應(yīng)該做什么
public class InjectorProcessor {
public void process(final Object object) {
Class class1 = object.getClass();
//找到類里所有變量Field
Field[] fields = class1.getDeclaredFields();
//遍歷Field數(shù)組
for(Field field:fields){
//找到相應(yīng)的擁有Add注解的Field
Add addMethod = field.getAnnotation(Add.class);
if (addMethod != null){
if(object instanceof Activity){
//獲取注解中ele1和ele2兩個(gè)數(shù)字弧呐,然后把他們相加
double d = addMethod.ele1() + addMethod.ele2();
try {
//把相加結(jié)果的值賦給該Field
field.setDouble(object,d);
}catch (Exception e){
}
}
}
}
}
}
就這樣闸迷,我們利用了反射,告訴了Add這個(gè)注解俘枫,在代碼里找到你的時(shí)候腥沽,你該做什么,把工作做好鸠蚪,你就有飯吃今阳。
3.使用
很快,我們就用第一種方式實(shí)現(xiàn)了給出的題目茅信;確實(shí)盾舌,在代碼量上這種方式比較簡單粗暴,但是這種方式并不常用汹押。
一矿筝、編譯時(shí)自定義注解:
有不常用的方式,肯定就有常用的方式棚贾,下面我們就來介紹這個(gè)常用的方式——注解處理器
著名的第三方框架ButterKnife也就是用這種方式去實(shí)現(xiàn)注解綁定控件的功能的。
注解處理器是(Annotation Processor)是javac的一個(gè)工具榆综,用來在編譯時(shí)掃描和編譯和處理注解(Annotation)妙痹。你可以自己定義注解和注解處理器去搞一些事情。一個(gè)注解處理器它以Java代碼或者(編譯過的字節(jié)碼)作為輸入鼻疮,生成文件(通常是java文件)怯伊。這些生成的java文件不能修改,并且會(huì)同其手動(dòng)編寫的java代碼一樣會(huì)被javac編譯判沟」⑶郏看到這里加上之前理解,應(yīng)該明白大概的過程了挪哄,就是把標(biāo)記了注解的類吧秕,變量等作為輸入內(nèi)容,經(jīng)過注解處理器處理迹炼,生成想要生成的java代碼砸彬。
我們可以看到所有注解都會(huì)在編譯的時(shí)候就把代碼生成颠毙,而且高效、避免在運(yùn)行期大量使用反射砂碉,不會(huì)對(duì)性能造成損耗蛀蜜。
下面我們就看看怎么去實(shí)現(xiàn)一個(gè)注解處理器:
1. 建立工程:
- 首先創(chuàng)建一個(gè)project;
- 創(chuàng)建lib_annotations增蹭,
這是一個(gè)純java的module滴某,不包含任何android代碼,只用于存放注解滋迈。 - 創(chuàng)建lib_compiler壮池,
這同樣是一個(gè)純java的module。該module依賴于步驟2創(chuàng)建的module_annotation,處理注解的代碼都在這里杀怠,該moduule最終不會(huì)被打包進(jìn)apk椰憋,所以你可以在這里導(dǎo)入任何你想要的任意大小依賴庫。 - 創(chuàng)建lib_api,
對(duì)該module不做要求赔退,可以是android library或者java library或者其他的橙依。該module用于調(diào)用步驟3生成的輔助類方法。
為什么我們要新建這么多module呢硕旗,原因很簡單窗骑,因?yàn)橛行煸诰幾g時(shí)起作用,有些在運(yùn)行時(shí)起作用漆枚,把他們放在同一個(gè)module下會(huì)報(bào)錯(cuò)创译,所以我們秉著各司其職的理念把他們都分開了。
2.在module的lib_annotations創(chuàng)建Add注解
跟第一種方法不同墙基,我們?cè)贎Retention選擇的是CLASS软族,雖然選擇RUNTIME也是可以的,但是為了顯示區(qū)別残制,我們還是作了修改立砸。
3.寫注解處理器
在寫注解處理器之前我們必須在lib_compiler中引入兩個(gè)庫輔助我們成就大業(yè):
- auto-service:
AutoService會(huì)自動(dòng)在META-INF文件夾下生成Processor配置信息文件,該文件里就是實(shí)現(xiàn)該服務(wù)接口的具體實(shí)現(xiàn)類初茶。而當(dāng)外部程序裝配這個(gè)模塊的時(shí)候颗祝,
就能通過該jar包META-INF/services/里的配置文件找到具體的實(shí)現(xiàn)類名,并裝載實(shí)例化恼布,完成模塊的注入螺戳。
基于這樣一個(gè)約定就能很好的找到服務(wù)接口的實(shí)現(xiàn)類,而不需要再代碼里制定折汞,方便快捷倔幼。 - javapoet:JavaPoet是square推出的開源java代碼生成框架,提供Java Api生成.java源文件字支。這個(gè)框架功能非常有用凤藏,我們可以很方便的使用它根據(jù)注解奸忽、數(shù)據(jù)庫模式、協(xié)議格式等來對(duì)應(yīng)生成代碼揖庄。通過這種自動(dòng)化生成代碼的方式栗菜,可以讓我們用更加簡潔優(yōu)雅的方式要替代繁瑣冗雜的重復(fù)工作。
我們先在lib_compiler中創(chuàng)建一個(gè)基類
public class AnnotatedClass {
public Element mClassElement;
/**
* 元素相關(guān)的輔助類
*/
public Elements mElementUtils;
public TypeMirror elementType;
public Name elementName;
//加法的兩個(gè)值
private float value1;
private float value2;
public AnnotatedClass(Element classElement) {
this.mClassElement = classElement;
this.elementType = classElement.asType();
this.elementName = classElement.getSimpleName();
value1 = mClassElement.getAnnotation(Add.class).ele1();
value2 = mClassElement.getAnnotation(Add.class).ele2();
}
Name getElementName() {
return elementName;
}
TypeMirror getElementType(){
return elementType;
}
Float getTotal(){
return (value1 + value2);
}
/**
* 包名
*/
public String getPackageName(TypeElement type) {
return mElementUtils.getPackageOf(type).getQualifiedName().toString();
}
/**
* 類名
*/
private static String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
}
}
然后我們的主角就要出場(chǎng)了——注解處理器
我們創(chuàng)建一個(gè)文件蹄梢,然后繼承AbstractProcessor
@AutoService(Processor.class)
public class AddProcessor extends AbstractProcessor{
private static final String ADD_SUFFIX = "_Add";
private static final String TARGET_STATEMENT_FORMAT = "target.%1$s = %2$s";
private static final String CONST_PARAM_TARGET_NAME = "target";
private static final char CHAR_DOT = '.';
private Messager messager;
private Types typesUtil;
private Elements elementsUtil;
private Filer filer;
/**
* 解析的目標(biāo)注解集合疙筹,一個(gè)類里可以包含多個(gè)注解,所以是Map<String, List<AnnotatedClass>>
*/
Map<String, List<AnnotatedClass>> annotatedElementMap = new LinkedHashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
messager = processingEnv.getMessager();
typesUtil = processingEnv.getTypeUtils();
elementsUtil = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<String>();
annotataions.add(Add.class.getCanonicalName());
return annotataions;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
因?yàn)樵摲椒赡軙?huì)執(zhí)行多次禁炒,所以每次進(jìn)來必須clear
annotatedElementMap.clear();
//1.遍歷每個(gè)有Add注解的Element而咆,
//2.然后把它加入Map里面,一個(gè)類里可以包含多個(gè)注解,所以是Map<String, List<AnnotatedClass>>幕袱,
//3.賦予它工作任務(wù)暴备,告訴他你該做什么,
//4.然后生成Java文件
for (Element element : roundEnv.getElementsAnnotatedWith(Add.class)) {
//判斷被注解的類型是否符合要求
if (element.getKind() != ElementKind.FIELD) {
messager.printMessage(Diagnostic.Kind.ERROR, "Only FIELD can be annotated with @%s");
}
TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
String fullClassName = encloseElement.getQualifiedName().toString();
AnnotatedClass annotatedClass = new AnnotatedClass(element);
//把類名和該類里面的所有關(guān)于Add注解的注解放到Map里面
if(annotatedElementMap.get(fullClassName) == null){
annotatedElementMap.put(fullClassName, new ArrayList<AnnotatedClass>());
}
annotatedElementMap.get(fullClassName).add(annotatedClass);
}
//因?yàn)樵摲椒〞?huì)執(zhí)行多次,所以size=0時(shí)返回true結(jié)束
if (annotatedElementMap.size() == 0) {
return true;
}
//用javapoet生成類文件
try {
for (Map.Entry<String, List<AnnotatedClass>> entry : annotatedElementMap.entrySet()) {
MethodSpec constructor = createConstructor(entry.getValue());
TypeSpec binder = createClass(getClassName(entry.getKey()), constructor);
JavaFile javaFile = JavaFile.builder(getPackage(entry.getKey()), binder).build();
javaFile.writeTo(filer);
}
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR, "Error on creating java file");
}
return true;
}
//以下是javapoet創(chuàng)建各種方法的實(shí)現(xiàn)方式
private MethodSpec createConstructor(List<AnnotatedClass> randomElements) {
AnnotatedClass firstElement = randomElements.get(0);
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.get(firstElement.mClassElement.getEnclosingElement().asType()), CONST_PARAM_TARGET_NAME);
for (int i = 0; i < randomElements.size(); i++) {
addStatement(builder, randomElements.get(i));
}
return builder.build();
}
private void addStatement(MethodSpec.Builder builder, AnnotatedClass randomElement) {
builder.addStatement(String.format(
TARGET_STATEMENT_FORMAT,
randomElement.getElementName().toString(),
randomElement.getTotal())
);
}
private TypeSpec createClass(String className, MethodSpec constructor) {
return TypeSpec.classBuilder(className + ADD_SUFFIX)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(constructor)
.build();
}
private String getPackage(String qualifier) {
return qualifier.substring(0, qualifier.lastIndexOf(CHAR_DOT));
}
private String getClassName(String qualifier) {
return qualifier.substring(qualifier.lastIndexOf(CHAR_DOT) + 1);
}
}
我們可以看到注解處理器總共有四個(gè)方法,他們分別的作用是:
-
init() 可選
在該方法中可以獲取到processingEnvironment對(duì)象次泽,借由該對(duì)象可以獲取到生成代碼的文件對(duì)象, debug輸出對(duì)象,以及一些相關(guān)工具類
-
getSupportedSourceVersion()
返回所支持的java版本障癌,一般返回當(dāng)前所支持的最新java版本即可
-
getSupportedAnnotationTypes()
你所需要處理的所有注解,該方法的返回值會(huì)被process()方法所接收
-
process() 必須實(shí)現(xiàn)
掃描所有被注解的元素辩尊,并作處理涛浙,最后生成文件。該方法的返回值為boolean類型摄欲,若返回true,則代表本次處理的注解已經(jīng)都被處理轿亮,不希望下一個(gè)注解處理器繼續(xù)處理,否則下一個(gè)注解處理器會(huì)繼續(xù)處理蒿涎。
4.使用
好了哀托,打了這么多代碼,我們先看下編譯時(shí)生成的代碼和文件是怎么樣的劳秋,就會(huì)使用了:
我們可以看到,我們?cè)谧⒔馓幚砥骼锩鎸懥四敲炊啻a胖齐,就是為了生成Build目錄下的.class文件玻淑,是自動(dòng)生成的。
看到了生成的AnnotationActivity_Add的文件呀伙,我們下面就去寫一個(gè)注入方法补履,把我們想要結(jié)果拿出來展示:
我們看到Util里面我們實(shí)現(xiàn)了想要的東西,把AnnotationActivity_Add的結(jié)果找出來再賦值給相應(yīng)的變量剿另。
總結(jié)
我們成功的用兩種不同的注解方式實(shí)現(xiàn)了兩數(shù)相加的運(yùn)算箫锤,1.運(yùn)用的是反射贬蛙,2.運(yùn)用的是注解處理器。雖然看上去注解處理器的方式比較繁瑣谚攒,但是使用比較普遍阳准,而且有很多好處,這里就不一一述說馏臭。如果有興趣學(xué)習(xí)的同學(xué)可以下載源碼去學(xué)習(xí)一下野蝇,互相交流,共同學(xué)習(xí)括儒。Demo源碼下載鏈接
參考文章: