Java Spi

374 阅读7分钟

Java SPI机制

1. 为什么需要SPI

SPI: Service Provider Interface ,服务发现机制. 类似IOC,IOC将装配控制权从程序中移到Spring容器.

在面向对象设计中,推荐的都是基于接口编程, 不直接对实现类进行硬编码, 一旦涉及到实现类,就违反了可插拔原则. 如果需要替换为另外一种实现,就需要修改代码.

为了实现模块在装配的时候不用在程序中动态申明,就需要一种服务发现机制. 这就是SPI.

SPI主要使用就是各种产商(jdbc: 数据库产商)和插件(eclise: 插件).

2. 怎么使用Java SPI

package com.markbolo.samples.spi;
public interface Search{
    void search();
}
public class MemorySearch{

    @override 
    public void search(){
        system.out.println("Memory Serach.")
    }
}

public class FileSearch{
    @overrider
}

在接口实现(FileSearch|MemorySearch)项目resources目录加上META-INF/services文件夹. 加上 com.markbolo.samples.spi.Search文件(文件名称为接口的全限定名称). 文件内容: com.markbolo.samples.spi.FileSearch com.markbolo.samples.spi.MemorySearch

public class SpiClient {
    public static void main(String[] args){
        ServiceLoader<Search> serviceLoader = ServiceLoader.load(Search.class);
        for (Search search : serviceLoader) {
            search.search();
        }
    }
}

可以得到console的内容:
FileSearch
MemorySearch

3. SPI解决了什么问题

  1. 服务发现
  2. 解耦

以JDBC为例,在SPI机制出现之前,要获取数据驱动就需要通过 Driver driver = Class.forName("com.mysql.jdbc.Driver") 有了SPI之后,就可以通过DriverMananer.getDriver(url)就可以直接获取数据库驱动.

将具体的数据库产商提供的驱动实现类从调用者(要获取Driver|Connection的类)中解耦出来.

Like: com.mysql.jdbc.Driver 是mysql-connector-java 5中的;com.mysql.cj.jdbc.Driver 是mysql-connector-java 6中的 如果还是通过老的方式获取jdbc driver,就会将具体的数据库驱动类硬编码到程序中,要想切换数据库驱动连接版本 5->6, 需要改动的地方: 1. maven数据库jar包 2. 数据库驱动类配置 ; 使用SPI之后,就只需要变更maven中的数据库jdbc jar包的版本即可, DriverManager.getDriver(url) 会自己去发现数据库驱动

4. SPI案例

1. JDBC

