最近比較流行起一個(gè)比較牛逼的題目岁钓,蹭個(gè)熱點(diǎn),可能沒那么牛逼偎巢,可是對(duì)于使用和了解原理是足夠了蔼夜。
??想必大家多多少少聽過spi,具體的解釋我就不多說了压昼。但是它具體是怎么實(shí)現(xiàn)的呢求冷?它的原理是什么呢?下面我就圍繞這兩個(gè)問題來解釋:
實(shí)現(xiàn): 其實(shí)具體的實(shí)現(xiàn)類就是java.util.ServiceLoader這個(gè)類窍霞。
??要想了解一個(gè)機(jī)制的原理匠题,首先得知道它是怎么運(yùn)行的,需要什么配置但金,才能運(yùn)行起來韭山。然后再分解來了解實(shí)現(xiàn)。對(duì)于技術(shù)實(shí)現(xiàn)也是一樣,先看這個(gè)類是怎么實(shí)現(xiàn)的钱磅,先讓它跑起來巩踏,看到效果。然后再講原理续搀。
按照使用說明文檔,應(yīng)該分下面幾個(gè)步驟來使用:
- 創(chuàng)建一個(gè)接口文件
- 在resources資源目錄下創(chuàng)建META-INF/services文件夾
- 在services文件夾中創(chuàng)建文件菠净,以接口全名命名
- 創(chuàng)建接口實(shí)現(xiàn)類
我們想測(cè)試一下禁舷,一般是在這個(gè)工程中建立一個(gè)測(cè)試類來測(cè)試。來看下代碼片段:
接口:
public interface IMyServiceLoader {
String sayHello();
String getName();
}
實(shí)現(xiàn)類:
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";
}
}
測(cè)試類:
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());
}
}
}
正常情況下這里應(yīng)該輸出
name2hello2
name1hello1
看了這些步驟毅往,想必你也知道原理了牵咙,我在這里總結(jié)下。
原理:在ServiceLoader.load的時(shí)候攀唯,根據(jù)傳入的接口類洁桌,遍歷META-INF/services
目錄下的以該類命名的文件中的所有類,并實(shí)例化返回侯嘀。
??相信看到這里另凌,有的看客該爆粗話了,說啥子看著一篇就夠了戒幔,這些知識(shí)點(diǎn)隨便一搜吠谢,到處都是好伐。是的诗茎,上面說的工坊,確實(shí)隨便一搜都可以搜到,所以這里我要?jiǎng)澲攸c(diǎn)了:
問題
??上面說了敢订,正常情況下會(huì)那樣輸出王污,但是你運(yùn)行程序你就會(huì)發(fā)現(xiàn),馬丹楚午,怎么不起作用啊昭齐,我哪里做錯(cuò)了,都是按照文章步驟來做的醒叁。弄的你都開始懷疑人生了司浪。不要懷疑人生,在一個(gè)工程中做測(cè)試把沼,確實(shí)不能實(shí)現(xiàn)想要的效果啊易。
回憶場(chǎng)景
回憶一下spi的使用場(chǎng)景。它是給制作標(biāo)準(zhǔn)的一放用的饮睬,用來指定標(biāo)準(zhǔn)租谈,然后不同實(shí)現(xiàn)方,用不同的方式實(shí)現(xiàn)標(biāo)準(zhǔn)供使用方使用。那標(biāo)準(zhǔn)方和實(shí)現(xiàn)方必然不是一個(gè)割去。想到這里窟却,你應(yīng)該能夠向明白了吧。
解決方案
要解決問題呻逆,就把之前做的打jar包夸赫,引入新工程測(cè)試,這樣就可以了咖城。
疑問
但是有人會(huì)說標(biāo)準(zhǔn)方和實(shí)現(xiàn)方也可能是一個(gè)啊茬腿,好比標(biāo)準(zhǔn)方我提供一個(gè)內(nèi)部的實(shí)現(xiàn)方案也是可以的啊。也確實(shí)有道理啊宜雀,那這種怎么實(shí)現(xiàn)呢切平?
思考
當(dāng)然也有辦法,下面就說下實(shí)現(xiàn)方法辐董。想要實(shí)現(xiàn)上面的需求悴品,首先要知道攔阻這個(gè)需求實(shí)現(xiàn)的問題,然后把這些問題都解決了简烘,需求自然也就實(shí)現(xiàn)了苔严。那就先來分析問題吧,為什么在一個(gè)工程中獲取不到接口的實(shí)現(xiàn)類呢夸研?經(jīng)過觀察發(fā)現(xiàn)是因?yàn)橘Y源文件沒有在classPath中邦蜜,為什么這么說呢,可以看下build的目錄下面是沒有META-INF文件夾『ブ粒現(xiàn)在知道了原因悼沈,這么解決呢?
疑問臨時(shí)解決方案
最簡(jiǎn)單的方法姐扮,把資源下的META-INF文件夾拷貝到build目錄下絮供,然后再運(yùn)行,發(fā)現(xiàn)可以了茶敏,這也就驗(yàn)證了壤靶,確實(shí)是這個(gè)問題造成的。搞定惊搏!
再次發(fā)出疑問
這樣就結(jié)束了贮乳,那我總不能手動(dòng)拷貝吧,這不算解決方案恬惯,只是臨時(shí)方案向拆。那要怎么解決呢?
??我就不賣關(guān)子了酪耳。其實(shí)要解決這個(gè)問題浓恳,只要在編譯的時(shí)候把這些文件放到build目錄中就行了,是不是很簡(jiǎn)單。思路是有了颈将,可是怎么實(shí)現(xiàn)呢梢夯?這個(gè)時(shí)候要用到攔截編譯處理,然后再里面做這件事情晴圾。
方案一
繼承AbsStractProcessor颂砸,在process方法中把資源文件移到build目錄下。
方案二
這里用到了google開源的AutoService
大概看了下autoService的源碼死姚,其實(shí)它也是使用方案一的方法沾凄,攔截編譯過程,然后再build目錄下生成配置文件知允,這里來大概看下它的process方法:
public 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;
}
這里你會(huì)發(fā)現(xiàn)其實(shí)就是generateConfigFiles()
和processAnnotations(annotations, roundEnv)
看名字可以猜到processAnnotations
是處理注解的,這里實(shí)現(xiàn)類都實(shí)現(xiàn)了注解叙谨,所以這里應(yīng)該是找到實(shí)現(xiàn)類温鸽。
private 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);
}
}
確實(shí)如此,這里會(huì)把所有的實(shí)現(xiàn)類存起來手负。
再來看看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下創(chuàng)建META-INF目錄涤垫。和我們想的一模一樣。
總結(jié)
好了竟终,要實(shí)現(xiàn)文章開頭的需求蝠猬,除非你覺得你比google開源AutoService的工程師寫的更好,不然就直接使用AutoService吧统捶。這篇文章不僅是分析ServiceLoader的原理榆芦,實(shí)現(xiàn)我們的需求,更重要的是高速我們遇到問題該怎么分析問題喘鸟,解決問題匆绣。
擴(kuò)展
其實(shí)還有很多比較好玩的,比如在攔截到編譯過程時(shí)什黑,可以再編譯期生成一些有意思的代碼崎淳,來幫我們實(shí)現(xiàn)一些自動(dòng)化處理。這就需要?jiǎng)佑梦覀兊拇竽X就想了愕把,介紹一下生成代碼的庫(kù)javapoet拣凹,大家可以了解一下。