美文网首页Java服务端面试hello world后端java
ServiceLoader使用看这一篇就够了

ServiceLoader使用看这一篇就够了

作者: 卓码 | 来源:发表于2017-09-08 20:21 被阅读0次

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

      想必大家多多少少听过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,大家可以了解一下。

    相关文章

      网友评论

        本文标题:ServiceLoader使用看这一篇就够了

        本文链接:https://www.haomeiwen.com/subject/tucnjxtx.html