Java中SPI原理

1 SPI是什么

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它能够用来启用框架扩展和替换组件。常见的 SPI 有 JDBC、日志门面接口、Spring、SpringBoot相关starter组件、Dubbo、JNDI等。java

Java SPI 其实是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,在JDK中提供了工具类:“java.util.ServiceLoader”来实现服务查找。mysql

系统设计之初为了各功能模块之间解耦,通常都是基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类的耦合,就违反了可插拔、闭开等原则,若是咱们但愿实如今模块装配的时候可以不在程序硬编码指定,那就须要一种服务发现的机制(PS:不要和如今微服务的服务发现机制搞混淆了)。web

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

这些 SPI 的接口由 Java 核心库来提供(由启动类加载器Bootstrap Classloader负责加载),而这些 SPI 的实现代码则是做为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码常常须要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。启动类加载器是没法找到 SPI 的实现类的,由于依照双亲委派模型,BootstrapClassloader没法委派AppClassLoader来加载类。因而加载SPI实现类的重任就落到了线程上下文类加载器(破坏了“双亲委派模型”,能够在执行线程中抛弃双亲委派加载链模式,使程序能够逆向使用类加载器)的身上。关于类加载器这部分后面将有文章单独讲,这里很少说了。sql

总体机制图以下:
clipboard.png
通俗的讲就是JDK提供了一种帮助第三方实现者加载服务(如数据库驱动、日志库)的便捷方式,只要第三方遵循约定(把类名写在/META-INF/services里),当服务启动时就会去扫描全部jar包里符合约定的类名,再调用forName加载,因为启动类加载器无法加载实现类,就把加载它的任务交给了线程上下文类加载器。数据库

2 使用介绍

要使用Java SPI,须要遵循以下约定:
一、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下建立一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
二、接口实现类所在的jar包放在主程序的classpath中;
三、主程序经过java.util.ServiceLoder动态装载实现模块,它经过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
四、SPI的实现类必须携带一个不带参数的构造方法;apache

3 原理解析

首先看ServiceLoader类的签名类的成员变量:编程

public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";

    // 表明被加载的类或者接口
    private final Class<S> service;

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

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

    // 缓存providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懒查找迭代器(内部类,真正加载服务类)
    private LazyIterator lookupIterator;
    ......
}

参考具体ServiceLoader具体源码,代码量很少,梳理了一下,实现的流程以下:缓存

  1. 应用程序调用ServiceLoader.load方法

load方法建立了一些属性,重要的是实例化了内部类,LazyIterator。最后返回ServiceLoader的实例。tomcat

public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
    //load方中初始化了ServiceLoader对象
    return new ServiceLoader<>(service, loader);
}

//
public final class ServiceLoader<S> implements Iterable<S>
    // ServiceLoader构造器
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //要加载的接口
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //(ClassLoader类型,类加载器)
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        //(AccessControlContext类型,访问控制器)
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        //(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
        providers.clear();//先清空
        //实例化内部类(实现迭代器功能)
        LazyIterator lookupIterator = new LazyIterator(service, loader);
    }
}
//查找实现类和建立实现类的过程,都在LazyIterator完成。
private class LazyIterator implements Iterator<S>{
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null; 
    private boolean hasNextService() {
        //第二次调用的时候,已经解析完成了,直接返回
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
             try {
                 //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
                //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);//将文件路径转成URL对象
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }   
        }
        while ((pending == null) || !pending.hasNext()) {
             if (!configs.hasMoreElements()) {
                return false;
            }
            //解析URL文件对象,读取内容,最后返回
            pending = parse(service, configs.nextElement());
        }
        //拿到第一个实现类的类名
        nextName = pending.next();
        return true;
    }
}
  1. 应用程序经过迭代器接口获取对象实例

ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,若是有缓存,直接返回。
若是没有缓存,执行类的装载,实现以下:

//当咱们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。
public Iterator<S> iterator() {
        return new Iterator<S>() {

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

            public boolean hasNext() {
                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();
            }

        };
    }


//因此,咱们重点关注lookupIterator.hasNext()方法,它最终会调用到hasNextService。
private class LazyIterator implements Iterator<S>{
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null; 
    private boolean hasNextService() {
        //第二次调用的时候,已经解析完成了,直接返回
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
             try {
                 //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
                //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);//将文件路径转成URL对象
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }   
        }
        while ((pending == null) || !pending.hasNext()) {
             if (!configs.hasMoreElements()) {
                return false;
            }
            //解析URL文件对象,读取内容,最后返回
            pending = parse(service, configs.nextElement());
        }
        //拿到第一个实现类的类名
        nextName = pending.next();
        return true;
    }
}

