深入剖析Spring boot自动装配原理

    ServiceLoader是jdk6里面引进的一个特性。它用来实现SPI(Service Provider Interface),一种服务发现机制,很多框架用它来做来做服务的扩展发现。

系统里抽象的各个模块一般会有很多种不同的实现,如JDBC、日志等。通常模块之间我们均是基于接口进行编程,而不是对实现类进行硬编码。这时候就需要一种动态替换发现的机制,即在运行时动态地给接口添加实现,而不需要在程序中指明。

引用自JDK文档对于java.util.ServiceLoader的描述:

      服务是一个熟知的接口和类(通常为抽象类)集合。服务提供者是服务的特定实现。提供者中的类通常实现接口,并子类化在服务本身中定义的子类。服务提供者可以以扩展的形式安装在 Java 平台的实现中,也就是将 jar 文件放入任意常用的扩展目录中。也可通过将提供者加入应用程序类路径,或者通过其他某些特定于平台的方式使其可用。

      为了加载,服务由单个类型表示,也就是单个接口或抽象类。一个给定服务的提供者包含一个或多个具体类,这些类扩展了此服务类型,具有特定于提供者的数据和代码。提供者类通常不是整个提供者本身而是一个代理,它包含足够的信息来决定提供者是否能满足特定请求,还包含可以根据需要创建实际提供者的代码。提供者类的详细信息高度特定于服务;任何单个类或接口都不能统一它们,因此这里没有定义任何这种类型。此设施唯一强制要求的是,提供者类必须具有不带参数的构造方法,以便它们可以在加载中被实例化。

当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/ 目录里同时创建一个以服务接口命名的该服务接口具体实现类文件。当外部程序装配该模块时,通过该jar包META-INF/services/里的配置文件找到具体的实现类名,从而完成模块的注入,而不需要在代码里定制。

在JDBC中使用了ServiceLoader对不同数据库驱动进行加载。

DriverManager通过ServiceLoader加载数据库驱动,JDBC原理可移步【JDBC原理】。

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

ServiceLoader 使用小栗子

首先定义一个SPIService接口

package com.study;

public interface SPIService {
 
    void excute();
}

再定义两个实现类

package com.study.impl;
import com.study.SPIService;

public class SPIServiceImpl1 implements SPIService {
 
  @Override
  public void excute() {
    System.out.println("SPIServiceImpl1");
  }
}

package com.study.impl;
import com.study.SPIService;

public class SPIServiceImpl2 implements SPIService {
 
  @Override
  public void excute() {
    System.out.println("SPIServiceImpl2");
  }
}

在ClassPath路径下配置添加文件,META-INF/services/com.study.SPIService,文件名为接口的全限定类名。在配置文件中加入两个实现类的全限定类名。

com.study.impl.SPIServiceImpl1
com.study.impl.SPIServiceImpl2

写一个测试类SPITest.java

public class SPITest {
  public static void main(String[] args) {
    ServiceLoader<SPIService> loaders = ServiceLoader.load(SPIService.class);
    Iterator<SPIService> it = loaders.iterator();
    while (it.hasNext()) {
      SPIService spiSer= it.next();
      spiSer.excute();
    }
  }
}

运行结果:

SPIServiceImpl1
SPIServiceImpl2

ServiceLoader的load方法将在META-INF/services/com.study.SPIService中配置的子类都进行了加载。

ServiceLoader的核心源码分析

public final class ServiceLoader<S> implements Iterable<S>{
    // 需要加载的资源的配置文件路径
    private static final String PREFIX = "META-INF/services/";
    // 加载的服务类或接口
    private Class<S> service;
    // 类加载时用到的类加载器
    private ClassLoader loader;
    // 基于实例的已加载的顺序缓存类,其中Key为实现类的全限定类名
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // "懒查找"迭代器,ServiceLoader的核心
    private LazyIterator lookupIterator;
 
    public void reload() {
        // 清空缓存
        providers.clear();
        // 构造LazyIterator实例
        lookupIterator = new LazyIterator(service, loader);
    }
     
    // 私有构造方法
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = svc;
        loader = cl;
        reload();
    }
}

ServiceLoader只有一个私有的构造函数,也就是它不能通过构造函数实例化,但是要实例化ServiceLoader必须依赖于它的静态方法调用私有构造去完成实例化操作。

来看ServiceLoader的提供的静态方法,这几个方法都可以用于构造ServiceLoader的实例。

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取当前线程的线程上下文类加载器实例,确保通过此classLoader也能加载到项目中的资源文件
    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);
}
 
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    ClassLoader prev = null;
    while (cl != null) {
        prev = cl;
        cl = cl.getParent();
    }
    return ServiceLoader.load(service, prev);
}

