JDBC【3】-- SPI技术使用以及在数据库连接中的使用

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也是一样的机制去实现拓展功能。

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

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

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

公众号:秦怀杂货店


__EOF__

本文作者秦怀杂货店
本文链接https://www.cnblogs.com/Damaer/p/13992170.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   第十六封  阅读(304)  评论(0编辑  收藏  举报
编辑推荐:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示