Java SPI机制的原理和实践

SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的 API,它是一种用于动态加载服务的机制。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。

image-20230808213108346

一、概述

Java SPI是SPI的一种重要实现方式,是JDK内置的一种服务发现方法,实现逻辑是:调用方通过调用JDK提供的标准化的服务接口,通过本地服务发现,加载第三方或者本地实现了该接口的类,通过这种方式,服务规范制定者制定接口规范,服务提供者按照接口进行实现。在JDK中实现数据库驱动按需加载就是利用SPI的方式实现的,JDK规定了java.sql.Driver接口,其具体实现可以是MySQL或者PostgreSQL,具体实现是第三方的驱动服务方提供,通过SPI机制加载供调用方使用。

img

1.应用场景

SPI机制应用在服务的规范制定者和服务的实现者需要分离的情况,就是接口中方法的规定和实现是分离的,这种机制经常在可选组件和可选插件的场景中使用,通过SPI的机制将具体的实现加载进来。具体的应用在 Java 的 java.util.spi package 中就约定了很多 SPI 接口。下面,列举一些 SPI 接口:

  1. TimeZoneNameProvider: 为 TimeZone 类提供本地化的时区名称。
  2. DateFormatProvider: 为指定的语言环境提供日期和时间格式。
  3. NumberFormatProvider: 为 NumberFormat 类提供货币、整数和百分比值。
  4. Driver: 从 4.0 版开始,JDBC API 支持 SPI 模式。旧版本使用 Class.forName() 方法加载驱动程序。
  5. PersistenceProvider: 提供 JPA API 的实现。

2.优点和缺点

优点:

SPI机制的优点是灵活性高,可以通过简单地添加或替换实现类来扩展应用程序的功能。同时,SPI机制也具有一定的可扩展性和可维护性,因为它将应用程序和具体实现解耦,实现了高内聚、低耦合的目标。

缺点:

SPI机制的缺点是需要程序员手动编写实现类并在META-INF/services目录下创建配置文件,这样会增加代码量和工作量。同时,SPI机制也存在安全风险,因为实现类是由外部提供的,可能存在恶意实现类的风险。

二、原理

1.主要组成和原理说明

要通过SPI实现动态服务发现,首先需要了解其主要的组成部分:

  1. **SPI 接口:**为服务提供者实现类约定的的接口或抽象类。
  2. **SPI 实现类:**实际提供服务的实现类。
  3. **SPI 配置:**Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。
  4. **ServiceLoader:**Java SPI 的核心类,用于加载 SPI 实现类。ServiceLoader 中有各种实用方法来获取特定实现、迭代它们或重新加载服务。

SPI原理说明

实现SPI的动态加载实现类主要是通过ServiceLoader类来实现。首先调用方通过ServiceLoader.load ()静态方法来加载SPI服务,ServiceLoader的成员变量中保存了实现类配置文件位置前缀、实现类信息、实现类ClassLoader信息、实现类实例信息等。

//实现类配置文件位置前缀
private static final String PREFIX = "META-INF/services/";// The class or interface representing the service being loaded
private final Class<S> service;// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();// The current lazy-lookup iterator
private LazyIterator lookupIterator;

ServiceLoader.load ()方法中调用构造方法ServiceLoader(),构造方法中调用reload() 方法,通过lookupIterator = new LazyIterator(service, loader);来初始化懒加载的迭代器,查看其next()方法,其核心就是从META-INF/services/ 目录中读取文件,将其转换为Class类对象加载进来。最后Class类对象通过newInstance()方法实例化,这样就根据SPI中的接口信息加载了实现类。

