Loading

Spring中的SPI机制

前言

在面向对象编程领域中,六大原则之一的依赖倒置原则提到的原则规定:

  • 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口;
  • 抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口;

参考:[https://en.wikipedia.org/wiki/Dependency_inversion_principle]

 

应用模块中应该依赖接口而不是具体的实现,然而接口最终是需要落地于具体的实现类,假如应用引用了一个jar包依赖,因业务调整,需要替换jar包某个接口的实现,通过修改源码的方式修改该实现是可以的,但是每次修改一次就发布一次,那这种耦合度是不是有点大?

是否有一种机制可以通过外部化配置指定接口加载所需的实现?

熟悉Dubbo的开发者会想到Dubbo中的SPI机制;SPI全称为Service Provider Interface服务提供接口,它可以通过一个指定的接口/抽象类,寻找到预先配置好的实现类(并创建实现类对象);然而SPI并不是最早出现在Dubbo,在JDK 1.6中引入了SPI的具体实现,但是Dubbo中的SPI没有使用JDK原生的SPI,而是自己实现了一套,功能更为强大的SPI;

在Spring 3.2中也引入了SPI的实现,而且也比JDK的原生实现更加强大;

 

Spring SPI

Spring中的SPI相比于JDK原生的,它的功能更为强大,因为它可以替换的类型不仅仅局限于接口/抽象类,它可以是任何一个类,接口,注解;

正因为Spring SPI是支持替换注解类型的SPI,这个特性在Spring Boot中的自动装配有体现(EnableAutoConfiguration注解):

 

Spring的SPI文件是有规矩的,它需要放在工程的META-INF下,且文件名必须为spring.factories ,而文件的内容本质就是一个properties;如spring-boot-autoconfigure包下的META-INF/spring.factories文件,用于自动装配的;

Spring SPI加载spring.factories文件的操作是使用SpringFactoriesLoader,SpringFactoriesLoader它不仅可以加载声明的类的对象,而且可以直接把预先定义好的全限定名都取出来;

SpringFactoriesLoader#loadFactories加载spring.factories文件,最终会调用SpringFactoriesLoader#loadSpringFactories;

通过类加载器获取类路径下的FACTORIES_RESOURCE_LOCATION,之后获取到的资源路径,以properties的方式解析配置文件,其中配置文件的key为声明的类型,value为具体的实现的列表,最后将结果添加到缓存,其中缓存的key为类加载器,value为配置文件的内容;

 

使用示例

下面是一个SPI加载配置类的示例,通过SPI结合条件装配选择合适配置类加载;

模拟两个数据库Oracle,MySQL根据配置加载合适的配置类;

 

配置文件

查看代码
database.type=mysql

 

spring.factories文件

查看代码
 org.example.factoryLoader.EnableDataBase=\
  org.example.factoryLoader.OracleConfig,\
  org.example.factoryLoader.MySQLConfig

 

配置类

查看代码
 @Configuration
@ConditionalOnDataBaseType("mysql")
public class MySQLConfig {

	@Bean
	public DataBaseType mysqlDataBaseType() {
		DataBaseType dataBaseType = new DataBaseType();
		dataBaseType.setDatabaseType("mysql");
		return dataBaseType;
	}
}
查看代码
 @Configuration
@ConditionalOnDataBaseType("oracle")
public class OracleConfig {

	@Bean
	public DataBaseType mysqlDataBaseType() {
		DataBaseType dataBaseType = new DataBaseType();
		dataBaseType.setDatabaseType("oracle");
		return dataBaseType;
	}
}
查看代码
 @Data
public class DataBaseType {
	private String databaseType;
}

 

定义一个条件装配的注解

查看代码
 public class OnDataBaseTypeConditional implements Condition {

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		String annotationData = (String) Objects.requireNonNull(metadata
						.getAnnotationAttributes(ConditionalOnDataBaseType.class.getName()))
				.get("value");
		String dataBaseType = context.getEnvironment().getProperty("database.type");
		return dataBaseType.equalsIgnoreCase(annotationData);
	}

}
查看代码
 @Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(OnDataBaseTypeConditional.class)
public @interface ConditionalOnDataBaseType {
    
    String value();
}

 

定义一个模块装配的注解

查看代码
 @Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(DataBaseConfigSelector.class)
public @interface EnableDataBase {
}

 

SPI根据配置文件的key加载对应的配置类实例;

ImportSelector接口的实现类可以根据指定的筛选标准(通常是一个或者多个注解)来决定导入哪些配置类;但是ImportSelector也可以导入普通类;

selectImports方法根据导入的@Configuration类的 AnnotationMetadata选择并返回要导入的类的类名,即全限定类名;
查看代码
 public class DataBaseConfigSelector implements ImportSelector {

	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		List<String> configClassNames = SpringFactoriesLoader
				.loadFactoryNames(EnableDataBase.class, this.getClass().getClassLoader());
		return configClassNames.toArray(new String[0]);
	}
}
查看代码
 @Configuration
@EnableDataBase
@PropertySource("database.properties")
public class SpringFactoriesLoaderDemo {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
		ctx.register(SpringFactoriesLoaderDemo.class);
		ctx.refresh();
		System.out.println(ctx.getBean(DataBaseType.class));
	}
}

当前database.type的配置为mysql,运行结果如下:

当database.type的配置修改为oracle,运行结果如下:

 

posted @ 2022-08-28 01:11  街头卖艺的肖邦  阅读(2561)  评论(0编辑  收藏  举报