SPI:JDK 与 SpringBoot

浅谈Java SPI原理与其在JDBC、Flink中的应用

SPI:JDK 与 SpringBoot_第1张图片
API:由被调方提供的实现了某个完整功能的接口,主调方直接调用该接口来享用该功能,而无需关注该接口的具体实现。比如使用 JDK 的 InputStream#read 从文件系统中读取数据。
SPI:被调方(框架)提供的功能扩展点接口,主调方可实现这些接口与被调方(框架)进行互动。

MySQL 应用

JDBC 中的 DriverManager 中定义的模板代码:将 Driver 实现类的 Class 的加载工作委托给 SPI。

// From DriverManager.java

private static final String JDBC_DRIVERS_PROPERTY = "jdbc.drivers";

private static Connection getConnection(
    // 省略代码 ...

	// 通过 SPI 加载 Driver,而无需再通过 Class.forName("com.mysql.jdbc.Driver")
    ensureDriversInitialized();

    // Walk through the loaded registeredDrivers attempting to make a connection.
    // Remember the first exception that gets raised so we can reraise it.
    SQLException reason = null;

	// 「驱动实现类」的 Class 被加载后,就会通过「静态代码块」将当前驱动对象注册到 registeredDrivers 中,比如见后文的 MySQL 驱动类 FabricMySQLDriver.java 的实现
    for (DriverInfo aDriver : registeredDrivers) {
        // If the caller does not have permission to load the driver then
        // skip it.
        if (isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                println("    trying " + aDriver.driver.getClass().getName());
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    // Success!
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }

        } else {
            println("    skipping: " + aDriver.getClass().getName());
        }

    }

	// 省略代码...
}

private static void ensureDriversInitialized() {
    if (driversInitialized) {
        return;
    }

    synchronized (lockForInitDrivers) {
        if (driversInitialized) {
            return;
        }
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty(JDBC_DRIVERS_PROPERTY);
                }
            });
        } 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();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try {
                    while (driversIterator.hasNext()) {
	                    // SPI 的 iterator 是懒加载,全部遍历后才能确保「驱动实现类」的 Class 已经通过 Class.forName 完成加载
                        driversIterator.next();
                    }
                } catch (Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });

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

        if (drivers != null && !drivers.equals("")) {
            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);
                }
            }
        }

        driversInitialized = true;
        println("JDBC DriverManager initialized");
    }
}

SPI 会通过 Class.forName 形式加载 Class 文件

// From ServiceLoader.java : ServiceLoader.LazyClassPathLookupIterator

private Class<?> nextProviderClass() {
    if (configs == null) {
        try {
            String fullName = PREFIX + service.getName();
            if (loader == null) {
                configs = ClassLoader.getSystemResources(fullName);
            } else if (loader == ClassLoaders.platformClassLoader()) {
                // The platform classloader doesn't have a class path,
                // but the boot loader might.
                if (BootLoader.hasClassPath()) {
                    configs = BootLoader.findResources(fullName);
                } else {
                    configs = Collections.emptyEnumeration();
                }
            } else {
                configs = loader.getResources(fullName);
            }
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return null;
        }
        pending = parse(configs.nextElement());
    }
    String cn = pending.next();
    try {
        return Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service, "Provider " + cn + " not found");
        return null;
    }
}

mysql-connector-java SPI 实现
SPI:JDK 与 SpringBoot_第2张图片

// From com.mysql.fabric.jdbc.FabricMySQLDriver.java

public class FabricMySQLDriver extends NonRegisteringDriver implements Driver {
    // may be extended to support other protocols in the future
    public static final String FABRIC_URL_PREFIX = "jdbc:mysql:fabric://";

    // 省略代码 ...

    // Register ourselves with the DriverManager
    static {
        try {
            DriverManager.registerDriver(new FabricMySQLDriver());
        } catch (SQLException ex) {
            throw new RuntimeException("Can't register driver", ex);
        }
    }
    // 省略代码 ...
}

深入剖析Spring Boot 的SPI机制

- META-INF/services/以「接口全限定名」命名的文件
- ServiceLoader#load

虽然java提供的SPI机制的思想非常好,但是也存在相应的弊端。

  1. 接口实现类只能通过程序员手动遍历来确认;
  2. 服务提供接口必须放到 META-INF/services/ 目录下。

针对java的spi存在的问题,Spring的SPI机制沿用的SPI的思想,但对其进行扩展和优化。Spring采用 spring.factories 方式实现SPI机制,有极强的扩展性。
Java SPI 机制每个服务提供接口都在 META-INF/services/ 目录下分别对应一个配置文件,配置文件中存放当前接口的所有实现类;Spring SPI 机制则在 META-INF/spring.factories 配置文件存放多个接口及对应的实现类:以接口全限定名作为key,实现类作为value来配置,多个实现类用逗号隔开。

SpringBoot 实现「自动装配」的原理就是通过 @EnableAutoConfiguration 执行了 SpringBoot 的 SPI 机制,加载、解析 ClassPath 路径下的所有 spring.factories 文件来实现的。

// From SpringFactoriesLoader.java

/**
 * The location to look for factories.
 * 

Can be present in multiple JAR files. */ public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) { // ... try { Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION); while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String) entry.getValue()); for (String factoryImplementationName : factoryImplementationNames) { result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>()) .add(factoryImplementationName.trim()); } } } // ... return result; }

META-INF/spring.factories 文件可以存在多个 JAR 文件中,基于这个「约定」,可以围绕 Spring 生态做许多扩展。

其它

  • Java SPI META-INF/services 详解

API (Application Programming Interface)在大多数情况下,都是实现方制定接口协议并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

  • Java Service Provider Interface:SPI 使用案例

The Service Provider is installed in the form of extensions, a jar file which we place in the application classpath, the Java extension classpath or the user-defined classpath.

  • what’s the java classpath

classpaths contain:JAR files, and Paths to the top of package hierarchies.
how to set the classpath:1)通过操作系统环境变量设置;2) 通过 JVM 参数设置:java -cp parameter

你可能感兴趣的:(Spring,java,spring,boot)