浅谈ServiceLoader

本文主要介绍ServiceLoader类,首先介绍ServiceLoader的简单使用,以及它可以达到什么样的效果,然后介绍了ServiceLoader的实现原理,最后通过对ServiceLoader源码的理解,自定义实现一个简单的类似ServiceLoader的功能。

引言

ServiceLoader是JDK1.6基于SPI(Service Provider Interfaces)思想新引入的一个类。SPI即服务提供接口,基于服务的注册与发现机制,服务提供者向系统注册服务,服务使用者通过查找发现服务,可以达到服务的提供与使用的分离,甚至完成对服务的管理。通过解耦服务具体实现和使用,使得程序的扩展性大大增强,甚至可插拔。

ServiceLoader在许多框架都有着很好的应用,比如在Dubbo、JDBC,目前讨论比较多的Android组件化,有时也会借助于ServiceLoader,甚至当JDK中实现无法满足对性能要求时,也可以自定义一个ServiceLoader,美团技术团队就采用了自定义的ServiceLoader。

示例介绍

如下是示例的结构图:

一般使用ServiceLoader分为如下几个步骤:

  • 创建一个接口文件,声明需要实现的方法;
  • 在resources资源目录下创建META-INF/services文件夹,resources其实是和类的根路径在同一级目录,示例中resources目录仅仅是为了易于区分资源文件和Java类文件,其实也可以将META-INF/services建立在src目录下,运行效果也是一样的。如果使用的是IDEA开发工具,META-INF/services可以放在与java目录同级的某个资源文件夹下。
  • 在services文件夹中创建文件,以接口全限定名命名,文件必须使用UTF-8编码,可以使用"#"作为注释符。
  • 创建接口实现类,并将该实现类的全限定名注册到接口文件中。

在示例中主工程是008-serviceloader工程,lib库工程是009-servicelib工程,在主工程实现类的execute()方法中仅仅是输出一行字符串,在lib库工程实现类中调用了ServiceLoader的方法,并输出所有的实现类名称。

// 声明一个接口
public interface ServiceEngine {
	public void execute();
}

// lib中接口实现类
public class LibServiceEngine implements ServiceEngine{
	@Override
	public void execute() {
		System.out.println("LibServiceEngine execute");
		System.out.println("-----------------------------");
		ServiceLoader<ServiceEngine> loader=ServiceLoader.load(ServiceEngine.class);
		Iterator<ServiceEngine> it=loader.iterator();
		while(it.hasNext()) {
			System.out.println("lib:" + it.next().getClass().getSimpleName());
		}
	}
}

// app工程接口实现类
public class AppServiceEngine implements ServiceEngine{
	@Override
	public void execute() {
		System.out.println("AppServiceEngine execute");
	}
}

// lib库中SPI文件
com.sunny.lib.LibServiceEngine

// app工程中SPI文件
com.sunny.demo.AppServiceEngine

// 测试类
public class MainTest {
	public static void main(String[] args) throws Exception {
		ServiceLoader<ServiceEngine> loader = ServiceLoader.load(ServiceEngine.class);
		Iterator<ServiceEngine> it = loader.iterator();
		while (it.hasNext()) {
			it.next().execute();
		}
	}
}

示例运行结果如下:

AppServiceEngine execute
LibServiceEngine execute
-----------------------------
lib:AppServiceEngine
lib:LibServiceEngine

从输出结果可以看出,lib库工程中也可以拿到主工程的实现类,其实这就是ServiceLoader的强大之处。我们知道,工程中要尽量避免循环依赖,在单向依赖的情况下,库工程中是无法调用主工程中的类和方法的,但是使用ServiceLoader打破了这个限制,其实这也是Android组件化依赖ServiceLoader的目的所在。

ServiceLoader原理

ServiceLoader在实现方式上采用的是迭代器模式。在迭代器实现中采用的是懒加载方式,即用到时才加载(这里加载指的是解析SPI接口资源文件),而不是在调用load()或iterator()方法时加载。ServiceLoader首先从类加载路径下读取SPI接口配置文件,将所有的配置文件地址解析到一个Enumeration的集合对象中,然后逐个解析配置文件,将配置文件中的每一行解析为一个类的全限定名存入一个Iterator中,通过对Iterator逐个迭代读取类名,并将解析的类名通过Class.forName()得到每个类的Class对象,最后通过反射的方式创建SPI实现类并缓存到一个Map集合中。

public final class ServiceLoader<S> implements Iterable<S>{
    // 资源目录前缀,此处是写死的,所以每次使用SPI接口时必须在META-INF/services/目录下创建文件
	private static final String PREFIX = "META-INF/services/";
	
	// 缓存服务提供者
	private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
	
	// 解析资源的迭代器,在访问时才解析 采用懒加载模式
	private LazyIterator lookupIterator;
	