DriverManager  
// DriverManager初始化数据库驱动:加载到DriverManager中来 ; ps:服务发现
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

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() {
                // 发现数据库产商提供的数据库驱动Driver (这里只发现符合SPI机制的驱动)

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator 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{
                    // 这里还是把类加载了,实际将Driver注册到DriverManager还是靠的Driver实现类中的 静态块注册的
                    while(driversIterator.hasNext()) {
                        println(" Loading done by the java.util.ServiceLoader :  "+driversIterator.next());
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

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

        // ps: 这里是对不符合SPI机制的Driver进行加载; 还是采用老的模式Class.forName(driverClassName)进行加载的
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                // Class.forName(driverClassName)也还是要靠Driver中的静态代码块将Driver注册到DriverManager中
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
}


mysql Driver中的代码:
  static {
        // 具体数据库产商提供的Driver在类加载的时候自己注册到DriverManager中
        try {
            DriverManager.registerDriver(new FabricMySQLDriver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver", var1);
        }
    }

再插入一句: Spring集成数据源获取Connection也是靠DriverMananger来管理.

org.springframework.jdbc.datasource.DriverManagerDataSource

protected Connection getConnectionFromDriverManager(String url, Properties props) throws SQLException {
        // 通过DriverMananger获取数据库连接
        return DriverManager.getConnection(url, props);
    }

2. Dubbo

后期才完成Dubbo部分,欢迎来看Dubbo SPI

3. Eclipse插件拓展

4. common-logging

5. Hotspot

6. Jetty/Tomcat自定义SessionManager

7. Spring

5. SPI怎么就破坏了双亲委派模型?

5.1 jdbc4.0之前加载数据库驱动

先看不使用SPI是怎么加载mysql数据库驱动:

Driver dirver = Class.forName("com.mysql.jdbc.Driver")
DriverMananger.getConnection("jdbc:mysql://localhost:3306/testdb")

关键的就是通过Class.forName()触发数据库驱动Driver的类加载,然后自己注册到DriverManager:

 static {
        // 具体数据库产商提供的Driver在类加载的时候自己注册到DriverManager中
        try {
            DriverManager.registerDriver(new FabricMySQLDriver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver", var1);
        }
    }

Driver注册到DriverManager之后就可以通过DriverManager来获取数据库连接.

总结:
jdbc4.0之前的版本在Application中使用Class.forName("com.mysql.jdbc.Driver")加载驱动类,这时候 ClassLoder是ApplicationClassLoader:负责加载appliction下面的类,符合双亲委派模型的定义

5.2 jdbc4.0 spi注册driver

在jdbc 4.0之后,就支持SPI方式来注册Driver. 具体怎么实现就不过多描述. 可以分析SPI加载Driver的过程:

  1. 从mysql jar包中 resources/META-INF/services/java.sql.Driver文件中可发现具体的实现类为: com.mysql.jdbc.Driver
  2. 知道具体的实现类名后, 通过Class.forName()加载这个实现类, 类加载完成就触发静态块将自己类的实例注册到DriverManager

Question: 在了解SPI加载数据库Driver的过程后, 会发现各种数据库Driver类是在DriverManager中进行的Class.forName来完成类加载工作的.
而DriverMananger是在${java_home}\lib\rt.jar中的,ClassLoader自然就是BootstrapClassLoader. 那么这个时候使用DriverManager的classLoader来加载应用lib目录下数据库驱动jar里面的Driver类自然是加载不到的. 那么怎么解决这个问题? ps: 这也是双亲委派模型的局限性, 自身缺陷: 父加载器不能加载子加载器路径中的类 (JNDI)

Answer: Driver实现类又必须要要用ApplicationClassLoader才能加载. 看起来唯一的解决方式就是: 通过某种方式获取到ApplicationClassLoader,然后在让ApplicationClassLoader来加载Driver实现类.
这里就是线程上下文加载器ThreadClassLoader. 可以看到在创建serviceLoader实例的时候,用到的就是ThreadClassLoader

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

5.3 线程上下文加载器

thread.getContextClassLoader()获取线程上下文加载器 thread.setContextClassLoader(ClassLoader)设置线程上下文加载器,默认为ApplicationClassLoader.

这里, 本该是BootstrapClassLoader加载的Driver实现类,现在可以通过线程上线文加载器获的子加载器ApplicationClassLoader, 让ApplicationClassLoader来加载本该是父加载器加载的Driver实现类. 很明显就破坏了双亲委派模型.

双亲委派模型: 当一个类加载器收到类加载请求时,不是先尝试自己加载这个类,而是将请求委派给父加载器去完成. 一直到顶级加载器. 只有当父加载器无法完成类加载的时候,子加载器才会去尝试自己加载.

正常的双亲委派模型类加载流程: 类加载请求 --> ApplicationClassLoader --委托--> ExtensionClassLoader --> bootstrapClassLoader --无法完成类加载 --> ExtensionClassLoader --> ApplicationClassLoder. Application在去加载类.

JNDI SPI的类加载请求: 类加载请求 --> BootstrapClassLoader --无法完成类加载 --> getContextLoader获取到ApplictionClassLoader 去加载Driver类.

6. SPI实现原理

通过前面的内容,我们了解到Driver实现类都是通过ServiceLoader.load(Driver.class)获取到的. 并且Driver实现类的类加载请求是通过上下文类加载器加载的.
下面的内容就是详细剖析 1. ServiceLoader是怎么发现Driver实现类的 2. 又是怎么样一种方式类获取到ApplicationClassLoader来加载Driver实现类的.

ServiceLoader实现原理剖析: 源码阅读
 public static <S> ServiceLoader<S> load(Class<S> service) {
        // 这获取线程上下文加载器来加载Service实现类
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

thred.getContextClassLoader() 返回null就表示采用Appcation ClassLoader

ServiceLoader.load(service, cl):

public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        // load的时候构造一个ServiceLoader对象,在构造方法中加载
        return new ServiceLoader<>(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = svc;
        loader = cl;
        // ServiceLoader构造方法中的加载Service实现类的地方
        reload();
    }

     public void reload() {
        providers.clear();
        // 加载service实现类还是通过LazyIterator来加载的
        lookupIterator = new LazyIterator(service, loader);
    }
    想要获得到具体的Service实现类,只能通过ServiceLoader<service>.iterator().hasNext()/next()来访问.   
    ServicerLoader.iterator.hasNext()/next()实际还是委托给lookupIterator:LazyIterator的: 
    具体委托:
     public Iterator<S> iterator() {
        // iterator都委托给lookupIterator:LazyIterator
        return new Iterator<S>() {

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                    // ServiceLoader.iterator.hasNext()委托给lookupIterator:LazyIterator
                return lookupIterator.hasNext();
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                    // 同上,ServiceLoader.iterator.next()委托给lookupIterator:LazyIterator
                return lookupIterator.next();
            }

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

        };
    }

ServiceLoader.iterator().hasNext()/next()的具体实现都委托给ServiceLoader.lookupIterator.hasNext()/next().ServiceLoader.load(Service.class)获取到的Service实现类只能通过iterator()来访问. 所以就可以将类加载过程推迟到iterator.next()的时候. 也就是lookupIterator:LazyIterator.next().

LazyIterator: 

 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;
        }

        public boolean hasNext() {
            // 加载META-INF/services/java.sql.Driver 中内容 nextName = 'xxxDriver'
            ...
        }

        public S next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                // 这里用loader: App ClassLoader加载
                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,
                     x);
            }
            throw new Error();          // This cannot happen
        }