(1) 读取META-INF/services下的配置文件,得到全部能被实例化的类的名称,值得注意的是,ServiceLoader能够跨越jar包获取META-INF下的配置文件,具体加载配置的实现代码以下:

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

(2) 经过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
固然,调用next方法的时候,实际调用到的是,lookupIterator.nextService。它经过反射的方式,建立实现类的实例并返回。

private class LazyIterator implements Iterator<S>{
    private S nextService() {
        //全限定类名
        String cn = nextName;
        nextName = null;
        //建立类的Class对象
        Class<?> c = Class.forName(cn, false, loader);
        //经过newInstance实例化
        S p = service.cast(c.newInstance());
        //放入集合,返回实例
        providers.put(cn, p);
        return p; 
    }
}

(3) 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)
而后返回实例对象。

4 常见场景分析

适用于:调用者根据实际使用须要,启用、扩展、或者替换框架的实现策略。
下面分别针对JDBC、Spring、Dubbo、tomcat、日志作简要分析

4.1 JDBC中的应用

SPI机制为不少框架的扩展提供了可能,JDBC就应用到了这一机制。

// 加载Class到AppClassLoader(系统类加载器),而后注册驱动类
// Class.forName("com.mysql.jdbc.Driver").newInstance(); 
String url = "jdbc:mysql://localhost:3306/testdb";    
// 经过java库获取数据库链接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");

以上就是mysql注册驱动及获取connection的过程,各位能够发现常常写的Class.forName被注释掉了,但依然能够正常运行,这是为何呢?这是由于从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就能够注册mysql驱动。

那究竟是在哪一步自动注册了mysql driver的呢?重点就在DriverManager.getConnection()中。咱们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块,DriverManager的静态代码块就是:

4.1.1 加载

它在静态代码块里面作了一件比较重要的事。很明显,它已经经过SPI机制,把数据库驱动链接初始化了。

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

先看下MySQL的jar包,文件内容为:com.mysql.cj.jdbc.Driver。
clipboard.png
具体过程还得看loadInitialDrivers,它在里面查找的是Driver接口的服务类,因此它的文件路径就是:META-INF/services/java.sql.Driver。

public class 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;
        }
    
        // 经过SPI加载驱动类
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //很明显,它要加载Driver接口的服务类,Driver接口的包为:java.sql.Driver
                //因此它要找的就是META-INF/services/java.sql.Driver文件
                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;
            }
        });
         // 继续加载系统属性中的驱动类
        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);
                // 使用AppClassloader加载
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
}

从上面能够看出JDBC中的DriverManager的加载Driver的步骤顺序依次是:

  1. 经过SPI方式,读取 META-INF/services 下文件中的类名,使用线程上下文类加载器加载;
  2. 经过System.getProperty("jdbc.drivers")获取设置,而后经过系统类加载器加载。

下面详细分析SPI加载的那段代码。

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
    //查到以后建立对象
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {
    // Do nothing
}

注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法,也就是最开始咱们注释掉的那一句代码。讲到这里,那句因SPI而省略的代码如今解释清楚了,那咱们继续看给这个方法传的loader是怎么来的。

由于这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,所以传给 forName 的 loader 必然不能是BootrapLoader。这时候只能使用线程上下文类加载器了,也就是说把本身加载不了的类加载到线程上下文类加载器中(经过Thread.currentThread()获取)。上面那篇文章末尾也讲到了线程上下文类加载器默认使用当前执行的代码所在应用的系统类加载器AppClassLoader。

再看下看ServiceLoader.load(Class)的代码,的确如此:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
//看load方法,调用了带有service, loader参数的构造器
public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}
//若是传过来的类加载器c1=null则赋值为系统类加载器
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;
        。。。
    }

ContextClassLoader默认存放了AppClassLoader的引用,因为它是在运行时被放在了线程中,因此无论当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何须要的时候均可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成须要的操做。

4.1.2 建立实例

上一步已经找到了MySQL中的com.mysql.cj.jdbc.Driver全限定类名,当调用next方法时,就会建立这个类的实例。它就完成了一件事,向DriverManager注册自身的实例。

到这里数据源驱动类已经加载到了线程上下文类环境中,下面将driver实例注册到系统的java.sql.DriverManager类中。
com.mysql.jdbc.Driver加载后运行的静态代码块:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            // Driver已经加载到线程上下文中了,此时调用DriverManager类的注册方法往registeredDrivers集合中加入实例
            java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
 public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

其实就是调用java.sql.DriverManager.registerDriver()方法将driver实例add到它的一个名为registeredDrivers的静态成员CopyOnWriteArrayList中 。