//ServiceLoader.java//静态方法load()
public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}//将实现类的全限定类名转换为完整路径private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {String fullName = PREFIX + service.getName();if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}pending = parse(service, configs.nextElement());}nextName = pending.next();return true;}
//Class.java//根据类对象信息生成对象实例
public T newInstance()throws InstantiationException, IllegalAccessException
{if (System.getSecurityManager() != null) {checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false);}// NOTE: the following code may not be strictly correct under// the current Java memory model.// Constructor lookupif (cachedConstructor == null) {if (this == Class.class) {throw new IllegalAccessException("Can not call newInstance() on the Class for java.lang.Class");}try {Class<?>[] empty = {};final Constructor<T> c = getConstructor0(empty, Member.DECLARED);// Disable accessibility checks on the constructor// since we have to do the security check here anyway// (the stack depth is wrong for the Constructor's// security check to work)java.security.AccessController.doPrivileged(new java.security.PrivilegedAction<Void>() {public Void run() {c.setAccessible(true);return null;}});cachedConstructor = c;} catch (NoSuchMethodException e) {throw (InstantiationException)new InstantiationException(getName()).initCause(e);}}Constructor<T> tmpConstructor = cachedConstructor;// Security check (same as in java.lang.reflect.Constructor)int modifiers = tmpConstructor.getModifiers();if (!Reflection.quickCheckMemberAccess(this, modifiers)) {Class<?> caller = Reflection.getCallerClass();if (newInstanceCallerCache != caller) {Reflection.ensureMemberAccess(caller, this, null, modifiers);newInstanceCallerCache = caller;}}// Run constructortry {return tmpConstructor.newInstance((Object[])null);} catch (InvocationTargetException e) {Unsafe.getUnsafe().throwException(e.getTargetException());// Not reachedreturn null;}
}

在代码阅读过程中,有点需要注意,在调用ServiceLoader.load ()方法时,会进行一次重载调用,会多传入了一个新的ClassLoader,这个ClassLoader是ApplicationClassLoader,他的作用是加载目前运行中应用的类对象,需要这样做的原因是JDK的双亲委派机制决定的,即加载ServiceLoader类的ClassLoader是BootstrapClassLoader,所以默认情况通过他去创建的对象也是BootstrapClassLoader,但通过SPI机制需要加载的实现类都在classpath中,无法被加载,所以通过Thread.currentThread().getContextClassLoader();这个方法来获取ApplicationClassLoader。

//ServiceLoader.javapublic static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}
//Thread.javapublic ClassLoader getContextClassLoader() {if (contextClassLoader == null)return null;SecurityManager sm = System.getSecurityManager();if (sm != null) {ClassLoader.checkClassLoaderPermission(contextClassLoader,Reflection.getCallerClass());}return contextClassLoader;
}

2.SPI在 JDBC DriverManager上的应用案例分析

在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName(“com.mysql.jdbc.Driver”)这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。

具体的使用方法是:

  1. JDBC接口定义:首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
  2. MySQL实现:在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
  3. 服务使用:通过以下方法就能直接使用
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);

原理分析

DriverManager.getConnection(url,username,password);执行的代码逻辑。

//DriverManager.java//通过用户名密码等信息获取连接
public static Connection getConnection(String url,String user, String password) throws SQLException {java.util.Properties info = new java.util.Properties();if (user != null) {info.put("user", user);}if (password != null) {info.put("password", password);}return (getConnection(url, info, Reflection.getCallerClass()));
}//通过实例化后的驱动连接数据库private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {/** When callerCl is null, we should check the application's* (which is invoking this class indirectly)* classloader, so that the JDBC driver class outside rt.jar* can be loaded from here.*/ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;synchronized(DriverManager.class) {// synchronize loading of the correct classloader.if (callerCL == null) {callerCL = Thread.currentThread().getContextClassLoader();}}if(url == null) {throw new SQLException("The url cannot be null", "08001");}println("DriverManager.getConnection(\"" + url + "\")");// Walk through the loaded registeredDrivers attempting to make a connection.// Remember the first exception that gets raised so we can reraise it.SQLException reason = null;for(DriverInfo aDriver : registeredDrivers) {// If the caller does not have permission to load the driver then// skip it.if(isDriverAllowed(aDriver.driver, callerCL)) {try {println("    trying " + aDriver.driver.getClass().getName());Connection con = aDriver.driver.connect(url, info);if (con != null) {// Success!println("getConnection returning " + aDriver.driver.getClass().getName());return (con);}} catch (SQLException ex) {if (reason == null) {reason = ex;}}} else {println("    skipping: " + aDriver.getClass().getName());}}// if we got here nobody could connect.if (reason != null)    {println("getConnection failed: " + reason);throw reason;}println("getConnection: no suitable driver found for "+ url);throw new SQLException("No suitable driver found for "+ url, "08001");}

DriverManager.java的静态代码段执行loadInitialDrivers();方法,该方法的主要逻辑为:

  1. 从系统变量中获取有关驱动的定义。
  2. 使用SPI来获取驱动的实现。
  3. 遍历使用SPI获取到的具体实现,实例化各个实现类。
  4. 根据第一步获取到的驱动列表来实例化具体实现类。
//DriverManager.java//静态代码段
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);}}}

通过以上方式,就能把实现Driver接口的驱动实现类加载进来了。

SPI在slf4j上应用的案例分析

todo

三、实践

1.实现步骤

  1. 定义接口:定义一个接口,声明一些抽象方法。
  2. 创建实现类:创建一个或多个实现该接口的类。
  3. 配置文件:在META-INF/services/目录下创建一个以接口全限定名为命名的文件,内容为实现类的全限定名,每行一个。
  4. 加载配置:使用ServiceLoader类加载配置文件并解析出实现类。

2.代码实现

实现通过SPI加载不同数据库实现方式的功能,数据库的实现方式包括Mysql和redis。

(1)实现接口

public interface DataStorage {String search(String key);
}

(2)创建实现类

创建2个实现类,分别是Mysql和redis的实现方式。

public class MysqlStorage implements DataStorage{@Overridepublic String search(String key) {return "【Mysql】搜索" + key + ",结果:No";}
}
public class RedisStorage implements DataStorage{@Overridepublic String search(String key) {return "【Redis】搜索" + key + ",结果:Yes";}
}

(3)配置文件

image-20230808205129264

(4)加载配置

打印出通过SPI加载到的数据库驱动。

import java.util.ServiceLoader;public class SpiDemo {public static void main(String[] args) {ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);System.out.println("============ Java SPI 测试============");serviceLoader.forEach(loader -> System.out.println(loader.search("Yes Or No")));}}

(5)验证效果

image-20230808205020924

四、总结

(1)SPI的核心思路是什么?
SPI源码的代码逻辑还是比较复杂,但是核心的思路就是通过ApplicationClassloader加载到配置好的实现类,获取ApplicationClassloader的方法是getContextClassLoader()方法,配置类信息记录在约定好的META-INF/services/目录中。

(2)SPI模式和API模式的区别?

两种模式都区分服务提供者和服务调用者的,API的服务实现放在服务提供者方那边,和接口一起提供;SPI的服务实现可以第三方实现或者自己实现,和接口是分开的。


TODO

  • 补充SPI在slf4j上应用的案例分析;
  • 增加Java、Spring、Dubbo三者SPI机制区别的总结;

参考资料

  1. 鲜为人知的Java SPI机制:https://juejin.cn/post/7224756843713036345#heading-3
  2. Java常用机制 - SPI机制详解:https://www.pdai.tech/md/java/advanced/java-advanced-spi.html
  3. 源码级深度理解 Java SPI:https://zhuanlan.zhihu.com/p/580004065?utm_medium=social&utm_oi=26977696219136&utm_psn=1573735461662785536&utm_source=wechat_session
  4. 阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别:https://juejin.cn/post/7210601680554246202

本文由博客一文多发平台 OpenWrite 发布!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/58881.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java经典面试题总结(一)

Java经典面试题总结&#xff08;一&#xff09; 题一&#xff1a;Java编译运行原理题二&#xff1a;JDK&#xff0c;JVM&#xff0c;JRE三者之间的关系题三&#xff1a;谈一下对冯诺依曼体系的了解题四&#xff1a;重载与重写的区别题五&#xff1a;拆箱装箱是指什么&#xff1…

通过Idea部署Tomcat服务器(详细图文教学)

1.在idea中创建项目 有maven构建工具就创建maven&#xff0c;没有就正常创建一个普通的java程序 创建普通java项目 2.添加框架 3.配置 Tomcat 注意&#xff1a;创建web项目后我们需要配置tomcat才能运行&#xff0c;下面我们来进行配置。 4.添加部署 回到服务器 5.完善配置 6…

