序言
注解是Java程序和Android程序中常見的語法,之前雖然知道有這么個東西,但并沒有深入了解注解。寫EventBus源碼解析和ButterKnife源碼解析的時候,發現注解在其中起到很大作用,就決定專門寫一篇文章介紹注解。
下面將會從這幾個方面展開介紹:
- 注解的概念和語法
- 運行時注解
- 編譯時注解(APT技術)
- 對比運行時和編譯時注解
- 總結
注解的概念和語法
1. 注解的概念
定義:注解用于為Java提供元數據,作為元數據,注解不影響代碼執行,但某些類型注解也可以用于這一目的,注解從Java5開始引入
2. 注解的語法
注解通過@interface
關鍵字來定義。
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface MyAnnotation {}
3. 元注解
在上面的定義中,Retention
和Target
是什么東西?它們為什么能夠修飾注解。
實際上,它們是元注解:元注解是可以注解到注解上的注解,簡單來說就是一種基本注解,可以作用到其他注解上。
Java中總共有5中元注解:@Retention,@Documented,@Target,@Inherited,@Repeatable
。下面分別介紹它們:
@Retention
用來說明注解的存活時間,有三種取值:
- RetentionPolicy.SOURCE:注解只在源碼階段保留,編譯器開始編譯時它將被丟棄忽視
- RetentionPolicy.CLASS:注解會保留到編譯期,但運行時不會把它加載到JVM中
- RetentionPolicy.RUNTIME:注解可以保留到程序運行時,它會被加載到JVM中,所以程序運行過程中可以獲取到它們
編譯期注解和運行時注解使用得比較多,下面會有兩個主題專門介紹。
@Target
指定注解可作用的目標,取值如下:
- ElementType.PACKAGE:可作用在包上
- ElementType.TYPE:可作用在類、接口、枚舉上
- ElementType.ANNOTATION_TYPE:可以作用在注解上
- ElementType.FIELD:可作用在屬性上
- ElementType.CONSTRUCTOR:可作用在構造方法上
- ElementType.METHOD:可作用在方法上
- ElementType.PARAMETER:可作用在方法參數上
- ElementType.LOCAL_VARIABLE:可作用在局部變量上,例如方法中定義的變量
它接收一個數組作為參數,即可以指定多個作用對象,就像上面的Demo:
@Target({ElementType.FIELD, ElementType.TYPE})
@Documented
從名字可知,這個注解跟文檔相關,它的作用是能夠將注解中的元素包含到Javadoc中去。
@Inherited
Inherited是繼承的意思,但并不是注解本身可被繼承,而是指一個父類SuperClass被該類注解修飾,那么它的子類SubClass如果沒有任何注解修飾,就會繼承父類的這個注解。
舉個栗子:
@Inherited
@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}
@Test
public class A {}
public class B extens A {}
解釋:注解Test被@Inherited修飾,A被Test修飾,B繼承A(B上又無其他注解),那么B就會擁有Test這個注解。
@Repeatable
這個詞是可重復的意思,它是java1.8引入的,算一個新特性。
什么樣的注解可以多次應用來呢,通常是注解可以取多個值,舉個栗子:
public @Interface Persons {
Person[] value();
}
@Repeatable(Persons.class)
public @Interface Person {
String role() default ""
}
@Person("artist")
@Person("developer")
@Person("superman")
public class Me {}
解釋:@Person被@Repeatable修飾,所以Person可以多次作用在同一個對象Me上,而Repeatable接收一個參數,這個參數是個容器注解,用來存放多個@Person。
4. 注解的屬性
注解中可以定義屬性,也可以叫成員變量,不能定義方法。
就如上面的例子:
- @Person中定義了一個屬性role,在使用的過程中就可以傳一個字符串
- 又給role設置了默認值為空字符串,以就算不傳可以直接使用
- 如果有多個屬性,就必須以
key=value
的形式指定屬性值
注解中的屬性支持8種基本類型外加字符串、類、接口、注解及以上類型的數組
5. Java預置注解
Java中提供了很多注解,如:
- @Override:表示覆寫父類中的方法
- @Depracated:標記過時的類、方法、成員變量
- @FunctionalInterface:Java1.8引入的新特性,表示函數式接口(只有一個方法的普通接口),主要用于lambda表達式。
- ......
運行時注解
上面介紹過,用Retention(RetentionPolicy.RUNTIME)
修飾的就是運行時注解。使用這種注解,多數情況是為了在運行時做一些事情。至于具體做什么事?就看各位同學自己的意愿了。
這里,我通過一個例子來介紹怎么使用運行時注解。
現在,我打算通過運行時注解實現一個功能,跟ButterKnife類似,即自動注入功能,不需要我手動調用findViewById。
下面是實現的步驟:
1. 定義注解
/**
* author : user_zf
* date : 2018/11/6
* desc : 運行時通過反射自動注入View,不再需要寫findViewById
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectView {
@IdRes int id() default -1;
}
這個注解中有一個屬性id,表示待注入控件的id
2. 定義注解解析工具
/**
* author : user_zf
* date : 2018/11/6
* desc : 用來解析注解InjectView
*/
public class AnnotationUtil {
/**
* 解析注解InjectView
*
* @param activity 使用InjectView的目標對象
*/
public static void inject(Activity activity) {
Field[] fields = activity.getClass().getDeclaredFields();
//通過該方法設置所有的字段都可訪問,否則即使是反射,也不能訪問private修飾的字段
AccessibleObject.setAccessible(fields, true);
for (Field field : fields) {
boolean needInject = field.isAnnotationPresent(InjectView.class);
if (needInject) {
InjectView anno = field.getAnnotation(InjectView.class);
int id = anno.id();
if (id == -1) continue;
View view = activity.findViewById(id);
Class fieldType = field.getType();
try {
//把View轉換成field聲明的類型
field.set(activity, fieldType.cast(view));
} catch (Exception e) {
Log.e(InjectView.class.getSimpleName(), e.getMessage());
}
}
}
}
}
主要是通過反射,找到Activity中使用了@InjectView的字段,然后通過findViewById來初始化控件。
3. 使用注解
class MainActivity : AppCompatActivity() {
@InjectView(id = R.id.tvHello)
private var tvHello: TextView? = null
@InjectView(id = R.id.btnHello)
private var btnHello: Button? = null
@InjectView(id = R.id.rlRoot)
private var rlRoot: RelativeLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//通過注解初始化控件
AnnotationUtil.inject(this@MainActivity)
//設置控件
tvHello?.text = "Hello World!"
btnHello?.text = "Hello Button"
btnHello?.setOnClickListener {
Toast.makeText(this@MainActivity, "點擊按鈕了", Toast.LENGTH_SHORT).show()
}
rlRoot?.setBackgroundColor(resources.getColor(R.color.colorAccent, null))
}
}
在控件上使用注解,就不需要我們手動初始化,注解解析工具會自動幫我們初始化。大大減少重復代碼。
原理:運行時注解主要通過反射進行解析,代碼運行過程中,通過反射我們可以知道哪些屬性、方法使用了該注解,并且可以獲取注解中的參數,做一些我們想做的事情
編譯時注解(APT技術)
使用Retention(RetentionPolicy.CLASS)
修飾的注解就是編譯時注解。
說到編譯時注解,就需要引出我們今天的主角:APT(編譯時解析技術)。
APT技術主要是通過編譯期解析注解,并且生成java代碼的一種技術,一般會結合Javapoet技術來生成代碼。
下面,我們還是通過一個栗子來介紹APT技術。
在寫Bean的時候經常需要寫Getter和Setter方法,我們想通過一個注解,在編譯的過程中自動幫我們生成Getter和Setter方法,這里會生成一個新的類,而不是修改原來的類。
1. 編寫注解
/**
* author : user_zf
* date : 2018/11/7
* desc : 編譯期給bean生成getter和setter方法的注解(限java類使用)
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface GenerateGS {}
2. 編寫注解解析器Processor
這里的注解解析器和運行時注解解析器不一樣,這里需要繼承AbstractProcessor類。
在Android Module和Android Library Module中是不能使用AbstractProcessor類的,需要新建一個Java Library Module,把注解解析器放在這個Java Module中,然后用Android Module依賴這個Java Module。
接下來,看一下我們的GenerateGSProcessor的實現:
/**
* author : user_zf
* date : 2018/11/7
* desc : generateGS編譯時注解解析器
*/
//@AutoService(Processor.class)
//@SupportedAnnotationTypes("study.com.aptlib.GenerateGS")
//@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class GenerateGSProcessor extends AbstractProcessor {
private Filer mFiler;
/**
* 初始化Processor和一些工具類
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
/**
* 返回該Processor能夠處理的注解
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
LinkedHashSet<String> types = new LinkedHashSet<>();
types.add(GenerateGS.class.getCanonicalName());
return types;
}
/**
* 返回Java的版本號
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 真正處理注解的方法
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
HashMap<String, HashSet<Element>> nameMap = new HashMap<>();
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(GenerateGS.class);
//遍歷處理帶注解的Element,把他們分類保存在Map中,key=包裹類 value=類中所有使用注解的Element
for (Element element : annotatedElements) {
Element parent = element.getEnclosingElement();
String parentName = parent.getSimpleName().toString();
HashSet<Element> set = nameMap.get(parentName);
if (set == null) {
set = new HashSet<>();
}
set.add(element);
nameMap.put(parentName, set);
}
generateJavaFile(nameMap);
return true;
}
/**
* 根據Map生成Java文件
*/
private void generateJavaFile(Map<String, HashSet<Element>> map) {
System.out.println("開始生成代碼");
Set<Map.Entry<String, HashSet<Element>>> nameSet = map.entrySet();
for(Map.Entry<String, HashSet<Element>> entry : nameSet) {
String className = entry.getKey();
Set<Element> fields = entry.getValue();
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(className + "$Bean")
.addModifiers(Modifier.PUBLIC);
//遍歷添加屬性和對應的getter/setter方法
for (Element element : fields) {
//只處理field
if (element.getKind().isField()) {
//獲取字段名稱
String fieldName = element.getSimpleName().toString();
//字段名稱首字母變成大寫
char[] cs = fieldName.toCharArray();
cs[0] -= 32;
String firstUpperName = String.valueOf(cs);
//獲取字段類型
TypeName type = TypeName.get(element.asType());
//生成字段
FieldSpec fieldSpec = FieldSpec.builder(type, fieldName, Modifier.PRIVATE).build();
//生成getter/setter方法
MethodSpec getterMethod = MethodSpec.methodBuilder("get" + firstUpperName)
.addModifiers(Modifier.PUBLIC)
.returns(type)
.addStatement("return " + fieldName)
.build();
MethodSpec setterMethod = MethodSpec.methodBuilder("set" + firstUpperName)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(type, fieldName)
.addStatement("this." + fieldName + " = " + fieldName)
.build();
//給$Bean添加字段及對應的getter和setter方法
typeSpecBuilder.addField(fieldSpec)
.addMethod(getterMethod)
.addMethod(setterMethod);
}
}
TypeSpec typeSpec = typeSpecBuilder.build();
JavaFile javaFile = JavaFile.builder("study.com.aptlib", typeSpec).build();
try {
javaFile.writeTo(mFiler);
System.out.println("生成" + className + "$Bean" + "類");
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("代碼生成完畢");
}
}
GenerateGSProcessor中主要有四個方法:init方法,getSupportedAnnotationTypes方法,getSupportedSourceVersion方法和process方法。代碼注釋中給出了這四種方法的作用,其中最主要的方法就是process方法, 這個方法就是用來解析注解的。而getSupportedAnnotationTypes和getSupportedSourceVersion兩個方法可以用注解來替代,分別是@SupportedAnnotationTypes
和@SupportedAnnotationTypes
,在GenerateGsProcessor的注釋中可以看到他們。
有人會問,除了那兩個注解之外,還有一個@AutoService
注解,這個是干嘛的呢?
別著急,下面我們會介紹的。
3. 添加SPI配置文件
我們先來簡單介紹一下SPI(服務提供接口Service Provider Interface)機制,主要做作用是為接口尋找服務實現。
舉個栗子,我們現在有三個模塊:common、A、B,并且A和B都依賴與common。現在,common模塊中有個接口Fly(有一個fly方法)而A中定義Fly的實現類Bird,B中定義Fly的實現類Butterfly。
在A和B中都添加配置文件,A的配置文件中寫上Bird的帶包全名,B的配置文件中寫上Butterfly帶包全名,接著在需要使用的地方,A和B都可以使用下面一段代碼:
ServiceLoader<Fly> serviceLoader = ServiceLoader.load(Fly.class, Fly.class.getClassLoader());
Iterator<Fly> it = serviceLoader.iterator();
if (it.hasNext()) {
it.next().fly();
}
這樣,在A中的效果就是Bird在飛,B中的效果是Butterfly在飛。有點類似于策略模式,可以通過配置文件動態加載。
接下來,總結一下配置方法:
1、定義接口和接口實現類
2、創建resources/META-INF/services目錄
3、在該目錄下創建一個文件,文件名為接口名(帶包全名),內容為接口實現類的帶包全名
4、在代碼中通過ServiceLoader動態加載并且調用實現類的內部方法。
好,現在讓我們回到APT技術來,APT技術中的Processor
就使用了SPI機制,接口是Process,實現類是GenerateGSProcessor,所以我們需要做下面幾件事:
-
在main目錄下創建resources/META-INF/services目錄
image.png
-
在該目錄下新建javax.annotation.processing.Processor文件
image.png
- 在文件中添加內容study.com.aptlib.GenerateGSProcessor
study.com.aptlib.GenerateGSProcessor
到這里SPI配置完畢。
可能大家會覺得這種配置方式比較麻煩,對,確實比較麻煩。我們可以使用Google提供的auto-service庫來簡化這些操作:
compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.google.auto:auto-common:0.10'
然后在GenerateGSProcessor類上添加注解:
@AutoService(Processor.class)
這就是上面提到的AutoService,用這個注解可以替代SPI的配置文件。
4. 在Android Module使用注解
首先,添加項目依賴
annotationProcessor project(':aptlib')
api project(':aptlib')
這里為什么要添加兩次呢?
- annotationProcessor:指定專門的注解解析庫
- api:表示添加注解依賴,因為我們的GenerateGs寫在aptlib庫,所以需要單獨添加這個依賴,如果注解和解析器放在不同的module,就不需要這么寫
接下來,在代碼中使用注解:
public class Person {
@GenerateGS
private String name;
@GenerateGS
private int gender;
@GenerateGS
private String hobby;
}
通過rebuild來編譯我們的項目,就會生成Person$Bean類:
package study.com.aptlib;
import java.lang.String;
public class Person$Bean {
private String hobby;
private String name;
private int gender;
public String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getGender() {
return gender;
}
public void setGender(int gender) {
this.gender = gender;
}
}
有沒有很神奇。
對比運行時和編譯時注解
在很多情況下,運行時注解和編譯時注解可以實現相同的功能,比如依賴注入框架,我們既可以在運行時通過反射來初始化控件,也可以再編譯時就生成控件初始化代碼。那么,這兩者有什么區別呢?
答:編譯時注解性能比運行時注解好,運行時注解需要使用到反射技術,對程序的性能有一定影響,而編譯時注解直接生成了源代碼,運行過程中直接執行代碼,沒有反射這個過程。
很多框架的實現都是用到了編譯時注解,如ButterKnife、EventBus、Dagger2等等。
項目中使用這些庫的時候,會有一個比較讓人疑惑的地方。就拿ButterKnife舉例。
我們使用ButterKnife時,會添加依賴:
annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'
但我們在項目結構中怎么也找不到ButterKnifeProcessor和編譯庫的代碼。這個是為什么呢?
經過一番研究,總算知道原因了,一些插件庫、注解解析庫并不會放在項目結構中,而是會放在gradle的緩存目錄中:
/Users/user_zf/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-compiler/8.6.0/d3defb48a63aa0591117d0cec09f47a13fffda19
,在這個路徑中,總算找到了butterknife-compiler-8.6.0.jar
。
總結
經過上面的介紹,相信大家對注解有了比較全面的認識。各位同學可以嘗試在項目開發過程中去使用注解,它可以大大提升我們的開發效率,減少不必要的重復代碼。