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,运行结果如下:
标签:Spring,接口,public,SPI,机制,dataBaseType,class,加载 来源: https://www.cnblogs.com/coder-zyc/p/16629715.html