天天看点

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

导言

在看双亲委派机制的时候,有人提到

SPI

的概念,于是学习了一番。

SPI

,即

Service Provider Interface

,服务提供者的接口。这个

SPI

接口是由服务端提供和调用的,这个接口定义了一些规范,但不提供具体的接口功能,功能由客户端决定,客户端根据需要实现这个接口,并进行配置,将接口的实现实例和配置打包部署在服务端,服务端不会直接调用接口的实现实例,而是 调用

SPI

接口,其产生的行为取决于配置文件。

SPI

设计思想如下图所示:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

SPI

接口一般由客户端和服务端共同确定(更多是服务端根据不同客户端的需求,形成一个兼容并包的接口)。客户端根据需求实现这个接口,得到一个自己的实例。服务端会有一个以这个

SPI

接口为文件名的配置文件,配置文件当中指定服务端将调用哪个客户端的接口实例。

SPI

的本质是多态,多态的好处是在解耦的同时,实现了适配。

SPI的实现之ServiceLoader

Java是通过

ServiceLoader

提供

SPI

功能的,我们来模拟服务端、客户端的角色,理解

SPI

是怎么工作的。

场景:不同的数据库想要通过Java连接,必须实现一个规范的接口,并提供相应的JDBC驱动。

一、服务端与客户端协商提供统一的SPI接口

新建一个Java项目,给出统一的

SPI

接口,代码如下,其中有一个

connect()

方法。

package com.java.db;

/**
 * @ClassName DBDriver
 * @Description TODO
 * @Date 2021/1/11 22:19
 */
public interface DBDriver {
    public String connect();
}
           

把这个项目打包成

jar

包,记名为

DBDriverInterface.jar

二、客户端提供带有配置文件的驱动类

新建一个Java项目,创建一个

MySQLDriver

类,实现

DBDriver

接口,因此这个项目中需要导入

DBDriverInterface.jar

包,目录结构如下图所示:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

代码如下:

package com.java.mysql.godriver;

import com.java.db.DBDriver;

/**
 * @ClassName MySQLDriver
 * @Description TODO
 * @Date 2021/1/11 22:21
 */
public class MySQLDriver implements DBDriver {
    public String connect() {
        System.out.println("MySQL连接成功");
        return "一个MySQL连接实例";
    }
}
           

resources

目录下,添加

META-INF\services

目录,并创建文本文件,文件名为

com.java.mysql.DBDriver

,就是

SPI

接口的

全名

,写入以下内容,这就是接口实现类的

全名

把这个项目打包成

jar

包,记名为

MySQLDBDriver.jar

三、服务端利用ServiceLoader加载驱接口

利用

ServiceLoader

加载接口的

class

对象,就可以加载配置文件当中实现了这个接口的所有驱动类,并执行相应的动作。

新建一个Java项目,导入上述两个包:

DBDriverInterface.jar

包和

MySQLDBDriver.jar

包,并利用

ServiceLoader

去加载MySQL数据库连接驱动,目录结构如下所示:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

主类代码如下:

package com.java.db;

import java.util.Iterator;
import java.util.ServiceLoader;

/**
 * @ClassName Main
 * @Description TODO
 * @Date 2021/1/11 22:43
 */
public class Main {
    public static void main(String[] args) {
        // 加载
        ServiceLoader<DBDriver> drivers = ServiceLoader.load(DBDriver.class);
        Iterator<DBDriver> iterator = drivers.iterator();

        // 加载了哪些内容?
        while (iterator.hasNext()){
            DBDriver next = iterator.next();
            String connect = next.connect();
            System.out.println(connect);
        }
    }
}
           

结果如下所示:

MySQL连接成功
一个MySQL连接实例

Process finished with exit code 0
           

主方法当中,通过

ServiceLoader

加载的

class

对象是

DBDriver.class

,没有指明是哪个实现类,但在配置文件当中指明了。

如果你想实现其他数据库,例如

Oracle

redis

等,可以按照

第二步

的方法操作。

JDBC4.0之ServiceLoader

MySQL驱动源码

JDBC4.0开始使用ServiceLoader的方式加载数据库连接的驱动,导入

mysql-connector-java-xxxx.jar

包之后,连接

MySQL

的代码如下:

package com.java.www.day20210111.dbconnection;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * @ClassName MySQLConnectionTest
 * @Description TODO
 * @Date 2021/1/11 19:48
 */
public class MySQLConnectionTest {

