JAVA SPI机制分析

简介

SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

SPI具体约定

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

简单示例

首先我们提供一个接口类 IOperation 以及它的两个实现类 PlusOperationImplDivisionOperationImpl,都在 com.qhong.spi 这个包路径下。

package com.qhong.spi;

/**
 * @author qhong
 * @date 2020/3/22 0:16
 **/
public interface IOperation {

    int operation(int numberA, int numberB);
}
package com.qhong.spi;

/**
 * @author qhong
 * @date 2020/3/22 0:17
 **/
public class PlusOperationImpl implements IOperation {

    public PlusOperationImpl() {
        System.out.println("plusOperation construct");
    }

    public int operation(int numberA, int numberB) {
        System.out.println("Operation: plus");

        return numberA + numberB;
    }
}
package com.qhong.spi;

/**
 * @author qhong
 * @date 2020/3/22 0:17
 **/
public class DivisionOperationImpl implements IOperation {

    public DivisionOperationImpl() {
        System.out.println("division construct");
    }

    @Override
    public int operation(int numberA, int numberB) {
        System.out.println("Operation: division");
        return numberA / numberB;
    }
}

新建spi配置文件

在resources文件夹下新建META-INF文件夹,并创建/services/com.qhong.spi.IOperation文件

测试

public class testSPI {
    public static void main(String[] args) {
        ServiceLoader<IOperation> operations = ServiceLoader.load(IOperation.class);

        int numberA = 6;
        int numberB = 3;
        System.out.println("NumberA: " + numberA + ", NumberB: " + numberB);
        Iterator<IOperation> iterator = operations.iterator();
        while (iterator.hasNext()) {
            IOperation operation = iterator.next();
            System.out.println(operation.operation(numberA, numberB));
        }
    }
}

Output:

NumberA: 6, NumberB: 3
division construct
Operation: division
2
plusOperation construct
Operation: plus
9

SPI 实现原理

应用程序调用 ServiceLoader.load 方法

ServiceLoader.load 方法内先创建一个新的ServiceLoader,并实例化该类中的成员变数,包括:

  • ClassLoader loader(类载入器)
  • AccessControlContext acc(访问控制器)
  • LinkedHashMap<String, S> providers(用于缓存载入成功的类)
  • LazyIterator lookupIterator(实现迭代器功能)

应用程序通过迭代器获取对象实例

ServiceLoader 先判断成员变量 providers 对象中否有缓存实例对象,如果有缓存,直接返回。

如果没有缓存,执行类的装载:读取 META-INF/services/ 下的配置文件,获得所有能被实例化的类的名称,通过反射方法 Class.forName() 载入类对象,并用 instance() 方法将类实例化。把实例化后的类缓存到providers 对象中然后返回实例对象。

JDBC为例

java.sql.DriverManager

    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

java.util.ServiceLoader

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

引用

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

引用

  private ServiceLoader(Class<S> 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 class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

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

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            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;
                }
                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 {
                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
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

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

    }

注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法

因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader,这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中(通过Thread.currentThread()获取,简直作弊啊!)。上面那篇文章末尾也讲到了TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader。

再看下看ServiceLoader.load(Class)的代码

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

到这儿差不多把SPI机制解释清楚了。直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

总结

JDK 内置的 SPI 机制本身有它的优点,但由于实现比较简单,也有不少缺点。

优点

使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用程序可以根据实际业务情况启用或替换具体组件。

缺点

  • 不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
  • 加载不到实现类时抛出并不是真正原因的异常,错误很难定位。

鉴于 SPI 的诸多缺点,很多系统都是自己实现了一套类加载机制,例如 dubbo。用户也可以自定义classloader+反射机制来加载,实现并不复杂。此外开源的类加载解决方案有 Plugin Framework for Java (PF4J) 等。

参考:

Java SPI 机制分析及其优缺点

真正理解线程上下文类加载器(多案例分析)

原文地址:https://www.cnblogs.com/hongdada/p/12543654.html

时间: 2024-11-05 23:34:09

JAVA SPI机制分析的相关文章

你应该了解的 Java SPI 机制

前言 不知大家现在有没有去公司复工,我已经在家办公将近 3 周了,同时也在家呆了一个多月:还好工作并没有受到任何影响,我个人一直觉得远程工作和 IT 行业是非常契合的,这段时间的工作效率甚至比在办公室还高,同时由于我们公司的业务在海外,所以疫情几乎没有造成太多影响. 扯远了,这次主要是想和大家分享一下 Java 的 SPI 机制.周末没啥事,我翻了翻我之前的写的博客 <设计一个可拔插的 IOC 容器>,发现当时的实现并不那么优雅. 还没看过的朋友的我先做个前景提要,当时的需求: 我实现了一个类

