java之SPI机制
前言:本文根据其他文章和文档理解整理,非原创,首先对各位作者表示感谢
java中API的含义与其他语言的API含义类似,提供功能性API接口供开发人员使用。
java中的SPI机制是java语言特有的,SPI机制用于框架开发人员根据一个接口规范按照需要实现不同的功能,以扩展框架的功能。
以java.sql.Driver接口为例,我们的spring-boot-web项目调用java.sql.Driver接口提供的方法(也就是在调用API接口)。
当我们使用不同的数据库时,各大厂商(如Mysql、Oracle)会根据一个统一的规范(java.sql.Driver)开发各自的驱动实现逻辑,客户端使用jdbc时不需要去改变代码,直接引入不同的spi接口服务即可,客户端会通过SPI机制自动加载对应的jdbc服务。Mysql的则是com.mysql.jdbc.Drive,Oracle则是oracle.jdbc.driver.OracleDriver。
优点:SPI机制(Service Provider Interface)其实源自服务提供者框架(Service Provider Framework),是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了spi接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔。
另外,它弥补了类加载双亲委派模型的局限、做了很好的补充。
jdk自带的SPI机制
要使用Java SPI,需要遵循如下约定:
- 1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
- 2、接口实现类所在的jar包放在主程序的classpath中;
- 3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
- 4、SPI的实现类必须携带一个不带参数的构造方法;
示例代码
步骤1、定义一组接口 (假设是org.foo.demo.Animal),并写出接口的一个或多个实现,(假设是org.foo.demo.animal.Dog、org.foo.demo.animal.Cat)。
public interface Animal {
void shout();
}
public class Cat implements Animal {
@Override
public void shout() {
System.out.println("miao miao");
}
}
public class Dog implements Animal {
@Override
public void shout() {
System.out.println("wang wang");
}
}
步骤2、在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (org.foo.demo.Animal文件),内容是要应用的实现类(这里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一个类)。
文件位置
- src
-main
-resources
- META-INF
- services
- org.foo.demo.Animal
复制代码
文件内容
org.foo.demo.animal.Dog
org.foo.demo.animal.Cat
复制代码
步骤3、使用 ServiceLoader 来加载配置文件中指定的实现。
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<Animal> shouts = ServiceLoader.load(Animal.class);
for (Animal s : shouts) {
s.shout();
}
}
}
代码输出:
wang wang
miao miao
链接:https://juejin.cn/post/6844903679431016456
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
上述定义的接口实现类没有实例成员变量,当我们需要spi加载一个有实例成员变量的实现类时该怎么办,如下所示
public interface Animal {
void shout();
}
public class Cat implements Animal {
private String name;
public Cat(String name) {
this.name=name;
}
@Override
public void shout() {
System.out.println(this.name+":miao miao");
}
}
如果简单的SPI加载,必然会报错,没有对应的构造器。
这就需要借助AnimalFactory接口,定义一个create方法去生成想要的类,
// 改造一下Cat和Dog的构造方法
public class Cat implements Animal {
private String name;
public Cat(Map<Class<?>, String> catalogWithName) {
this.name = catalogWithName.get(Cat.class);
}
@Override
public void shout() {
System.out.println(this.name + ":miao miao");
}
}
public class Dog implements Animal {
private final String name;
public Dog(Map<Class<?>, String> catalogWithName) {
this.name = catalogWithName.get(Dog.class);
}
@Override
public void shout() {
System.out.println(this.name+":wang wang");
}
}
// 工厂类生成Cat和Dog
public interface AnimalFactory {
Animal create(Map<Class<?>, String> catalogWithName);
}
public class CatFactory implements AnimalFactory {
public Cat create(Map<Class<?>, String> catalogWithName) {
return new Cat(catalogWithName);
}
}
public class DogFactory implements AnimalFactory {
public Dog create(Map<Class<?>, String> catalogWithName) {
return new Dog(catalogWithName);
}
}
使用如下:
- src
-main
-resources
- META-INF
- services
- org.foo.demo.AnimalFactory
文件内容为:
org.foo.demo.animal.CatFactory
org.foo.demo.animal.DogFactory
demo里面获得想要的Cat和Dog
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<AnimalFactory> factories = ServiceLoader.load(AnimalFactory.class);
Map<Class<?>, String> catalogWithName = new HashMap<>();
catalogWithName.put(Dog.class, "da huang");
catalogWithName.put(Cat.class, "hello kitty");
for (AnimalFactory factory : factories) {
Animal animal = factory.create(catalogWithName);
animal.shout();
}
}
}
// 输出
// hello kitty:miao miao
// da huang:wang wang
springboot 中的SPI扩展机制
-
在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,找到标识为EnableAutoConfiguration的配置类,解析properties文件,然后将其中定义的bean注入到Spring容器。
-
需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
-
跟Java的SPI更多的是为了面向接口编程和克服双亲委派局限不同,Spring的这种SPI可能更多的是体现一种框架的可扩展性:在springboot工程中我们都知道,默认是会加载主类所在目录及其所有子目录下的自动注入bean的,比如主类在
com.esa.stack,则
com.esa.stack.controller,
com.esa.stack.service等等都会加载并注入;但如果第三方开发的jar包、大概率情况下目录是跟工程目录不同的,比如esaStack公司的合作伙伴lb公司开发了一个组件用的是
com.lb.*`,这个组件的类就没法自动的注入到spring,而通过上面讲的SPI机制就可以解决这个问题。 -
详细讲解参看这位大佬的文档: