1.SPI是什么?

SPI,即是Service Provider Interface,是一种服务提供(接口实现)发现机制,可以通过ClassPath路径下的META-INF/Service文件查找文件,加载里面定义的类。

一般可以用来启用框架拓展和替换组件,比如在最常见的数据库连接JDBC中,java.sql.Driver,不同的数据库产商可以对接口做不一样的实现,但是JDK怎么知道别人有哪些实现呢?这就需要SPI,可以查找到接口的实现,对其进行操作。

用两个字解释:解耦

2.如何使用SPI来提供自定义服务?

我们来写一个简单的例子:



整个项目结构:

  • SPI-Project:maven项目

    • DBInterface:maven项目,parent是SPI-Project,定义了一个接口com.aphysia.sqlserver.DBConnectionService,自己不做实现。
    • MysqlConnection:prarent是SPI-Project,实现了接口DBConnectionService,也就是MysqlConnectionServiceImpl
    • SqlServerConnection:prarent 也是SPI-Project,实现了DBConnectionService,也就是SqlServerConnectionServiceImpl
    • WebProject:测试项目,模拟web项目里面使用数据库驱动。

不管是MySqlConnection还是SqlServerConnection两个module中,都是去实现了DBInterface的接口,并且在resource/META-INF/services下都需要声明所实现的类,文件名就是实现的接口全限定名com.aphysia.sql.DBConnectionService,文件里面就是具体的实现类的全限定名,比如:com.aphysia.mysql.MysqlConnectionServiceImpl

SPI-Project的pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <groupId>com.aphysia</groupId>
<artifactId>SPI-Project</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version> <modules>
<module>DbInterface</module>
<module>MySqlConection</module>
<module>SqlServerConnection</module>
<module>WebProject</module>
</modules>
</project>

2.1 DBInterface定义接口

DBInterface是SPIProject的一个module,主要是定义一个规范(接口),不做任何实现。

pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SPI-Project</artifactId>
<groupId>com.aphysia</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion> <artifactId>DbInterface</artifactId>
</project>

定义的接口(模拟了java提供的数据库驱动的情景,定义了驱动规范):DBConnectionService.java

package com.aphysia.sql;
public interface DBConnectionService {
void connect();
}

2.2 模拟Mysql实现驱动

接口的第一种实现,相当于模拟第三方Mysql对接口做了自己的拓展:

pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SPI-Project</artifactId>
<groupId>com.aphysia</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion> <artifactId>MySqlConection</artifactId> <dependencies>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>DbInterface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

实现了前面定义的接口:

MysqlConnectionServiceImpl

package com.aphysia.mysql;

import com.aphysia.sqlserver.DBConnectionService;

public class MysqlConnectionServiceImpl implements DBConnectionService {
public void connect() {
System.out.println("mysql 正在连接...");
}
}

声明实现,在resource/META-INF.services/下定义一个文件,名为com.aphysia.sql.DBConnection,里面内容是:

com.aphysia.mysql.MysqlConnectionServiceImpl

2.3 模拟SqlServer实现驱动

SqlServerConnection也是一个module,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SPI-Project</artifactId>
<groupId>com.aphysia</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion> <artifactId>SqlServerConnection</artifactId> <dependencies>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>DbInterface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

接口的第二种实现,相当于第三方SqlServer对接口做了自己的拓展:SqlServerConnectionServiceImpl

package com.aphysia.sqlserver;

public class SqlServerConnectionServiceImpl implements DBConnectionService {
public void connect() {
System.out.println("sqlServer 正在连接...");
}
}

声明实现,在resource/META-INF.services/下定义一个文件,名为com.aphysia.sql.DBConnection,里面内容是:

com.aphysia.sqlserver.SqlServerConnectionServiceImpl

2.4 模拟用户使用不同驱动

上面两种不同的接口实现,注意需要在resource下声明,文件名是基类的全限定名,里面内容是具体实现类的全限定名

而我们自己使用项目的时候呢?肯定是需要哪一个驱动就引入哪一个驱动的jar包。

