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. 【DeepLearning】GoogLeNet

    InceptionV1 论文原文:Going deeper with convolutions    中英文对照 InceptionBN 论文原文:Batch Normalization: Accel ...

  2. Luogu P4208 [JSOI2008]最小生成树计数

    题意 给定一个 \(n\) 个点 \(m\) 条边的图,求最小生成树的个数. \(\texttt{Data Range:}1\leq n\leq 100,1\leq m\leq 10^4\) 题解 一 ...

  3. CSS动画之转换模块

    2D转换模块:注意点:1.可以类似于过渡模块一样简写,但是这里不是用逗号隔开而是用空格 2.2D的转换模块会修改元素的坐标系,所以旋转之后的平移就不是水平平移 格式:旋转:transform: rot ...

  4. 浅谈 Tarjan 算法

    目录 简述 作用 Tarjan 算法 原理 出场人物 图示 代码实现 例题 例题一 例题二 例题三 例题四 例题五 总结 简述 对于初学 Tarjan 的你来说,肯定和我一开始学 Tarjan 一样无 ...

  5. NOIP 2013 P1967 货车运输

    倍增求LCA+最大生成树 题目给出的是一张图,在图上有很多算法无法实现,所以要将其转化为树 题中可以发现货车的最后的载重量是由权值最小的一条边决定的,所以我们求最大生成树 求完最大生成树后我们得到一个 ...

  6. 【Kata Daily 190927】Counting sheep...(数绵羊)

    题目: Consider an array of sheep where some sheep may be missing from their place. We need a function ...

  7. Layui弹出层详解

    今天空了学习一下弹出层 还是一步步展示把 首先,layer可以独立使用,也可以通过Layui模块化使用.我个人一直是用的模块化的 所以下面素有的都是基于模块化的. 引入好相关文件就可以开始啦  今天放 ...

  8. Redis学习(一)——初识Redis

    1.Redis是什么 1)REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统. 2)Redis的特点 Red ...

  9. 快速识别烂项目!试试这款项目代码统计IDEA插件

    编程是一个很奇妙的事情,大部分的我们把大部分时间实际都花在了复制粘贴,而后修改代码上面. 很多时候,我们并不关注代码质量,只要功能能实现,我才不管一个类的代码有多长.一个方法的代码有多长. 因此,我们 ...

  10. vi&vim 基本使用方法

    vi/&vim 基本使用方法 本文介绍了vi (vim)的基本使用方法,但对于普通用户来说基本上够了!i/vim的区别简单点来说,它们都是多模式编辑器,不同的是vim 是vi的升级版本,它不仅 ...