Java SPI机制和使用示例

JAVA SPI 简介 SPI 是 Java 提供的一种服务加载方式,全名为 Service Provider Interface.根据 Java 的 SPI 规范,我们可以定义一个服务接口,具体的实现由对应的实现者去提供,即服务提供者.然后在使用的时候再根据 SPI 的规范去获取对应的服务提供者的服务实现.通过 SPI 服务加载机制进行服务的注册和发现,可以有效的避免在代码中将服务提供者写死.从而可以基于接口编程,实现模块间的解耦. SPI 机制的约定 1 在 META-INF/service

java反射机制分析

本文转自:http://www.cnblogs.com/gulvzhe/archive/2012/01/27/2330001.html 浅显易懂,值得收藏 Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象, 都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制.反射的概念是由Smith在1982年 首次提出的,主要是指程序可以访问.检测和修改它本身状态或行为的一种能力.这一概念的提出很快引发了

Java反射机制分析指南

一.JAVA是动态语言吗? 一般而言,说到动态言,都是指在程序运行时允许改变程序结构或者变量类型,从这个观点看,JAVA和C++一样,都不是动态语言. 但JAVA它却有着一个非常突出的动态相关机制:反射.通过反射,Java可以于运行时加载.探知和使用编译期间完全求和的类.生成其对象实体,调用其方法或者对属性设值.所以Java算是一个半动态的语言吧. 反射的概念: 在Java中的反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法; 对于任意一个对象,都能够调用它的任意一个方

Java 反射机制分析指南

一.JAVA是动态语言吗? 一般而言,说到动态言,都是指在程序运行时允许改变程序结构或者变量类型,从这个观点看,JAVA和C++一样,都不是动态语言. 但JAVA它却有着一个非常突出的动态相关机制:反射.通过反射,Java可以于运行时加载.探知和使用编译期间完全求和的类.生成其对象实体,调用其方法或者对属性设值.所以Java算是一个半动态的语言吧. 反射的概念: 在Java中的反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法; 对于任意一个对象,都能够调用它的任意一个方

Java SPI机制简介

SPI是Service Provider Interfaces的简称.根据Java的SPI规范,我们可以定义一个服务接口,具体的实现由对应的实现者去提供,即Service Provider(服务提供者).然后在使用的时候只要根据SPI的规范去获取对应的服务提供者的服务实现即可.为了便于理解,我们先来看一个使用SPI的示例.下载 假设我们有一个日志服务LogService,其只定义了一个info方法用于输出日志信息,我们希望把它作为SPI,然后具体的实现由对应的服务提供者去实现.LogServic

Java SPI机制原理和使用场景

SPI的全名为Service Provider Interface.这个是针对厂商或者插件的.一般来说对于未知的实现或者对扩展开放的系统,通常会把一些东西抽象出来,抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块.jdbc模块的方案等.这个可以通过我们的抽象工厂方法来理解这个含义,实现是可以又厂商或者开发人员自己实现.由于代码上是处于上层的一个封装者,是不会知道底层怎么去实现,那么只能通过spi的形式,让上层知道应该调用哪个抽象的具体实现.所以这里可以理解为某些jar

Dubbo的SPI机制与JDK机制的不同及原理分析

从今天开始,将会逐步介绍关于DUbbo的有关知识.首先先简单介绍一下DUbbo的整体概述. 概述 Dubbo是SOA(面向服务架构)服务治理方案的核心框架.用于分布式调用,其重点在于分布式的治理. 简单的来说,可以把它分为四个角色.服务提供方(Provider).服务消费方(Consumer).注册中心和监控中心.通过注册中心对服务进行注册和订阅,通过监控中心对服务进行监控. 核心功能 Remoting:远程通讯,提供对多种NIO框架抽象封装,包括"同步转异步"和"请求-响应

Java的SPI机制与简单的示例

一.SPI机制 这里先说下SPI的一个概念,SPI英文为Service Provider Interface单从字面可以理解为Service提供者接口,正如从SPI的名字去理解SPI就是Service提供者接口:我对SPI的定义:提供给服务提供厂商与扩展框架功能的开发者使用的接口. 在我们日常开发的时候都是对问题进行抽象成Api然后就提供各种Api的实现,这些Api的实现都是封装与我们的Jar中或框架中的虽然当我们想要提供一种Api新实现时可以不修改原来代码只需实现该Api就可以提供Api的新实