比如我们在webProject中导入两种实现:MysqlConnectionSqlServerConnection:

    <dependencies>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>DbInterface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>MySqlConection</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>SqlServerConnection</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

测试代码如下:

import com.aphysia.sql.DBConnectionService;

import java.util.ServiceLoader;

public class Test {
public static void main(String[] args) { ServiceLoader<DBConnectionService> serviceLoader= ServiceLoader.load(DBConnectionService.class);
for (DBConnectionService dbConnectionService : serviceLoader) {
dbConnectionService.connect();
}
}
}

输出:

mysql 正在连接...
sqlServer 正在连接...

如果我们只在pom文件里面引入mysql的实现呢?答案很明显,只会输出下面一句:

mysql 正在连接...

也就是对于使用的人来说,不需要自己再做什么操作,只需要把包引入进来即可,简单易用。

具体完整代码: https://github.com/Damaer/DemoCode/tree/main/SPI-Project,仅供参考

3. ServiceLoader实现原理

ServiceLoader位于java.util包下,其主要代码如下:


public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
private final Class<S> service; private final ClassLoader loader; private final AccessControlContext acc; // Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // The current lazy-lookup iterator
private LazyIterator lookupIterator; public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
} private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
} 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 {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = 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;
} private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
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 {
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
} public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
} public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
} public void remove() {
throw new UnsupportedOperationException();
} }
public Iterator<S> iterator() {
return new Iterator<S>() { Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator(); public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
} public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
} public void remove() {
throw new UnsupportedOperationException();
} };
} public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
} public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
} public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
} }

我们调用ServiceLoader.load()获取接口的实现,实际上也是调用了 ServiceLoader(Class<S> svc, ClassLoader cl),里面都是调用reload()reload()里面做了些什么操作呢?

先把provider清空,然后创建了LazyIterator对象,LazyIterator是一个内部类,实现了Iterator接口,实际上就是一个懒加载的迭代器。什么时候加载呢?

在迭代器调用的时候,调用hasNextService(),去解析resource/META-INF/services下面的实现,并完成实现类的实例化。这里的实例化是使用反射,也是通过全限定类名。class.forName()

解析的时候,每一行代表一个实现类,将已经发现的接口进行缓存,放到private LinkedHashMap<String,S> providers中,同时对外提供遍历迭代的方法。

4. SPI的应用

我们在使用mysql驱动的时候,在mysql-connector-java-version.jar中,有一个文件是Resource/service/java.sql.Driver文件,里面记录的是:

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

也就是声明了java.sql.Driver的实现类是com.mysql.jdbc.Driver,不需要手动使用Class.forName()手动加载。

同样的,slf4j也是一样的机制去实现拓展功能。

这种思想,通过服务约定-->服务实现-->服务自动注册-->服务发现和使用,完成了提供者和使用方的解耦,真的很强...

此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

技术之路不在一时,山高水长,纵使缓慢,驰而不息。

公众号:秦怀杂货店