到此驱动注册基本完成,接下来咱们回到最开始的那段样例代码:java.sql.DriverManager.getConnection()。它最终调用了如下方法:

在DriverManager.getConnection()方法就是建立链接的地方,它经过循环已注册的数据库驱动程序,调用其connect方法,获取链接并返回。
private static Connection getConnection(
     String url, java.util.Properties info, Class<?> caller) throws SQLException {
     /* 传入的caller由Reflection.getCallerClass()获得,该方法
      * 可获取到调用本方法的Class类,这儿获取到的是当前应用的类加载器
      */
     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
     synchronized(DriverManager.class) {
         if (callerCL == null) {
             callerCL = Thread.currentThread().getContextClassLoader();
         }
     }

     if(url == null) {
         throw new SQLException("The url cannot be null", "08001");
     }

     SQLException reason = null;
     // 遍历注册到registeredDrivers里的Driver类
     for(DriverInfo aDriver : registeredDrivers) {
         // 检查Driver类有效性
         if(isDriverAllowed(aDriver.driver, callerCL)) {
             try {
                 println("    trying " + aDriver.driver.getClass().getName());
                 // 调用com.mysql.jdbc.Driver.connect方法获取链接
                 Connection con = aDriver.driver.connect(url, info);
                 if (con != null) {
                     // Success!
                     return (con);
                 }
             } catch (SQLException ex) {
                 if (reason == null) {
                     reason = ex;
                 }
             }

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

     }
     throw new SQLException("No suitable driver found for "+ url, "08001");
 }
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
        // 传入的classLoader为调用getConnetction的当前类加载器,从中寻找driver的class对象
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }
    // 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个
    // driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
        result = ( aClass == driver.getClass() ) ? true : false;
    }
    return result;
}

因为线程上下文类加载器本质就是当前应用类加载器,因此以前的初始化就是加载在当前的类加载器中,这一步就是校验存放的driver是否属于调用者的Classloader防止由于不一样的类加载器致使类型转换异常(ClassCastException)。

例如在下文中的tomcat里,多个webapp都有本身的Classloader,若是它们都自带 mysql-connect.jar包,那底层Classloader的DriverManager里将注册多个不一样类加载器的Driver实例,想要区分只能靠线程上下文类加载器了。

4.2 common-logging

apache最先提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现,发现日志提供商是经过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,经过读取该文件的内容找到日志提供商实现类。只要咱们的日志实现里包含了这个文件,并在文件里制定LogFactory工厂接口的实现类便可。

4.3 Tomcat中的类加载器

在Tomcat目录结构中,有三组目录(“/common/”,“/server/”和“shared/”)能够存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/”,把java类库放置在这些目录中的含义分别是:

  • 放置在common目录中:类库可被Tomcat和全部的Web应用程序共同使用。
  • 放置在server目录中:类库可被Tomcat使用,但对全部的Web应用程序都不可见。
  • 放置在shared目录中:类库可被全部的Web应用程序共同使用,但对Tomcat本身不可见。
  • 放置在/WebApp/WEB-INF目录中:类库仅仅能够被此Web应用程序使用,对Tomcat和其余Web应用程序都不可见。

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,以下图所示
clipboard.png
灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的做用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 本身定义的类加载器,它们分别加载 /common/、/server/、/shared/ 和 /WebApp/WEB-INF/ 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器一般会存在多个实例,每个 Web 应用程序对应一个 WebApp 类加载器,每个 JSP 文件对应一个 Jsp 类加载器。

从图中的委派关系中能够看出,CommonClassLoader 能加载的类均可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 本身能加载的类则与对方相互隔离。WebAppClassLoader 可使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并经过再创建一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

4.4 Spring加载问题

Tomcat 加载器的实现清晰易懂,而且采用了官方推荐的“正统”的使用类加载器的方式。这时做者提一个问题:若是有 10 个 Web 应用程序都用到了spring的话,能够把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的做用是管理每一个web应用程序的bean,getBean时天然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?其实spring根本不会去管本身被放在哪里,它通通使用线程上下文类加载器来加载类,而线程上下文类加载器默认设置为了WebAppClassLoader,也就是说哪一个WebApp应用调用了spring,spring就去取该应用本身的WebAppClassLoader来加载bean,简直完美。

有兴趣的能够接着看看源码分析的具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener,它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法以下(删去了部分不相关内容):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    try {
        // 建立WebApplicationContext
        if (this.context == null) {
            this.context = createWebApplicationContext(servletContext);
        }
        // 将其保存到该webapp的servletContext中        
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
        // 获取线程上下文类加载器,默认为WebAppClassLoader
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        // 若是spring的jar包放在每一个webapp本身的目录中
        // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = this.context;
        }
        else if (ccl != null) {
            // 若是不一样,也就是上面说的那个问题的状况,那么用一个map把刚才建立的WebApplicationContext及对应的WebAppClassLoader存下来
            // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出
            currentContextPerThread.put(ccl, this.context);
        }
        
        return this.context;
    }
    catch (RuntimeException ex) {
        logger.error("Context initialization failed", ex);
        throw ex;
    }
    catch (Error err) {
        logger.error("Context initialization failed", err);
        throw err;
    }
}

具体说明都在注释中,spring考虑到了本身可能被放到其余位置,因此直接用线程上下文类加载器来解决全部可能面临的状况。

4.5 Dubbo框架中SPI机制分析

须要说明的是虽然Java 提供了对SPI机制的默认实现支持,可是并不表示全部的框架都会默认使用这种Java自带的逻辑,SPI机制更多的是一种实现思想,而具体的实现逻辑,则是能够本身定义的。例如咱们说Dubbo框架中大量使用了SPI技术,可是Dubbo并无使用JDK原生的ServiceLoader,而是本身实现了ExtensionLoader来加载扩展点,因此咱们看Dubbo框架源码的时候,千万不要被配置目录是/META-INF/dubbo/internal,而不是META-INF/services/所迷惑了。
clipboard.png
相应地若是其余框架中也使用了自定义的SPI机制实现,也不要疑惑,它们也只是从新定义了相似于ServiceLoader类的加载逻辑而已,其背后的设计思想和原理则都是同样的!例如,以Dubbo中卸载协议的代码举例:

private void destroyProtocols() {
        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        for (String protocolName : loader.getLoadedExtensions()) {
            try {
                Protocol protocol = loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    protocol.destroy();
                }
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }

而ExtensionLoader中扫描的配置路径以下:

public class ExtensionLoader<T> {

    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

    private static final String SERVICES_DIRECTORY = "META-INF/services/";

    private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

    private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

    private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");

    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();

    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
}

以上经过对Java自带SPI机制的示例以及对Dubbo和JDBC驱动框架中对SPI机制应用的分析,相信你们应该是有了一个整体的原理性的认识了。若是须要更加深刻的了解一些细节的实现逻辑就须要你们好好去看看ServiceLoader的源码了,若是其余框架单独实现了SPI机制,其相应的实现加载工具类也须要具体看看它们的源码是怎么实现的了!

4.6 小结

经过上面的几个案例分析,咱们能够总结出线程上下文类加载器的适用场景:
当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须经过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不一样的调用者,能够取调用者各自的线程上下文类加载器代为托管。

5 扩展

既然咱们知道JDBC是这样建立数据库链接的,咱们能不能再扩展一下呢?若是咱们本身也建立一个java.sql.Driver文件,自定义实现类MyDriver,那么,在获取链接的先后就能够动态修改一些信息。

仍是先在项目ClassPath下建立文件,文件内容为自定义驱动类
clipboard.png
咱们的MyDriver实现类,继承自MySQL中的NonRegisteringDriver,还要实现java.sql.Driver接口。这样,在调用connect方法的时候,就会调用到此类,但实际建立的过程还靠MySQL完成。

package com.viewscenes.netsupervisor.spi

public class MyDriver extends NonRegisteringDriver implements Driver{
    static {
        try {
            java.sql.DriverManager.registerDriver(new MyDriver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    public MyDriver()throws SQLException {}
    
    public Connection connect(String url, Properties info) throws SQLException {
        System.out.println("准备建立数据库链接.url:"+url);
        System.out.println("JDBC配置信息:"+info);
        info.setProperty("user", "root");
        Connection connection =  super.connect(url, info);
        System.out.println("数据库链接建立完成!"+connection.toString());
        return connection;
    }
}
--------------------输出结果---------------------
准备建立数据库链接.url:jdbc:mysql:///consult?serverTimezone=UTC
JDBC配置信息:{user=root, password=root}
数据库链接建立完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f

6 优点&劣势

1. 优点

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

2. 劣势

  • 虽然ServiceLoader也算是使用的延迟加载,可是基本只能经过遍历所有获取,也就是接口的实现类所有加载并实例化一遍。若是你并不想用某些实现类,它也被加载并实例化了,这就形成了浪费。
  • 获取某个实现类的方式不够灵活,只能经过Iterator形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用ServiceLoader类的实例是不安全的。
  • 使用线程上下文加载类,也要注意保证多个须要通讯的线程间的类加载器应该是同一个,防止由于不一样的类加载器致使类型转换异常(ClassCastException)。

声明:为避免重复造轮子,文章部份内容摘自网络。

相关文章
相关标签/搜索