轉載:https://www.jianshu.com/p/7601ba434ff4
想必大家多多少少聽過spi,具體的解釋我就不多說了。但是它具體是怎么實現的呢?它的原理是什么呢?下面我就圍繞這兩個問題來解釋:
實現: 其實具體的實現類就是java.util.ServiceLoader這個類。
要想了解一個機制的原理,首先得知道它是怎么運行的,需要什么配置,才能運行起來。然后再分解來了解實現。對于技術實現也是一樣,先看這個類是怎么實現的,先讓它跑起來,看到效果。然后再講原理。
按照使用說明文檔,應該分下面幾個步驟來使用:
創建一個接口文件
在resources資源目錄下創建META-INF/services文件夾
在services文件夾中創建文件,以接口全名命名
創建接口實現類
我們想測試一下,一般是在這個工程中建立一個測試類來測試。來看下代碼片段:
接口類
public interfaceIMyServiceLoader {
String sayHello();
String getName();
}
View Code
實現類:
public class MyServiceLoaderImpl1 implementsIMyServiceLoader {
@OverridepublicString sayHello() {return "hello1";
}
@OverridepublicString getName() {return "name1";
}
}public class MyServiceLoaderImpl2 implementsIMyServiceLoader {
@OverridepublicString sayHello() {return "hello2";
}
@OverridepublicString getName() {return "name2";
}
}
View Code
測試類:
public classTestMyServiceLoader {public static voidmain(String[] argus){
ServiceLoader 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{returnprocessImpl(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 voidgenerateConfigFiles() {
Filer filer=processingEnv.getFiler();for(String providerInterface : providers.keySet()) {
String resourceFile= "META-INF/services/" +providerInterface;try{
SortedSet allServices =Sets.newTreeSet();try{
FileObject existingFile= filer.getResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
Set oldServices =ServicesFiles.readServiceFile(existingFile.openInputStream());
allServices.addAll(oldServices);
}catch(IOException e) {
}
Set newServices = new HashSet(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,大家可以了解一下。