JDBC【3】-- SPI技术使用以及在数据库连接中的使用的更多相关文章

  1. JDBC 学习笔记(三)—— 数据源(数据库连接池):DBCP数据源、C3P0 数据源以及自定义数据源技术

    本文目录:        1.应用程序直接获取连接的缺点(图解)        2.使用数据库连接池优化程序性能(图解)        3.可扩展增强某个类方法的功能的三种方式        4.自定 ...

  2. JDBC实例--JDBC连接池技术解密,连接池对我们不再陌生

    一.为什么我们要用连接池技术? 前面的数据库连接的建立及关闭资源的方法有些缺陷.统舱传统数据库访问方式:一次数据库访问对应一个物理连接,每次操作数据库都要打开.关闭该物理连接, 系统性能严重受损. 解 ...

  3. C#-数据库访问技术 ado.net——创建 数据库连接类 与 数据库操作方法 以及简单的数据的添加、删除、修改、查看

    数据库访问技术 ado.net 将数据库中的数据,提取到内存中,展示给用户看还可以将内存中的数据写入数据库中去 并不是唯一的数据库访问技术,但是它是最底层的数据库访问技术 1.创建数据库,并设置主外键 ...

  4. 配置ssh框架启动tomcat服务器报异常Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment]

    在Spring中配置jdbc时,引用的是dbcp.jar包,在db.properties配置文件中,使用了之前的properties配置文件的用户名username(MySql用户名) 然后在启动服务 ...

  5. JDBC(Java Data Base Connectivity,java数据库连接)

    JDBC概述 JDBC(Java Data Base Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言 ...

  6. jdbc数据访问技术

    jdbc数据访问技术 1.JDBC如何做事务处理? Con.setAutoCommit(false) Con.commit(); Con.rollback(); 2.写出几个在Jdbc中常用的接口 p ...

  7. Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment]

    使用hibernate的时候,报出这个错误Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvir ...

  8. php单例模式在数据库连接中的使用

    今天同事问到一个关于单例模式在php中是否有用的问题,我们知道,单例的目的是为了避免重复生产相同的对象,一般情况在数据库连接中,为了避免多次拿到相同数据库连接,使用到单例模式,我们来看一下单例模式数据 ...

  9. 【Hook技术】实现从"任务管理器"中保护进程不被关闭 + 附带源码 + 进程保护知识扩展

    [Hook技术]实现从"任务管理器"中保护进程不被关闭 + 附带源码 + 进程保护知识扩展 公司有个监控程序涉及到进程的保护问题,需要避免用户通过任务管理器结束掉监控进程,这里使用 ...

随机推荐

  1. Luogu P3602 Koishi Loves Segments

    传送门 题解 既然是选取区间,没说顺序 肯定先排遍序 都是套路 那么按什么排序呢??? 为了方便处理 我们把区间按左端点从小到大排序 把关键点也按从小到大排序 假设当扫到 \(i\) 点时,i 点之前 ...

  2. 02.django配置跨域并开发测试接口

    1.创建一个测试项目   1.1 创建项目和APP   '''1.创建项目和APP''' django-admin startproject BookManage # 创建项目 python mana ...

  3. 一篇搞定Java集合类原理

    Java集合类实现原理 1.Iterable接口 定义了迭代集合的迭代方法 iterator() forEach() 对1.8的Lambda表达式提供了支持 2. Collection接口 定义了集合 ...

  4. 3.1 spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖

    本次博客的目标 1. 手写spring循环依赖的整个过程 2. spring怎么解决循环依赖 3. 为什么要二级缓存和三级缓存 4. spring有没有解决构造函数的循环依赖 5. spring有没有 ...

  5. C/C++四种取整函数floor,ceil,trunc,round

    处理浮点数操作常用到取整函数,C/C++提供了四种取整函数 floor函数 floor函数:向下取整函数,或称为向负无穷取整 double floor(double x); floor(-5.5) = ...

  6. ubuntu常见问题有效解决办法

    1.关于weget "无法建立SSL连接"的解决方法 wget在使用HTTPS协议时,默认会去验证网站的证书,而这个证书验证经常会失败. 解决办法 原命令加上"--no- ...

  7. win10免费通用永久激活秘钥分享 win1020H2正式版序列号推荐

    win10最新永久激活密钥神key如下: win10免费密钥 NW06D-722C0-5X6A1-MQ83B-2ER3D win10免费密钥 NP3KM-NQZD6-X406E-1QPKR-4VRZD ...

  8. youtube-dl 源码看看,例子是下载网页

    1, 跑起来 下载 youtube-dl, 配合 launch.json, # 本文中 himala 是代指,具体见文末的 github repo "configurations" ...

  9. Python基础教程分享,视频教程免费下载!

    给大家分享一套我初学Python时看的一套基础教程,是视频教程,免费分享给大家,希望对正在学习Python或者打算学习Python的朋友有帮助哈~~ 废话不多说 为期92天的Python基础教程视频教 ...

  10. Python对MySQL进行增删查改

    python连接MySQL数据库:pymysql # 测试操作 import pymysql # 打开数据库 db = pymysql.connect("localhost", & ...