Java SPI服务发现及Dubbo和Spring中SPI的应用总结
一、SPI简介
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现, 简单来说,它就是一种动态替换发现的机制, 举个例子来说, 有个接口,想运行时动态的给它添加实现,你只需要添加一个实现,而后,把新加的实现,描述给JDK知道就行啦(通过改一个文本文件即可)我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。
SPI 机制的思想来源正是:开闭原则,对扩展开放,对修改关闭。对于一个特定的程序,如 Java 的 java.sql.Driver,就是一个接口,Mysql 的 Driver 实现就是通过 SPI 机制被程序加载的。不难想到,其他 Driver 的实现也可以依靠 SPI 机制去实现。
SPI 机制的思想来源正是:开闭原则,对扩展开放,对修改关闭。对于一个特定的程序,如 Java 的 java.sql.Driver,就是一个接口,Mysql 的 Driver 实现就是通过 SPI 机制被程序加载的。不难想到,其他 Driver 的实现也可以依靠 SPI 机制去实现。
类图中,接口对应定义的抽象SPI接口;实现方实现SPI接口;调用方依赖SPI接口。
SPI接口的定义在调用方,在概念上更依赖调用方;组织上位于调用方所在的包中;实现位于独立的包中。
当接口属于实现方的情况,实现方提供了接口和实现,这个用法很常见,属于API调用。我们可以引用接口来达到调用某实现类的功能。
二、Java SPI
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务实现的工具类是:java.util.ServiceLoader。
2.1 SPI接口
package com.springboot.study.tests.spi; /** * @Author: guodong * @Date: 2021/5/19 18:31 * @Version: 1.0 * @Description: */ public interface Robot { public void sayHello(); }
接口定义了一个机器人接口,有一个方法sayHello。
2.2 SPI具体实现
package com.springboot.study.tests.spi.impl; import com.springboot.study.tests.spi.Robot; /** * @Author: guodong * @Date: 2021/5/19 18:33 * @Version: 1.0 * @Description: */ public class ARobot implements Robot { @Override public void sayHello() { System.out.println("---ARobot----"); } }
package com.springboot.study.tests.spi.impl; import com.springboot.study.tests.spi.Robot; /** * @Author: guodong * @Date: 2021/5/19 18:33 * @Version: 1.0 * @Description: */ public class BRobot implements Robot { @Override public void sayHello() { System.out.println("--BRobot---"); } }
2.3 增加META-INF目录文件
Resource下面创建META-INF/services 目录里创建一个以服务接口命名的文件
2.4 增加RobotService
package com.springboot.study.tests.spi; import java.util.Optional; import java.util.ServiceLoader; import java.util.stream.StreamSupport; /** * @Author: guodong * @Date: 2021/5/19 18:35 * @Version: 1.0 * @Description: */ public class RobotService { public Robot getRobot() { ServiceLoader<Robot> serializers = ServiceLoader.load(Robot.class); final Optional<Robot> serializer = StreamSupport.stream(serializers.spliterator(), false) .findFirst(); return serializer.get(); } public static void main(String[] args) { RobotService robotService = new RobotService(); Robot robot = robotService.getRobot(); robot.sayHello(); } }
2.5 SPI的用途
数据库DriverManager、Spring、ConfigurableBeanFactory等都用到了SPI机制,这里以数据库DriverManager为例,看一下其实现的内幕。
DriverManager是jdbc里管理和注册不同数据库driver的工具类。针对一个数据库,可能会存在着不同的数据库驱动实现。我们在使用特定的驱动实现时,不希望修改现有的代码,而希望通过一个简单的配置就可以达到效果。 在使用mysql驱动的时候,会有一个疑问,DriverManager是怎么获得某确定驱动类的?我们在运用Class.forName(“com.mysql.jdbc.Driver”)加载mysql驱动后,就会执行其中的静态代码把driver注册到DriverManager中,以便后续的使用。
在JDBC4.0之前,连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要Class.forName来加载驱动,直接获取连接即可,这里使用了Java的SPI扩展机制来实现。
在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
2.6 Mysql DriverManager实现
我们怎么去确定使用哪个数据库连接的驱动呢?这里就涉及到使用Java的SPI扩展机制来查找相关驱动的东西了,关于驱动的查找其实都在DriverManager中,DriverManager是Java中的实现,用来获取数据库连接,在DriverManager中有一个静态代码块如下:
static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
可以看到其内部的静态代码块中有一个loadInitialDrivers方法,loadInitialDrivers用法用到了上文提到的spi工具类ServiceLoader:
private static void loadInitialDrivers() { 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<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); /* 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{ while(driversIterator.hasNext()) { driversIterator.next(); } } 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); } } }
遍历使用SPI获取到的具体实现,实例化各个实现类。在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类。
2.7 SPI解决的问题场景描述
在我们设计一套API供别人调用的时候,如果同一个功能的要求特别多,或者同一个接口要面对很复杂的业务场景,这个时候我们该怎么办呢?
- 其一:我们可以规范不同的系统调用,也就是传递一个系统标识;
- 其二:我们在内部编码的时候可以使用不同的条件判断语句进行处理;
- 其三:我们可以写几个策略类来来应对这个复杂的业务逻辑,比如同一个功能的实现,A实现类与B实现类的逻辑一点也不一样,但是目标是一样的,这个时候使用策略类是毋庸置疑的?
三、Spring SPI
Spring中使用的类是SpringFactoriesLoader,在org.springframework.core.io.support包中
- SpringFactoriesLoader 会扫描 classpath 中的 META-INF/spring.factories文件。
- SpringFactoriesLoader 会加载并实例化 META-INF/spring.factories 中的制定类型
- META-INF/spring.factories 内容必须是 properties 的Key-Value形式,多值以逗号隔开。
3.1 使用
和 SPI 不同,由于 SpringFactoriesLoader 中的配置文件格式是 properties 文件,因此,不需要要像 SPI 中那样为每个服务都创建一个文件, 而是选择直接把所有服务都扔到 META-INF/spring.factories 文件中。
com.fsx.serviceloader.IService=com.fsx.serviceloader.HDFSService,com.fsx.serviceloader.LocalService // 若有非常多个需要换行 可以这么写 // 前面是否顶头没关系(Spring在4.x版本修复了这个bug) com.fsx.serviceloader.IService=\ com.fsx.serviceloader.HDFSService,\ com.fsx.serviceloader.LocalService
public static void main(String[] args) throws IOException { List<IService> services = SpringFactoriesLoader.loadFactories(IService.class, Main.class.getClassLoader()); List<String> list = SpringFactoriesLoader.loadFactoryNames(IService.class, Main.class.getClassLoader()); System.out.println(list); //[com.fsx.serviceloader.HDFSService, com.fsx.serviceloader.LocalService] System.out.println(services); //[com.fsx.serviceloader.HDFSService@794cb805, com.fsx.serviceloader.LocalService@4b5a5ed1] }
使用细节:
- spring.factories内容的key不只能是接口,也可以是抽象类、具体的类。但是有个原则:=后面必须是key的实现类(子类)
- key还可以是注解,比如SpringBoot中的的key:org.springframework.boot.autoconfigure.EnableAutoConfiguration,它就是一个注解
- 文件的格式需要保证正确,否则会返回[](不会报错)
- =右边必须不是抽象类,必须能够实例化。且有空的构造函数~
- loadFactories依赖方法loadFactoryNames。loadFactoryNames方法只拿全类名,loadFactories拿到全类名后会立马实例化
- 此处特别注意:loadFactories实例化完成所有实例后,会调用AnnotationAwareOrderComparator.sort(result)排序,所以它是支持Ordered接口排序的,这个特点特别的重要。
3.2 原理剖析
因为Spring的这个配置文件和上面的不一样,它的名字是固定的spring.factories,里面的内容是key-value形式,因此一个文件里可以定义N多个键值对。我认为它比源生JDK的SPI是更加灵活些的~
它主要暴露了两个方法:loadFactories和loadFactoryNames
public abstract class SpringFactoriesLoader { private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class); /** * The location to look for factories. * <p>Can be present in multiple JAR files. */ public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; /** * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader. * <p>The returned factories are sorted in accordance with the {@link AnnotationAwareOrderComparator}. * <p>If a custom instantiation strategy is required, use {@link #loadFactoryNames} * to obtain all registered factory names. * @param factoryClass the interface or abstract class representing the factory * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default) * @see #loadFactoryNames * @throws IllegalArgumentException if any factory implementation class cannot * be loaded or if an error occurs while instantiating any factory */ public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) { Assert.notNull(factoryClass, "'factoryClass' must not be null"); ClassLoader classLoaderToUse = classLoader; if (classLoaderToUse == null) { classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); } List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse); if (logger.isTraceEnabled()) { logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames); } List<T> result = new ArrayList<T>(factoryNames.size()); for (String factoryName : factoryNames) { result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse)); } AnnotationAwareOrderComparator.sort(result); return result; } /** * Load the fully qualified class names of factory implementations of the * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given * class loader. * @param factoryClass the interface or abstract class representing the factory * @param classLoader the ClassLoader to use for loading resources; can be * {@code null} to use the default * @see #loadFactories * @throws IllegalArgumentException if an error occurs while loading factory names */ public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); try { Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List<String> result = new ArrayList<String>(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } } @SuppressWarnings("unchecked") private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) { try { Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader); if (!factoryClass.isAssignableFrom(instanceClass)) { throw new IllegalArgumentException( "Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]"); } Constructor<?> constructor = instanceClass.getDeclaredConstructor(); ReflectionUtils.makeAccessible(constructor); return (T) constructor.newInstance(); } catch (Throwable ex) { throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex); } } }
四、Spring-Boot-Starter
SpringBoot的 @SpringBootApplication 注解,里面还有一个 @EnableAutoConfiguration 注解,开启自动配置的。
可以发现这个自动配置注解在另一个工程,而这个工程里也有个spring.factories文件,如下图:
我们知道在SpringBoot的目录下有个spring.factories文件,里面都是一些接口的具体实现类,可以由SpringFactoriesLoader加载。
那么同样的道理,spring-boot-autoconfigure模块也能动态加载了。看一下其中的内容:
为了证明这一点,我们看一下 dubbo-spring-boot-starter 的结构
<?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.demo</groupId> <artifactId>custom-spring-boot-autoconfigure</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>2.1.6.RELEASE</version> </dependency> </dependencies> </project>
@Configuration @ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet") public class CustomAutoConfiguration { @Bean public CustomConfig customConfig(){ System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!第三方自定义的配置"); return new CustomConfig(); } }
/** * 自定义配置 */ public class CustomConfig { private String value = "Default"; }
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.demo.CustomAutoConfiguration
五、Dubbo SPI
我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。该方法的逻辑比较简单,本章就不进行分析了。下面我们从 ExtensionLoader 的 getExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。
- 对Dubbo进行扩展,不需要改动Dubbo的源码
- 自定义的Dubbo的扩展点实现,是一个普通的Java类,Dubbo没有引入任何Dubbo特有的元素,对代码侵入性几乎为零。
- 将扩展注册到Dubbo中,只需要在ClassPath中添加配置文件。使用简单。而且不会对现有代码造成影响。符合开闭原则。
- dubbo的扩展机制设计默认值:@SPI(“dubbo”) 代表默认的spi对象
- Dubbo的扩展机制支持IoC,AoP等高级功能
- Dubbo的扩展机制能很好的支持第三方IoC容器,默认支持Spring Bean,可自己扩展来支持其他容器,比如Google的Guice。
- 切换扩展点的实现,只需要在配置文件中修改具体的实现,不需要改代码。使用方便。
六、ServiceLoader类分析
ServiceLoader.class是一个工具类,根据META-INF/services/xxxInterfaceName下面的文件名,加载具体的实现类.
从load(Search.class)进去,我们来扒一下这个类,下面主要是贴代码,分析都在代码注释内.
1、可以看到,里面并没有很多逻辑,主要逻辑都交给了LazyIterator这类
/* *入口, 获取一下当前类的类加载器,然后调用下一个静态方法 */ public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } /* *这个也没有什么逻辑,直接调用构造方法 */ public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } /** * 也没有什么逻辑,直接调用reload */ 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(); } /** * 直接实例化一个懒加载的迭代器 */ public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); }
2、LazyIterator这个迭代器只需要关心hasNext()和next(), hasNext()里面又只是单纯地调用hasNextService(). 不用说, next()里面肯定也只是单纯地调用了nextService();
private boolean hasNextService() {
if (nextName != null) {
// nextName不为空,说明加载过了,而且服务不为空
return true;
}
// configs就是所有名字为PREFIX + service.getName()的资源
if (configs == null) {
try {
// PREFIX是 /META-INF/services
// service.getName() 是接口的全限定名称
String fullName = PREFIX + service.getName();
// loader == null, 说明是bootstrap类加载器
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 通过名字加载所有文件资源
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//遍历所有的资源,pending用于存放加载到的实现类
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
//遍历完所有的文件了,直接返回
return false;
}
// parse方法主要调用了parseLine,功能:
// 1. 分析每个PREFIX + service.getName() 目录下面的所有文件
// 2. 判断每个文件是否是合法的java类的全限定名称,如果是就add到pending变量中
pending = parse(service, configs.nextElement());
}
// 除了第一次进来,后面每次调用都是直接到这一步了
nextName = pending.next();
return true;
}
3、再来看看nextService干了啥
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"); } // 是不是service的子类,或者同一个类 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 }