源码分析
java作为一个把interface看得很重要的语言,发展出了各种自动发现机制。比如eureka服务发现,slf4j日志自动发现日志实现,亦或者jdbc定义的接口自动根据URL选择对应的jdbc实现等。自动发现机制将接口和实现很好地隔离开,使得代码高度解耦合,是一种非常好地设计思路。
前面文章中,我们简单了解了java内置的自动发现(ServiceLoader)是怎么使用的:https://www.cnblogs.com/lay2017/p/11194625.html
本文接续使用的内容,将打开ServiceLoader的源代码,阅读一下它的加载逻辑,为此,我们从以下这两行代码开始
ServiceLoader<Speaker> speakers = ServiceLoader.load(Speaker.class); speakers.forEach(Speaker::say);
ServiceLoader.load()加载配置
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); }
继续跟进构造方法,重点落在了reload方法上
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; reload(); }
跟进reload方法,providers缓存了已经加载的实现类,以全路径名作为key。
clear方法在每次load的时候作清空,然后初始化一个Iterator迭代器(Lazy),可以想象后面的代码就是迭代这个迭代器,迭代一个加载一个实现类
private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); }
再进入LazyIterator构造方法看看,单纯的赋值操作
Class<S> service; ClassLoader loader; private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; }
ServiceLoader方法就是单纯地清空原有地缓存,然后初始化一个迭代器而已,比较简单。
迭代LazyIterator
ServiceLoader方法初始化LazyIterator,那么我们看看这个迭代器地迭代过程做了什么。
hasNextService方法
首先看看判断是否有下一个Service的方法
private static final String PREFIX = "META-INF/services/"; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; private boolean hasNextService() { if (nextName != null) { return true; } // 从类路径加载配置文件地URL if (configs == null) { try { // 拼接全路径 META-INF/services/接口类全路径名 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); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } // 迭代URL地址,获取实现类的全路径名 pending = parse(service, configs.nextElement()); } // 将获取到的全路径名作为下一个加载的类全路径名 nextName = pending.next(); return true; }
hasNextService先根据接口类的全路径名获取了所有配置的URL地址
然后迭代一个URL地址,通过parse方法解析出下一个待加载的配置文件中包含的实现类集合。我们跟进parse方法,看看它是怎么把URL地址解析成全路径配置的。
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError{ InputStream in = null; BufferedReader r = null; // 集合包含多个全路径名 ArrayList<String> names = new ArrayList<>(); try { // 文件流 in = u.openStream(); r = new BufferedReader(new InputStreamReader(in, "utf-8")); int lc = 1; // 解析文件流 while ((lc = parseLine(service, u, r, lc, names)) >= 0); } catch (IOException x) { fail(service, "Error reading configuration file", x); } finally { try { if (r != null) r.close(); if (in != null) in.close(); } catch (IOException y) { fail(service, "Error closing configuration file", y); } } // 返回迭代器 return names.iterator(); }
parseLine执行一次,则解析一行配置,也就获得一个实现类的全路径,跟进parseLine看看
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError{ // 读取1行 String ln = r.readLine(); if (ln == null) { // 空文件 return -1; } int ci = ln.indexOf('#'); if (ci >= 0) ln = ln.substring(0, ci); ln = ln.trim(); int n = ln.length(); if (n != 0) { if ((ln.indexOf(' ') >= 0) || (ln.indexOf(' ') >= 0)) fail(service, u, lc, "Illegal configuration-file syntax"); int cp = ln.codePointAt(0); if (!Character.isJavaIdentifierStart(cp)) fail(service, u, lc, "Illegal provider-class name: " + ln); for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { cp = ln.codePointAt(i); if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) fail(service, u, lc, "Illegal provider-class name: " + ln); } // 先从缓存判断是否存在该实现类,再从names中判断是否包含(二者加在一起,防止相同配置文件重复配置,也防止不同配置文件重复配置) if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln); } // 下一行 return lc + 1; }
readline一行一行地读取配置文件的实现类配置,如果没有被加载过,那么就添加为本次待加载地类。
hasNextService方法,其实就是从配置文件中不断地读取配置到集合当中,供nextService使用
nextService加载实现类
hasNextService做了前置工作,获取了全路径名供迭代。而nextService方法将负责加载该类到虚拟机当中。我们打开nextService看看
private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { // 反射加载实现类 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); } throw new Error(); }
方法比较简单,单纯地通过Class.forName加载类,并反射获取实例对象强转了一次,并返回。
总结
ServiceLoader作为JAVA内置地spi机制,使用起来非常地简单。如同前面文章说的,其实就是面向接口 + 配置文件 + 多态,我们再一次见到了java从面向对象不断延伸地东西有多么灵活、干净。