java的spi(Service Provider Interface)服务提供机制

本文详细介绍了Java SPI(Service Provider Interface)的工作原理和实现过程。通过ServiceLoader类,展示了如何创建接口和实现类,并在classpath下配置服务文件。解释了SPI的大致原理,即在运行时动态加载实现类。文章还探讨了SPI在logback日志和MySQL JDBC驱动中的应用,并分析了在jdbc4规范中如何使用SPI加载驱动。最后,讨论了SPI解决的解耦问题以及使用注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、先说spi的实现过程:

spi由java.util.ServiceLoader类实现:
1、创建接口和其实现类:

public interface Fruit {
    void sayHello();
}
public class Banana implements Fruit {
    @Override
    public void sayHello() {
        System.out.println("Hello Banana");
    }
}
public class Apple implements Fruit {
    @Override
    public void sayHello() {
        System.out.println("Hello Apple");
    }
}

2、在classpath路径下创建目录结构/META-INF/services/
并在services目录下创建文件,名称时接口的全限定名com.spi.Fruit
并在com.spi.Fruit文件中写其实现类的全限定名,一行写一个,如下:

com.spi.Banana
#这是注释
com.spi.Apple

效果如下:
在这里插入图片描述

3、先说下spi的大致原理,后面再说测试类FruitTest:

java.util.ServiceLoader<S>作为spi服务的基类,它内部会维护一个private LazyIterator lookupIterator;,如下,可见继承IteratorLazyIterator类必须实现hasNext()next()方法,这两个方法的作用如下:

private class LazyIterator implements Iterator<S> {
        public boolean hasNext() {
                return hasNextService();
        }

        public S next() {
                return nextService();
        }
}

来看下hasNextService()nextService()分别做了什么:

    //以下为伪代码,只做原理分析
    Configs configs = null;
    String nextName = null;
    
    private boolean hasNextService() {
        //如果已经拿到实现类nextName,则直接返回true
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            String fullName = "META-INF/services/" + "META-INF/services/目录下的文件名(也就是接口名)";
            //configs实际上获取到了META-INF/services/下文件的内容(也就是实现类的集合)
            configs = loader.getResources(fullName);
        }
        //拿出集合里的第一个实现类放到nextName
        pending = parse(service, configs.nextElement());
        nextName = pending.next();
        return true;
    }

    private S nextService() {
        hasNextService();
        //hasNextService()已经把第一个实现类nextName拿到,故而直接加载
        Class<?> c = Class.forName(nextName, false, loader);
        //直接实例化
        S p = service.cast(c.newInstance());
        //置空nextName,让hasNextService()从configs集合里拿下一个实现类
        nextName = null;
        return p;
    }

可见, 当调用java.util.ServiceLoader<S>内部维护的LazyIterator集合的next()方法后才会真正的去实例化具体的实现类。

4、根据上面的解析,分析测试类FruitTest,如注释所示:

public class FruitTest {
    public static void main(String[] args) {
        //ServiceLoader.load()只是去在内部维护一个Iterator,并没有加载任何实现类
        ServiceLoader<Fruit> s = ServiceLoader.load(Fruit.class);
        //拿到内部维护的Iterator
        Iterator<Fruit> iterator = s.iterator();
        /*
         * 调用hasNext()也就是调用了hasNextService(),
         * 第一次会获取实现类名称集合,然后拿出第一个实现类名称nextName 
         */
        while (iterator.hasNext()) {
            /*
             * 调用next(),就是去把当前的实现类名称nextName,进行实例化,并返回
             */
            Fruit fruit = iterator.next();
            //获取到之后,就可以使用了
            fruit.sayHello();
        }
    }
}

二、spi机制的实际应用
1、logback日志,LogbackServletContainerInitializer实现了servlet3.0的ServletContainerInitializer接口,当容器启动会自动调用所有实现这个接口的实现类的onStartup()方法,因此在web.xml文件中根本不用配置相关Listener。
在这里插入图片描述

2、据说mysql从jdbc4开始,利用spi机制加载驱动,不用再写Class.forName(),如下:
在这里插入图片描述
下面来详细分析spi机制是如何在jdbc4规范和mysql驱动的加载中发挥作用的:
1、首先,根据spi规范,mysql驱动jar包下肯定已经提供了/META-INF/services/java.sql.Driver这样的配置文件,并写好了如上图所示的两个实现类。
2、在实现了spi的前提下,省略Class.forName("com.mysql.jdbc.Driver")
直接调用java.sql.DriverManager.getConnection(url)
根据类的加载规则,先是初始化类的静态内容,然后才开始调用静态方法,
DriverManager有个静态块:

    static {
        loadInitialDrivers();//方法如下
        println("JDBC DriverManager initialized");
    }
    //为了简洁去除部分代码
    private static void loadInitialDrivers() {
        ... ...
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(java.sql.Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        while(driversIterator.hasNext()) {
            driversIterator.next();
        }
        ... ...
    }

显而易见,DriverManager类的静态块利用了spi机制,通过ServiceLoader.load(java.sql.Driver.class)来加载jar包的/META-INF/services/目录下提供的实现类,并且通过调用driversIterator.next()方法实例化。

那把com.mysql.jdbc.Driver实例化之后,DriverManager.getConnection(url)是如何调用到这个实例的呢?
其实com.mysql.jdbc.Driver类在实例化之前会先初始化,在初始化时候会执行静态块,如下:

    static {
            java.sql.DriverManager.registerDriver(new Driver());
    }

可见,它又调用回DriverManager的注册方法把自己的一个实例注册进去了。

最终,DriverManager.getConnection()就是遍历已经注册的所有实现类,再调用实现类的connect()方法获取连接。

三、spi解决了什么?如何进行使用?

  • spi可以实现解耦,比如sql驱动,只需要按照接口获取链接并执行代码,不用关心具体是mysql还是oracle提供的驱动实现,由导入的jar包或者集成的组件来决定。
  • 如果同时引入多个同类的服务组件,无法指定哪个会生效!!
  • 并非引入具体组件(jar包)之后就会自动初始化具体的实现类,还先需要有个地方触发ServiceLoader的spi发现功能,这个触发一般是在接口规范类中来做这个事情,而且ServiceLoader的调用跟具体实现类是完全解耦的。
  • 使用spi的前提是,接口或者超类规范!目的是,具体服务解耦!如果只是用来初始化和运行具体的类,那么直接new和调用就行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值