我是靠谱客的博主 完美水杯,最近开发中收集的这篇文章主要介绍[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

导言

在看双亲委派机制的时候,有人提到SPI的概念,于是学习了一番。SPI,即Service Provider Interface,服务提供者的接口。这个SPI接口是由服务端提供和调用的,这个接口定义了一些规范,但不提供具体的接口功能,功能由客户端决定,客户端根据需要实现这个接口,并进行配置,将接口的实现实例和配置打包部署在服务端,服务端不会直接调用接口的实现实例,而是 调用SPI接口,其产生的行为取决于配置文件。

SPI设计思想如下图所示:
在这里插入图片描述

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包,目录结构如下图所示:
在这里插入图片描述
代码如下:

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-INFservices目录,并创建文本文件,文件名为com.java.mysql.DBDriver,就是SPI接口的全名,写入以下内容,这就是接口实现类的全名

com.java.mysql.godriver.MySQLDriver

把这个项目打包成jar包,记名为MySQLDBDriver.jar

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

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

新建一个Java项目,导入上述两个包:DBDriverInterface.jar包和MySQLDBDriver.jar包,并利用ServiceLoader去加载MySQL数据库连接驱动,目录结构如下所示:
在这里插入图片描述
主类代码如下:

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,没有指明是哪个实现类,但在配置文件当中指明了。

如果你想实现其他数据库,例如Oracleredis等,可以按照第二步的方法操作。

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包,展开如下图所示:
在这里插入图片描述
META-INFservices目录下的文件名为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.Driverconnect()等方法,所以应该是在其父类NonRegisteringDriver当中有实现,点开源码可以看到有SPI接口java.sql.Driver中的方法:
在这里插入图片描述
看一下connect()方法源码:
在这里插入图片描述
鉴于我们讨论的是ServiceLoader,这个源码先不看。

回到com.mysql.jdbc.Driver.class字节码中,静态代码块里面调用了一句代码:

DriverManager.registerDriver(new Driver());

即注册了一个Driver实例,点进registerDriver()源码:
在这里插入图片描述
registerDriver()方法中,将当前的这个Driver()实例添加到registeredDrivers中,registeredDrivers是一个全局的集合对象:

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

源码看到这里,居然还没谈到ServiceLoader。再看看DriverManager.java,代码中搜索一下ServiceLoader.load或者Driver.class,发现:
在这里插入图片描述
这是明显在加载SPI接口的class对象,这个loadInitialDrivers()是在静态代码块中执行的:
在这里插入图片描述

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

在上面的代码中,Driver.class是一个class对象被ServiceLoader.load()加载了,说明ServiceLoader.load()应该有一个类加载器,点进load()源码:
在这里插入图片描述
可以看到关键代码:Thread.currentThread().getContextClassLoader();,下面捋一捋:

  • DriverManager.javajava.sql下的类,属于JDK核心类库,只能由根加载器加载:
    在这里插入图片描述
  • 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进行懒加载的。

最后

以上就是完美水杯为你收集整理的[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader的全部内容,希望文章能够帮你解决[设计与架构]Java当中的SPI(Service Provider Interface)设计思想(由双亲委派引出的问题)导言SPI的实现之ServiceLoaderJDBC4.0之ServiceLoader所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(40)

评论列表共有 0 条评论

立即
投稿
返回
顶部