	public static <S> ServiceLoader<S> load(Class<S> service) {
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		return ServiceLoader.load(service, cl);
	}
	
	// 每次调用load()方法都会创建一个新ServiceLoader对象,自定义方式时这里可以优化下
	public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
		return new ServiceLoader<>(service, loader);
	}
	private ServiceLoader(Class<S> svc, ClassLoader cl) {
		loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
		reload();
	}
	
	public void reload() {
		providers.clear();
		lookupIterator = new LazyIterator(service, loader);
	}
	
	// 实现Iterable的迭代器方法,调用LazyIterator中相对应的方法
	public Iterator<S> iterator() {
        return new Iterator<S>() {

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

            public boolean hasNext() {
                return lookupIterator.hasNext();
            }

            public S next() {
                return lookupIterator.next();
            }
        };
    }
	
	private class LazyIterator implements Iterator<S> {
		Class<S> service;
		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() {
			// 调用nextService()方法之后才会置空
			if (nextName != null) {
				return true;
			}
			if (configs == null) {
				// 类加载器将同一类型的SPI接口文件解析到一个Enumeration集合中
				String fullName = PREFIX + service.getName();
				configs = loader.getResources(fullName);
			}
			// 每一个URL对应一个Iterator对象,因为一个SPI文件中可能包含多个实现类
			// 当前URL对应的Iterator迭代完成后,才会继续解析接下来的一个URL
			while ((pending == null) || !pending.hasNext()) {
				if (!configs.hasMoreElements()) {
					return false;
				}
				pending = parse(service, configs.nextElement());
			}
			nextName = pending.next();
			return true;
		}

		private S nextService() {
			String cn = nextName;
			nextName = null;
			// 获取Class对象
			Class<?> c = Class.forName(cn, false, loader);
			
			// 创建SPI实例
			S p = service.cast(c.newInstance());
			
			// 缓存到Map集合中
			providers.put(cn, p);
			return p;
		}

		public boolean hasNext() {
			return hasNextService();  
		}

		public S next() {
			return nextService(); 
		}
	}
}

模拟ServiceLoader简易实现

在代码实现之前,先看下几个API:

public URL getResource(String name)
public Enumeration<URL> getResources(String name) 
public static Enumeration<URL> getSystemResources(String name)

getResource()在Class与ClassLoader类中都有该方法,如果传入的是空字符串,Class类中方法返回是class文件所在根目录URL(包括包名目录),ClassLoader中方法返回的类所在的根目录URL,不包括包名目录。

getResources()该方法仅存在于ClassLoader类中,返回的是一个Enumeration集合类型的URL。

getSystemResources()该方法也仅存在于ClassLoader类中,如果是直接运行的Java程序,那么的确是调用JVM的ClassLoader,返回类所在的根目录URL,不包括包名目录。但是,在tomcat中,由于Java类并不是由系统自带的ClassLoader装载的,而是由一个叫WebappClassLoader来装载的, JVM读取ClassLoader取到的目录返回null。

public class MainTest {
	public static void main(String[] args) throws Exception {
		ClassLoader loader = MainTest.class.getClassLoader();
		URL url = loader.getResource("META-INF\\services\\com.sunny.lib.ServiceEngine");
		InputStream in = url.openStream();
		BufferedReader br = new BufferedReader(new InputStreamReader(in, "utf-8"));
		String line;
		while ((line = br.readLine()) != null) {
			System.out.println("class:" + line);
			
			// 通过类的全限定名获取Class实例
			Class<?> c = Class.forName(line);
			Class<ServiceEngine> clazz = ServiceEngine.class;
			
			// 判断c是否是clazz类型或者子类类型
			if (clazz.isAssignableFrom(c)) {
				
				// 通过反射创建一个ServiceEngine类型对象
				ServiceEngine serviceEngine = clazz.cast(c.newInstance());
				serviceEngine.execute();
			}
		}
	}
}

示例运行结果如下:

class:com.sunny.demo.AppServiceEngine
AppServiceEngine execute

结束语

ServiceLoader总代码行不是太多,在JDK 1.8.0_111中加上注释一起不到600行,所以研读起来应该比较容易。

本文整理的ServiceLoader内容,其实并不多,首先介绍了一下如何使用ServiceLoader,它能够达到什么效果,然后通过分析源代码的方式,简单介绍了下其实现原理,最后根据ServiceLoader的实现原理,使用简短的代码自定义实现一个类似ServiceLoader的功能。

在ServiceLoader出现后,许多库中都可以看到它的影子,比如Dubbo、JDBC以及APT,其实本文梳理ServiceLoader也是为了后续介绍编译时注解做铺垫。如果使用ServiceLoader需要注意一定要在SPI文件中注册相关实现类,当然了目前也有第三方库可以完成这个功能,比如Google的AutoService注解库,AutoService本身就是利用注解处理器实现的。

评论

您确定要删除吗?删除之后不可恢复