JAVA SPI 机制


前言

一、SPI是什么?

SPI全称Service Provider Interface( 服务提供商接口)。
在面向对象的设计中,根据依赖倒转原则,模块间应该基于接口编程,而不是对实现类进行硬编码。一旦在代码中设计到具体的实现类,那么当我们需要替换另一种实现时,就需要修改代码。为了实现在模块装配的时候,不在模块里写死代码,我们需要一种服务发现机制,而java spi就是提供了这样一种机制。这种机制就类似于IoC的思想,将代码装配的控制权移交到了代码之外。

依赖倒转原则:
1)高层模块不应该依赖底层模块,二者都应该依赖其抽象
2)抽象不应该依赖细节,细节应该依赖抽象
3)依赖倒转的中心思想是面向接口编程

二、JAVA SPI的具体约定

当使用JAVA SPI时,需要在src/main/resources文件夹下创建目录META-INF/services/,并在这个目录下创建指定接口的全限定性类名文件,该文件的内容就是该接口的具体实现类的全限定性类名。
当服务消费者需要调用这个接口的实现类时,就能通过 META-INF/services/ 里的配置文件得到具体的实现类名,并加载实例化,完成代码的装配。

三、具体案例

1.创建接口

代码如下(示例):

package com.wgc.spi;

public interface Animal {

    /**
     * 输出动物叫声
     */
    void say();
}

2.创建实现类

共创建了两个实现类,Cat和Dog
代码如下(示例):

package com.wgc.spi.impl;

import com.wgc.spi.Animal;

public class Cat implements Animal {
    @Override
    public void say() {
        System.out.println("喵喵");
    }
}
package com.wgc.spi.impl;

import com.wgc.spi.Animal;

public class Dog implements Animal {
    @Override
    public void say() {
        System.out.println("汪汪");
    }
}

3、在META-INF/services文件夹下创建接口同名文件

在这里插入图片描述
配置文件com.wgc.spi.Animal的内容是:

com.wgc.spi.impl.Cat
com.wgc.spi.impl.Dog

注意:如果有多个实现类需要换行表示。

4、在类中使用

public class SpiMainClass {
    public static void main(String[] args) {
        ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);

        for (Animal animal:load) {
            animal.say();
        }
    }
}

输出结果为:

喵喵
汪汪

因为我们在配置文件中配置了两个实现类Cat、Dog,所以ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);加载了两个实现类,也就在循环中输出了“喵喵”、“汪汪”。

四、原理分析

我们debug一下ServiceLoader.load(Animal.class);

public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

这里面又执行了一些简单的代码,直到运行到reload()方法,实例化了LazyIterator对象lookupIterator = new LazyIterator(service, loader);

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

LazyIterator是ServiceLoader的内部类,当我们迭代遍历ServiceLoader对象时,就会执行LazyIterator类的nextService()hasNextService()方法,代码如下:

private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                // SPI查询配置文件路径
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                    // 加载SPI配置文件
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                // 将SPI配置文件的内容加载到ArrayList后,返回list的Iterator对象
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
            // 生成SPI实现类的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 {
            // 实例化一个实现类对象,并将其存放到ServiceLoader的成员变量providers中
                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
        }

        ......

    }

五、实际案例

JDBC(Java Database Connectivity)为访问不同的数据库提供了一种统一的途径,使程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统。而jdbc加载mysql数据库驱动的就用到了SPI技术。
我们使用jdbc创建一个连接的方式是:

DriverManager.getConnection(conf.getUrl(), conf.getUser(), conf.getPwd());

而加载DriverManager类时,会先执行下面的静态代码块:

public class DriverManager {


    // 将jdbc驱动类注册到这个list
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
 
    ......
 
    private DriverManager(){}


    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

loadInitialDrivers();方法中就是通过ServiceLoader加载的mysql的jar包中META-INF/services文件夹下的java.sql.Driver文件中配置的具体驱动,代码如下:

/**
 * 注意:我已将源程序中的英文注释删除,请自行查阅
 */
private static void loadInitialDrivers() {
        String drivers;
        try {
        // 查看系统属性中是否存在名为jdbc.drivers的属性,根据debug接口,返回为null
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
     
        // 下面的代码通过SPI加载了驱动程序
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
			    // 通过ServiceLoader加载驱动类,mysql8的驱动包中Driver的实现类是com.mysql.cj.jdbc.Driver
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{ 
                    while(driversIterator.hasNext()) {
                    	// 此时会加载com.mysql.cj.jdbc.Driver
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);
        if (drivers == null || drivers.equals("")) {
            return;
        }
        // 如果在系统属性中jdbc.drivers的配置,那么就在classpath下加载对应的类
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

在加载com.mysql.cj.jdbc.Driver时,会将Driver对象注册registeredDrivers 集合中,代码如下:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    // 将驱动类对象加载到 DriverManager
    static {
        try {
            // 这里将com.mysql.cj.jdbc.Driver的对象存放在了DriverManager的registeredDrivers 对象中
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值