聊一聊什么是Java SPI

什么是SPI

A service is a well-known set of interfaces and (usually abstract)classes. A service provider is a specific implementation of a service.The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers can be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers can also be made available by adding them to the application’s class path or by some other platform-specific means.

咱英文也不是很好,就不献丑翻译了。大致意思是说,提供了一个接口或者抽象类,服务提供者呢可以实现或者继承,更好的使用扩展来实现,但是必须将实现的jar包放到class path下面,为什么要放到class path下面呢?我偏要放到别的地方!好吧下面就没必要看了,为什么要放到class path路径下面,请参考上一篇文章类加载。

举个例子

看完定义之后其实有点懵逼,不懂啊,好抽象啊!那这样说吧,我们使用Java访问的数据库的时候需要用到JDBC(JDBC 4.0之后采用spi自动加载,无需Class.forName),还要添加驱动包到class path路径,然后呢想想为什么这样做呢?Java不能自己实现一个系统类库吗?这样做一是维护成本太高,数据库每升级一次都要跟着一起更新代码,而且众多数据库厂商每个厂商标准及接口都不一致,维护起来太复杂了。所以呢为什么不这样呢,定义一套标准的接口,让每个厂商自己去完成实现,然后需要用哪个数据库就直接把jar包拿过来加到class path下面,然后加载实现类等等等。到这里差不多该理解SPI的用途了吧!

怎么使用

1)服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”(接口全限定就是包名加上类名)为命名的文件,内容为实现类的全限定名;
2)接口实现类所在的jar包放在主程序的classpath中;
3)主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
4)SPI的实现类必须携带一个不带参数的构造方法;

实际操作

首先定义一个接口假设叫ITestService

public interface ITestService {

    public String helloSpi();
}

然后定义两个实现类,OneServiceImpl和TwoServiceImpl。

public class OneServiceImpl implements ITestService {


    public String helloSpi() {
        return "Hello one";
    }
}
public class TwoServiceImpl implements ITestService {


    public String helloSpi() {
        return "helloTwo";
    }
}

然后重要的一步来了,在META-INF/services目录下创建一个以“接口全限定名”(接口全限定就是包名加上类名)为命名的文件,如下图:
在这里插入图片描述
咱们这个只是一个操作演示,没有将接口和实现分离,所以直接跳过将jar包放到classpath目录下的步骤。接下来就是类的加载代码:

public static void main(String[] args) {

        ServiceLoader<ITestService> loads = ServiceLoader.load(ITestService.class);
        Iterator<ITestService> iterator = loads.iterator();
        while (iterator.hasNext()) {
            ITestService next = iterator.next();
            System.out.println(next.helloSpi());
        }

    }

运行结果:

Hello one
helloTwo

对于第四点因为咱们没有定义构造方法,所以java会自动为我们生成一个。如果我们定义了一个有参构造方法,而没有自己定义无参构造方法则会抛出以下错误:

Exception in thread "main" java.util.ServiceConfigurationError: com.cmmsky.spi.demo.service.ITestService: Provider com.cmmsky.spi.demo.service.impl.OneServiceImpl could not be instantiated
	at java.util.ServiceLoader.fail(ServiceLoader.java:232)
	at java.util.ServiceLoader.access$100(ServiceLoader.java:185)
	at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:384)
	at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)
	at java.util.ServiceLoader$1.next(ServiceLoader.java:480)
	at com.cmmsky.spi.demo.Main.main(Main.java:22)
Caused by: java.lang.InstantiationException: com.cmmsky.spi.demo.service.impl.OneServiceImpl
	at java.lang.Class.newInstance(Class.java:427)
	at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:380)
	... 3 more
Caused by: java.lang.NoSuchMethodException: com.cmmsky.spi.demo.service.impl.OneServiceImpl.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.newInstance(Class.java:412)
	... 4 more

定义了有参构造方法之后,java不会再为我们生成无参构造方法,所以会报错。
再看ServiceLoader加载过程:

public final class ServiceLoader<S>
    implements Iterable<S>
{

    private static final String PREFIX = "META-INF/services/";

首先就是spi所定义的目录位置,ServiceLoad使用load方法时看看

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

第一步获取了线程上下文类加载器。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方 法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个。然后调用
load(Class<> service,ClassLoader loader)返回ServiceLoad实例,再看一下ServiceLoader构造方法

private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

在这里判断一下线程上下文类加载器如果为null的话,则使用getSystemClassLoader(上一篇类加载的文章中提到过应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值)所以如果上下文加载器为null则会使用Application Class Loader。最后调用了一下reload方法,如下

public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

实例化了一下懒加载迭代器,然后追进LazyIterator构造方法发现并没有发现类加载的过程,那么什么时候加载呢?既然名为lazy,所以很懒吗!用的时候才会去初始化类,所以当我们调用ServiceLoader实例的iterator方法时来了:

public Iterator<S> iterator() {
        return new Iterator<S>() {
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

到这里应该看明白了吧,provides在reload阶段clear了一下,所以它的迭代器的hasNext的方法绝对是为false(第一次使用时),然后返回的是lookupIterator的hasNext,lookupIterator是在reload阶段实例化的。看一下lookupIterator(ServiceLoader的内部类)的hasNext:

public boolean hasNext() {
       if (acc == null) {
              return hasNextService();
       } else {
              PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

acc有设置安全策略直接调用hasNextService方法,否则以特权方式调用hasNextService方法。继续跟到

private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            // configs为null则加载我们配置的文件
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
            	// 如果文件里面没有内容,直接返回false
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

因为实例LazyIterator的时候只初始化了两个参数,所以直接看第二步,configs为null的情况,首先构造接口的文件的访问路径,然后加载该文件,如果文件内容为空则直接返回false,反则parse文件内容

private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            // 逐行解析文件内容
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        return names.iterator();
    }
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
    	// 读取一行文件内容
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        //  剔除注释
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        // 去除空格
        ln = ln.trim();
        int n = ln.length();
        // 去除空格和可能存在的注释之后如果还存在内容继续解析
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            // 判断命名是否合法
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            // 如果provides和names结合都没有包含这个全限定类名直接添加
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

看到这里hasNext的过程并没有初始化实现类再往下继续看next方法:

public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

过程同hasNext过程不再重复。看一下nextService:

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
            	// 根据类的全限定名获取Class实例
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
            	// 实例化实现类,并强转为接口类型
                S p = service.cast(c.newInstance());
                // 将实例缓存起来
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

也就是知道调用迭代器的next的方法时才会将接口实现类真正的实例化,这在一定程度上对性能有一定的提升。但是也可以看到,ServiceLoader虽然将接口实现类延迟加载,但是也只能通过遍历的方式将实现类实例化,如果有一百个实现类,你只想用第100个的实现类,这会对资源极大的浪费。
更多精彩欢迎关注公众号
公众号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三寸花笺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值