详解 SPI 机制

SPI(Service Provider Interface) 是 JDK 内置的一种服务提供发现机制:可以用来启用框架扩展和替换组件,主要用于框架中开发。例如:Dubbo、Spring、Common-Logging,JDBC 等都是采用 SPI 机制,针对同一接口采用不同的实现提供给不同的用户,从而提高了框架的扩展性

1. Java SPI 实现

Java 内置的 SPI 通过 java.util.ServiceLoader 类解析 classPath 和 jar 包的 META-INF/services/ 目录下的以接口全限定名命名的文件,并加载该文件中指定的接口实现类,以此完成调用

1.1 案例

对于智能家居系统,只要是相同品牌下的产品,连上 wifi 就能够通过手机 app 控制了,非常方便。虽然产品不断更新换代,型号更新层出不穷,但是同种家电在 app 上操作起来,功能一般都是一样的。就拿空调来说,我们在 app 上操作起来一般也就三个主要功能:开关,选模式,调节温度

假设:我现在在客厅、卧室、书房安装了 3 款不同型号的空调,并把它们都接入到了我 app 中,那么之后的操作都是相同的几个按键,简单粗暴。

问题:无论是开关还是调温,都是通过 app 去调用设备的接口罢了,那么如果不同型号的空调各写各的接口,后端 app 在开发的时候光对接接口都麻烦的要死

解决方法:我先定义一套接口规范,不管你以后什么型号的空调,都按我的规范来实现接口。以后只要我能发现你的设备,那么都可以按相同的方法来调用接口

①:定义接口

新建一个 maven 项目 aircondition-standard,定义一个接口:

public interface AirConditionService {

    // 获取型号
    String getType();

    // 开关
    void turnOnOff();

    // 调节温度
    void adjustTemperature(int temperature);

    // 模式变更
    void changeModel(int modelId);

}

用 maven 把它打成 jar 包,供后续的服务实现使用(服务提供者在项目中就可以引入这个 jar 包)

mvn clean install

有了这套规范,就保证了产品后期不管怎么更新换代,都能接入到系统来

②:服务实现

现有两个类型的空调:挂式空调(HangingType)、立式空调(VerticalType)

挂式空调: 新建一个项目 aircondition-hanging-type,并引入上述 jar:

<dependency>
    <groupId>com.zzc</groupId>
    <artifactId>aircondition-standard</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

创建服务类,并实现前面定义的接口:

public class HangingTypeAirConditionService implements AirConditionService {

    @Override
    public String getType() {
        return "HangingType";
    }

    @Override
    public void turnOnOff() {
        // TODO
        System.out.println("挂式空调开关");
    }

    @Override
    public void adjustTemperature(int temperature) {
        // TODO
        System.out.println("挂式空调调节温度");
    }

    @Override
    public void changeModel(int modelId) {
        // TODO
        System.out.println("挂式空调更换模式");
    }

}

在项目的 resources 的目录下,创建 META-INF/services目录,然后以前面定义的接口名 com.zzc.airconditionstandard.service.AirConditionService 创建文件,并在文件中写入实现类的全限定名:

com.zzc.airconditionhangingtype.service.HangingTypeAirConditionService

如下图:

在这里插入图片描述
这样,一个服务方的简单实现就搞定了,用 maven 打成 jar 包,之后就可以提供给调用方使用了

同理,我们可以再创建一个立式空调的项目 aircondition-vertical-type

public class VerticalTypeAirConditionService implements AirConditionService {

    @Override
    public String getType() {
        // TODO
        return "VerticalType";
    }

    @Override
    public void turnOnOff() {
        // TODO
        System.out.println("立式空调开关");
    }

    @Override
    public void adjustTemperature(int i) {
        // TODO
        System.out.println("立式空调调节温度");
    }

    @Override
    public void changeModel(int i) {
        // TODO
        System.out.println("立式空调更换模式");
    }

}

在项目的 resources 的目录下,创建 META-INF/services目录,然后以前面定义的接口名 com.zzc.airconditionstandard.service.AirConditionService 创建文件,并在文件中写入实现类的全限定名:

com.zzc.service.VerticalTypeAirConditionService
③:服务发现

现在两个服务提供方都实现了接口,下面关键的一步就是服务发现,这一步 Java 中的 spi 发现机制已经帮我们实现好了

创建一个新项目 aircondition-app,引入上面打好的两个 jar

<dependency>
    <groupId>com.zzc</groupId>
    <artifactId>aircondition-hanging-type</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com.zzc</groupId>
    <artifactId>aircondition-vertical-type</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

按照上面的说法,虽然每个服务提供者对于接口都有不同的实现,但是作为调用者来说,它并不需要关心具体的实现类,我们要做的是通过接口来调用服务提供者实现的方法

