組件化之AutoService使用與源碼解析

*本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨家發(fā)布

在JDK 1.5之后,java提供了對注解的支持,這些注解與普通代碼一樣,在運行期間發(fā)揮作用。在JDK 1.6中實現(xiàn)了JSR-269規(guī)范,提供了一組插入式注解處理器的標準API在編譯期間對注解進行處理,可以看作是一組編譯器的插件,可以讀取/修改/添加抽象語法樹中的任意元素。

Android模塊開發(fā)之APT技術(shù)介紹了自定義注解處理器的一些知識,自定義注解處理器注冊才能被Java虛擬機調(diào)用,在上面的博客第四小節(jié)中用的方法是手動注冊,這比較違反程序員懶的特點,在里面也提到了自動注冊的方法,就是AutoService,今天這篇博客就是來扒一扒谷歌提供的這個開源庫。

先通過一個栗子看下AutoService怎么用的。

1.使用

定義一個簡單的接口:

public interface Display {
    String display();
}

有兩個Module A和B分別實現(xiàn)了這個接口,然后在app Module中調(diào)用這兩個實現(xiàn)類, 比較低級的辦法就是在app Module中直接依賴這兩個模塊,然后就可以調(diào)用實現(xiàn)類了。這有兩個壞處,一個是app Module直接強依賴A和B兩個Module,另外如果開發(fā)中拿不到依賴的模塊呢,有可能模塊是第三方的,這個時候強依賴這種方式就行不通了。

看下AutoService是怎么實現(xiàn)的,先看下包結(jié)構(gòu),interfaces只簡單包含上面的Display接口,modulea和moduleb實現(xiàn)這個接口,app統(tǒng)一加載所有這個接口的實現(xiàn)類。


包結(jié)構(gòu).png

看下modulea和moduleb實現(xiàn),方法實現(xiàn)里面簡單返回一個字符串,主要是上面的@AutoService(Display.class)注解,注解值是接口的名稱,也就是implements實現(xiàn)的類接口名稱。

// 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里面的怎么調(diào)用上面的ADispaly和BDisplay,加載原理就是通過ServiceLoader去加載,可以得到接口Display的所有實現(xiàn)類,在我們這個栗子中就是上面的ADisplayBDisplay兩個實現(xiàn)者。DisplayFactory通過getDisplay可以拿到所有的實現(xiàn)類。

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實現(xiàn)原理。

2.實現(xiàn)原理

首先先簡單介紹下Javac的編譯過程,大致可以分為3個過程:

  • 解析與填充符號表
  • 插入式注解處理器的注解處理過程
  • 分析與字節(jié)碼生成過程

看下一個圖片,圖片來源深入理解Java虛擬機,首先會進行詞法和語法分析,詞法分析將源代碼的字符流轉(zhuǎn)變?yōu)門oken集合,關(guān)鍵字/變量名/字面量/運算符讀可以成為Token,詞法分析過程由com.sun.tools.javac.parserScanner類實現(xiàn);

語法分析是根據(jù)Token序列構(gòu)造抽象語法樹的過程,抽象語法樹AST是一種用來描述程序代碼語法結(jié)構(gòu)的樹形表示,語法樹的每一個節(jié)點讀代表著程序代碼中的一個語法結(jié)構(gòu),例如包/類型/修飾符/運算符/接口/返回值/代碼注釋等,在javac的源碼中,語法分析是由com.sun.tools.javac.parser.Parser類實現(xiàn),這個階段產(chǎn)出的抽象語法樹由com.sun.tools.javac.tree.JCTree類表示。經(jīng)過上面兩個步驟編譯器就基本不會再對源碼文件進行操作了,后續(xù)的操作讀建立在抽象語法樹上。

完成了語法和詞法分析后就是填充符號表的過程。符號表是由一組符號地址和符號信息構(gòu)成的表格。填充符號表的過程由com.sun.tools.javac.comp.Enter類實現(xiàn)。

如前面介紹的,如果注解處理器在處理注解期間對語法樹進行了修改,編譯器將回到解析與填充符號表的過程重新處理,直到所有插入式注解處理器都沒有再對語法樹進行修改為止,每一次循環(huán)稱為一個Round,如下圖中的環(huán)。

javac.jpeg

上面簡單回顧了下編譯注解的一些東西,接下來看下AutoService這個注解的實現(xiàn),使用它有三個限定條件;

  • 不能是內(nèi)部類和匿名類,必須要有確定的名稱
  • 必須要有公共的,可調(diào)用的無參構(gòu)造函數(shù)
  • 使用這個注解的類必須要實現(xiàn)value參數(shù)定義的接口
@Documented
@Target(TYPE)
public @interface AutoService {
  /** Returns the interface implemented by this service provider. */
  Class<?> value();
}

有注解,必須要有對應(yīng)的注解處理器,AutoServiceProcessor繼承AbstractProcessor,一般我們會實現(xiàn)其中的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方法調(diào)用processImpl,接著看下這個方法的實現(xiàn),先看下方法實現(xiàn),就兩個邏輯判斷,如果上一次循環(huán)中注解處理器已經(jīng)處理完了,就調(diào)用generateConfigFiles生成MEATA_INF配置文件;如果上一輪沒有處理就調(diào)用processAnnotations處理注解。返回true就代表改變或者生成語法樹中的內(nèi)容;返回false就是沒有修改或者生成,通知編譯器這個Round中的代碼未發(fā)生變化。

  private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (roundEnv.processingOver()) {
      generateConfigFiles();
    } else {
      processAnnotations(annotations, roundEnv);
    }

    return true;
  }

再接著往下看代碼之前先看下兩個環(huán)境變量,RoundEnvironmentProcessingEnvironment。