算法与数据结构(二十一)二叉树(纲领篇)

备注&#xff1a;本文旨在通过 labuladong 的二叉树&#xff08;纲领篇&#xff09;理解框架思维&#xff0c;用于个人笔记及交流学习&#xff0c;版权归原作者 labuladong 所有&#xff1b; 我刷了这么多年题&#xff0c;浓缩出二叉树算法的一个总纲放在这里&#xff0c;也许…

jupyter lab环境配置

1.jupyterlab 使用虚拟环境 conda install ipykernelpython -m ipykernel install --user --name tf --display-name "tf" #例&#xff1a;环境名称tf2. jupyter lab kernel管理 show kernel list jupyter kernelspec listremove kernel jupyter kernelspec re…

Nginx反向代理配置+负载均衡集群部署

文章目录 负载均衡反向代理基础环境部署&#xff1a;什么是代理实验环境图流量过程 环境部署准备两台Web服务器安装Nginx准备页面内容添加主机名 代理服务器配置 修改windos hosts文件测试&#xff1a;终端浏览器 负载均衡反向代理基础环境部署&#xff1a; 什么是代理 正向代…

浅析基于安防监控EasyCVR视频汇聚融合技术的运输管理系统

一、项目背景 近年来&#xff0c;随着物流行业迅速发展&#xff0c;物流运输费用高、运输过程不透明、货损货差率高、供应链协同能力差等问题不断涌现&#xff0c;严重影响了物流作业效率&#xff0c;市场对于运输管理数字化需求愈发迫切。当前运输行业存在的难题如下&#xf…

docker 安装hive

记录一下使用docker快速搭建部署hive环境 目录 写在前面 步骤 安装docker 安装docker安装docker-compose配置docker国内镜像源&#xff08;可选&#xff09; 安装git & 配置github部署Hive docker-hive开始部署 使用Hive命令行收尾工作 安装vi、lrzsz关闭相关命令 END…

day5gdb调试模式和makefile

一、gdb调试 1.1gdb调试的作用 gdb调试检查的是逻辑错误&#xff0c;而非语法错误 1.2gdb流程 1、gcc -g 1.c ---->加-g参数的作用&#xff0c;生成可以调试的gdb文件 2、gdb 可执行文件名/a.out ---->进入gdb工具进行调试 3、输入l&#xff0c;带行号打印文件信息…

二、web核心防御机制(上)

文章目录 一、Web应用程序与风险1.1 Web应用程序的发展历程1.2 Web应用程序安全 二、核心防御机制2.1 处理用户访问2.2 处理用户输入2.2.1输入的多样性2.2.2 输入处理方法2.2.3 边界确认2.2.4 多步确认与规范化 一、Web应用程序与风险 1.1 Web应用程序的发展历程 在因特网发展…

旷视科技AIoT软硬一体化走向深处,生态和大模型成为“两翼”?

齐奏AI交响曲的当下&#xff0c;赛道玩家各自精彩。其中&#xff0c;被称作AI四小龙的商汤科技、云从科技、依图科技、旷视科技已成长为业内标杆&#xff0c;并积极追赶新浪潮。无论是涌向二级市场还是布局最新风口大模型&#xff0c;AI四小龙谁都不甘其后。 以深耕AIoT软硬一…

14_基于Flink将pulsar数据写入到HBase

3.7.基于Flink将数据写入到HBase 3.7.1.编写Flink完成数据写入到Hbase操作, 完成数据备份, 便于后续进行即席查询和离线分析 3.7.1.1.HBase基本介绍 hbase是基于Google发布bigTable论文产生一款软件, 是一款noSQL型数据, 不支持SQL. 不支持join的操作, 没有表关系, 不支持事…

Nacos基本应用

Nacos 基本应用 Nacos 提供了 SDK 和 OpenAPI 方式来完成服务注册与发现等操作&#xff0c;SDK 实际上是对于 http 请求的封装。 微服务架构的电子商务平台&#xff0c;其中包含订单服务、商品服务和用户服务。可以使用 Nacos 作为服务注册和发现的中心&#xff0c;以便各个微…