ServiceLoader使用看这一篇就够了

最近比较流行起一个比较牛逼的题目,蹭个热点,可能没那么牛逼,可是对于使用和了解原理是足够了。

  想必大家多多少少听过spi,具体的解释我就不多说了。但是它具体是怎么实现的呢?它的原理是什么呢?下面我就围绕这两个问题来解释:


实现: 其实具体的实现类就是java.util.ServiceLoader这个类。


  要想了解一个机制的原理,首先得知道它是怎么运行的,需要什么配置,才能运行起来。然后再分解来了解实现。对于技术实现也是一样,先看这个类是怎么实现的,先让它跑起来,看到效果。然后再讲原理。
按照使用说明文档,应该分下面几个步骤来使用:

  1. 创建一个接口文件
  2. 在resources资源目录下创建META-INF/services文件夹
  3. 在services文件夹中创建文件,以接口全名命名
  4. 创建接口实现类

我们想测试一下,一般是在这个工程中建立一个测试类来测试。来看下代码片段:
接口:

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包,引入新工程测试,这样就可以了。

疑问

但是有人会说标准方和实现方也可能是一个啊,好比标准方我提供一个内部的实现方案也是可以的啊。也确实有道理啊,那这种怎么实现呢?

思考

当然也有办法,下面就说下实现方法。想要实现上面的需求,首先要知道拦阻这个需求实现的问题,然后把这些问题都解决了,需求自然也就实现了。那就先来分析问题吧,为什么在一个工程中获取不到接口的实现类呢?经过观察发现是因为资源文件没有在classPath中,为什么这么说呢,可以看下build的目录下面是没有META-INF文件夹。现在知道了原因,这么解决呢?

疑问临时解决方案

最简单的方法,把资源下的META-INF文件夹拷贝到build目录下,然后再运行,发现可以了,这也就验证了,确实是这个问题造成的。搞定!


再次发出疑问

这样就结束了,那我总不能手动拷贝吧,这不算解决方案,只是临时方案。那要怎么解决呢?

  我就不卖关子了。其实要解决这个问题,只要在编译的时候把这些文件放到build目录中就行了,是不是很简单。思路是有了,可是怎么实现呢?这个时候要用到拦截编译处理,然后再里面做这件事情。

方案一
继承AbsStractProcessor,在process方法中把资源文件移到build目录下。

方案二
这里用到了google开源的AutoService

大概看了下autoService的源码,其实它也是使用方案一的方法,拦截编译过程,然后再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;
  }

这里你会发现其实就是generateConfigFiles()processAnnotations(annotations, roundEnv)看名字可以猜到processAnnotations是处理注解的,这里实现类都实现了注解,所以这里应该是找到实现类。

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

确实如此,这里会把所有的实现类存起来。


再来看看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,大家可以了解一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 141,558评论 1 298
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 60,739评论 1 254
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 93,327评论 0 211
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,752评论 0 174
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,452评论 1 252
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,617评论 1 171
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,286评论 2 267
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,083评论 0 165
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,839评论 6 227
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,413评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,186评论 2 213
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,506评论 1 223
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,171评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,049评论 2 213
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,417评论 3 202
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,588评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 25,942评论 0 163
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,392评论 2 228
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,499评论 2 229

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 133,996评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,364评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,061评论 6 345
  • 有许久许久没写过文章了。 这次去三亚游玩准备了一个多月,话不多说,直入主题。 Day1: 9/23日晚上10点多从...
    罗十殿阅读 383评论 0 1
  • 爱是什么,没有人能解释明白。因为这是一种感觉,一种自然的不能在自然的感觉。 在我看来,秋就是一场童话,一场梦,但却...
    白小源家的燕砸阅读 219评论 0 0