【java編程】ServiceLoader使用看這一篇就夠了


轉載:https://www.jianshu.com/p/7601ba434ff4

想必大家多多少少聽過spi,具體的解釋我就不多說了。但是它具體是怎么實現的呢?它的原理是什么呢?下面我就圍繞這兩個問題來解釋:

實現: 其實具體的實現類就是java.util.ServiceLoader這個類。

要想了解一個機制的原理,首先得知道它是怎么運行的,需要什么配置,才能運行起來。然后再分解來了解實現。對於技術實現也是一樣,先看這個類是怎么實現的,先讓它跑起來,看到效果。然后再講原理。
按照使用說明文檔,應該分下面幾個步驟來使用:

  1. 創建一個接口文件
  2. 在resources資源目錄下創建META-INF/services文件夾
  3. 在services文件夾中創建文件,以接口全名命名
  4. 創建接口實現類

我們想測試一下,一般是在這個工程中建立一個測試類來測試。來看下代碼片段:

接口類

public interface IMyServiceLoader {

    String sayHello();

    String getName();
}
View Code

實現類:

public class MyServiceLoaderImpl1 implements IMyServiceLoader {
    @Override
    public String sayHello() {
        return "hello1";
    }

    @Override
    public String getName() {
        return "name1";
    }
}

public class MyServiceLoaderImpl2 implements IMyServiceLoader {
    @Override
    public String sayHello() {
        return "hello2";
    }

    @Override
    public String getName() {
        return "name2";
    }
}
View Code

測試類:

public class TestMyServiceLoader {
    public static void main(String[] argus){
        ServiceLoader<IMyServiceLoader> serviceLoader = ServiceLoader.load(IMyServiceLoader.class);
        for (IMyServiceLoader myServiceLoader : serviceLoader){
            System.out.println(myServiceLoader.getName() + myServiceLoader.sayHello());
        }
    }
}
View Code

正常情況下這里應該輸出

name2hello2
name1hello1
View Code

看了這些步驟,想必你也知道原理了,我在這里總結下。

 

 原理:在ServiceLoader.load的時候,根據傳入的接口類,遍歷META-INF/services目錄下的以該類命名的文件中的所有類,並實例化返回。

 

相信看到這里,有的看客該爆粗話了,說啥子看着一篇就夠了,這些知識點隨便一搜,到處都是好伐。是的,上面說的,確實隨便一搜都可以搜到,所以這里我要划重點了:

 

一、問題

上面說了,正常情況下會那樣輸出,但是你運行程序你就會發現,馬丹,怎么不起作用啊,我哪里做錯了,都是按照文章步驟來做的。弄的你都開始懷疑人生了。不要懷疑人生,在一個工程中做測試,確實不能實現想要的效果。

二、回憶場景

回憶一下spi的使用場景。它是給制作標准的一放用的,用來指定標准,然后不同實現方,用不同的方式實現標准供使用方使用。那標准方和實現方必然不是一個。想到這里,你應該能夠向明白了吧。

 

三、解決方案

要解決問題,就把之前做的打jar包,引入新工程測試,這樣就可以了。

 

四、疑問

但是有人會說標准方和實現方也可能是一個啊,好比標准方我提供一個內部的實現方案也是可以的啊。也確實有道理啊,那這種怎么實現呢?

 

五、思考

當然也有辦法,下面就說下實現方法。想要實現上面的需求,首先要知道攔阻這個需求實現的問題,然后把這些問題都解決了,需求自然也就實現了。那就先來分析問題吧,為什么在一個工程中獲取不到接口的實現類呢?經過觀察發現是因為資源文件沒有在classPath中,為什么這么說呢,可以看下build的目錄下面是沒有META-INF文件夾。現在知道了原因,這么解決呢?

六、疑問臨時解決方案

最簡單的方法,把資源下的META-INF文件夾拷貝到build目錄下,然后再運行,發現可以了,這也就驗證了,確實是這個問題造成的。搞定!

七、再次發出疑問

這樣就結束了,那我總不能手動拷貝吧,這不算解決方案,只是臨時方案。那要怎么解決呢?

  我就不賣關子了。其實要解決這個問題,只要在編譯的時候把這些文件放到build目錄中就行了,是不是很簡單。思路是有了,可是怎么實現呢?這個時候要用到攔截編譯處理,然后再里面做這件事情。

方案一
繼承AbsStractProcessor,在process方法中把資源文件移到build目錄下。

方案二
這里用到了google開源的AutoService

大概看了下autoService的源碼,其實它也是使用方案一的方法,攔截編譯過程,然后再build目錄下生成配置文件,這里來大概看下它的process方法:

ublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    try {
      return processImpl(annotations, roundEnv);
    } catch (Exception e) {
      ...
      return true;
    }
  }

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

這里你會發現其實就是generateConfigFiles()processAnnotations(annotations, roundEnv)看名字可以猜到processAnnotations是處理注解的,這里實現類都實現了注解,所以這里應該是找到實現類。

rivate void processAnnotations(Set<? extends TypeElement> annotations,
      RoundEnvironment roundEnv) {

    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
    for (Element e : elements) {
      TypeElement providerImplementer = (TypeElement) e;
      AnnotationMirror providerAnnotation = getAnnotationMirror(e, AutoService.class).get();
      DeclaredType providerInterface = getProviderInterface(providerAnnotation);
      TypeElement providerType = (TypeElement) providerInterface.asElement();
      ...
      String providerTypeName = getBinaryName(providerType);
      String providerImplementerName = getBinaryName(providerImplementer);
      providers.put(providerTypeName, providerImplementerName);
    }
  }
View Code

確實如此,這里會把所有的實現類存起來。

再來看看generateConfigFiles()方法

private void generateConfigFiles() {
    Filer filer = processingEnv.getFiler();

    for (String providerInterface : providers.keySet()) {
      String resourceFile = "META-INF/services/" + providerInterface;
      try {
        SortedSet<String> allServices = Sets.newTreeSet();
        try {
          FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
              resourceFile);
          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
          allServices.addAll(oldServices);
        } catch (IOException e) {
        }

        Set<String> newServices = new HashSet<String>(providers.get(providerInterface));

        allServices.addAll(newServices);
        FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
            resourceFile);
        OutputStream out = fileObject.openOutputStream();
        ServicesFiles.writeServiceFile(allServices, out);
        out.close();
      } catch (IOException e) {
        return;
      }
    }
  }
View Code

這里是在build下創建META-INF目錄。和我們想的一模一樣。

 

八、總結

好了,要實現文章開頭的需求,除非你覺得你比google開源AutoService的工程師寫的更好,不然就直接使用AutoService吧。這篇文章不僅是分析ServiceLoader的原理,實現我們的需求,更重要的是高速我們遇到問題該怎么分析問題,解決問題。

九、擴展

其實還有很多比較好玩的,比如在攔截到編譯過程時,可以再編譯期生成一些有意思的代碼,來幫我們實現一些自動化處理。這就需要動用我們的大腦就想了,介紹一下生成代碼的庫javapoet,大家可以了解一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM