Java、Spring中的SPI机制

SPI机制

简介

为Service Provider Interface,简单的总结下java spi机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制

整体机制如下图:

img SPI实际上就是基于接口的编程+策略模式+配置文件组合实现的动态加载机制

系统设计的各个抽象,往往有很多不同的实现方案,在面向对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可插拔原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能够在不同程序里动态指名,这就需要一种服务发现机制。

Java SPI机制就是提供这样一个机制:为某个接口寻找服务发现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设中这个机制尤为重要,所以SPI的核心思想,就是解耦。

类加载机制

双亲委派模型

java中的类加载器负责加载来自文件系统、网络或者其他来源的类文件。

jvm的类加载器默认使用的是双亲委派模式。

类加载规则

自上而下:尝试去加载类

自下而上:检测类是否已加载

具体描述

当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。

好处:

使用双亲委派模式可以保证类加载的安全性,不管是哪个加载器加载这个类,最终都是委托给顶层的Bootstrap Classloader来加载的,只有父类无法加载才尝试加载,这样就可以保证任何类加载器最终得到的都是同样的一个Object对象。

缺陷:

子类加载器可以使用父类加载器已经加载的类,反过来则不行,这就意味着子类加载器要是无法加载类,父类更加没办法加载,这就意味着双亲委派模型并不能保证所有类的加载问题。

例子:

对于SPI接口,都是由核心类库提供的,但是却是由第三方类库去实现,这样就存在一个问题.

SPI的接口是由AppClassloader来加载,BootStrapClassLoader是无法找到实现的,因为它只加载java的核心类库,它也不能代理给AppClassloader,因为它是最顶层的类加载器。

解决方案:

使用线程上下文类加载器ContextClassloader加载

类加载器

  • Bootstrap Classloader 加载jdk自带的rt包下的类文件,是所有类加载的父类
  • Extension Classloader 加载扩展类库jre/lib/etc或者java.ext.dirs系统目录下的类
  • System Classloader 加载classpath环境变量中加载类文件

ContextClassLoader线程上下文类加载器

jdk1.2开始引入,通过Thread.getContextClassLoader()和setContextClassLoader()来获取和设置类加载器,由此来加载实现了SPI接口的实现类

使用场景

概括的说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略

比较常见的例子:

  • 数据库驱动加载接口实现类

    jdbc加载不同类型的数据库驱动

  • 日志门面接口实现类加载

    SLF4J加载不同提供商的日志实现类

  • Spring

  • Dubbo

使用介绍

要使用java SPI,需要遵循以下约定:

  1. 当服务提供者提供了接口一种具体实现后,在jar包的META-INF/services目录下创建一个以接口全限名为命名的文件,内容为实现类的全限定名
  2. 接口实现类所在的jar包放在主程序的classpath
  3. 主程序提供java.uti.ServiceLoader动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM
  4. SPI的实现类必须携带一个不带参数的构造方法

示例代码

1.定义顶级接口JdbcSpiInterface

public interface JdbcSpiInterface {
    /**
     * 获取数据库连接
     *
     * @param user            用户名
     * @param password        密码
     * @param driverClassName 驱动类
     */
    void getJdbcConnection(String user, String password, String driverClassName);
}

2.新增2个实现类MysqlJdbc、OracleJdbc

public class OracleJdbc implements JdbcSpiInterface {

    @Override
    public void getJdbcConnection(String user, String password, String driverClassName) {
        System.out.println("Oracle数据库驱动连接");
    }

}
public class MysqlJdbc implements JdbcSpiInterface {

    @Override
    public void getJdbcConnection(String user, String password, String driverClassName) {
        System.out.println("Mysql数据库驱动连接");
    }

}

3.编写测试方法

public class Test {

    public static void main(String[] args) {
        ServiceLoader<JdbcSpiInterface> shouts = ServiceLoader.load(JdbcSpiInterface.class);
        for (JdbcSpiInterface s : shouts) {
            s.getJdbcConnection("user", "password", "cp.ds.xds.Class");
        }
    }
}
  1. 测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KEyMicIo-1591170766365)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1590026903115.png)]

SpringSPI扩展机制

简介

Springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个jar包中搜索所有META-INF/spring.factories

配置文件,其实这里不仅仅是去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;
}

参考文档:https://www.jianshu.com/p/0d196ad23915

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值