深入剖析Java SPI机制

2019/11/13

1. 什么是SPI

SPI(Service Provider Interface,服务提供者接口),比如:java.sql.Driver接口,不同厂商针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。 SPI经典的实现就是JDK1.6引进的 final类: java.util.ServiceLoader

官方注释

  * A simple service-provider loading facility.
 *
 * <p> A <i>service</i> is a well-known set of interfaces and (usually
 * abstract) classes.  A <i>service provider</i> 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.

大概意思是:ServiceLoader是一个简单的服务提供者加载设备,主要用来装载一系列的service provider。 而且ServiceLoader可以通过service provider的配置文件来装载指定的service provider。说了这么多是否还是一头雾水,下面先看个简单的例子,主要有4个类:

IServiceLoader (接口)
AServiceLoaderImpl (实现类A)
BServiceLoaderImpl (实现类B)
ServiceLoaderTest (测试类)

步骤:

  • 创建一个接口文件
  • 在resources资源目录下创建META-INF/services文件夹
  • 在services文件夹中创建文件,以接口全名命名,eg:com.xxx.test.spi.IServiceLoader,在文件中添加接口的实现类,全名
  • 创建接口实现类
  • ServiceLoader还有一个特定的限制,就是我们提供的这些具体实现的类必须提供无参数的构造函数,否则ServiceLoader就会报错。

IServiceLoader.java 该接口就提供一个简单的方法 say()

public interface IServiceLoader {
    String say();
}

AServiceLoaderImpl.java

 public class AServiceLoaderImpl implements IServiceLoader {
    @Override
    public String say() {
        return "A say()";
    }
}

BServiceLoaderImpl.java

 public class BServiceLoaderImpl implements IServiceLoader {
    @Override
    public String say() {
        return "B say()";
    }
}

ServiceLoaderTest.java

 import java.util.ServiceLoader;
 public class ServiceLoaderTest {

    public static void main(String[] argus) {
    
            ServiceLoader<IServiceLoader> serviceLoader = ServiceLoader.load(IServiceLoader.class);
            Iterator<IServiceLoader> iterator = serviceLoader.iterator();
            while (iterator.hasNext()) {
                IServiceLoader next = iterator.next();
                System.out.println(next + " : " + next.say());
            }
    }
}

META-INF/services下的 com.xxx.test.spi.IServiceLoader 文件,内容如下,两个实现类的全称

com.xxx.test.spi.AServiceLoaderImpl
com.xxx.test.spi.BServiceLoaderImpl

执行的结果值

com.xxx.test.spi.AServiceLoaderImpl@6fffcba5 : A say()
com.xxx.test.spi.BServiceLoaderImpl@2aafb23c : B say()

以上就是一个SPI的实现,整个例子里面我们并没有去直接new出接口IServiceLoader的实现类,最终却把实现类的结果都输出,原因是什么?先看看ServiceLoader load()方法的源码

/**
 * Creates a new service loader for the given service type, using the
 * current thread's {@linkplain java.lang.Thread#getContextClassLoader
 * context class loader}.
 *
 * <p> An invocation of this convenience method of the form
 *
 * <blockquote><pre>
 * ServiceLoader.load(<i>service</i>)</pre></blockquote>
 *
 * is equivalent to
 *
 * <blockquote><pre>
 * ServiceLoader.load(<i>service</i>,
 *                    Thread.currentThread().getContextClassLoader())</pre></blockquote>
 *
 * @param  <S> the class of the service type
 *
 * @param  service
 *         The interface or abstract class representing the service
 *
 * @return A new service loader
 */
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

通过源码我们知道,load()方法会把IServiceLoader的所有子类型都装载到JVM中并可以使用, 那么java是如何找到他的子类而且有且仅有两个子类呢?再看看前面定义的META-INF下面的配置文件, 是不是就可以发现java通过查找META-INF目录下对应IServiceLoader全名的文件,然后把文件里所有的类全名并装载它们

2. 将博客搬到自己的服务器

ServiceLoader使用的思考

目前来说,我们已经看到了ServiceLoader的一种简单使用场景。在前面这个示例中,我们对接口的实现是在同一个工程里面,
如果我们需要使用他们的时候,完全没必要通过ServiceLoader再装载具体实现进来,我们完全可以通过一个ArrayList再将他们的具体实现一个个加进去就可以了。
那么,什么时候用ServiceLoader比较合适呢?既然在同一个工程里我们jvm可以直接装载他们的实现,那么很可能就是我们要装载的实现不在同一个工程里,
可能是需要我们动态添加的,这个时候,他们的引入不是编译时候加进来的,是在运行的时候加入的,我们不能像使用普通引入的静态类库那样来使用他们。
所以这就是ServiceLoader的优点所在了。比如说我们有一组接口,有的实现是我们本地的,我们可以在使用代码里直接引入进来。而有的却是第三方实现的,
他们可能会在运行的时候加入进来。那么我们事先是不清楚的,也就不可能定死了他们的实现是哪个具体的类名。在前面ServiceLoader的使用里,
我不用把你具体的实现引用到代码里,而只是在配置文件里指定就可以了。这一点也是我们后面要讨论的DriverManager的一个重要的核心思想。

