java spi机制

  • Post author:
  • Post category:java


什么是SPI?

SPI(service provider interface)表示服务提供接口。主要用于被第三方实现或扩展的接口。


SPI的作用就是为这些被扩展的接口寻找实现类。


API和SPI的区别?

api是提供方制定接口,并对接口进行实现。调用方只能调用,不能选择实现类。

spi是提供方制定接口。调用方选择自己的实现类调用。


spi优点:

  1. 解耦。不需要改动源码就可实现扩展。
  2. 没有侵入性。扩展也不需要改动原来代码
  3. 只需要添加配置就可实现扩展,符合开闭原则(对扩展开放,对修改关闭)

一、java原生SPI

应用或第三方包在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类的完全限定名。而当框架调用ServiceLoader.load(XXX.class),就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成SPI实现的注入。

规范:

使用示例:


1、定义一个接口

public interface Bird {
    String say();
}


2、接口实现两个实现类

public class BlueBird implements Bird {
    @Override
    public String say() {
        return "我是蓝鸟";
    }
}
public class RedBird implements Bird {
    @Override
    public String say() {
        return "我是红鸟";
    }
}


3、在类路径下创建两层文件夹META-INF/services,增加一个文件,文件名是接口的全限定名


文件内容是接口实现类的全限定名,可以有多个实现类



4、增加一个主类

public class mainApp {
    public static void main(String[] args) {
        ServiceLoader<Bird> birds = ServiceLoader.load(Bird.class);
        for (Bird bird : birds) {
            System.out.println(bird.say());
        }
    }
}


5、执行结果


SPI主要的类:

public final class ServiceLoader<S> implements Iterable<S> {
    //扫描目录前缀
    private static final String PREFIX = "META-INF/services/";
    // 被加载的类或接口
    private final Class<S> service;
    // 用于定位、加载和实例化实现方实现的类的类加载器
    private final ClassLoader loader;
    // 上下文对象
    private final AccessControlContext acc;
    // 按照实例化的顺序缓存已经实例化的类
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
    // 懒查找迭代器
    private java.util.ServiceLoader.LazyIterator lookupIterator;
    // 私有内部类,提供对所有的service的类的加载与实例化
    private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        String nextName = null;
        //...
        private boolean hasNextService() {
            if (configs == null) {
                try {
                    //获取目录下所有的类
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    //...
                }
                //....
            }
        }
        private S nextService() {
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //反射加载类
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
            }
            try {
                //实例化
                S p = service.cast(c.newInstance());
                //放进缓存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                //..
            }
            //..
        }
    }
}

就是直接扫描META-INF/services路径下的文件,找到后则解析文件中的内容。通过反射方式创建实现类的实例。

缺点:

  1. 代码中写死了路径,只能放在META-INF/services路径


  2. 代码中只能遍历所有的实现,并全部示例化。如果不是都需要用实现类,则会浪费性能。
  3. 接口名文件内容可以配置所有的扩展实现,但是没有命名(别名)。有多个实现类时导致程序很难寻找。

二、dubbo框架中的SPI

dubbo是一个高度可扩展的rpc框架,也依赖于java的spi机制。并且dubbo对java原生spi做了扩展(解决了上面的缺点),使功能更强大

关键一:@SPI注解

使用示例:


1、定义一个接口添加上@SPI注解(设置默认值red)

@SPI("red")
public interface Bird {
    String say();
}


2、接口实现两个实现类

public class BlueBird implements Bird {
    @Override
    public String say() {
        return "我是蓝鸟";
    }
}
public class RedBird implements Bird {
    @Override
    public String say() {
        return "我是红鸟";
    }
}


3、在类路径下创建两层文件夹META-INF/dubbo,增加一个文件,文件名是接口的全限定名


文件内容是接口实现类的全限定名,可以有多个实现类。前面是实现类的别名,K-V结构


4、增加一个主类

public class mainApp {
    public static void main(String[] args) {
        Bird bird = ExtensionLoader.getExtensionLoader(Bird.class).getDefaultExtension();
        System.out.println(bird.say()+"(默认)");
        Bird bird2 = ExtensionLoader.getExtensionLoader(Bird.class).getExtension("red");
        System.out.println(bird2.say());
        Bird bird3 = ExtensionLoader.getExtensionLoader(Bird.class).getExtension("blue");
        System.out.println(bird3.say());
    }
}

getExtension方法可以通过别名获取实现类。getDefaultExtension方法是获取默认实现类(@SPI注解的值)


5、查看结果



更多方法,功能待发掘:

关键二:@Adaptive注解

用在扩展接口的方法上。表示一个自适应方法。以后有时间在研究。

关键点三:@Activate注解

有时间在研究。

三、使用SPI经典的例子

java中jdbc驱动包。只是定义了接口规范,具体的实现由各大数据库厂商提供



版权声明:本文为sumengnan原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。