轉載:https://www.jianshu.com/p/7601ba434ff4
想必大家多多少少聽過spi,具體的解釋我就不多說了。但是它具體是怎么實現的呢?它的原理是什么呢?下面我就圍繞這兩個問題來解釋:
實現: 其實具體的實現類就是java.util.ServiceLoader這個類。
要想了解一個機制的原理,首先得知道它是怎么運行的,需要什么配置,才能運行起來。然后再分解來了解實現。對於技術實現也是一樣,先看這個類是怎么實現的,先讓它跑起來,看到效果。然后再講原理。
按照使用說明文檔,應該分下面幾個步驟來使用:
- 創建一個接口文件
- 在resources資源目錄下創建META-INF/services文件夾
- 在services文件夾中創建文件,以接口全名命名
- 創建接口實現類
我們想測試一下,一般是在這個工程中建立一個測試類來測試。來看下代碼片段:
接口類

public interface IMyServiceLoader { String sayHello(); String getName(); }
實現類:

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"; } }
測試類:

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()); } } }
正常情況下這里應該輸出

name2hello2
name1hello1
看了這些步驟,想必你也知道原理了,我在這里總結下。
原理:在ServiceLoader.load的時候,根據傳入的接口類,遍歷META-INF/services
目錄下的以該類命名的文件中的所有類,並實例化返回。
相信看到這里,有的看客該爆粗話了,說啥子看着一篇就夠了,這些知識點隨便一搜,到處都是好伐。是的,上面說的,確實隨便一搜都可以搜到,所以這里我要划重點了:
一、問題
上面說了,正常情況下會那樣輸出,但是你運行程序你就會發現,馬丹,怎么不起作用啊,我哪里做錯了,都是按照文章步驟來做的。弄的你都開始懷疑人生了。不要懷疑人生,在一個工程中做測試,確實不能實現想要的效果。
二、回憶場景
回憶一下spi的使用場景。它是給制作標准的一放用的,用來指定標准,然后不同實現方,用不同的方式實現標准供使用方使用。那標准方和實現方必然不是一個。想到這里,你應該能夠向明白了吧。
三、解決方案
要解決問題,就把之前做的打jar包,引入新工程測試,這樣就可以了。
四、疑問
但是有人會說標准方和實現方也可能是一個啊,好比標准方我提供一個內部的實現方案也是可以的啊。也確實有道理啊,那這種怎么實現呢?
五、思考
六、疑問臨時解決方案
最簡單的方法,把資源下的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; }
這里你會發現其實就是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); } }
確實如此,這里會把所有的實現類存起來。
再來看看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; } } }
這里是在build下創建META-INF目錄。和我們想的一模一樣。
八、總結
好了,要實現文章開頭的需求,除非你覺得你比google開源AutoService的工程師寫的更好,不然就直接使用AutoService吧。這篇文章不僅是分析ServiceLoader的原理,實現我們的需求,更重要的是高速我們遇到問題該怎么分析問題,解決問題。
九、擴展
其實還有很多比較好玩的,比如在攔截到編譯過程時,可以再編譯期生成一些有意思的代碼,來幫我們實現一些自動化處理。這就需要動用我們的大腦就想了,介紹一下生成代碼的庫javapoet,大家可以了解一下。