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. GoSDK的安装及环境变量配置 入门详解 - 精简归纳

    GoSDK的安装及环境变量配置 入门详解 - 精简归纳 JERRY_Z. ~ 2020 / 10 / 29 转载请注明出处!️ 目录 GoSDK的安装及环境变量配置 入门详解 - 精简归纳 一.进入G ...

  2. python实现有趣的数学逻辑程序

    1.无重复数字的三位数 题目:有1.2.3.4个数字, 能组成多少个互不相同且无重复数字的三位数? 都是多少? for i in range(1,5): for j in range(1,5): fo ...

  3. Masking Personal Information

    Masking Personal Information We are given a personal information string S, which may represent eithe ...

  4. .netcore 简单使用ElasticSearch

    .netcore 简单使用ElasticSearch(7.6) 最近在捣鼓学习了下ElasticSearch,在此记录下使用.netcore操作elastic search 的实现(简单的封装,使用) ...

  5. 寻找性能更优秀的动态 Getter 和 Setter 方案

    反射获取 PropertyInfo 可以对对象的属性值进行读取或者写入,但是这样性能不好.所以,我们需要更快的方案. 方案说明 就是用表达式编译一个 Action<TObj,TValue> ...

  6. 【Kata Daily 190906】Vasya - Clerk(职员)

    题目: The new "Avengers" movie has just been released! There are a lot of people at the cine ...

  7. 记git一些基本用法

    git init        在合适的位置建一个文件夹,并在当前目录下右键打开    git Bash,利用git init把这个目录改成git可以管理的仓库 git add 要添加的文件名    ...

  8. 交换机基于接口划分VLAN(汇聚层设备作为网关)

    组网图形 简介 划分VLAN的方式有:基于接口.基于MAC地址.基于IP子网.基于协议.基于策略(MAC地址.IP地址.接口).其中基于接口划分VLAN,是最简单,最常见的划分方式,如接入层设备作为网 ...

  9. ElementUI表格行编辑单元格编辑支持(输入框,选择框)Demo

    嗯,需要做成这个样子,所以网上查了些资料.整理了下.提供几个一个思路.不足之处请小伙伴指出来.  普通版的table可编辑内嵌select选择框,输出框,编辑删除添加等 <!DOCTYPE ht ...

  10. 部署Dotnet Core应用到Kubernetes(二)

    前一篇文章,概念性地介绍了K8s的一些基础组件,如Pod.部署和服务.这篇文章,我打算写写如何使用YAML清单定义和配置这些资源.   实际上,在K8s集群中创建对象有几种方式 - 命令,或声明.两种 ...