引入
我们知道每种数据库都是一个完整的独立的系统,假设我们现在的产品要兼容大多数数据库厂商,比如MySQL,Oracle,DB2等,那么怎么做呢?可以制作一些约定,大家都基于这个约定进行相关操作,比如调用者可以通过约定去连接数据库,而数据库厂商则根据这个约定实现连接数据库的具体操作。
这个看起来有点像设计模式中的门面模式,调用者只需要对这个约定进行相关操作,而具体的实现由相应的厂商实现,假设要从MySQL切换到DB2,只需要在底层悄悄的替换即可,那么具体是怎么替换的呢?
在maven项目中,假设我们目前使用的是MySQL,那么我们的maven依赖是不是有 mysql-connector-java ,换言之,我们可以把这个依赖替换成我们想要的数据库依赖
那么它到底是怎么实现替换依赖就可以切换数据库的呢 以MySQL为例,打开MySQL的jar,看到了META-INF 和 com 目录 进入META-INF/services/ 目录下,发现了 java.sql.Driver 文件 查看就发现了我们熟悉的驱动,而这个驱动就是MySQL厂商实现的 那他是怎么和我们的java应用程序相关联的呢,通过源码,我们发现是实现了java的Driver接口
package com.mysql.cj.jdbc;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
}
}
这个接口正好是jdk的拓展包里的 现在可以知道上面描述的约定便是这个 java.sql.Driver 接口。大家都基于这个约定进行相应的操作 这个便是我们常说的SPI机制
SPI
什么是SPI
SPI的全名为Service Provider Interface,当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
JAVA中的实现
在Java中的实现就是ServiceLoader(源码部分省略)
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;
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
private LazyIterator lookupIterator;
.........
}
简单来说就是会在jar包下寻找 META-INF/services/ 目录下的文件,文件名就是接口名,文件内容就是这个接口的实现类,可以有多个实现类。
解析文件的内容
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
在这里进行加载,并且放进缓存中
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();
}
实战模拟
这是一个约定,在一个单独的模块中,打jar包
public interface IDataBase {
String getDataBaseName();
}
这也是一个单独的模块,依赖上面制定的约定,实现DB2数据库,打jar包
public class DB2 implements IDataBase {
public String getDataBaseName() {
return "db2";
}
}
这也是一个单独的模块,依赖上面制定的约定,实现MySQL数据库,打jar包
public class Mysql implements IDataBase{
public String getDataBaseName() {
return "mysql";
}
}
测试
客户端依赖
<dependencies>
<dependency>
<groupId>com.luo</groupId>
<artifactId>db-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.luo</groupId>
<artifactId>db2</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
public class Test {
public static void main(String[] args) {
ServiceLoader<IDataBase> dataBases = ServiceLoader.load(IDataBase.class);//加载指定类的所有实现
Iterator<IDataBase> iterator = dataBases.iterator();
while (iterator.hasNext()){ //遍历每一个实现
IDataBase next = iterator.next();
System.out.println(next.getDataBaseName());
}
}
}
因为我只依赖了DB2,所以输出db2
优缺点
优点:解耦合,懒加载
缺点:会一次性加载配置文件中的所有实现类
|