在JDK 1.5以後,java提供了對註解的支持,這些註解與普通代碼同樣,在運行期間發揮做用。在JDK 1.6中實現了JSR-269規範,提供了一組插入式註解處理器的標準API在編譯期間對註解進行處理,能夠看做是一組編譯器的插件,能夠讀取/修改/添加抽象語法樹中的任意元素。java
在Android模塊開發之APT技術介紹了自定義註解處理器的一些知識,自定義註解處理器註冊才能被Java虛擬機調用,在上面的博客第四小節中用的方法是手動註冊,這比較違反程序員懶的特色,在裏面也提到了自動註冊的方法,就是AutoService,今天這篇博客就是來扒一扒谷歌提供的這個開源庫。git
先經過一個栗子看下AutoService怎麼用的。程序員
定義一個簡單的接口:github
public interface Display {
String display();
}
複製代碼
有兩個Module A和B分別實現了這個接口,而後在app Module中調用這兩個實現類, 比較低級的辦法就是在app Module中直接依賴這兩個模塊,而後就能夠調用實現類了。這有兩個壞處,一個是app Module直接強依賴A和B兩個Module,另外若是開發中拿不到依賴的模塊呢,有可能模塊是第三方的,這個時候強依賴這種方式就行不通了。bash
看下AutoService是怎麼實現的,先看下包結構,interfaces只簡單包含上面的Display接口,modulea和moduleb實現這個接口,app統一加載全部這個接口的實現類。 app
看下modulea和moduleb實現,方法實現裏面簡單返回一個字符串,主要是上面的@AutoService(Display.class)註解,註解值是接口的名稱,也就是implements實現的類接口名稱。框架
// modulea
import com.google.auto.service.AutoService;
@AutoService(Display.class)
public class ADisplay implements Display{
@Override
public String display() {
return "A Display";
}
}
// moduleb
@AutoService(Display.class)
public class BDisplay implements Display {
@Override
public String display() {
return "B Display";
}
}
複製代碼
再看下app Module裏面的怎麼調用上面的ADispaly和BDisplay,加載原理就是經過ServiceLoader去加載,能夠獲得接口Display
的全部實現類,在咱們這個栗子中就是上面的ADisplay
和BDisplay
兩個實現者。DisplayFactory
經過getDisplay
能夠拿到全部的實現類。ide
import com.example.juexingzhe.interfaces.Display;
import java.util.Iterator;
import java.util.ServiceLoader;
public class DisplayFactory {
private static DisplayFactory mDisplayFactory;
private Iterator<Display> mIterator;
private DisplayFactory() {
ServiceLoader<Display> loader = ServiceLoader.load(Display.class);
mIterator = loader.iterator();
}
public static DisplayFactory getSingleton() {
if (null == mDisplayFactory) {
synchronized (DisplayFactory.class) {
if (null == mDisplayFactory) {
mDisplayFactory = new DisplayFactory();
}
}
}
return mDisplayFactory;
}
public Display getDisplay() {
return mIterator.next();
}
public boolean hasNextDisplay() {
return mIterator.hasNext();
}
}
複製代碼
使用就是這麼幾個步驟,比較簡單,下面看下AutoService實現原理。函數
首先先簡單介紹下Javac的編譯過程,大體能夠分爲3個過程:工具
看下一個圖片,圖片來源深刻理解Java虛擬機,首先會進行詞法和語法分析,詞法分析將源代碼的字符流轉變爲Token集合,關鍵字/變量名/字面量/運算符讀能夠成爲Token,詞法分析過程由com.sun.tools.javac.parserScanner類實現;
語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹AST是一種用來描述程序代碼語法結構的樹形表示,語法樹的每個節點讀表明着程序代碼中的一個語法結構,例如包/類型/修飾符/運算符/接口/返回值/代碼註釋等,在javac的源碼中,語法分析是由com.sun.tools.javac.parser.Parser類實現,這個階段產出的抽象語法樹由com.sun.tools.javac.tree.JCTree類表示。通過上面兩個步驟編譯器就基本不會再對源碼文件進行操做了,後續的操做讀創建在抽象語法樹上。
完成了語法和詞法分析後就是填充符號表的過程。符號表是由一組符號地址和符號信息構成的表格。填充符號表的過程由com.sun.tools.javac.comp.Enter類實現。
如前面介紹的,若是註解處理器在處理註解期間對語法樹進行了修改,編譯器將回到解析與填充符號表的過程從新處理,直到全部插入式註解處理器都沒有再對語法樹進行修改成止,每一次循環稱爲一個Round,以下圖中的環。
上面簡單回顧了下編譯註解的一些東西,接下來看下AutoService這個註解的實現,使用它有三個限定條件;
@Documented
@Target(TYPE)
public @interface AutoService {
/** Returns the interface implemented by this service provider. */
Class<?> value();
}
複製代碼
有註解,必需要有對應的註解處理器,AutoServiceProcessor
繼承AbstractProcessor
,通常咱們會實現其中的3個方法, 在getSupportedAnnotationTypes
中返回了支持的註解類型AutoService.class
;getSupportedSourceVersion
,用來指定支持的java版本,通常來講咱們都是支持到最新版本,所以直接返回 SourceVersion.latestSupported()便可;主要仍是process方法。
public class AutoServiceProcessor extends AbstractProcessor {
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoService.class.getName());
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
try {
return processImpl(annotations, roundEnv);
} catch (Exception e) {
// We don't allow exceptions of any kind to propagate to the compiler StringWriter writer = new StringWriter(); e.printStackTrace(new PrintWriter(writer)); fatalError(writer.toString()); return true; } } } 複製代碼
process方法調用processImpl
,接着看下這個方法的實現,先看下方法實現,就兩個邏輯判斷,若是上一次循環中註解處理器已經處理完了,就調用generateConfigFiles
生成MEATA_INF配置文件;若是上一輪沒有處理就調用processAnnotations
處理註解。返回true就表明改變或者生成語法樹中的內容;返回false就是沒有修改或者生成,通知編譯器這個Round中的代碼未發生變化。
private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
generateConfigFiles();
} else {
processAnnotations(annotations, roundEnv);
}
return true;
}
複製代碼
再接着往下看代碼以前先看下兩個環境變量,RoundEnvironment
和ProcessingEnvironment
。
RoundEnvironment
提供了訪問到當前這個Round中語法樹節點的功能,每一個語法樹節點在這裏表示爲一個Element,在javax.lang.model包中定義了16類Element,包括經常使用的元素:包,枚舉,類,註解,接口,枚舉值,字段,參數,本地變量,異常,方法,構造函數,靜態語句塊即static{}塊,實例語句塊即{}塊,參數化類型即檢討尖括號內的類型,還有未定義的其餘語法樹節點。
public enum ElementKind {
PACKAGE,
ENUM,
CLASS,
ANNOTATION_TYPE,
INTERFACE,
ENUM_CONSTANT,
FIELD,
PARAMETER,
LOCAL_VARIABLE,
EXCEPTION_PARAMETER,
METHOD,
CONSTRUCTOR,
STATIC_INIT,
INSTANCE_INIT,
TYPE_PARAMETER,
OTHER,
RESOURCE_VARIABLE;
private ElementKind() {
}
public boolean isClass() {
return this == CLASS || this == ENUM;
}
public boolean isInterface() {
return this == INTERFACE || this == ANNOTATION_TYPE;
}
public boolean isField() {
return this == FIELD || this == ENUM_CONSTANT;
}
}
複製代碼
看下RoundEnvironment
的源碼,errorRaised
方法返回上一輪註解處理器是否產生錯誤;getRootElements
返回上一輪註解處理器生成的根元素;最後兩個方法返回包含指定註解類型的元素的集合,畫重點,這個就是咱們自定義註解處理器須要常常打交道的方法。
public interface RoundEnvironment {
boolean processingOver();
boolean errorRaised();
Set<? extends Element> getRootElements();
Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1);
}
複製代碼
另一個參數ProcessingEnvironment
,在註解處理器初始化的時候(init()方法執行的時候)建立,表明了註解處理器框架提供的一個上下文環境,要建立新的代碼或者向編譯器輸出信息或者獲取其餘工具類等都須要用到這個實例變量。看下它的源碼。
Messager
用來報告錯誤,警告和其餘提示信息;Filer
用來建立新的源文件,class文件以及輔助文件;Elements
中包含用於操做Element的工具方法;Types
中包含用於操做類型TypeMirror的工具方法;public interface ProcessingEnvironment {
Map<String, String> getOptions();
Messager getMessager();
Filer getFiler();
Elements getElementUtils();
Types getTypeUtils();
SourceVersion getSourceVersion();
Locale getLocale();
}
複製代碼
介紹完一些基礎變量後,咱們就接着上面先看下processAnnotations
方法,方法看起來有點長,可是結構很簡單,首先第一步經過RoundEnvironment
的getElementsAnnotatedWith(AutoService.class)
拿到全部的標註了AutoService
註解的元素。
private void processAnnotations(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
// 1.
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
log(annotations.toString());
log(elements.toString());
for (Element e : elements) {
// TODO(gak): check for error trees?
// 2.
TypeElement providerImplementer = (TypeElement) e;
// 3.
AnnotationMirror providerAnnotation = getAnnotationMirror(e, AutoService.class).get();
// 4.
DeclaredType providerInterface = getProviderInterface(providerAnnotation);
TypeElement providerType = (TypeElement) providerInterface.asElement();
log("provider interface: " + providerType.getQualifiedName());
log("provider implementer: " + providerImplementer.getQualifiedName());
// 5.
if (!checkImplementer(providerImplementer, providerType)) {
String message = "ServiceProviders must implement their service provider interface. "
+ providerImplementer.getQualifiedName() + " does not implement "
+ providerType.getQualifiedName();
error(message, e, providerAnnotation);
}
// 6.
String providerTypeName = getBinaryName(providerType);
String providerImplementerName = getBinaryName(providerImplementer);
log("provider interface binary name: " + providerTypeName);
log("provider implementer binary name: " + providerImplementerName);
providers.put(providerTypeName, providerImplementerName);
}
}
public static Optional<AnnotationMirror> getAnnotationMirror(Element element,
Class<? extends Annotation> annotationClass) {
String annotationClassName = annotationClass.getCanonicalName();
for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
TypeElement annotationTypeElement = asType(annotationMirror.getAnnotationType().asElement());
if (annotationTypeElement.getQualifiedName().contentEquals(annotationClassName)) {
return Optional.of(annotationMirror);
}
}
return Optional.absent();
}
複製代碼
AutoService
只能做用於非內部非匿名類或者接口,第二步在for循環中強轉Element爲TypeElement,這個就是被AutoService
標註的元素,這裏簡稱爲T。接下來這個可能讓人容易亂,在前面說過每個javac是一個循環過程,在第一次掃描到AutoService
註解的時候是尚未T的class對象,因此也就不能經過反射來拿到這個註解和註解的參數值value。這個時候第三步就得經過AnnotationMirror
,用來表示一個註解,經過它能夠拿到註解類型和註解參數。在getAnnotationMirror
會判斷這個T的註解(經過element.getAnnotationMirrors()
)名稱是否是等於AutoService
,相等就返回這個AutoService
的AnnotationMirror
。
public interface AnnotationMirror {
DeclaredType getAnnotationType();
Map<? extends ExecutableElement, ? extends AnnotationValue> getElementValues();
}
複製代碼
拿到這個註解了,接下來就是要拿到註解的參數value值了,這個在第四步getProviderInterface
方法中完成。
private DeclaredType getProviderInterface(AnnotationMirror providerAnnotation) {
Map<? extends ExecutableElement, ? extends AnnotationValue> valueIndex =
providerAnnotation.getElementValues();
log("annotation values: " + valueIndex);
AnnotationValue value = valueIndex.values().iterator().next();
return (DeclaredType) value.getValue();
}
複製代碼
這裏也是同上面的緣由,在這個階段咱們不可能經過下面的代碼反射來拿到註解的參數值,由於這個時候還拿不到class對象。因此上面費了很大的勁去經過AnnotationMirror
來拿到註解的參數值,在咱們這個栗子中就是Display.class
了。
AutoService autoservice = e.getAnnotation(AutoService.class);
Class<?> providerInterface = autoservice.value()
複製代碼
接下來第5步檢查類型T是否是實現了註解參數值說明的接口,也就是ADisplay
和BDisplay
是否是實現了Display
接口,沒有實現確定就是沒有意義了。第6步就是獲取到接口名和實現類名,註冊到map中,相似於Map<Display, [ADisplay, BDisplay]>這種形式,即key是接口名,value是實現了接口也就是註解AutoService
標註的實現類。
經過上面的步驟就已經掃描獲得了全部的經過AutoService
標註的實現類和對應接口的映射關係,而且在processImpl
裏面返回了true
,下個Round就是生成配置文件了。看下processImpl
if分支裏面的generateConfigFiles
方法。
private void generateConfigFiles() {
Filer filer = processingEnv.getFiler();
// 1.
for (String providerInterface : providers.keySet()) {
String resourceFile = "META-INF/services/" + providerInterface;
log("Working on resource file: " + resourceFile);
try {
SortedSet<String> allServices = Sets.newTreeSet();
try {
// 2.
FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
log("Looking for existing resource file at " + existingFile.toUri());
// 3.
Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
log("Existing service entries: " + oldServices);
allServices.addAll(oldServices);
} catch (IOException e) {
// According to the javadoc, Filer.getResource throws an exception
// if the file doesn't already exist. In practice this doesn't
// appear to be the case. Filer.getResource will happily return a
// FileObject that refers to a non-existent file but will throw
// IOException if you try to open an input stream for it.
log("Resource file did not already exist.");
}
// 4.
Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
if (allServices.containsAll(newServices)) {
log("No new service entries being added.");
return;
}
allServices.addAll(newServices);
log("New service file contents: " + allServices);
// 5.
FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
OutputStream out = fileObject.openOutputStream();
ServicesFiles.writeServiceFile(allServices, out);
out.close();
log("Wrote to: " + fileObject.toUri());
} catch (IOException e) {
fatalError("Unable to create " + resourceFile + ", " + e);
return;
}
}
}
複製代碼
主要分紅5個步驟要生成配置文件,分別來看下:
META-INF/services/com.example.juexingzhe.interfaces.Display
allServices
中processAnnotations
方法輸出的映射map是否不存在上面的allServices
,不存在則添加,存在則直接返回不須要生成新的文件resourceFile
,文件內容就是allServices
中的全部實現類。最後咱們看下編譯編譯的結果,在每一個module
中都會生成配置文件
最後在apk中會合並全部的META-INF文件目錄,能夠看到在接口文件Display下面包含了全部module中經過AutoService
註解標註的實現類。
最後經過ServiceLoader
就能夠經過反射拿到全部的實現類,ServiceLoader
的源碼分析能夠參考個人另一片博客Android模塊開發之SPI.
此次的源碼分析實際上是完成以前在Android模塊開發之APT技術立下的flag,到今天才補上有點慚愧。AutoService源碼解析結合APT和SPI服用效果可能更佳哦。
這個也是組件化技術中經常使用的一個技術點,後面會再更新一些組件化gradle相關的一些知識,有須要的小夥伴們歡迎關注。
完。
參考: 深刻理解Java虛擬機