DriverManager的应用和设计思路

DriverManager是jdbc里管理和注册不同数据库driver的工具类。从它设计的初衷来看,和我们前面讨论的场景有相似之处。首先一个,针对一个数据库 可能会存在着不同的数据库驱动实现。我们在使用特定的驱动实现时不希望修改现有的代码才能达到目的,而希望通过一个简单的配置就可以达到效果。

比如说,我们现在有一个数据库的驱动A,我们希望在程序里使用它而不修改代码。一种理想的选择就是我们将驱动A的信息加入到一个配置文件中,程序通过读取配置文件信息将A加载进来。而以后如果我们希望改用另外一个驱动B的时候,我们之需要将配置文件里的信息修改成驱动B的。我们肯定不希望在代码里写什么registerDriver(new A());之类的代码。尤其在有的情况下我们根本没有使用这些驱动的源代码。

看下DriverManger的思路图,图片摘自互联网

这里mysql JDBC 的实例来阐述SPI以及DriverManager的思想

首先在工程中加入 mysql-connector-java 依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.45</version>
</dependency>

申明一个测试类,使用 DriverManager.getConnection(url, username, password);去连接数据库

public class MysqlTest {

    public static void main(String[] args) throws SQLException {
        Connection connection = null;
        try {
            String url = "jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&createDatabaseIfNotExist=true";
            String username = "root";
            String password = "123456";

            String sql = "select * from user;";

            connection = DriverManager.getConnection(url, username, password);
            Statement statement = connection.createStatement();
            final ResultSet rs = statement.executeQuery(sql);
            while (rs.next()) {
                int id = rs.getInt(1);
                String name = rs.getString(2);
                System.out.println("id = " + id + ", name = " + name);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if(null != connection) {
                connection.close();
            }
        }
    }
}

DriverManager.getConnection(),对DriverManager类的主动使用,进入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");
    }
    
    
     private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        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);
            }
        }
    }

上面的源码中可以看到DriverManager.java中有个静态的方法,因为MysqlTest.java主动使用DriverManager.java,根据类加载机制, 导致DriverManager的静态方法loadInitialDrivers()被初始化,loadInitialDrivers()方法中ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);这里就回归到上面我们例子降到的SPI,接下来展开mysql-connector-java.jar如下图,在META-INF/service中有两个实现类

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

两个类都实现了java.mysql.Driver,进入com.mysql.jdbc.Driver 源码类

import java.sql.SQLException;

/**
 * The Java SQL framework allows for multiple database drivers. Each driver should supply a class that implements the Driver interface
 * 
 * <p>
 * The DriverManager will try to load as many drivers as it can find and then for any given connection request, it will ask each driver in turn to try to
 * connect to the target URL.
 * 
 * <p>
 * It is strongly recommended that each Driver class should be small and standalone so that the Driver class can be loaded and queried without bringing in vast
 * quantities of supporting code.
 * 
 * <p>
 * When a Driver class is loaded, it should create an instance of itself and register it with the DriverManager. This means that a user can load and register a
 * driver by doing Class.forName("foo.bah.Driver")
 */
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     * 
     * @throws SQLException
     *             if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

源码可以看出com.mysql.jdbc.Driver 实现了 java.sql.Driver

    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

进入java.sql.DriverManager源码

   private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
   
    /**
     * Registers the given driver with the {@code DriverManager}.
     * A newly-loaded driver class should call
     * the method {@code registerDriver} to make itself
     * known to the {@code DriverManager}. If the driver is currently
     * registered, no action is taken.
     *
     * @param driver the new JDBC Driver that is to be registered with the
     *               {@code DriverManager}
     * @exception SQLException if a database access error occurs
     * @exception NullPointerException if {@code driver} is null
     */
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
    }
    
    
        /**
     * Registers the given driver with the {@code DriverManager}.
     * A newly-loaded driver class should call
     * the method {@code registerDriver} to make itself
     * known to the {@code DriverManager}. If the driver is currently
     * registered, no action is taken.
     *
     * @param driver the new JDBC Driver that is to be registered with the
     *               {@code DriverManager}
     * @param da     the {@code DriverAction} implementation to be used when
     *               {@code DriverManager#deregisterDriver} is called
     * @exception SQLException if a database access error occurs
     * @exception NullPointerException if {@code driver} is null
     * @since 1.8
     */
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

源码可以看出 com.mysql.jdbc.Driver类最终被加载,并且缓存到registeredDrivers这个线程安全的ArrayList中,整个SPI过程就全部实现,那么在MysqlTest.java类中,JVM知道要找的是com.mysql.jdbc.Driver驱动,而不是其他数据库的驱动呢,看下面的代码

try{
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {
// Do nothing
}

通过上面的解析,可以发现,我们使用SPI查找具体的实现的时候,需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要实现。这应该也是最大的缺点,需要把所有的实现都实例化了,即便我们不需要,也都给实例化了。

说明: 对于springboot的mysql自动装配等等方式,其实大多数是触发了SPI,和MysqlTest.java中使用DriverManager.connect()是等同的


如果您觉得文章帮到了您,那就打赏给个赞呗🙂

(转载本站文章请注明作者和出处 程序猿小尾巴

Show Disqus Comments