load(Class<S> service, ClassLoader loader)是典型的静态工厂方法,直接调用ServiceLoader的私有构造器进行实例化,除了需要指定加载类的目标类型,还需要传入类加载器的实例。load(Class<S>service)实际上也是委托到load(Class<S> service, ClassLoader loader),不过它使用的类加载器指定为线程上下文类加载器,一般情况下线程上下文类加载器获取到的就是应用类加载器(系统类加载器)。loadInstalled(Class<S> service)方法又看出了"双亲委派模型"的影子,它指定类加载器为最顶层的启动类加载器,最后也是委托到load(Class<S> service, ClassLoader loader)。






这里重点关注为什么类加载器使用线程上下文类加载器?以JDBC加载MySQL驱动举例。

Java类加载的过程通常是遵循双亲委派模型的。但是对于SPI接口实现类的加载就需要破坏双亲委派模型。

首先java.sql.DriverManager是由启动类加载器加载的,创建真正的Dirver对象时需要使用到mysql提供的实现:com.mysql.jdbc.Dirver,即要初始化该类。但是启动类加载器加载DirverManager的时候,使用到了启动类加载器无法加载的类,这时候就需要由系统类加载器来加载。com.mysql.jdbc.Dirver通常放在类路径下的(其实不一定)。到这里和线程上下文类加载器没由任何关系。在DriverManager中使用系统类加载的时候,可以直接使用静态方法ClassLoader.getSystemClassLoader(),但是这种情况的前提是com.mysql.jdbc.Dirver类在类路径下。如果不在类路径下,而且在系统环境中有其他的类加载器,在通过其他类加载器可能出现无法正确加载扩展点的情况。比如某个类的字节码是在数据库中存储,这时我们需要自定义一个类加载器去加载它,这个类加载器会告诉DriverManager去我们指定放的地方取。因此Thread.currentThread().setContextClassLoader(自定义加载器/默认是系统类加载器); 这个就是线程上下文类加载器起到的介质作用。线程上下文中默认放的是系统类加载器。

ServiceLoader实现了Iterable接口。

public Iterator<S> iterator() {
    // Iterator的匿名实现
    return new Iterator<S>() {
        // 基于实例的已加载的顺序缓存类Map的Entry迭代器实例
        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
         
        // 先从缓存中判断是否有下一个实例,否则通过懒加载迭代器LazyIterator去判断是否存在下一个实例
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }
         
        // 如果缓存中判断是否有下一个实例,如果有则从缓存中的值直接返回,否则通过懒加载迭代器LazyIterator获取下一个实例
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
         
        // 不支持移除
        public void remove() {
            throw new UnsupportedOperationException();
        }
 
    };
}

LazyIterator本身也是一个Iterator接口的实现,它是ServiceLoader的一个私有内部类。

private class LazyIterator implements Iterator<S>
{
 
    Class<S> service;
    ClassLoader loader;
    // 加载的资源的URL集合
    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() {
        // 如果下一个需要加载的实现类的全限定类名不为null,则说明资源中存在内容
        if (nextName != null) {
            return true;
        }
        // 如果加载的资源的URL集合为null则尝试进行加载
        if (configs == null) {
            try {
                // 资源的名称,META-INF/services/ + '需要加载的类的全限定类名'
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    // 从ClassPath加载资源
                    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;
    }
 
    public S next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        String cn = nextName;
        nextName = null;
        try {
            // 反射构造Class<S>实例,同时进行初始化,并且强制转化为对应的类型的实例
            S p = service.cast(Class.forName(cn, true, loader).newInstance());
            // 添加进缓存,Key为实现类的全限定类名,Value为实现类的实例
            providers.put(cn, p);
            return p;
        } catch (ClassNotFoundException x) {
            fail(service, "Provider " + cn + " not found");
        } catch (Throwable x) {
            fail(service, "Provider " + cn + " could not be instantiated: " + x, x);
        }
        throw new Error();          // This cannot happen
    }
 
    public void remove() {
        throw new UnsupportedOperationException();
    }
 
}
ServiceLoader总结

JDK提供了一种帮第三方实现者加载服务的便捷方式,如JDBC、日志等,第三方实现者需要遵循约定把具体实现的类名放在/META-INF里。当JDK启动时会去扫描所有jar包里符合约定的类名,再调用forName进行加载,如果JDK的ClassLoader无法加载,就使用当前执行线程的线程上下文类加载器。

但是在通过SPI查找服务的具体实现时,会遍历所有的实现进行实例化,并在循环中找到需要的具体实现。

作者:java知路


欢迎关注微信公众号 :java知路