深入理解JAVA中的SPI机制

什么是SPI

SPI全名为Service Provider Interface是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件,简单来说,它就是一种动态替换发现的机制。

在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦

 

JAVA SPI的具体约定

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

为什么一定要在classes中的META-INF/services下呢?

因为JDK提供服务实现查找的一个工具类:java.util.ServiceLoader默认指定从这个路径下查找实现类

深入理解JAVA中的SPI机制_第1张图片

 

SPI的使用场景

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

比较常见的例子:

  • JDBC加载不同类型的数据库驱动
  • 日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
  • Spring/SpringBoot中大量使用SPI
    • 对servlet3.0规范
    • 对ServletContainerInitializer的实现
    • 自动类型转换Type Conversion SPI(Converter SPI 、Formatter SPI)等
  • Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现
  • SpringBoot的自动配置机制

 

SPI的demo入门

新建工程,层级结构如下:

深入理解JAVA中的SPI机制_第2张图片

具体代码实现:

//SPI接口
public interface SpiService {
     void study();  
}

//SPI接口实现1
public class JimSpiServiceImpl implements SpiService {

    @Override
    public void study() {
       System.out.println("学习mysql");
       
        
    }

}


//SPI接口实现2

public class TomSpiServiceImpl implements SpiService{

    @Override
    public void study() {
        System.out.println("学习java");      
    }

}

//接口权限名的文件内容
com.pingan.haofang.demo.JimSpiServiceImpl
com.pingan.haofang.demo.TomSpiServiceImpl


//测试类
public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader services = ServiceLoader.load(SpiService.class);
        for (SpiService spiService : services) {
            spiService.study();
        }
    }
    
}


//运行结果:
学习mysql
学习java

ServiceLoader源码解析

1 、应用程序调用ServiceLoader.load方法去先创建一个新的ServiceLoader,并实例化该类中的成员变量

serviceLoader的成员变量

public final class ServiceLoader
    implements Iterable
{

    // 加载具体实现类信息的前缀
    private static final String PREFIX = "META-INF/services/";

    // 代表被加载的类或者接口
    private Class service;

    // 用于定位,加载和实例化providers的类加载器
    private ClassLoader loader;

    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;

     // 用于缓存已经加载的接口实现类,其中key为实现类的完整类名
    private LinkedHashMap providers = new LinkedHashMap<>();

    // 懒查找迭代器
    private LazyIterator lookupIterator;

serviceLoader的load方法

    public static  ServiceLoader load(Class service) {
        //使用的是线程上下文类加载器(不遵循双亲委派)
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

     public static  ServiceLoader load(Class service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

    //初始化相关的成员变量
    private ServiceLoader(Class svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() :                 null;
        reload();
    }
    
    public void reload() {
        providers.clear();// 清空已经缓存的加载的接口实现类
        lookupIterator = new LazyIterator(service, loader);// 创建新的延迟加载迭代器
    }


    private LazyIterator(Class service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

 

2、应用程序通过迭代器接口获取对象实例ServiceLoader先判断成员变量providers对象中(LinkedHashMap类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,则调用lookupIterator延迟加载迭代器进行加载service:

  • (1) 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,具体加载配置的实现代码如下
  • (2) 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
  • (3) 把实例化后的类缓存到providers对象中,(LinkedHashMap类型)
    然后返回实例对象。
 //获取迭代器进行加载
 public Iterator iterator() {
       // 返回迭代器
        return new Iterator() {
            // 查询缓存中是否存在实例对象
            Iterator> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                // 如果缓存中已经存在返回true
                if (knownProviders.hasNext())
                    return true;
                // 如果不存在则使用延迟加载迭代器进行判断是否存在
                return lookupIterator.hasNext();
            }

            public S next() {
                // 如果缓存中存在则直接返回
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // 调用延迟加载迭代器进行返回
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }


    //LazyIterator的类加载

      // 判断是否拥有下一个实例
        private boolean hasNextService() {
            // 如果拥有直接返回true
            if (nextName != null) {
                return true;
            }

            // 具体实现类的全名 ,Enumeration config
            // 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                // 转换config中的元素,或者具体实现类的真实包结构
                pending = parse(service, configs.nextElement());
            }
            // 具体实现类的包结构名
            nextName = pending.next();
            return true;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class c = null;
            try {
                // 加载类对象
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                // 通过c.newInstance()实例化
                S p = service.cast(c.newInstance());
                // 将实现类加入缓存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

SPI在实际项目中的使用

在实际项目中,主应用,spi接口和spi实现类可能是不同的应用,比如需求:针对不同的银行数据源需要对接,需要不同的具体实现类,在A系统中需要使用提供数据接口供其他系统使用,需要引入不同的银行实现,可以利用SPI实现,主要包括以下系统:

A系统:主应用的SpringBoot 项目,需要引入提供b-spi接口的依赖

b-spi:spi接口的Maven项目,提供一个DataSourceService接口( 或者是抽象类)

c-data:spi实现类的Maven项目,同样需要引入提供b-spi接口的依赖,根据不同银行的需求,实现DataSourceService接口,提供不同的实现类,并在source/META-INF/services目录下提供配置DataSourceService的全限定名的配置文件,文件的内容就是不同实现类的全限类名

最后,为了能够是的A系统通过ServiceLoader加载到c_data项目中的实现类,需要保证c_data的jar包在主程序的classpath中这样可以通过ServiceLoader.load(DataSourceService.class)直接加载,当然实际使用过程中,为了解耦,需要根据c_data打包后的路径直接去加载jar包,这里使用URLClassLoader去加载,大致的代码如下:

//根据路径加载

  @PostConstruct
    public void init() {
        log.info("ExtDataSourceManager init start path:{},dstemplateName:{}", externalDataSourceTemplateConfig.getPath());
        ExternalClassLoader externalClassLoader = null;
        List jarFileList = externalDataSourceTemplateConfig.getJarFiles();
        if(jarFileList.isEmpty()){
            log.warn("ExtDataSourceManager loadConfig not find ext datasource jar file.");
            return;
        }
        for(String filePath:jarFileList){
            log.info("ExtDataSourceManager loadConfig error filePath:{}",filePath);
            try {
                externalClassLoader = new ExternalClassLoader(filePath);
            } catch (MalformedURLException e) {
            }
            ServiceLoader services = ServiceLoader
                    .load(DataSourceService.class, externalClassLoader);
            for (DataSourceServiceservice : services) {
                String templateName = service.getTemplate().getName();
                //放入Map中,后面直接获取
                templateServiceByName.put(templateName, service);
            }
        }
    }

 class ExternalClassLoader extends URLClassLoader {
        public ExternalClassLoader(final String path) throws MalformedURLException {
            super(new URL[] { new URL(path)},Thread.currentThread().getContextClassLoader());
        }
    }

//获取jar的路径

 private static final String JAR_FILE_PATH_HEAD = "jar:file:///";

    @Value("${spi.datasource-dir}")
    private String path; //多个spi实现类的jar所在的路径

    private List jarFiles = Lists.newArrayList();

    /**
     * 获取spi实现类的路径
     */
    @PostConstruct
    public void init() {
        File directory = new File(path);
        if (directory != null && directory.isDirectory()) {
            Collection tempFiles = FileUtils.listFiles(directory, new String[]{"jar"}, false);
            if (tempFiles == null) {
                log.error("ExtDsConfig getJarFiles datasource jar dir config error path:{}", path);
            }
            for (File f : tempFiles) {
                if (f.isFile()) {
                    String fileName = f.getName();
                    jarFiles.add(buildJarFilePath(fileName));
                }
            }
        }
        log.info("ExtDsConfig getJarFiles find datasource file jar size:{}", jarFiles.size());
    }

 

SPI总结

优点
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

缺点

  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用ServiceLoader类的实例是不安全的。

 

你可能感兴趣的:(源码)