一文搞懂Java/Spring/Dubbo框架中的SPI机制
几天前和一位前辈聊起了Spring技术,大佬突然说了SPI,作为一个熟练使用Spring的民工,心中一紧,咱也不敢说不懂,而是在聊完之后赶紧打开了浏览器,开始的学习之路,所以也就有了这篇文章。废话不多说,咱们开始正文。
定义
SPI的英文全称就是Service Provider Interface,看到全称,心里就有了底了,这是一种将服务接口与服务实现分离以达到解耦可拔插以最大提升了程序可扩展性的机制,
这个机制最大的优点就是无须在代码里指定,进而避免了代码污染,实现了模块的可拔插。在JDK、Spring、Dubbo中都有着它的身影,毕竟框架最核心的作用之一就是解耦,下面详细介绍SPI在JDK、Spring、Dubbo中具体的实现;
SPI基础
java中加载类的方式使用的双亲委派,而在双亲委派模型中,子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的类。这就导致了双亲委派模型并不能解决所有的类加载器问题。
双亲委派案例
案例:Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些SPI的接口由核心类库提供,却由第三方实现,这样就存在一个问题:SPI 的接口是 Java 核心库的一部分,是由BootstrapClassLoader加载的;SPI实现的Java类一般是由AppClassLoader来加载的。BootstrapClassLoader是无法找到 SPI 的实现类的,因为它只加载Java的核心库。它也不能代理给AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。 ----> 所以,使用SPI 打破双亲委派模式。
解决方式
使用线程上下文类加载器(ContextClassLoader)加载:如果不做任何的设置,Java应用的线程的上下文类加载器默认就是AppClassLoader。在核心类库使用SPI接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。
通常我们可以通过Thread.currentThread().getClassLoader()和Thread.currentThread().getContextClassLoader()获取线程上下文类加载器。
JDK SPI
JDK提供了一种比较简单SPI实现,其规范具体如下:
- 制定统一的规范(接口,比如 java.sql.Driver);
- 服务提供商提供这个规范具体的实现,在自己jar包的META-INF/services/目录里创建一个以服务接口命名的文件,内容是实现类的全命名(比如:com.mysql.jdbc.Driver);
- 平台引入外部模块的时候,就能通过该jar包META-INF/services/目录下的配置文件找到该规范具体的实现类名,然后装载实例化,完成该模块的注入;
在java中使用spi最常见的场景就是连接数据库时使用,下面从源码层面对java中spi机制进行解析,下面关键代码已经在代码行后进行数字标识,阅读代码时大家可以参考。
DriverManager类:
static {
//加载初始驱动,跳到方法具体实现
loadInitialDrivers(); // 1
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() { // 2
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//这一步其实是初始化完成serviceLoader
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 3
//初始化完成,对加载的具体实现类进行遍历加载
Iterator<Driver> driversIterator = loadedDrivers.iterator(); // 11
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
//迭代器后续判断,可以调到13
while(driversIterator.hasNext()) { // 12
driversIterator.next(); // 15
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
ServiceLoader类
//根据服务类型初始化serviceloader
public static <S> ServiceLoader<S> load(Class<S> service) { //4
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader) // 5
{
return new ServiceLoader<>(service, loader); //6
}
// 构造器私有,每次new新对象,但进行lazy load
private ServiceLoader(Class<S> svc, ClassLoader cl) { // 7
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload(); // 8
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader); // 9
}
// ServiceLoader类中的内部类LazyIterator
private LazyIterator(Class<S> service, ClassLoader loader) { // 10
this.service = service;
this.loader = loader;
}
//固定路径前缀META-INF/services下的文件
private boolean hasNextService() { // 14
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;
}
//使用class.forname进行类的加载初始化
private S nextService() { // 18
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(); // 13
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() { // 16
if (acc == null) {
return nextService(); // 17
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
以上就是整个java spi的实现,只是简单地的进行扫描加载,并没有实现按需加载。
使用案例
common-logging apache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现, 发现日志提供商是通过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,通过读取该文件的内容找到日志提工商实现类。只要我们的日志实现里包含了这个文件,并在文件里制定 LogFactory工厂接口的实现类即可。
Spring SPI
Spring中接口BeanDefinitionDocumentReader是使用SPI机制解析包含spring bean 定义的xml文档,在进行xml命名空间解析时使用默认实现类DefaultNamespaceHandlerResolver,会懒加载spring.handler文件内配置的实现类进内存,加载逻辑如下:
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
// 多线程二次验证
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}
读取META-INF/spring.handlers目录下的实现类进jvm,spring.handlers的结构如下:
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler
基本结构就是映射关系,前面是具体xsd路径,后面接具体实现类,有人会有疑问,那我要接的接口类哪里去了,是不用找了吗,那怎么对应呢?这里的命名空间解析类对应的接口为NamespaceHandler,因为只有一个,所有不需要进行额外指定。
spring中自定义标签的加载过程会和以上过程重度相关,后续会专门分析spring中自定义标签的过程。
Spring boot SPI
在springboot的自动装配过程中,最终会加载META-INF/spring.factories
文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories
配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
自动装配jar中的spring.factory的结构如下,由于没有在文件名上指定接口的名称,所有在每个第一行都会对要实现的接口进行申明
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
......
不同场景对应不同的文件格式,但其基本原理都是一样的。
dubbo SPI
由于对dubbo的接触比较少,这里暂时空起,后续会补齐,哈哈哈。//TODO
总结
SPI这个很神奇的机制,解耦神器,如果要进行代码重构,分离,我觉得可以重点考虑这个东东,同时也是大框架必备机制。