Java SPI

作者: AC编程 | 来源:发表于2023-02-16 13:43 被阅读0次

    一、什么是SPI机制

    SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制,可以轻松实现面向服务的注册与发现,完成服务提供与使用的解耦,并且可以实现动态加载。

    引入服务提供者就是引入了SPI接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔,SPI实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载,为某个接口寻找服务实现的机制。

    我的理解就是上层提供接口,我们需要去实现,并且上层只需要根据我们的配置文件即可拿到我们的实现类(反射获取)。

    二、SPI具体约定

    2.1 Java SPI的具体约定为

    当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要在代码里制定。JDK提供服务实现查找的一个工具类:java.util.ServiceLoader

    2.2 JDK提供服务实现查找的一个工具类

    主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到 实现类的全限定名,把类加载到JVM。

    2.3 SPI的理解举例

    假设JDK中支持音乐播放,且只支持mp3的播放,有些厂商想在这个基础之上支持mp4播放,有些想支持mp5播放,而这些厂商都是第三方厂商,如果没有提供SPI这种实现标准,那就只有修改JAVA的源代码了,那这个弊端也是显而易见的,而有了SPI标准,SUN公司只需要提供一个播放接口,在实现播放的功能上通过ServiceLoad的方式加载服务,那么第三方只需要实现这个播放接口,再按SPI标准的约定进行打包,再放到classpath下面就OK了,没有一点代码的侵入性。

    三、应用场景

    3.1 JDBC驱动

    典型应用,JDBC的实现机制。通常各大厂商(如MySQL、Oracle)会根据一个统一的规范(java.sql.Driver)开发各自的驱动实现逻辑。客户端使用JDBC时不需要去改变代码,直接引入不同的SPI接口服务即可。MySQL的则是com.mysql.jdbc.Drive,Oracle则是oracle.jdbc.driver.OracleDriver

    MySql的jar包
    3.2 日志框架SLF4J

    SLF4J,即Java的简单日志门面( Simple Logging Facade for Java SLF4J),作为一个简单的门面或抽象,用来服务于各种各样的日志框架,比如java.util.logging、logback和log4j。SLF4J允许最终用户在部署时集成自己想要的日志框架(SPI机制)。简单来说,SLF4J是Java日志的一个标准或规范,logging、logback和log4j是对该规范的具体实现。可参考从SLF4J谈到依赖倒置原则、面向抽象编程、开闭原则

    logback的jar包 LogbackServletContainerInitializer ServletContainerInitializer
    3.3 SpringBoot中的类SPI扩展机制

    在SpringBoot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。(例如:数据库的自动配置功能)

    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    // spring.factories文件的格式为:key=value1,value2,value3
    // 从所有的jar包中找到META-INF/spring.factories文件
    // 然后从文件中解析出key=factoryClass类名称的所有value值
    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        // 取得资源文件的URL
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<String>();
        // 遍历所有的URL
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            // 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            String factoryClassNames = properties.getProperty(factoryClassName);
            // 组装数据,并返回
            result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
        }
        return result;
    }
    

    可以看到,它并没有采用JDK中的SPI机制来加载这些类,不过原理差不多。都是通过一个配置文件,加载并解析文件内容,然后通过反射创建实例。

    假如你希望参与到SpringBoot初始化的过程中,现在我们又多了一种方式。我们也创建一个spring.factories文件,自定义一个初始化器。

    org.springframework.context.ApplicationContextInitializer=com.youyouxunyin.config.context.MyContextInitializer
    
    image.png

    然后定义一个MyContextInitializer类

    public class MyContextInitializer implements ApplicationContextInitializer {
        @Override
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            System.out.println(configurableApplicationContext);
        }
    }
    
    3.4 Dubbo中的应用

    我们熟悉的Dubbo也不例外,它也是通过 SPI 机制加载所有的组件。同样的,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。

    参考文章

    SPI机制的原理和应用
    Dubbo源码分析(三)Dubbo中的SPI和自适应扩展机制
    java之spi机制简介
    Java SPI 实战

    相关文章

      网友评论

        本文标题:Java SPI

        本文链接:https://www.haomeiwen.com/subject/atypkdtx.html