浅谈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本身就是利用注解处理器实现的。