    private final static String DB_URL = "jdbc:mysql://ip:3306/dbname?useSSL=true&useUnicode=true&characterEncoding=utf-8";
    private final static String DB_USER = "root";
    private final static String DB_PASSWD = "123456";

    public static void main(String[] args) {
        try {
            // Class.forName("com.mysql.jdbc.Driver");
            Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWD);
            System.out.println(connection);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
           

JDBC4.0

之后就不需要

Class.forName("com.mysql.jdbc.Driver");

这句代码了,因为已经通过

ServiceLoader

的方式进行加载了。我们既然已经知道了

ServiceLoader

可以通过配置文件加载类,去查找一下

mysql-connector-java-xxxx.jar

包,展开如下图所示:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

META-INF\services

目录下的文件名为

java.sql.Driver

,根据前面的讨论可知,这是

SPI

接口名,说明在

DriverManager

类当中通过

SeriviceLoader

加载了

java.sql.Driver

这个接口,这是

JDK

的核心类库,点进源码看一下:

package java.sql;

import java.util.logging.Logger;


public interface Driver {

    Connection connect(String url, java.util.Properties info)
        throws SQLException;

    boolean acceptsURL(String url) throws SQLException;
    
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;

    // 省略代码
}
           

java.sql.Driver

接口当中,有很多方法。

DriverManager

获取连接对象调用的是

connect()

方法,因此可以推断:MySQL驱动对这个接口的实现类当中一定有这个方法的实现,因此再看一下

mysql-connector-java-xxxx.jar

包的配置文件的内容:

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

对于

com.mysql.jdbc.Driver

而言,加载的类是

com.mysql.jdbc

包下的

Driver.java

的字节码文件,点进字节码看一下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.mysql.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}
           

这个类名和其实现的

java.sql.Driver

同名(没关系),但没有实现

java.sql.Driver

connect()

等方法,所以应该是在其父类

NonRegisteringDriver

当中有实现,点开源码可以看到有

SPI

接口

java.sql.Driver

中的方法:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

看一下

connect()

方法源码:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

鉴于我们讨论的是

ServiceLoader

,这个源码先不看。

回到

com.mysql.jdbc.Driver.class

字节码中,静态代码块里面调用了一句代码:

即注册了一个

Driver

实例,点进

registerDriver()

源码:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

registerDriver()

方法中,将当前的这个

Driver()

实例添加到

registeredDrivers

中,

registeredDrivers

是一个全局的集合对象:

private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = 
    new CopyOnWriteArrayList<>();
           

源码看到这里,居然还没谈到

ServiceLoader

。再看看

DriverManager.java

,代码中搜索一下

ServiceLoader.load

或者

Driver.class

,发现:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

这是明显在加载

SPI

接口的

class

对象,这个

loadInitialDrivers()

是在静态代码块中执行的:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

打破双亲委派之线程上下文加载器

在上面的代码中,

Driver.class

是一个

class

对象被

ServiceLoader.load()

加载了,说明

ServiceLoader.load()

应该有一个类加载器,点进

load()

源码:

[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader

可以看到关键代码:

Thread.currentThread().getContextClassLoader();

,下面捋一捋:

  • DriverManager.java

    java.sql

    下的类,属于JDK核心类库,只能由根加载器加载:
    [设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader
  • com.mysql.jdbc.Driver.class

    是第三方类,只能由应用加载器加载。

我们看到的是在

DriverManager.java

当中加载了

java.sql.Driver

,虽然

java.sql.Driver

也是JDK核心类库,但我们知道本质上是加载的第三方类

com.mysql.jdbc.Driver.class

,问题是

DriverManager.java

是只能由根加载器加载,那么它为什么可以去加载第三方类库呢?

以前,我们需要通过

Class.forName("com.mysql.jdbc.Driver");

加载,这是通过应用加载器加载的。

JDBC4.0

之后就不需要这行代码了,因为

DriverManager.java

中有这句代码:

Thread.currentThread().getContextClassLoader();

测一下这句代码可以得到什么。

ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("loader = " + loader);
           

这句代码获取的是应用加载器:

loader = sun.misc.Launcher$AppClassLoader@18b4aac2

Process finished with exit code 0
           

这就是

线程上下文类加载器

,虽然只有一句代码,但是这种办法实现了在父加载器中加载了子加载器,弥补了双亲委派机制的缺陷。使用

线程上下文类加载器

是打破双亲委派机制的两种方式之一。

至于接下来,当前线程的应用加载器如何加载第三方的类,可以再继续看

load()

源码,发现是通过

LazyIterator

进行懒加载的。