下面,就是关键的服务发现环节,我们写一个方法,根据型号去调用对应空调的开关方法:

public class AirConditionApp {

    public static void main(String[] args) {
        new AirConditionApp().turnOn("VerticalType");
    }

    public void turnOn(String type){
        ServiceLoader<AirConditionService> load = ServiceLoader.load(AirConditionService.class);

        for (AirConditionService iAircondition : load) {
            System.out.println("检测到:"+iAircondition.getClass().getSimpleName());
            if (type.equals(iAircondition.getType())){
                iAircondition.turnOnOff();
            }
        }
    }
}

测试结果:

在这里插入图片描述

可以看到,测试过程中,通过定义的接口 AirConditionService 发现了两个实现类,并通过参数,调用了特定实现类的某个方法。整段代码中没有出现过具体的服务实现类,操作都是通过接口调用

1.3 JDBC 的 SPI 机制

深入理解 Java 中的 SPI 机制

2. Spring 的 SPI 机制

2.1 简介

Spring SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单:

//获取所有factories文件中配置的LoggingSystemFactory
List<LoggingSystemFactory>> factories = SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

Spring 也是支持 ClassPath 中存在多个 spring.factories 文件的,加载时会按照 classpath 的顺序依次加载这些 spring.factories 文件,添加到一个 ArrayList 中。由于没有别名,所以也没有去重的概念,有多少就添加多少

但由于 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个 spring.factories 文件,那么你项目中的文件会被第一个加载,得到的 Factories中,项目中 spring.factories 里配置的那个实现类也会排在第一个

如果我们要扩展某个接口的话,只需要在你的项目(spring boot)里新建一个 META-INF/spring.factories 文件,只添加你要的那个配置,不要完整的复制一遍 Spring Boot 的 spring.factories 文件然后修改

2.2 案例

①:定义接口:HelloService

public interface HelloService {
    void sayHello();
}

②:其实现类:HelloServiceImpl1

public class HelloServiceImpl1 implements HelloService {

    @Override
    public void sayHello() {
        System.out.println("Hello World from HelloServiceImpl1!");
    }
}

HelloServiceImpl2

public class HelloServiceImpl2 implements HelloService {

    @Override
    public void sayHello() {
        System.out.println("Hello World from HelloServiceImpl2!");
    }
}

③:spring.factories 文件:resources/META-INF 路径下

com.zzc.service.HelloService=\
com.zzc.service.impl.HelloServiceImpl1,\
com.zzc.service.impl.HelloServiceImpl2

idea当中如何创建*.factories文件

④:HelloServiceLoader

public class HelloServiceLoader {

    public static void main(String[] args) {
        // 使用Spring SPI机制动态加载HelloService实现类
        List<HelloService> loader = SpringFactoriesLoader.loadFactories(HelloService.class, Thread.currentThread().getContextClassLoader());
        // 遍历所有实现类并调用sayHello方法
        for (HelloService helloService : loader) {
            helloService.sayHello();
        }
    }
}

2.3 实现原理

查看 SpringFactoriesLoader 类源码:

public final class SpringFactoriesLoader {
    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
    static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap();

    private SpringFactoriesLoader() {
    }
	
	// 加载并实例化给定类型的工厂实现
    public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
        List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
        List<T> result = new ArrayList(factoryImplementationNames.size());
        Iterator var5 = factoryImplementationNames.iterator();
        while(var5.hasNext()) {
            String factoryImplementationName = (String)var5.next();
            result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
        }
        return result;
    }
	
    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
    }
	
	// 获取所有 jar 包中 META-INF/spring.factories 文件路径,整合 factoryClass 类型的实现类名称,获取到实现类的全类名称后进行类的实例化操作
    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = (Map)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            HashMap result = new HashMap();

            try {
                Enumeration urls = classLoader.getResources("META-INF/spring.factories");
                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryTypeName = ((String)entry.getKey()).trim();
                        String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        String[] var10 = factoryImplementationNames;
                        int var11 = factoryImplementationNames.length;

                        for(int var12 = 0; var12 < var11; ++var12) {
                            String factoryImplementationName = var10[var12];
                            ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                                return new ArrayList();
                            })).add(factoryImplementationName.trim());
                        }
                    }
                }

                result.replaceAll((factoryType, implementations) -> {
                    return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
                });
                cache.put(classLoader, result);
                return result;
            } catch (IOException var14) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
            }
        }
    }
	
	// 实例化 Bean:通过反射来实现对应的初始化
    private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
        Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
        return ReflectionUtils.accessibleConstructor(factoryImplementationClass, new Class[0]).newInstance();
    }
}

3. Common-Logging 的 SPI

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值