源码级深度理解Java SPI

2022-12-16 0 664

原副标题:源代码级广度认知Java SPI

译者:vivo 网络服务项目器项目组- Zhang Peng

SPI 是一类用作静态读取服务项目的监督机制。它的中心价值观是解耦,归属于众所周知的Mach构架商业模式。SPI 在 Java 当今世界应用应用领域十分广为,如:Dubbo、Spring Boot 等架构。责任编辑从源代码侧发力预测,研讨 Java SPI 的优点、基本原理,和在许多较为经典之作应用领域的应用应用领域。

一、SPI 概要

SPI 全名 Service Provider Interface,是 Java 提供更多的,意在由服务器端同时实现或扩充的 API,它是一类用作静态读取服务项目的监督机制。Java 中 SPI 监督机制主要价值观是将换装的控股权移到流程以外,在模块化结构设计中那个监督机制特别关键,其中心价值观是解耦

Java SPI 有五个基本要素:

SPI USB: 为提供更多者同时实现类签订合同的的USB或tcsh。 SPI 同时实现类: 前述提供更多服务项目的同时实现类。 SPI 实用性: Java SPI 监督机制签订合同的命令行,提供更多查找服务项目同时实现类的方法论。命令行要放于 META-INF/services 产品目录中,因此,实用性文档应与提供更多者USB的全然限量发行名完全一致。文档中的每一行都有两个同时实现服务项目类的详细资料,反之亦然是提供更多者类的全然限量发行中文名称。 ServiceLoader: Java SPI 的核心理念类,用作读取 SPI 同时实现类。ServiceLoader 中有各式各样新颖

二、SPI 实例

正简而言之,课堂教学出圣皮耶尔县,我们何不透过两个具体内容的实例上看呵呵,怎样采用 Java SPI。

2.1 SPI USB

具体来说,须要表述两个 SPI USB,和一般USB并没有什么差异。

package io.github.dunwu.javacore.spi;

publicinterfaceDataStorage { Stringsearch( Stringkey);}

2.2 SPI 同时实现类

假设,我们须要在流程中采用两种不同的数据存储——MySQL 和 Redis。因此,我们须要两个不同的同时实现类去分别完成相应工作。

MySQL查询 MOCK 类

packageio.github.dunwu.javacore.spi;

publicclassMysqlStorageimplementsDataStorage{ @OverridepublicString search(String key){ return“【Mysql】搜索”+ key + “,结果:No”; }}

Redis 查询 MOCK 类

packageio.github.dunwu.javacore.spi;

publicclassRedisStorageimplementsDataStorage{ @OverridepublicString search(String key){ return“【Redis】搜索”+ key +“,结果:Yes”; }}

service 传入的是期望读取的 SPI USB类型 到目前为止,表述USB,并同时实现USB和一般的 Java USB同时实现没有任何不同。

2.3 SPI 实用性

如果想透过 Java SPI 监督机制来发现服务项目,就须要在 SPI 实用性中签订合同好发现服务项目的方法论。命令行要放于 META-INF/services 产品目录中,因此,实用性文档应与提供更多者USB的全然限量发行名完全一致。文档中的每带队都有两个同时实现服务项目类的详细资料,反之亦然是提供更多者类的全然限量发行中文名称。以本实例代码为例,其实用性文档应该为

io.github.dunwu.javacore.spi.DataStorage

文档中的内容如下:

io.github.dunwu.javacore.spi.MysqlStorageio.github.dunwu.javacore.spi.RedisStorage

2.4 ServiceLoader

完成了上面的步骤,就可以透过 ServiceLoader 来读取服务项目。实例如下:

importjava.util.ServiceLoader;

publicclassSpiDemo{

publicstaticvoid main( String[] args) { ServiceLoader< DataStorage> serviceLoader = ServiceLoader.load( DataStorage. class); System.out. println( “============ Java SPI 测试============”); serviceLoader.forEach(loader -> System.out. println(loader.search( “Yes Or No”))); }

}

输出:

============ Java SPI 测试============【Mysql】搜索Yes OrNo,结果:No【Redis】搜索Yes OrNo,结果:Yes

三、SPI 基本原理

上文中,我们已经了解 Java SPI 的基本要素和采用 Java SPI 的方法。你有没有想过,Java SPI 和一般 Java USB有何不同,Java SPI 是怎样工作的。前述上,Java SPI 监督机制依赖于 ServiceLoader 类去解析、读取服务项目。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的基本原理。ServiceLoader 的代码本身很精练,接下来,让我们透过走读源代码的方式,逐一认知 ServiceLoader 的工作流程。

3.1 ServiceLoader 的成员变量

先看呵呵 ServiceLoader 类的成员变量,大致有个印象,后面的源代码中都会采用到。

publicfinalclassServiceLoader< S> implementsIterable< S> {

// SPI 命令行产品目录privatestaticfinalString PREFIX = “META-INF/services/”;

// 将要被读取的 SPI 服务项目privatefinalClass<S> service;

// 用作读取 SPI 服务项目的类读取器privatefinalClassLoader loader;

// ServiceLoader 创建时的访问控制上下文privatefinalAccessControlContext acc;

// SPI 服务项目缓存,按实例化的顺序排列privateLinkedHashMap<String,S> providers = newLinkedHashMap<>;

// 懒查询迭代器privateLazyIterator lookupIterator;

// …}

3.2 ServiceLoader 的工作流程

(1)ServiceLoader.load 静态方法

应用应用领域流程读取 Java SPI 服务项目,都是先调用 ServiceLoader.load静态方法。

ServiceLoader.load 静态方法的作用是:

① 指定类读取 ClassLoader 和访问控制上下文;

② 然后,重新读取 SPI 服务项目

清空缓存中所有已实例化的 SPI 服务项目 根据 ClassLoader 和 SPI 类型,创建懒读取迭代器

这里,摘录 ServiceLoader.load 相关源代码,如下:

// service 传入的是期望读取的 SPI USB类型// loader 是用作读取 SPI 服务项目的类读取器publicstatic<S> ServiceLoader<S> load( Class<S> service, ClassLoader loader) { returnnewServiceLoader<>(service, loader);}

publicvoidreload( ) { // 清空缓存中所有已实例化的 SPI 服务项目providers.clear;// 根据 ClassLoader 和 SPI 类型,创建懒读取迭代器lookupIterator = newLazyIterator(service, loader); }

// 私有构造方法// 重新读取 SPI 服务项目privateServiceLoader( Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, “Service interface cannot be null”); // 指定类读取 ClassLoader 和访问控制上下文loader = (cl == null) ? ClassLoader.getSystemClassLoader : cl; acc = (System.getSecurityManager !=null) ? AccessController.getContext : null; // 然后,重新读取 SPI 服务项目reload;}

(2)应用应用领域流程透过 ServiceLoader 的 iterator 方法遍历 SPI 实例

ServiceLoader 的类表述,明确了 ServiceLoader 类同时实现了 Iterable<T>USB,所以,它是可以迭代遍历的。前述上,ServiceLoader 类维护了两个缓存 providers(LinkedHashMap对象),缓存 providers 中保存了已经被成功读取的 SPI 实例,那个 Map 的 key 是 SPI USB同时实现类的全限量发行名,value 是该同时实现类的两个实例对象。

当应用应用领域流程调用 ServiceLoader 的 iterator 方法时,ServiceLoader 会先判断缓存 providers 中是否有数据:如果有,则直接返回缓存 providers 的迭代器;如果没有,则返回懒读取迭代器的迭代器。

publicIterator<S> iterator{ returnnewIterator<S> {

// 缓存 SPI providersIterator<Map.Entry<String,S>> knownProviders= providers.entrySet.iterator;

// lookupIterator 是 LazyIterator 实例,用作懒读取 SPI 实例publicbooleanhasNext{ if(knownProviders.hasNext) returntrue; returnlookupIterator.hasNext; }

publicS next{ if(knownProviders.hasNext) returnknownProviders.next.getValue;returnlookupIterator.next; }

publicvoidremove{ thrownewUnsupportedOperationException; }

};}

(3)懒读取迭代器的工作流程

上面的源代码中提到了,lookupIterator 是 LazyIterator 实例,而 LazyIterator 用作懒读取 SPI 实例。那么, LazyIterator 是怎样工作的呢?

这里,摘取 LazyIterator 关键代码

hasNextService 方法:

拼接 META-INF/services/ + SPI USB全限量发行名 e

nextService 方法:

然后,尝试透过 Class 的 newInstance 方法实例化两个 SPI 服务项目对象。如果成功,则将那个对象加入到缓存 providers 中并返回该对象。privatebooleanhasNextService { if(nextName != null) { returntrue; }if(configs == null) { try{ // 1.拼接 META-INF/services/ + SPI USB全限量发行名// 2.透过类读取器,尝试读取资源文档// 3.解析资源文档中的内容StringfullName = PREFIX + service.getName;if(loader == null) configs = ClassLoader.getSystemResources(fullName);elseconfigs = 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; }

privateS nextService { if(!hasNextService)thrownewNoSuchElementException; Stringcn = 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 s”); }try{ S p = service.cast(c.newInstance);providers.put(cn, p);returnp; } catch(Throwable x) { fail(service,“Provider “+ cn +” could not be instantiated”, x);}thrownewError; // This cannot happen}

3.3 SPI 和类读取器

透过上面两个章节中,走读 ServiceLoader 代码,我们已经大致了解 Java SPI 的工作基本原理,即透过 ClassLoader 读取 SPI 命令行,解析 SPI 服务项目,然后透过反射,实例化 SPI 服务项目实例。我们不妨思考呵呵,为什么读取 SPI 服务项目时,须要指定类读取器 ClassLoader 呢?

学习过 JVM 的读者,想必都了解过类读取器的 双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的 BootstrapClassLoader 外,其余的类读取器都应有自己的父类读取器。这里类读取器之间的父子关系一般透过组合(Composition)关系来同时实现,而不是透过继承(Inheritance)的关系同时实现。

双亲委派监督机制签订合同了: 两个类读取器具体来说将类读取请求传送到父类读取器,只有当父类读取器无法完成类读取请求时才尝试读取。

双亲委派的好处:使得 Java 类伴随着它的类加载器,天然具备一类带有优先级的层次关系,从而使得类读取得到统一,不会出现重复读取的问题:

系统类防止内存中出现多份反之亦然的字节码 保证 Java 流程安全稳定运行

例如: java.lang.Object 存放在 rt.jar中,如果编写另外两个 java.lang.Object 的类并放到 classpath中,流程可以编译透过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 优先级更高,因为 rt.jar 中的 Object 采用的是启动类读取器,而 classpath 中的 Object 采用的是应用应用领域流程类读取器。正因为 rt.jar 中的 Object 优先级更高,因为流程中所有的 Object 都是那个 Object。

双亲委派的限制:子类读取器可以采用父类读取器已经读取的类,而父类读取器无法采用子类读取器已经读取的。——这就导致了双亲委派模型并不能解决所有的类读取器问题。Java SPI 就面临着这样的问题:

SPI 的USB是 Java 核心理念库的一部分,是由 BootstrapClassLoader 读取的; 而 SPI 同时实现的 Java 类一般是由 AppClassLoader 来读取的。BootstrapClassLoader 是无法找到 SPI 的同时实现类的,因为它只读取 Java 的核心理念库。它也不能代理给 AppClassLoader,因为它是最顶层的类读取器。这也解释了本节开

如果不做任何的设置,Java 应用应用领域的线程的上下文类读取器默认是 AppClassLoader。在核心理念类库采用 SPI USB时,传递的类读取器采用线程上下文类读取器,就可以成功的读取到 SPI 同时实现的类。线程上下文类读取器在很多 SPI 的同时实现中都会用到。

通常可以透过

Thread.currentThread.getClassLoader

Thread.currentThread.getContextClassLoader

3.4 Java SPI 的不足

Java SPI 存在许多不足:

不能按需读取,须要遍历所有的同时实现,并实例化,然后在循环中才能找到我们须要的同时实现。如果不想用某些同时实现类,或者某些类实例化很耗时,它也被载入现类。 多个并发多线程采用 ServiceLoader 类的实例是不安全的。

四、SPI 应用应用领域场景

SPI 在 Java 开发中应用应用领域十分广为。具体来说,在 Java 的java.util.spi package 中就签订合同了很多 SPI USB。下面,列举许多 SPI USB:

TimeZoneNameProvider: 为 TimeZone 类提供更多本地化的时区中文名称。 DateFormatProvider: 为指定的语言环境提供更多日期和时间格式。 NumberFormatProvider: 为 NumberFormat 类提供更多货币、整数和百分比值。 Driver: 从 4.0 版开始,JDBC API 支持 SPI 商业模式。旧版本采用 Class.forName 方法读取驱动流程。 PersistenceProvider: 提供更多 JPA API 的同时实现。 等等

除此以外,SPI 还有很多应用应用领域,下面列举几个经典之作案例。

4.1 SPI 应用应用领域案例之 JDBC DriverManager

4.1.1 创建数据库连接

我们先回顾呵呵,JDBC 怎样创建数据库连接的呢?

JDBC4.0 之前,连接数据库的时候,通常会用Class.forName(XXX)

Class.forName(” com.mysql.jdbc.Driver“)

而 JDBC4.0 之后,不再须要用

(1)JDBC USB:具体来说,Java 中内置了USBjava.sql.Driver

(2)JDBC USB同时实现:各个数据库的驱动自行同时实现 java.sql.Driver USB,用作管理数据库连接。

① MySQL:在 MySQL的 Java 驱动包 mysql-connector-java-XXX.jar 中,可以找到 META-INF/services 产品目录,该产品目录下会有两个名字为java.sql.Driver 的文档,文档内容是com.mysql.cj.jdbc.Driver。

com.mysql.cj.jdbc.Driver 正是 MySQL版的 java.sql.Driver 同时实现。如下图所示:

②PostgreSQL 同时实现:在 PostgreSQL 的 Java 驱动包 postgresql-42.0.0.jar 中,也可以找到反之亦然的命令行,文档内容是 org.postgresql.Driver,org.postgresql.Driver 正是 PostgreSQL 版的 java.sql.Driver 同时实现。

(3)创建数据库连接

以 MySQL 为例,创建数据库连接代码如下:

final String DB_URL = String.format(“jdbc:mysql://%s:%s/%s”, DB_HOST, DB_PORT, DB_SCHEMA); connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);

4.1.2 DriverManager

从前文,我们已经知道 DriverManager 是创建数据库连接的关键。它究竟是如何工作的呢?

可以看到是读取实例化驱动的,接着看 loadInitialDrivers 方法:

privatestaticvoidloadInitialDrivers { Stringdrivers; try{ drivers = AccessController.doPrivileged( newPrivilegedAction< String> { publicStringrun { returnSystem.getProperty(“jdbc.drivers”); }});} catch(Exception ex) { drivers = null; }ql.Driver 的驱动类AccessController.doPrivileged( newPrivilegedAction<Void> { publicVoid run { // 利用 SPI,记载所有 Driver 服务项目ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator;try{ // 遍历迭代器while(driversIterator.hasNext) { driversIterator.next;}} catch(Throwable t) { // Do nothing}returnnull; }});

// 打印数据库驱动信息println( “DriverManager.initialize: jdbc.drivers = “+ drivers);

if(drivers == null|| drivers.equals( “”)) { return; }String[] driversList = drivers.split(“:”); println( “number of Drivers:”+ driversList.length); for( StringaDriver : driversList) {try{ println( “DriverManager.Initialize: loading “+ aDriver); // 尝试实例化驱动Class.forName(aDriver,true, ClassLoader.getSystemClassLoader);} catch(Exception ex) { println( “DriverManager.Initialize: load failed: “+ ex); }}}

上面的代码主要步骤是:

所有驱动的同时实现类。 遍历所有驱动,尝试实例化各个同时实现类。

须要关注的是下面这行代码:

ServiceLoader < Driver> loadedDrivers = ServiceLoader.load(Driver.class);

java.util.ServiceLoader.LazyIterator迭代器。调用其 hasNext 方法时,会搜索 classpath 下和 jar 包中的META-INF/services产品目录,查找 java.sql.Driver 文档,并找到文档中的驱动同时实现类的全限量发行名。调用其 next 方法时,会根据驱动类的全限量发行名去尝试实例化两个驱动类的对象。

4.2 SPI 应用应用领域案例之 Common-Loggin

common-logging(也称 Jakarta Commons Logging,缩写 JCL)是常用的日志门面工具包。

common-logging 的核心理念类是入口是 LogFactory,LogFatory 是两个tcsh,它负责读取具体内容的日志同时实现。

其入口方法是 LogFactory.getLog方法,源代码如下:

publicstaticLog getLog(Class clazz)throwsLogConfigurationException { returngetFactory.getInstance(clazz); }

publicstaticLog getLog(String name)throwsLogConfigurationException { returngetFactory.getInstance(name); }

从以上源代码可

LogFatory.getFactory方法负责选出匹配的日志工厂,其源代码如下:

publicstaticLogFactory getFactory( ) throws LogConfigurationException { // 省略…

// 读取 commons-logging.properties 命令行Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);

// 省略…

// 决定创建哪个 LogFactory 实例// (1)尝试读取全局属性 org.apache.commons.logging.LogFactoryif(isDiagnosticsEnabled) {logDiagnostic( “[LOOKUP] Looking for system property [“+ FACTORY_PROPERTY + “] to define the LogFactory subclass to use…”); }

try{ // 如果指定了 org.apache.commons.logging.LogFactory 属性,尝试实例化具体内容同时实现类String factoryClass = getSystemProperty(FACTORY_PROPERTY,null); if(factoryClass != null) { if(isDiagnosticsEnabled) {logDiagnostic( “[LOOKUP] Creating an instance of LogFactory class “+ factoryClass + ” as specified by system property “+ FACTORY_PROPERTY); }factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);} else{ if(isDiagnosticsEnabled) { logDiagnostic( “[LOOKUP] No system property [“+ FACTORY_PROPERTY +“] defined.”); }}} catch(SecurityException e) { // 异常处理} catch(RuntimeException e) { // 异常处理}

// (2)利用 Java SPI 监督机制,尝试在 classpatch 的 META-INF/services 产品目录下寻找 org.apache.commons.logging.LogFactory 同时实现类if(factory == null) { if(isDiagnosticsEnabled) { logDiagnostic( “[LOOKUP] Looking for a resource file of name [“+ SERVICE_ID + “] to define the LogFactory subclass to use…”); }try{ final InputStreamis= getResourceAsStream(contextClassLoader, SERVICE_ID);

if( is!= null) { // This code is needed by EBCDIC and other strange systems.// Its a fix for bugs reported in xercesBufferedReader rd;try{ rd = newBufferedReader( newInputStreamReader( is, “UTF-8”)); } catch(java.io.UnsupportedEncodingException e) { rd =newBufferedReader( newInputStreamReader( is)); }

String 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 “+ SERVICE_ID +” which was present in the path of the context classloader.”); }factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );}} else{ // is == nullif(isDiagnosticsEnabled) { logDiagnostic(“[LOOKUP] No resource file with name “+ SERVICE_ID + ” found.”); }}} catch(Exception ex) { // note: if the specified LogFactory class wasnt compatible with LogFactory// for some reason, a ClassCastException will be caught here, and attempts will// continue to find a compatible class.if(isDiagnosticsEnabled) {logDiagnostic(“[LOOKUP] A security exception occurred while trying to create an”+ ” instance of the custom factory class”+ “: [“+ trim(ex.getMessage) + “]. Trying alternative implementations…”);}// ignore}}

// (3)尝试从 classpath 产品目录下的 commons-logging.properties 文档中查找 org.apache.commons.logging.LogFactory 属性

if(factory == null) { if(props != null) { if(isDiagnosticsEnabled) { logDiagnostic(“[LOOKUP] Looking in properties file for entry with key “+ FACTORY_PROPERTY + ” to define the LogFactory subclass to use…”); }String factoryClass = props.getProperty(FACTORY_PROPERTY);if(factoryClass != null) { if(isDiagnosticsEnabled) { logDiagnostic(“[LOOKUP] Properties file specifies LogFactory subclass “+ factoryClass +“”); }factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);

// TODO:think about whether we need to handle exceptions from newFactory} else{ if(isDiagnosticsEnabled) { logDiagnostic(“[LOOKUP] Properties file has no entry specifying LogFactory subclass.”); }}} else{ if(isDiagnosticsEnabled) {logDiagnostic( “[LOOKUP] No properties file available to determine”+ ” LogFactory subclass from..”); }}}

// (4)以上情况都不满足,实例化默认同时实现类 org.apache.commons.logging.impl.LogFactoryImpl

if(factory == null) { if(isDiagnosticsEnabled) { logDiagnostic(“[LOOKUP] Loading the default LogFactory implementation “+ FACTORY_DEFAULT + ” via the same classloader that loaded this LogFactory”+ ” class (ie not looking in the context classloader).”); }

factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);}

if(factory != null) { /*** Always cache using context class loader.*/cacheFactory(contextClassLoader, factory);

if(props != null) { Enumeration names = props.propertyNames;while(names.hasMoreElements) {String name = (String) names.nextElement;String value= props.getProperty(name); factory.setAttribute(name,value); }}}

returnfactory; }

从 getFactory 方法的源代码可以看出,其核心理念方法论分为 4 步:

具体来说,尝试查找全局属性 org.apache.commons.logging.LogFactory ,如果指定了具体内容类,尝试创建实例。 利用 Java SPI 监督机制,尝试在 classpatch 的 META-INF/services 产品目录下寻找 org.apache.commons.logging.LogFactory 的同时实现类。 尝试从 classpath 产品目录下的 commons-logging.properties 文档中查找 org.apache.commons.logging.LogFactory 属性,如果指定了具体内容类,尝试创建实例。 以上情况如果都不满足,则实例化默认同时实现类,即 org.apache.commons.logging.impl.LogFactoryImpl 。

4.3 SPI 应用应用领域案例之 Spring Boot

Spring Boot 是基于 Spring 构建的架构,其结构设计目的在于简化 Spring 应用应用领域的实用性、运行。在 Spring Boot 中,大量运用了自动换装来尽可能减少实用性。

下面是两个 Spring Boot 入口实例,可以看到,代码十分简洁。

importorg.springframework.boot.SpringApplication; importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.web.bind. annotation.GetMapping; importorg.springframework.web.bind.annotation.RequestParam; importorg.springframework.web.bind. annotation.RestController;

@SpringBootApplication@RestControllerpublicclassDemoApplication{

publicstatic void main(String[] args) {SpringApplication.run(DemoApplication. class, args); }

@GetMapping( “/hello”) publicString hello( @RequestParam(value =“name”, defaultValue = “World”) String name) { returnString.format( “Hello %s!”, name);}}

那么,Spring Boot 是怎样做到寥寥几行代码,就可以运行两个 Spring Boot 应用应用领域的呢。我们何不带着疑问,从源代码侧发力,一步步探究其基本原理。

4.3.1 @SpringBootApplication 注解

具体来说,Spring Boot 应用应用领域的启动类上都会标记两个

@SpringBootApplication 注解。

@SpringBootApplication 注解表述如下:

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM,classes = {TypeExcludeFilter.class}), @Filter(type = FilterType.CUSTOM,classes = {AutoConfigurationExcludeFilter.class})} )public@interfaceSpringBootApplication { // 略}

除了 @Target、 @Retention、@Documented、@Inherited 这几个元注解,

@SpringBootApplication 注解的表述中还标记了 @SpringBootConfiguration、

@EnableAutoConfiguration、@ComponentScan 三个注解。

4.3.2 @SpringBootConfiguration 注解

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Configurationpublic@interfaceSpringBootConfiguration { @AliasFor(annotation = Configuration.class)boolean proxyBeanMethods defaulttrue; }

4.3.3 @EnableAutoConfiguration 注解

@EnableAutoConfiguration 注解表述如下:

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class})public@interfaceEnableAutoConfiguration {String ENABLED_OVERRIDE_PROPERTY = “spring.boot.enableautoconfiguration”;

Class<?>[] excludedefault{};

String[] excludeName default{}; }

@EnableAutoConfiguration 注解包含了 @AutoConfigurationPackage

与 @Import({AutoConfigurationImportSelector.class}) 两个注解。

4.3.4 @AutoConfigurationPackage 注解

@AutoConfigurationPackage 会将被修饰的类作为主实用性类,该类所在的 package 会被视为根路径,Spring Boot 默认会自动扫描根路径下的所有 Spring Bean(被 @Component 和继承 @Component 的各个注解所修饰的类)。——这是为什么 Spring Boot 的启动类一般要放于根路径的原因。那个功能等同于在 Spring xml 实用性中透过context:component-scan来指定扫描路径。@Import 注解的作用是向 Spring 容器中直接注入指定组件。@AutoConfigurationPackage 注解中注明了 @Import({Registrar.class})。Registrar 类用作保存 Spring Boot 的入口类、根路径等信息。

4.3.5 SpringFactoriesLoader.loadFactoryNames 方法

@Import(AutoConfigurationImportSelector.class) 表示直接注入

AutoConfigurationImportSelector。

AutoConfigurationImportSelector 有两个核心理念方法

SpringFactoriesLoader.loadFactoryNames 方法,那个方法即为 Spring Boot SPI 的关键,它负责读取所有 META-INF/spring.factories 文档,读取的过程由 SpringFactoriesLoader 负责。

Spring Boot 的 META-INF/spring.factories 文档本质上是两个 properties 文档,数据内容是两个个键值对。

SpringFactoriesLoader.loadFactoryNames 方法的关键源代码:

// spring.factories 文档的格式为:key=value1,value2,value3// 遍历所有 META-INF/spring.factories 文档// 解析文档,获得 key=factoryClass 的类中文名称publicstaticList< String> loadFactoryNames(Class<?> factoryType,@NullableClassLoader classLoader) { StringfactoryTypeName = factoryType.getName; returnloadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList); }

privatestaticMap< String, List< String>> loadSpringFactories( @NullableClassLoader classLoader) { 中有数据,直接返回MultiValueMap< String, String> result = cache.get(classLoader); if(result != null) { returnresult;}

try{ Enumeration<URL> urls = (classLoader != null? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));result = newLinkedMultiValueMap<>;// 遍历所有路径while(urls.hasMoreElements) { URL url = urls.nextElement;UrlResource resource = newUrlResource(url); // 解析文档,得到对应的一组 PropertiesProperties properties = PropertiesLoaderUtils.loadProperties(resource);// 遍历解析出的 properties,组装数据for(Map.Entry<?, ?> entry : properties.entrySet) { StringfactoryTypeName = ((String) entry.getKey).trim; for( StringfactoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue)) { result.add(factoryTypeName, factoryImplementationName.trim);}}}cache.put(classLoader, result);returnresult; }catch(IOException ex) { thrownewIllegalArgumentException(“Unable to load factories from location [“+ FACTORIES_RESOURCE_LOCATION + “]”, ex); }}

归纳上面的方法,主要作了这些事:

读取所有 META-INF/spring.factories文档,读取过程有 SpringFactoriesLoader负责。

在 CLASSPATH 中搜寻所有 META-INF/spring.factories 命令行。 然后,解析 spring.f

4.3.6 Spring Boot 的 AutoConfiguration 类

Spring Boot 有各式各样 starter 包,可以根据前述项目,剩下的工作怎样处理呢 ?

以 spring-boot-starter-web 的 jar 包为例,查看其 maven pom,可以看到,它依赖于 spring-boot-starter,所有 Spring Boot 官方 starter 包都会依赖于那个 jar 包。而 spring-boot-starter 又依赖于 spring-boot-autoconfigure,Spring Boot 的自动换装秘密,就在于那个 jar 包。

从 spring-boot-autoconfigure 包的结构上看,它有两个 META-INF/spring.factories,显然利用了 Spring Boot SPI,来自动换装其中的实用性类。

下图是 spring-boot-autoconfigure 的 META-INF/spring.factories 文档的部分内容,可以看到其中注册了一长串会被自动读取的 AutoConfiguration 类。

以 RedisAutoConfiguration 为例,那个实用性类中,会根据 @ConditionalXXX 中的条件去决定是否实例化对应的 Bean,实例化 Bean 所依赖的关键参数则透过 RedisProperties 传入。

RedisProperties 中维护了 Redis 连接所须要的关键属性,只要在 yml 或 properties 实用性文件中,指定 spring.redis 开头的属性,都会被自动装载到 RedisProperties 实例中。

透过以上预测,已经一步步解读出 Spring Boot 自动装载的基本原理。

五、SPI 应用应用领域案例之 Dubbo

Dubbo 并未采用 Java SPI,而是自己封装了一套新的 SPI 监督机制。Dubbo SPI 所需的命令行需放置在 META-INF/dubbo 路径下,实用性内容形式如下:

optimusPrime= org.apache.spi.OptimusPrime bumblebee= org.apache.spi.Bumblebee

与 Java SPI 同时实现类实用性不同,Dubbo SPI 是透过键值对的方式进行实用性,这样可以按需读取指定的同时实现类。Dubbo SPI 除了支持按需读取USB同时实现类,还增加了 IOC 和 AOP 等优点。

5.1 Extensier 入口

Dubbo SPI 的相关方法论被封装在了 Extensier 类中,透过 Extensier,可以读取指定的同时实现类。

Extensier 的 getExtension 方法是其入口方法,其源代码如下:

publicT getExtension( String name) { if(name == null|| name.length == 0) thrownewIllegalArgumentException( “Extension name == null”); if( “true”. equals(name)) { returngetDefaultExtension; }// Holder,顾名思义,用作持有目标对象Holder<Object> holder = cachedInstances. get(name); if(holder == null) { cachedInstances.putIfAbsent(name,newHolder<Object>); holder = cachedInstances. get(name); }Object instance = holder. get; // 双重检查if(instance ==null) { synchronized (holder) {instance = holder. get; if(instance == null) { // 创建拓展实例instance = createExtension(name);// 设置实例到 holder 中holder. set(instance); }}}return(T) instance; }

可以看出,那个方法的作用是:具体来说检查缓存,缓存未命中则调用 createExtension 方法创建拓展对象。那么,createExtension 是怎样创建拓展对象的呢,其源代码如下:

privateT createExtension(String name) { // 从命令行中读取所有的拓展类,可得到“实用性项中文名称”到“实用性类”的映射关系表Class<?> clazz = getExtensionClasses. get(name);if(clazz == null) { throwfindException(name); }try{ T instance = (T) EXTENSION_INSTANCES. get(clazz); if(instance ==null) { // 透过反射创建实例EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance);instance = (T) EXTENSION_INSTANCES.get(clazz); }// 向实例中注入依赖injectExtension(instance);Set<Class<?>> wrapperClasses = cachedWrapperClasses;if(wrapperClasses != null&& !wrapperClasses.isEmpty) { // 循环创建 Wrapper 实例for(Class<?> wrapperClass : wrapperClasses) {// 将当前 instance 作为参数传给 Wrapper 的构造方法,并透过反射创建 Wrapper 实例。// 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));}}returninstance; } catch(Throwable t) { thrownewIllegalStateException(“…”); }}

createExtension 方法的的工作步骤可以归纳为:

透过反射创建拓展对象 向拓展对象中注入依赖 将拓展对象包裹在相应的 Wrapper 对象中

以上步骤中,第两个步骤是读取拓展类的关键,第三和第五个步骤是 Dubbo IOC 与 AOP 的具体内容同时实现。

中文名称到拓展类的映射关系表(Map<中文名称, 拓展类>),之后再根据拓展项中文名称从映射关系表中取出相应的拓展类即可。相关过程的代码预测如下:

privateMap<String, Class<?>> getExtensionClasses {Map<String, Class<?>> classes = cachedClasses. get; // 双重检查if(classes ==null) { synchronized (cachedClasses) {classes = cachedClasses. get; if(classes == null) { // 读取拓展类classes = loadExtensionClasses;cachedClasses. set(classes); }}}returnclasses; }

这里也是先检查缓存,若缓存未命中,则透过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则透过 loadExtensionClasses 读取拓展类。下面预测 loadExtensionClasses 方法的方法论。

private Map< String, Class<?>> loadExtensionClasses { 方法时传入的final SPI defaultAnnotation = type.getAnnotation(SPI.class);if(defaultAnnotation != null) { Stringvalue = defaultAnnotation.value; if((value = value.trim).length > 0) { // 对 SPI 注解内容进行切分String[] names = NAME_SEPARATOR.split(value);// 检测 SPI 注解内容是否合法,不合法则抛出异常if(names.length > 1) { thrownewIllegalStateException(“more than 1 default extension name on extension…”); }

// 设置默认中文名称,参考 getDefaultExtension 方法if(names.length == 1) { cachedDefaultName = names[ 0]; }}}

Map< String, Class<?>> extensionClasses = newHashMap<String, Class<?>>; // 读取指定文档夹下的命令行loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);loadDirectory(extensionClasses, DUBBO_DIRECTORY);loadDirectory(extensionClasses, SERVICES_DIRECTORY);returnextensionClasses; }

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法读取指定文档夹实用性文件。SPI 注解解析过程较为简单,无需多说。下面我们上看呵呵 loadDirectory 做了哪些事情。

privatevoidloadDirectory(Map< String, Class<?>> extensionClasses,Stringdir) { // fileName = 文档夹路径 + type 全限量发行名StringfileName = dir + type.getName; try{ Enumeration<java.net.URL> urls;ClassLoader classLoader = findClassLoader;// 根据实用性文档读取所有的同名文档if(classLoader !=null) { urls = classLoader.getResources(fileName);} else{ urls = ClassLoader.getSystemResources(fileName);}if(urls != null) { while(urls.hasMoreElements) { java.net.URL resourceURL = urls.nextElement;// 读取资源loadResource(extensionClasses, classLoader, resourceURL);}}} catch(Throwable t) { logger.error( “…”); }}

方法的同时实现。

privatevoidloadResource(Map<String, Class<?>> extensionClasses,ClassLoader classLoader, java.net.URL resourceURL){ try{ BufferedReader reader = newBufferedReader( newInputStreamReader(resourceURL.openStream,“utf-8”)); try{ String line;// 按行读取实用性内容while((line = reader.readLine) != null) { // 定位 # 字符finalintci = line.indexOf( #); if(ci >= 0) { // 截取 # 之前的字符串,# 之后的内容为注释,须要忽略line = line.substring( 0, ci); }line = line.trim;if(line.length > 0) { try{ String name = null; inti = line.indexOf( =); if(i > 0) { // 以等于号 = 为界,截取键与值name = line.substring( 0, i).trim; line = line.substring(i + 1).trim; }if(line.length > 0) { // 读取类,并透过 loadClass 方法对类进行缓存loadClass(extensionClasses, resourceURL,Class.forName(line, true, classLoader), name);}} catch(Throwable t) { IllegalStateException e = newIllegalStateException( “Failed to load extension class…”); }}}} finally{ reader.close;}} catch(Throwable t) { logger.error( “Exception when load extension class…”); }}

loadResource 方法用作读取和解析命令行,并透过反射读取类,最后调用 loadClass 方法进行其他操作。loadClass 方法用作主要用作操作缓存,该方法的方法论如下:

private voidloadClass( Map< String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz,Stringname) throws NoSuchMethodException {

if(!type.isAssignableFrom(clazz)) { thrownewIllegalStateException(“…”); }

// 检测目标类上是否有 Adaptive 注解if(clazz.isAnnotationPresent(Adaptive.class)) {if(cachedAdaptiveClass == null) { // 设置 cachedAdaptiveClass缓存cachedAdaptiveClass = clazz;} elseif(!cachedAdaptiveClass.equals(clazz)) {thrownewIllegalStateException( “…”); }

// 检测 clazz 是否是 Wrapper 类型} elseif(isWrapperClass(clazz)) { Set<Class<?>> wrappers = cachedWrapperClasses; if(wrappers == null) { cachedWrapperClasses =newConcurrentHashSet<Class<?>>; wrappers = cachedWrapperClasses;}// 存储 clazz 到 cachedWrapperClasses 缓存中wrappers.add(clazz);

// 流程进入此分支,表明 clazz 是两个一般的拓展类} else{ // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常clazz.getConstructor;if(name == null|| name.length == 0) { me,或采用小写的类名作为 namename = findAnnotationName(clazz);if(name.length == 0) { thrownewIllegalStateException(“…”); }}// 切分 nameString[] names = NAME_SEPARATOR.split(name); if(names != null&& names.length > 0) {Activate activate = clazz.getAnnotation(Activate.class);if(activate != null) { // 如果类上有 Activate 注解,则采用 names 数组的第两个元素作为键,// 存储 name 到 Activate 注解对象的映射关系cachedActivates.put(names[ 0], activate); }for( Stringn : names) { if(!cachedNames.containsKey(clazz)) { // 存储 Class 到中文名称的映射关系cachedNames.put(clazz, n);}Class<?> c = extensionClasses.get(n);if(c == null) { // 存储中文名称到 Class 的映射关系extensionClasses.put(n, clazz);} elseif(c != clazz) { thrownewIllegalStateException( “…”); }}}}}

如上,loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、

cachedWrapperClasses 和 cachedNames 等等。除此以外,该方法没有其他什么方法论了。

参考资料

Java SPI 价值观梳理 Dubbo SPI springboot 中 SPI 监督机制 SpringBoot 的自动换装基本原理、自表述 starter 与 spi 监督机制,一网打尽

【OSCHINA 2022 中国开源开发者问卷】来啦

你的反馈将有助于反映中国开源的全貌

问卷结尾还可抽取我们的周边好物哦~

期待来自你的反馈!

END

几款新颖开源下载工具

这里有最新开源资讯、软件更新、技术干货等内容

点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦~

责任编辑:

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务