什么是SPI机制?
SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用于启用框架扩展和替换组件
如:java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现
Java中SPI机制的主要思想就是将装配的控制权移到程序之外,在模块化设计中,这个机制尤其重要,其核心思想就是解耦
SPI简单案例
目录结构:
第一步:创建一个Search接口
package com.comtom.spi;
import java.util.List;
public interface Search {
public List<String> searchDoc(String keyword);
}
第二步:创建“文件搜索”实现类
package com.comtom.spi;
import java.util.List;
public class FileSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索"+keyword);
return null;
}
}
第三步:创建“数据库搜索”实现类
package com.comtom.spi;
import java.util.List;
public class DataBaseSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("数据库搜索"+keyword);
return null;
}
}
第四步:在resource目录下创建目录/META-INF/services
,在该目录下创建Search类的全限定名路径文件,如下图所示:
注:IDEA需要选择文件类型,右键选择Override File Type
,弹框Choose File Type
,选择textmate
在com.comtom.spi.Search
文件中添加FileSearch类的全限定名或DataBaseSearch类的全限定名
com.comtom.spi.FileSearch
com.comtom.spi.DataBaseSearch
开始测试
public class Main {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search next = iterator.next();
next.searchDoc("hello world");
}
}
}
结果:在com.comtom.spi.Search
文件添加几个类全限定名,就会有几条结果数据
文件搜索hello world
数据库搜索hello world
SPI机制的应用
JDBC DriverManager
在JDBC4.0之前,连接数据库需要使用
Class.forName("com.mysql.jdbc.Driver")
先加载数据库相关的驱动,然后再获取连接等操作,在JDBC4.0以后,不需要使用Class.forName("com.mysql.jdbc.Driver")
加载驱动,就可以直接获取连接,这种方式就是使用了SPI机制
1、 Java在rt.jar包中定义了java.sql.Driver
接口,没有具体的实现,类似于上述案例中的Search接口
2、 在mysql的jar包mysql-connector-java-5.1.49.jar
中,有META-INF/services
目录,里面的文件定义了驱动实现类,类似于上述案例的FileSearch和DataBaseSearch
1、 在建立连接时,就会在加载类的时候加载驱动
2、 loadInitialDrivers()方法就会调用Driver.class对应的权限定名文件,读取对应实现类,类似测试时的操作
loadInitialDrivers()方法实现步骤如下:
**1、** 从系统变量中获取有关驱动的定义
**2、** 使用SPI来获取驱动的实现
**3、** 遍历使用SPI获取到的具体实现,实例化各个实现类
**4、** 根据第一步获取到的驱动列表来实例化具体实现类
SPI机制的实现步骤:
1、 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
封装接口类型和类加载器,并初始化了一个迭代器
2、 Iterator<Driver> driversIterator = loadedDrivers.iterator();
获取迭代器
3、 遍历迭代器,调用driversIterator.hasNext()
方法时会去resource目录下所有的META-INF/services
目录下的java.sql.Driver
文件,并找到文件中的实现类的名字
4、 调用driversIterator.next();
方法,此时就会根据驱动名字具体实例化各个实现类
Common Logging
common-logging(也称为Jakarta Commons Logging,JCL)是常用的日志库门面
JCL依赖
<!--引入common-logging-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
日志的实例是通过LogFactory的getLog()方法创建的:
public static getLog(Class clazz) throws LogConfigurationException {
return getFactory().getInstance(clazz);
}
分析getFactory方法做了什么
public static LogFactory getFactory() throws LogConfigurationException {
//获取一个类加载器
ClassLoader contextClassLoader = getContextClassLoaderInternal();
//如果类加载器为空,且经过诊断确认,则输出类加载器为空
if (contextClassLoader == null && isDiagnosticsEnabled()) {
logDiagnostic("Context classloader is null.");
}
//返回这个类加载器注册过的日志工厂
LogFactory factory = getCachedFactory(contextClassLoader);
//如果注册过,直接返回,没有注册过,
if (factory != null) {
return factory;
} else {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] LogFactory implementation requested for the first time for context classloader " + objectId(contextClassLoader));
logHierarchy("[LOOKUP] ", contextClassLoader);
}
//加载properties文件
Properties props = getConfigurationFile(contextClassLoader, "commons-logging.properties");
ClassLoader baseClassLoader = contextClassLoader;
String factoryClass;
if (props != null) {
//如果commons-logging.properties配置文件存在,且文件中配置了use_tccl参数,参数值为false,则将当前类加载器赋值给获取到的日志类加载器
factoryClass = props.getProperty("use_tccl");
if (factoryClass != null && !Boolean.valueOf(factoryClass)) {
baseClassLoader = thisClassLoader;
}
}
//决定使用哪个factory
//首先尝试查找vm系统中org.apache.commons.logging.LogFactory,并判断其是否可以指定为factory
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Looking for system property [org.apache.commons.logging.LogFactory] to define the LogFactory subclass to use...");
}
try {
factoryClass = getSystemProperty("org.apache.commons.logging.LogFactory", (String)null);
if (factoryClass != null) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Creating an instance of LogFactory class '" + factoryClass + "' as specified by system property " + "org.apache.commons.logging.LogFactory");
}
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
} elseif (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] No system property [org.apache.commons.logging.LogFactory] defined.");
}
} catch (SecurityException var9) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] A security exception occurred while trying to create an instance of the custom factory class: [" + trim(var9.getMessage()) + "]. Trying alternative implementations...");
}
} catch (RuntimeException var10) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] An exception occurred while trying to create an instance of the custom factory class: [" + trim(var10.getMessage()) + "] as specified by a system property.");
}
throw var10;
}
//然后开始尝试使用java spi服务发现机制,在META-INF/services下寻找org.apache.commons.logging.LogFactory实现
String factoryClassName;
if (factory == null) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Looking for a resource file of name [META-INF/services/org.apache.commons.logging.LogFactory] to define the LogFactory subclass to use...");
}
try {
InputStream is = getResourceAsStream(contextClassLoader, "META-INF/services/org.apache.commons.logging.LogFactory");
if (is != null) {
BufferedReader rd;
try {
rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
} catch (UnsupportedEncodingException var7) {
rd = new BufferedReader(new InputStreamReader(is));
}
factoryClassName = rd.readLine();
rd.close();
if (factoryClassName != null && !"".equals(factoryClassName)) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Creating an instance of LogFactory class " + factoryClassName + " as specified by file '" + "META-INF/services/org.apache.commons.logging.LogFactory" + "' which was present in the path of the context classloader.");
}
factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader);
}
} elseif (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] No resource file with name 'META-INF/services/org.apache.commons.logging.LogFactory' found.");
}
} catch (Exception var8) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] A security exception occurred while trying to create an instance of the custom factory class: [" + trim(var8.getMessage()) + "]. Trying alternative implementations...");
}
}
}
//尝试从classpath根目录下的commons-logging.properties中查找org.apache.commons.logging.LogFactory属性指定的factory
if (factory == null) {
if (props != null) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Looking in properties file for entry with key 'org.apache.commons.logging.LogFactory' to define the LogFactory subclass to use...");
}
factoryClass = props.getProperty("org.apache.commons.logging.LogFactory");
if (factoryClass != null) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'");
}
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
} elseif (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Properties file has no entry specifying LogFactory subclass.");
}
} elseif (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] No properties file available to determine LogFactory subclass from..");
}
}
//最后,使用后备factory实现,org.apache.commons.logging.impl.LogFactoryImpl
if (factory == null) {
if (isDiagnosticsEnabled()) {
logDiagnostic("[LOOKUP] Loading the default LogFactory implementation 'org.apache.commons.logging.impl.LogFactoryImpl' via the same classloader that loaded this LogFactory class (ie not looking in the context classloader).");
}
factory = newFactory("org.apache.commons.logging.impl.LogFactoryImpl", thisClassLoader, contextClassLoader);
}
if (factory != null) {
cacheFactory(contextClassLoader, factory);
if (props != null) {
Enumeration names = props.propertyNames();
while(names.hasMoreElements()) {
String name = (String)names.nextElement();
factoryClassName = props.getProperty(name);
factory.setAttribute(name, factoryClassName);
}
}
}
return factory;
}
}
logFactory()方法实现步骤:
1、 从VM系统属性中查找org.apache.commons.logging.LogFactory
2、 使用SPI服务发现机制,发现org.apache.commons.logging.LogFactory的实现
3、 查找classpath根目录下的commons-logging.properties的org.apache.commons.logging.LogFactory属性是否指定factory实现
4、 使用默认factory实现,org.apache.commons.logging.impl.LogFactoryImpl
总结
LogFactory抽象类中的getLog()方法返回值类型为Log接口,提供了所有日志相关的方法,因此如果日志实现者需要提供一个日志系统,只需要实现该接口,并且使用继承自org.apache.commons.logging.LogFactory的子类创建Log,必然可以实现一个松耦合的日志系统
Spring
在springboot的自动装配过程中,最终会加载
META-INF/spring.factories
文件,而加载的过程是由SpringFactoriesLoader
加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories
配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
public staticfinal String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
// 取得资源文件的URL
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
// 遍历所有的URL
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
// 组装数据,并返回
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
插件
SPI机制使用最多的就是插件开发,SPI机制提供了一个万物皆可插件的思想
Eclipse使用OGSI作为插件系统的基础,动态添加新的插件,停止现有插件,以动态的方式管理组件的声明周期
插件的文件结构
在指定目录下包含以下三个文件:
META-INF/MANIFEST.MF
: 项目基本配置信息,版本、名称、启动器等build.properties
: 项目的编译配置信息,包括,源代码路径、输出路径plugin.xml
:插件的操作配置信息,包含弹出菜单及点击菜单后对应的操作执行类等
当eclipse启动时,会遍历plugins文件夹的目录,扫描每个插件的清单文件MANIFEST.MF
,并建立一个内部模型来记录它所找到的每一个插件的信息,这就实现了动态添加新的插件,eclipse不需要知道插件如何开发,只需要在启动时根据配置文件解析、加载到系统里就可以了,这就是SPI机制的体现
SPI深入理解
SPI的使用
现实生产中的使用步骤:
1、 由某组织或公司定义标准,就是定义接口,例如java.sql.Driver
2、 由具体的厂商或框架开发者实现接口
3、 在META-INF/services目录下定义一个名字为接口全限定名的文件,文件内容是具体的实现类文件名
实现接口,implements接口
4、 引用jar包来实现具体功能,完成定制业务
SPI和API的区别
SPI:接口位于调用方所在的包中
理解:
1、 由A先定义标准,A并不知道其他人怎么实现
2、 因为有了ServiceLoader,SPI服务提供发现机制,B直接拿A的标准来实现,C也拿A的标准来实现,可能存在多个不同的实现,甚至A都会拿自己的标准来实现
3、 调用方不关心这些不同的调用,直接利用ServiceLoader.load()调用标准接口,这样每换一个厂商,代码也不需要跟着改 松耦合思想
API(应用程序接口):接口位于实现方所在的包中
理解:
1、 由A定义标准,A自己实现
2、 没有其他开发厂商,调用方仅仅依赖并直接使用,无权选择具体不同的实现
SPI的实现原理
//实现Iterable接口,用于遍历所有的服务实现类
publicfinalclass ServiceLoader<S>
implements Iterable<S>
{
//查找配置文件的目录
privatestaticfinal String PREFIX = "META-INF/services/";
//表示被加载的服务的类或接口
privatefinal Class<S> service;
//类加载器,用于定位、加载、实例化服务提供者
privatefinal ClassLoader loader;
//访问控制上下文
privatefinal AccessControlContext acc;
//缓存已经被实例化的服务提供者,按照实例化的顺序存储
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//迭代器
private LazyIterator lookupIterator;
//重新加载;相当于重新创建ServiceLoader,用于新的服务提供者安装到正在运行的java虚拟机中的情况
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 static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
thrownew ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
thrownew ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
//解析服务提供者配置文件中的一行
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
//首先去掉注释校验,然后保存
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
//重复的配置项和已经被实例化的配置项不会被保存
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
//返回下一行行号
return lc + 1;
}
//解析配置文件,解析指定的url配置文件
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;
//使用parseLine方法进行解析,未被实例化的服务提供者会被保存到缓存中去
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();
}
//服务提供者查找的迭代器
privateclass LazyIterator
implements Iterator<S>
{
Class<S> service;//服务提供者接口
ClassLoader loader;//类加载器
Enumeration<URL> configs = null;//保存实现类的url
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) {
returntrue;
}
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()) {
returnfalse;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
returntrue;
}
private S nextService() {
if (!hasNextService())
thrownew 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);
}
thrownew 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() {
thrownew UnsupportedOperationException();
}
}
//获取迭代器
public Iterator<S> iterator() {
//返回遍历服务提供者的迭代器
returnnew Iterator<S>() {
//以懒加载的方式加载可用的服务提供者
//懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成
//按照实例化顺序返回已经缓存的服务提供者实例
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
returntrue;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
thrownew UnsupportedOperationException();
}
};
}
//为指定的服务使用指定的类加载器来创建一个ServiceLoader
publicstatic <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
returnnew ServiceLoader<>(service, loader);
}
//使用线程上下文的类加载器来创建ServiceLoader
publicstatic <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
//使用扩展类加载器为指定的服务创建ServiceLoader
//只能找到并加载已经安装到当前Java虚拟机中的服务提供者,应用程序类路径中的服务提供者将被忽略
publicstatic <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
实现了Iterable
接口,所以它具有迭代器的属性,这里主要是实现了迭代器的hasNext
和next
方法,这里的迭代器lookupIterator
是懒加载迭代器(LazyIterator
)LazyIterator
中的hasNext
方法,静态变量PREFIX
就是"META-INF/services/"
目录,这也就是需要在classpath
下的"META-INF/services/"
目录里创建一个以服务接口命名的文件的原因- 通过反射加载类对象(
Class.forName()
),并用newInstance
方法对类进行实例化,然后将实例化后的对象缓存到providers
对象中,providers
是LinkedHashMap<String,S>
类型的,S为接口规范类似上述SPI简单案例中的Search接口,然后返回实例对象s(多态,类似于Search s=new FileSearch()
)
所以这也是为什么ServiceLoader
不是实例化以后就去读取配置文件中的具体实现,并进行实例化。 而是等到使用迭代器去遍历的时候,才会加载对应的配置文件去解析,调用hasNext方法的时候会去加载配置文件进行解析,调用next方法的时候进行实例化并缓存
SPI机制的缺陷
- 不能按需加载,需要遍历所有实现,并全部实例化,然后在循环中才能找到我们需要的实现
如果不想用某些实现类,或者某些类实例化很耗时,这就造成了资源的浪费
- 获取某个实现类的方式不够灵活,只能通过Iterator迭代器获取,不能根据某个参数来获取对应的实现类
- 多个并发多线程使用ServiceLoader类的实例是不安全的