RoundEnvironment提供了訪問到當前這個Round中語法樹節(jié)點的功能,每個語法樹節(jié)點在這里表示為一個Element,在javax.lang.model包中定義了16類Element,包括常用的元素:包,枚舉,類,注解,接口,枚舉值,字段,參數(shù),本地變量,異常,方法,構(gòu)造函數(shù),靜態(tài)語句塊即static{}塊,實例語句塊即{}塊,參數(shù)化類型即反省尖括號內(nèi)的類型,還有未定義的其他語法樹節(jié)點。

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方法返回上一輪注解處理器是否產(chǎn)生錯誤;getRootElements返回上一輪注解處理器生成的根元素;最后兩個方法返回包含指定注解類型的元素的集合,畫重點,這個就是我們自定義注解處理器需要經(jīng)常打交道的方法。

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);
}

另外一個參數(shù)ProcessingEnvironment,在注解處理器初始化的時候(init()方法執(zhí)行的時候)創(chuàng)建,代表了注解處理器框架提供的一個上下文環(huán)境,要創(chuàng)建新的代碼或者向編譯器輸出信息或者獲取其他工具類等都需要用到這個實例變量??聪滤脑创a。

  • Messager用來報告錯誤,警告和其他提示信息;
  • Filer用來創(chuàng)建新的源文件,class文件以及輔助文件;
  • Elements中包含用于操作Element的工具方法;
  • Types中包含用于操作類型TypeMirror的工具方法;
public interface ProcessingEnvironment {
    Map<String, String> getOptions();

    Messager getMessager();

    Filer getFiler();

    Elements getElementUtils();

    Types getTypeUtils();

    SourceVersion getSourceVersion();

    Locale getLocale();
}

介紹完一些基礎(chǔ)變量后,我們就接著上面先看下processAnnotations方法,方法看起來有點長,但是結(jié)構(gòu)很簡單,首先第一步通過RoundEnvironmentgetElementsAnnotatedWith(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只能作用于非內(nèi)部非匿名類或者接口,第二步在for循環(huán)中強轉(zhuǎn)Element為TypeElement,這個就是被AutoService標注的元素,這里簡稱為T。接下來這個可能讓人容易亂,在前面說過每一個javac是一個循環(huán)過程,在第一次掃描到AutoService注解的時候是還沒有T的class對象,所以也就不能通過反射來拿到這個注解和注解的參數(shù)值value。這個時候第三步就得通過AnnotationMirror,用來表示一個注解,通過它可以拿到注解類型和注解參數(shù)。在getAnnotationMirror會判斷這個T的注解(通過element.getAnnotationMirrors())名稱是不是等于AutoService,相等就返回這個AutoServiceAnnotationMirror。

public interface AnnotationMirror {
    DeclaredType getAnnotationType();

    Map<? extends ExecutableElement, ? extends AnnotationValue> getElementValues();
}

拿到這個注解了,接下來就是要拿到注解的參數(shù)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();
  }

這里也是同上面的原因,在這個階段我們不可能通過下面的代碼反射來拿到注解的參數(shù)值,因為這個時候還拿不到class對象。所以上面費了很大的勁去通過AnnotationMirror來拿到注解的參數(shù)值,在我們這個栗子中就是Display.class了。

AutoService autoservice = e.getAnnotation(AutoService.class);
Class<?> providerInterface = autoservice.value()

接下來第5步檢查類型T是不是實現(xiàn)了注解參數(shù)值說明的接口,也就是ADisplayBDisplay是不是實現(xiàn)了Display接口,沒有實現(xiàn)肯定就是沒有意義了。第6步就是獲取到接口名和實現(xiàn)類名,注冊到map中,類似于Map<Display, [ADisplay, BDisplay]>這種形式,即key是接口名,value是實現(xiàn)了接口也就是注解AutoService標注的實現(xiàn)類。

通過上面的步驟就已經(jīng)掃描得到了所有的通過AutoService標注的實現(xiàn)類和對應(yīng)接口的映射關(guān)系,并且在processImpl里面返回了true,下個Round就是生成配置文件了??聪?code>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個步驟要生成配置文件,分別來看下:

  • 第一步遍歷上面拿到的映射關(guān)系map providers,我們這里就是com.example.juexingzhe.interfaces.Display,然后生成文件名resourceFile = META-INF/services/com.example.juexingzhe.interfaces.Display
  • 第二步先檢查下在類編譯class輸出位置有沒有存在配置文件,
  • 第三步就是如果第二步存在配置文件,需要把接口和所有實現(xiàn)類保存到allServices
  • 第四步檢查processAnnotations方法輸出的映射map是否不存在上面的allServices,不存在則添加,存在則直接返回不需要生成新的文件
  • 第五步就是通過Filer生成配置文件,文件名就是resourceFile,文件內(nèi)容就是allServices中的所有實現(xiàn)類。

最后我們看下編譯編譯的結(jié)果,在每個module中都會生成配置文件

Module META-INF.png

最后在apk中會合并所有的META-INF文件目錄,可以看到在接口文件Display下面包含了所有module中通過AutoService注解標注的實現(xiàn)類。

屏幕快照 2019-02-24 下午5.29.33.png

最后通過ServiceLoader就可以通過反射拿到所有的實現(xiàn)類,ServiceLoader的源碼分析可以參考我的另外一片博客Android模塊開發(fā)之SPI.

3.總結(jié)

這次的源碼分析其實是完成之前在Android模塊開發(fā)之APT技術(shù)立下的flag,到今天才補上有點慚愧。AutoService源碼解析結(jié)合APTSPI服用效果可能更佳哦。

這個也是組件化技術(shù)中常用的一個技術(shù)點,后面會再更新一些組件化gradle相關(guān)的一些知識,有需要的小伙伴們歡迎關(guān)注。

完。

參考:
深入理解Java虛擬機

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容