
我的个人小站 http://www.tianxiaobo.com,不定期发文章,欢迎访问
1. 简介 上一篇文章分析了集群容错的第一部分 -- 服务目录 Directory。服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由。上一篇文章关于服务路由相关逻辑没有细致分析,一笔带过了,本篇文章将对此进行详细的分析。首先,先来介绍一下服务目录是什么。服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。Dubbo 目前提供了三种服务路由实现,分别为条件路由 ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter。其中条件路由是我们最常使用的,标签路由暂未在我所分析的 2.6.4 版本中提供,该实现会在 2.7.0 版本中提供。本篇文章将分析条件路由相关源码,脚本路由和标签路由这里就不分析了。下面进入正题。 2. 源码分析 条件路由规则有两个条件组成,分别用于对服务消费者和提供者进行匹配。比如有这样一条规则: host = 10.20.153.10 => host = 10.20.153.11 该条规则表示 IP 为 10.20.153.10 的服务消费者只可调用 IP 为 10.20.153.11 机器上的服务,不可调用其他机器上的服务。条件路由规则的格式如下: [服务消费者匹配条件] => [服务提供者匹配条件] 如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。Dubbo 官方文档对条件路由进行了比较详细的介绍,大家可以参考下,这里就不过多说明了。 条件路由实现类 ConditionRouter 需要对用户配置的路由规则进行解析,得到一系列的条件。然后再根据这些条件对服务进行路由。本章将分两节进行说明,2.1节介绍表达式解析过程。2.2 节介绍服务路由的过程。接下来,我们先从表达式解析过程看起。 2.1 表达式解析 条件路由规则是一条字符串,对于 Dubbo 来说,它并不能直接理解字符串的意思,需要将其解析成内部格式才行。条件表达式的解析过程始于 ConditionRouter 的构造方法,下面一起看一下: public ConditionRouter(URL url) { this.url = url; // 获取 priority 和 force 配置 this.priority = url.getParameter(Constants.PRIORITY_KEY, 0); this.force = url.getParameter(Constants.FORCE_KEY, false); try { // 获取路由规则 String rule = url.getParameterAndDecoded(Constants.RULE_KEY); if (rule == null || rule.trim().length() == 0) { throw new IllegalArgumentException("Illegal route rule!"); } rule = rule.replace("consumer.", "").replace("provider.", ""); // 定位 => 分隔符 int i = rule.indexOf("=>"); // 分别获取服务消费者和提供者匹配规则 String whenRule = i < 0 ? null : rule.substring(0, i).trim(); String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim(); // 解析服务消费者匹配规则 Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<String, MatchPair>() : parseRule(whenRule); // 解析服务提供者匹配规则 Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ? null : parseRule(thenRule); this.whenCondition = when; this.thenCondition = then; } catch (ParseException e) { throw new IllegalStateException(e.getMessage(), e); } } 如上,ConditionRouter 构造方法先是对路由规则做预处理,然后调用 parseRule 方法分别对服务提供者和消费者规则进行解析,最后将解析结果赋值给 whenCondition 和 thenCondition 成员变量。ConditionRouter 构造方法不是很复杂,这里就不多说了。下面我们把重点放在 parseRule 方法上,在详细介绍这个方法之前,我们先来看一个内部类。 private static final class MatchPair { final Set<String> matches = new HashSet<String>(); final Set<String> mismatches = new HashSet<String>(); } MatchPair 内部包含了两个 Set 型的成员变量,分别用于存放匹配和不匹配的条件。这个类两个成员变量会在 parseRule 方法中被用到,下面来看一下。 private static Map<String, MatchPair> parseRule(String rule) throws ParseException { // 定义条件映射集合 Map<String, MatchPair> condition = new HashMap<String, MatchPair>(); if (StringUtils.isBlank(rule)) { return condition; } MatchPair pair = null; Set<String> values = null; // 通过正则表达式匹配路由规则,ROUTE_PATTERN = ([&!=,]*)\s*([^&!=,\s]+) // 这个表达式看起来不是很好理解,第一个括号内的表达式用于匹配"&", "!", "=" 和 "," 等符号。 // 第二括号内的用于匹配英文字母,数字等字符。举个例子说明一下: // host = 2.2.2.2 & host != 1.1.1.1 & method = hello // 匹配结果如下: // 括号一 括号二 // 1. null host // 2. = 2.2.2.2 // 3. & host // 4. != 1.1.1.1 // 5. & method // 6. = hello final Matcher matcher = ROUTE_PATTERN.matcher(rule); while (matcher.find()) { // 获取括号一内的匹配结果 String separator = matcher.group(1); // 获取括号二内的匹配结果 String content = matcher.group(2); // 分隔符为空,表示匹配的是表达式的开始部分 if (separator == null || separator.length() == 0) { // 创建 MatchPair 对象 pair = new MatchPair(); // 存储 <匹配项, MatchPair> 键值对,比如 <host, MatchPair> condition.put(content, pair); } // 如果分隔符为 &,表明接下来也是一个条件 else if ("&".equals(separator)) { // 尝试从 condition 获取 MatchPair if (condition.get(content) == null) { // 未获取到 MatchPair,重新创建一个,并放入 condition 中 pair = new MatchPair(); condition.put(content, pair); } else { pair = condition.get(content); } } // 分隔符为 = else if ("=".equals(separator)) { if (pair == null) throw new ParseException("Illegal route rule ..."); values = pair.matches; // 将 content 存入到 MatchPair 的 matches 集合中 values.add(content); } // 分隔符为 != else if ("!=".equals(separator)) { if (pair == null) throw new ParseException("Illegal route rule ..."); values = pair.mismatches; // 将 content 存入到 MatchPair 的 mismatches 集合中 values.add(content); } // 分隔符为 , else if (",".equals(separator)) { if (values == null || values.isEmpty()) throw new ParseException("Illegal route rule ..."); // 将 content 存入到上一步获取到的 values 中,可能是 matches,也可能是 mismatches values.add(content); } else { throw new ParseException("Illegal route rule ..."); } } return condition; } 以上就是路由规则的解析逻辑,该逻辑由正则表达式 + 一个 while 循环 + 数个条件分支组成。下面使用一个示例对解析逻辑进行演绎。示例为 host = 2.2.2.2 & host != 1.1.1.1 & method = hello。正则解析结果如下: 括号一 括号二 1. null host 2. = 2.2.2.2 3. & host 4. != 1.1.1.1 5. & method 6. = hello 现在线程进入 while 循环: 第一次循环:分隔符 separator = null,content = "host"。此时创建 MatchPair 对象,并存入到 condition 中,condition = {"host": MatchPair@123} 第二次循环:分隔符 separator = "=",content = "2.2.2.2",pair = MatchPair@123。此时将 2.2.2.2 放入到 MatchPair@123 对象的 matches 集合中。 第三次循环:分隔符 separator = "&",content = "host"。host 已存在于 condition 中,因此 pair = MatchPair@123。 第四次循环:分隔符 separator = "!=",content = "1.1.1.1",pair = MatchPair@123。此时将 1.1.1.1 放入到 MatchPair@123 对象的 mismatches 集合中。 第五次循环:分隔符 separator = "&",content = "method"。condition.get("method") = null,因此新建一个 MatchPair 对象,并放入到 condition 中。此时 condition = {"host": MatchPair@123, "method": MatchPair@ 456} 第六次循环:分隔符 separator = "=",content = "2.2.2.2",pair = MatchPair@456。此时将 hello 放入到 MatchPair@456 对象的 matches 集合中。 循环结束,此时 condition 的内容如下: { "host": { "matches": ["2.2.2.2"], "mismatches": ["1.1.1.1"] }, "method": { "matches": ["hello"], "mismatches": [] } } 路由规则的解析过程稍微有点复杂,大家可通过 ConditionRouter 的测试类对该逻辑进行测试。并且找一个表达式,对照上面的代码走一遍,加深理解。关于路由规则的解析过程就先到这,我们继续往下看。 2.2 服务路由 服务路由的入口方法是 ConditionRouter 的 router 方法,该方法定义在 Router 接口中。实现代码如下: public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException { if (invokers == null || invokers.isEmpty()) { return invokers; } try { // 先对服务消费者条件进行匹配,如果匹配失败,表明当前消费者 url 不符合匹配规则, // 无需进行后续匹配,直接返回 Invoker 列表即可。比如下面的规则: // host = 10.20.153.10 => host = 10.0.0.10 // 这条路由规则希望 IP 为 10.20.153.10 的服务消费者调用 IP 为 10.0.0.10 机器上的服务。 // 当消费者 ip 为 10.20.153.11 时,matchWhen 返回 false,表明当前这条路由规则不适用于 // 当前的服务消费者,此时无需再进行后续匹配,直接返回即可。 if (!matchWhen(url, invocation)) { return invokers; } List<Invoker<T>> result = new ArrayList<Invoker<T>>(); // 服务提供者匹配条件未配置,表明对指定的服务消费者禁用服务,也就是服务消费者在黑名单中 if (thenCondition == null) { logger.warn("The current consumer in the service blacklist..."); return result; } // 这里可以简单的把 Invoker 理解为服务提供者,现在使用服务消费者匹配规则对 // Invoker 列表进行匹配 for (Invoker<T> invoker : invokers) { // 匹配成功,表明当前 Invoker 符合服务提供者匹配规则。 // 此时将 Invoker 添加到 result 列表中 if (matchThen(invoker.getUrl(), url)) { result.add(invoker); } } // 返回匹配结果,如果 result 为空列表,且 force = true,表示强制返回空列表, // 否则路由结果为空的路由规则将自动失效 if (!result.isEmpty()) { return result; } else if (force) { logger.warn("The route result is empty and force execute ..."); return result; } } catch (Throwable t) { logger.error("Failed to execute condition router rule: ..."); } // 原样返回,此时 force = false,表示该条路由规则失效 return invokers; } router 方法先是调用 matchWhen 对服务消费者进行匹配,如果匹配失败,直接返回 Invoker 列表。如果匹配成功,再对服务提供者进行匹配,匹配逻辑封装在了 matchThen 方法中。下面来看一下这两个方法的逻辑: boolean matchWhen(URL url, Invocation invocation) { // 服务消费者条件为 null 或空,均返回 true,比如: // => host != 172.22.3.91 // 表示所有的服务消费者都不得调用 IP 为 172.22.3.91 的机器上的服务 return whenCondition == null || whenCondition.isEmpty() || matchCondition(whenCondition, url, null, invocation); // 进行条件匹配 } private boolean matchThen(URL url, URL param) { // 服务提供者条件为 null 或空,表示禁用服务 return !(thenCondition == null || thenCondition.isEmpty()) && matchCondition(thenCondition, url, param, null); // 进行条件匹配 } 这两个方法长的有点像,不过逻辑上还是有差别的,大家注意看。这两个方法均调用了 matchCondition 方法,不过它们所传入的参数是不同的,这个需要特别注意。不然后面的逻辑不好弄懂。下面我们对这几个参数进行溯源。matchWhen 方法向 matchCondition 方法传入的参数为 [whenCondition, url, null, invocation],第一个参数 whenCondition 为服务消费者匹配条件,这个前面分析过。第二个参数 url 源自 route 方法的参数列表,该参数由外部类调用 route 方法时传入。有代码为证,如下: private List<Invoker<T>> route(List<Invoker<T>> invokers, String method) { Invocation invocation = new RpcInvocation(method, new Class<?>[0], new Object[0]); List<Router> routers = getRouters(); if (routers != null) { for (Router router : routers) { if (router.getUrl() != null) { // 注意第二个参数 invokers = router.route(invokers, getConsumerUrl(), invocation); } } } return invokers; } 上面这段代码来自 RegistryDirectory,第二个参数表示的是服务消费者 url。matchCondition 的 invocation 参数也是从这里传入的。 接下来再来看看 matchThen 向 matchCondition 方法传入的参数 [thenCondition, url, param, null]。第一个参数不用解释了。第二个和第三个参数来自 matchThen 方法的参数列表,这两个参数分别为服务提供者 url 和服务消费者 url。搞清楚这些参数来源后,接下俩就可以分析 matchCondition 了。 private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) { // 将服务提供者或消费者 url 转成 Map Map<String, String> sample = url.toMap(); boolean result = false; // 遍历 condition 列表 for (Map.Entry<String, MatchPair> matchPair : condition.entrySet()) { // 获取匹配项名称,比如 host、method 等 String key = matchPair.getKey(); String sampleValue; // 如果 invocation 不为空,且 key 为 mehtod(s),表示进行方法匹配 if (invocation != null && (Constants.METHOD_KEY.equals(key) || Constants.METHODS_KEY.equals(key))) { // 从 invocation 获取调用方法名称 sampleValue = invocation.getMethodName(); } else { // 从服务提供者或消费者 url 中获取指定字段值,比如 host、application 等 sampleValue = sample.get(key); if (sampleValue == null) { // 尝试通过 default.xxx 获取相应的值 sampleValue = sample.get(Constants.DEFAULT_KEY_PREFIX + key); } } // -------------------- 分割线 -------------------- // if (sampleValue != null) { // 调用 MatchPair 的 isMatch 方法进行匹配 if (!matchPair.getValue().isMatch(sampleValue, param)) { // 只要有一个规则匹配失败,立即返回 false 结束方法逻辑 return false; } else { result = true; } } else { // sampleValue 为空,表明服务提供者或消费者 url 中不包含相关字段。此时如果 // MatchPair 的 matches 不为空,表示匹配失败,返回 false。比如我们有这样 // 一条匹配条件 loadbalance = random,假设 url 中并不包含 loadbalance 参数, // 此时 sampleValue = null。既然路由规则里限制了 loadbalance = random, // 但 sampleValue = null,明显不符合规则,因此返回 false if (!matchPair.getValue().matches.isEmpty()) { return false; } else { result = true; } } } return result; } 如上,matchCondition 方法看起来有点复杂,这里简单缕缕。分割线以上的代码实际上主要是用于获取 sampleValue 的值,分割线以下才是进行条件匹配。条件匹配调用的逻辑封装在 isMatch 中,代码如下: private boolean isMatch(String value, URL param) { // 情况一:matches 非空,mismatches 为空 if (!matches.isEmpty() && mismatches.isEmpty()) { // 遍历 matches 集合,检测入参 value 是否能被 matches 集合元素匹配到。 // 举个例子,如果 value = 10.20.153.11,matches = [10.20.153.*], // 此时 isMatchGlobPattern 方法返回 true for (String match : matches) { if (UrlUtils.isMatchGlobPattern(match, value, param)) { return true; } } // 如果所有匹配项都无法匹配到入参,则返回 false return false; } // 情况二:matches 为空,mismatches 非空 if (!mismatches.isEmpty() && matches.isEmpty()) { for (String mismatch : mismatches) { // 只要入参被 mismatches 集合中的任意一个元素匹配到,就返回 false if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) { return false; } } // mismatches 集合中所有元素都无法匹配到入参,此时返回 true return true; } // 情况三:matches 非空,mismatches 非空 if (!matches.isEmpty() && !mismatches.isEmpty()) { // matches 和 mismatches 均为非空,此时优先使用 mismatches 集合元素对入参进行匹配。 // 只要 mismatches 集合中任意一个元素与入参匹配成功,就立即返回 false,结束方法逻辑 for (String mismatch : mismatches) { if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) { return false; } } // mismatches 集合元素无法匹配到入参,此时使用 matches 继续匹配 for (String match : matches) { // 只要 matches 集合中任意一个元素与入参匹配成功,就立即返回 true if (UrlUtils.isMatchGlobPattern(match, value, param)) { return true; } } return false; } // 情况四:matches 和 mismatches 均为空,此时返回 false return false; } isMatch 方法逻辑比较清晰,由三个条件分支组成,用于处理四种情况。这里对四种情况下的匹配逻辑进行简单的总结,如下: 条件 动作 情况一 matches 非空,mismatches 为空 遍历 matches 集合元素,并与入参进行匹配。只要有一个元素成功匹配入参,即可返回 true。若全部失配,则返回 false。 情况二 matches 为空,mismatches 非空 遍历 mismatches 集合元素,并与入参进行匹配。只要有一个元素成功匹配入参,立即 false。若全部失配,则返回 true。 情况三 matches 非空,mismatches 非空 优先使用 mismatches 集合元素对入参进行匹配,只要任一元素与入参匹配成功,就立即返回 false,结束方法逻辑。否则再使用 matches 中的集合元素进行匹配,只要有任意一个元素匹配成功,即可返回 true。若全部失配,则返回 false 情况四 matches 为空,mismatches 为空 直接返回 false isMatch 方法逻辑不是很难理解,大家自己再看看。下面继续分析 isMatchGlobPattern 方法。 public static boolean isMatchGlobPattern(String pattern, String value, URL param) { if (param != null && pattern.startsWith("$")) { // 引用服务消费者参数,param 参数为服务消费者 url pattern = param.getRawParameter(pattern.substring(1)); } // 调用重载方法继续比较 return isMatchGlobPattern(pattern, value); } public static boolean isMatchGlobPattern(String pattern, String value) { // 对 * 通配符提供支持 if ("*".equals(pattern)) // 匹配规则为通配符 *,直接返回 true 即可 return true; if ((pattern == null || pattern.length() == 0) && (value == null || value.length() == 0)) // pattern 和 value 均为空,此时可认为两者相等,返回 true return true; if ((pattern == null || pattern.length() == 0) || (value == null || value.length() == 0)) // pattern 和 value 其中有一个为空,两者不相等,返回 false return false; // 查找 * 通配符位置 int i = pattern.lastIndexOf('*'); if (i == -1) { // 匹配规则中不包含通配符,此时直接比较 value 和 pattern 是否相等即可,并返回比较结果 return value.equals(pattern); } // 通配符 "*" 在匹配规则尾部,比如 10.0.21.* else if (i == pattern.length() - 1) { // 检测 value 是否以不含通配符的匹配规则开头,并返回结果。比如: // pattern = 10.0.21.*,value = 10.0.21.12,此时返回 true return value.startsWith(pattern.substring(0, i)); } // 通配符 "*" 在匹配规则头部 else if (i == 0) { // 检测 value 是否以不含通配符的匹配规则结尾,并返回结果 return value.endsWith(pattern.substring(i + 1)); } // 通配符 "*" 在匹配规则中间位置 else { // 通过通配符将 pattern 分成两半,得到 prefix 和 suffix String prefix = pattern.substring(0, i); String suffix = pattern.substring(i + 1); // 检测 value 是否以 prefix 变量开头,且以 suffix 变量结尾,并返回结果 return value.startsWith(prefix) && value.endsWith(suffix); } } 以上就是 isMatchGlobPattern 两个重载方法的全部逻辑,这两个方法分别对普通的匹配,以及”引用消费者参数“和通配符匹配做了支持。这两个方法的逻辑并不是很复杂,而且我也在代码上进行了比较详细的注释,大家自己看看吧,就不多说了。 3. 总结 本篇文章对条件路由的表达式解析和服务路由过程进行了较为细致的分析。总的来说,条件路由的代码还是有一些复杂的,需要耐下心来看。在阅读条件路由代码的过程中,要多调试。一般的框架都会有单元测试,Dubbo 也不例外,因此大家可以直接通过 ConditionRouterTest 对条件路由进行调试,无需自己手写测试用例。 好了,关于条件路由就先分析到这,谢谢阅读。 附录:Dubbo 源码分析系列文章 时间 文章 2018-10-01 Dubbo 源码分析 - SPI 机制 2018-10-13 Dubbo 源码分析 - 自适应拓展原理 2018-10-31 Dubbo 源码分析 - 服务导出 2018-11-12 Dubbo 源码分析 - 服务引用 2018-11-17 Dubbo 源码分析 - 集群容错之 Directory 2018-11-20 Dubbo 源码分析 - 集群容错之 Router 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处作者:田小波本文同步发布在我的个人博客:http://www.tianxiaobo.com 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 前面文章分析了服务的导出与引用过程,从本篇文章开始,我将开始分析 Dubbo 集群容错方面的源码。这部分源码包含四个部分,分别是服务目录 Directory、服务路由 Router、集群 Cluster 和负载均衡 LoadBalance。这几个部分的源码逻辑比较独立,我会分四篇文章进行分析。本篇文章作为集群容错的开篇文章,将和大家一起分析服务目录相关的源码。在进行深入分析之前,我们先来了解一下服务目录是什么。服务目录中存储了一些和服务提供者有关的信息,通过服务目录,服务消费者可获取到服务提供者的信息,比如 ip、端口、服务协议等。通过这些信息,服务消费者就可通过 Netty 等客户端进行远程调用。在一个服务集群中,服务提供者数量并不是一成不变的,如果集群中新增了一台机器,相应地在服务目录中就要新增一条服务提供者记录。或者,如果服务提供者的配置修改了,服务目录中的记录也要做相应的更新。如果这样说,服务目录和注册中心的功能不就雷同了吗。确实如此,这里这么说是为了方便大家理解。实际上服务目录在获取注册中心的服务配置信息后,会为每条配置信息生成一个 Invoker 对象,并把这个 Invoker 对象存储起来,这个 Invoker 才是服务目录最终持有的对象。Invoker 有什么用呢?看名字就知道了,这是一个具有远程调用功能的对象。讲到这大家应该知道了什么是服务目录了,它可以看做是 Invoker 集合,且这个集合中的元素会随注册中心的变化而进行动态调整。 好了,关于服务目录这里就先介绍这些,大家先有个大致印象即可。接下来我们通过继承体系图来了解一下服务目录的家族成员都有哪些。 2. 继承体系 服务目录目前内置的实现有两个,分别为 StaticDirectory 和 RegistryDirectory,它们均是 AbstractDirectory 的子类。AbstractDirectory 实现了 Directory 接口,这个接口包含了一个重要的方法定义,即 list(Invocation),用于列举 Invoker。下面我们来看一下他们的继承体系图。 如上,Directory 继承自 Node 接口,Node 这个接口继承者比较多,像 Registry、Monitor、Invoker 等继承了这个接口。这个接口包含了一个获取配置信息的方法 getUrl,实现该接口的类可以向外提供配置信息。另外,大家注意看 RegistryDirectory 实现了 NotifyListener 接口,当注册中心节点信息发生变化后,RegistryDirectory 可以通过此接口方法得到变更信息,并根据变更信息动态调整内部 Invoker 列表。 现在大家对服务目录的继承体系应该比较清楚了,下面我们深入到源码中,探索服务目录是如何实现的。 3. 源码分析 本章我将分析 AbstractDirectory 和它两个子类的源码。这里之所以要分析 AbstractDirectory,而不是直接分析子类是有一定原因的。AbstractDirectory 封装了 Invoker 列举流程,具体的列举逻辑则由子类实现,这是典型的模板模式。所以,接下来我们先来看一下 AbstractDirectory 的源码。 public List<Invoker<T>> list(Invocation invocation) throws RpcException { if (destroyed) { throw new RpcException("Directory already destroyed..."); } // 调用 doList 方法列举 Invoker,这里的 doList 是模板方法,由子类实现 List<Invoker<T>> invokers = doList(invocation); // 获取路由器 List<Router> localRouters = this.routers; if (localRouters != null && !localRouters.isEmpty()) { for (Router router : localRouters) { try { // 获取 runtime 参数,并根据参数决定是否进行路由 if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) { // 进行服务路由 invokers = router.route(invokers, getConsumerUrl(), invocation); } } catch (Throwable t) { logger.error("Failed to execute router: ..."); } } } return invokers; } // 模板方法,由子类实现 protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException; 上面就是 AbstractDirectory 的 list 方法源码,这个方法封装了 Invoker 的列举过程。如下: 调用 doList 获取 Invoker 列表 根据 Router 的 getUrl 返回值为空与否,以及 runtime 参数决定是否进行服务路由 以上步骤中,doList 是模板方法,需由子类实现。Router 的 runtime 参数这里简单说明一下,这个参数决定了是否在每次调用服务时都执行路由规则。如果 runtime 为 true,那么每次调用服务前,都需要进行服务路由。这个对性能造成影响,慎重配置。关于该参数更详细的说明,请参考官方文档。 介绍完 AbstractDirectory,接下来我们开始分析子类的源码。 3.1 StaticDirectory StaticDirectory 即静态服务目录,顾名思义,它内部存放的 Invoker 是不会变动的。所以,理论上它和不可变 List 的功能很相似。下面我们来看一下这个类的实现。 public class StaticDirectory<T> extends AbstractDirectory<T> { // Invoker 列表 private final List<Invoker<T>> invokers; // 省略构造方法 @Override public Class<T> getInterface() { // 获取接口类 return invokers.get(0).getInterface(); } // 检测服务目录是否可用 @Override public boolean isAvailable() { if (isDestroyed()) { return false; } for (Invoker<T> invoker : invokers) { if (invoker.isAvailable()) { // 只要有一个 Invoker 是可用的,就任务当前目录是可用的 return true; } } return false; } @Override public void destroy() { if (isDestroyed()) { return; } // 调用父类销毁逻辑 super.destroy(); // 遍历 Invoker 列表,并执行相应的销毁逻辑 for (Invoker<T> invoker : invokers) { invoker.destroy(); } invokers.clear(); } @Override protected List<Invoker<T>> doList(Invocation invocation) throws RpcException { // 列举 Inovker,也就是直接返回 invokers 成员变量 return invokers; } } 以上就是 StaticDirectory 的代码逻辑,很简单,大家都能看懂,我就不多说了。下面来看看 RegistryDirectory,这个类的逻辑比较复杂。 3.2 RegistryDirectory RegistryDirectory 是一种动态服务目录,它实现了 NotifyListener 接口。当注册中心服务配置发生变化后,RegistryDirectory 可收到与当前服务相关的变化。收到变更通知后,RegistryDirectory 可根据配置变更信息刷新 Invoker 列表。RegistryDirectory 中有几个比较重要的逻辑,第一是 Invoker 的列举逻辑,第二是接受服务配置变更的逻辑,第三是 Invoker 的刷新逻辑。接下来,我将按顺序对这三块逻辑。 3.2.1 列举 Invoker Invoker 列举逻辑封装在 doList 方法中,这是个模板方法,前面已经介绍过了。那这里就不过多啰嗦了,我们直入主题吧。 public List<Invoker<T>> doList(Invocation invocation) { if (forbidden) { // 服务提供者关闭或禁用了服务,此时抛出 No provider 异常 throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "No provider available from registry ...); } List<Invoker<T>> invokers = null; // 获取 Invoker 本地缓存 Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) { // 获取方法名和参数列表 String methodName = RpcUtils.getMethodName(invocation); Object[] args = RpcUtils.getArguments(invocation); // 检测参数列表的第一个参数是否为 String 或 enum 类型 if (args != null && args.length > 0 && args[0] != null && (args[0] instanceof String || args[0].getClass().isEnum())) { // 通过 方法名 + 第一个参数名称 查询 Invoker 列表,具体的使用场景暂时没想到 invokers = localMethodInvokerMap.get(methodName + "." + args[0]); } if (invokers == null) { // 通过方法名获取 Invoker 列表 invokers = localMethodInvokerMap.get(methodName); } if (invokers == null) { // 通过星号 * 获取 Invoker 列表 invokers = localMethodInvokerMap.get(Constants.ANY_VALUE); } if (invokers == null) { Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator(); if (iterator.hasNext()) { // 通过迭代器获取 Invoker 列表 invokers = iterator.next(); } } } // 返回 Invoker 列表 return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers; } 以上代码进行多次尝试,以期从 localMethodInvokerMap 中获取到 Invoker 列表。一般情况下,普通的调用可通过方法名获取到对应的 Invoker 列表,泛化调用可通过 获取到 Invoker 列表。按现有的逻辑,不管什么情况下, 到 Invoker 列表的映射关系 <*, invokers> 总是存在的,也就意味着 localMethodInvokerMap.get(Constants.ANY_VALUE) 总是有值返回。除非这个值是 null,才会通过通过迭代器获取 Invoker 列表。至于什么情况下为空,我暂时未完全搞清楚,我猜测是被路由规则(用户可基于 Router 接口实现自定义路由器)处理后,可能会得到一个 null。目前仅是猜测,未做验证。 本节的逻辑主要是从 localMethodInvokerMap 中获取 Invoker,localMethodInvokerMap 源自 RegistryDirectory 类的成员变量 methodInvokerMap。doList 方法可以看做是对 methodInvokerMap 变量的读操作,至于对 methodInvokerMap 变量的写操作,这个将在后续进行分析。 3.2.2 接收服务变更通知 RegistryDirectory 是一个动态服务目录,它需要接受注册中心配置进行动态调整。因此 RegistryDirectory 实现了 NotifyListener 接口,通过这个接口获取注册中心变更通知。下面我们来看一下具体的逻辑。 public synchronized void notify(List<URL> urls) { // 定义三个集合,分别用于存放服务提供者 url,路由 url,配置器 url List<URL> invokerUrls = new ArrayList<URL>(); List<URL> routerUrls = new ArrayList<URL>(); List<URL> configuratorUrls = new ArrayList<URL>(); for (URL url : urls) { String protocol = url.getProtocol(); // 获取 category 参数 String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY); // 根据 category 参数将 url 分别放到不同的列表中 if (Constants.ROUTERS_CATEGORY.equals(category) || Constants.ROUTE_PROTOCOL.equals(protocol)) { // 添加路由器 url routerUrls.add(url); } else if (Constants.CONFIGURATORS_CATEGORY.equals(category) || Constants.OVERRIDE_PROTOCOL.equals(protocol)) { // 添加配置器 url configuratorUrls.add(url); } else if (Constants.PROVIDERS_CATEGORY.equals(category)) { // 添加服务提供者 url invokerUrls.add(url); } else { // 忽略不支持的 category logger.warn("Unsupported category ..."); } } if (configuratorUrls != null && !configuratorUrls.isEmpty()) { // 将 url 转成 Configurator this.configurators = toConfigurators(configuratorUrls); } if (routerUrls != null && !routerUrls.isEmpty()) { // 将 url 转成 Router List<Router> routers = toRouters(routerUrls); if (routers != null) { setRouters(routers); } } List<Configurator> localConfigurators = this.configurators; this.overrideDirectoryUrl = directoryUrl; if (localConfigurators != null && !localConfigurators.isEmpty()) { for (Configurator configurator : localConfigurators) { // 配置 overrideDirectoryUrl this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl); } } // 刷新 Invoker 列表 refreshInvoker(invokerUrls); } 如上,notify 方法首先是根据 url 的 category 参数对 url 进行分门别类存储,然后通过 toRouters 和 toConfigurators 将 url 列表转成 Router 和 Configurator 列表。最后调用 refreshInvoker 方法刷新 Invoker 列表。这里的 toRouters 和 toConfigurators 方法逻辑不复杂,大家自行分析。接下来,我们把重点放在 refreshInvoker 方法上。 3.2.3 刷新 Invoker 列表 接着上一节继续分析,refreshInvoker 方法是保证 RegistryDirectory 随注册中心变化而变化的关键所在。这一块逻辑比较多,接下来一一进行分析。 private void refreshInvoker(List<URL> invokerUrls) { // invokerUrls 仅有一个元素,且 url 协议头为 empty,此时表示禁用所有服务 if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) { // 设置 forbidden 为 true this.forbidden = true; this.methodInvokerMap = null; // 销毁所有 Invoker destroyAllInvokers(); } else { this.forbidden = false; Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) { // 添加缓存 url 到 invokerUrls 中 invokerUrls.addAll(this.cachedInvokerUrls); } else { this.cachedInvokerUrls = new HashSet<URL>(); // 缓存 invokerUrls this.cachedInvokerUrls.addAll(invokerUrls); } if (invokerUrls.isEmpty()) { return; } // 将 url 转成 Invoker Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls); // 将 newUrlInvokerMap 转成方法名到 Invoker 列表的映射 Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // 转换出错,直接打印异常,并返回 if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) { logger.error(new IllegalStateException("urls to invokers error ...")); return; } // 合并多个组的 Invoker this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap; // 保存为本地缓存 this.urlInvokerMap = newUrlInvokerMap; try { // 销毁无用 Invoker destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); } catch (Exception e) { logger.warn("destroyUnusedInvokers error. ", e); } } } 上面方法的代码不是很多,但是逻辑却不少。首先时根据入参 invokerUrls 的数量和协议头判断是否禁用所有的服务,如果禁用,则将 forbidden 设为 true,并销毁所有的 Invoker。若不禁用,则将 url 转成 Invoker,得到 <url, Invoker> 的映射关系。然后进一步进行转换,得到 <methodName, Invoker 列表>。之后进行多组 Invoker 合并操作,并将合并结果赋值给 methodInvokerMap。methodInvokerMap 变量在 doList 方法中会被用到,doList 会对该变量进行读操作,在这里是写操作。当新的 Invoker 列表生成后,还要一个重要的工作要做,就是销毁无用的 Invoker,避免服务消费者调用已下线的服务的服务。 接下里,我将对上面涉及到的调用进行分析。按照顺序,这里先来分析 url 到 Invoker 的转换过程。 private Map<String, Invoker<T>> toInvokers(List<URL> urls) { Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>(); if (urls == null || urls.isEmpty()) { return newUrlInvokerMap; } Set<String> keys = new HashSet<String>(); // 获取服务消费端配置的协议 String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY); for (URL providerUrl : urls) { if (queryProtocols != null && queryProtocols.length() > 0) { boolean accept = false; String[] acceptProtocols = queryProtocols.split(","); // 检测服务提供者协议是否被服务消费者所支持 for (String acceptProtocol : acceptProtocols) { if (providerUrl.getProtocol().equals(acceptProtocol)) { accept = true; break; } } if (!accept) { // 若服务消费者协议头不被消费者所支持,则忽略当前 providerUrl continue; } } // 忽略 empty 协议 if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) { continue; } // 通过 SPI 检测服务端协议是否被消费端支持 if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) { logger.error(new IllegalStateException("Unsupported protocol...")); continue; } // 合并 url URL url = mergeUrl(providerUrl); String key = url.toFullString(); if (keys.contains(key)) { // 忽略重复 url continue; } keys.add(key); // 本地 Invoker 缓存列表 Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key); // 缓存未命中 if (invoker == null) { try { boolean enabled = true; if (url.hasParameter(Constants.DISABLED_KEY)) { // 获取 disable 配置,并修改 enable 变量 enabled = !url.getParameter(Constants.DISABLED_KEY, false); } else { enabled = url.getParameter(Constants.ENABLED_KEY, true); } if (enabled) { // 调用 refer 获取 Invoker invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl); } } catch (Throwable t) { logger.error("Failed to refer invoker for interface..."); } if (invoker != null) { // 缓存 Invoker 实例 newUrlInvokerMap.put(key, invoker); } } else { // 缓存命中,将 invoker 存储到 newUrlInvokerMap 中 newUrlInvokerMap.put(key, invoker); } } keys.clear(); return newUrlInvokerMap; } toInvokers 方法一开始会对服务提供者 url 进行检测,若服务消费端的配置不支持服务端的协议,或服务端 url 协议头为 empty 时,toInvokers 均会忽略服务提供方 url。必要的检测做完后,紧接着是合并 url,然后访问缓存,尝试获取与 url 对应的 invoker。如果缓存命中,直接将 Invoker 存入 newUrlInvokerMap 中即可。如果未命中,则需要新建 Invoker。Invoker 是通过 Protocol 的 refer 方法创建的,这个我在上一篇文章中已经分析过了,这里就不赘述了。 toInvokers 方法返回的是 <url, Invoker> 映射关系表,接下来还要对这个结果进行进一步处理,得到方法名到 Invoker 列表的映射关系。这个过程由 toMethodInvokers 方法完成,如下: private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) { // 方法名 -> Invoker 列表 Map<String, List<Invoker<T>>> newMethodInvokerMap = new HashMap<String, List<Invoker<T>>>(); List<Invoker<T>> invokersList = new ArrayList<Invoker<T>>(); if (invokersMap != null && invokersMap.size() > 0) { for (Invoker<T> invoker : invokersMap.values()) { // 获取 methods 参数 String parameter = invoker.getUrl().getParameter(Constants.METHODS_KEY); if (parameter != null && parameter.length() > 0) { // 切分 methods 参数值,得到方法名数组 String[] methods = Constants.COMMA_SPLIT_PATTERN.split(parameter); if (methods != null && methods.length > 0) { for (String method : methods) { // 方法名不为 * if (method != null && method.length() > 0 && !Constants.ANY_VALUE.equals(method)) { // 根据方法名获取 Invoker 列表 List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method); if (methodInvokers == null) { methodInvokers = new ArrayList<Invoker<T>>(); newMethodInvokerMap.put(method, methodInvokers); } // 存储 Invoker 到列表中 methodInvokers.add(invoker); } } } } invokersList.add(invoker); } } // 进行服务级别路由,参考:https://github.com/apache/incubator-dubbo/pull/749 List<Invoker<T>> newInvokersList = route(invokersList, null); // 存储 <*, newInvokersList> 映射关系 newMethodInvokerMap.put(Constants.ANY_VALUE, newInvokersList); if (serviceMethods != null && serviceMethods.length > 0) { for (String method : serviceMethods) { List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method); if (methodInvokers == null || methodInvokers.isEmpty()) { methodInvokers = newInvokersList; } // 进行方法级别路由 newMethodInvokerMap.put(method, route(methodInvokers, method)); } } // 排序,转成不可变列表 for (String method : new HashSet<String>(newMethodInvokerMap.keySet())) { List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method); Collections.sort(methodInvokers, InvokerComparator.getComparator()); newMethodInvokerMap.put(method, Collections.unmodifiableList(methodInvokers)); } return Collections.unmodifiableMap(newMethodInvokerMap); } 上面方法主要做了三件事情, 第一是对入参进行遍历,然后获取 methods 参数,并切分成数组。随后以方法名为键,Invoker 列表为值,将映射关系存储到 newMethodInvokerMap 中。第二是分别基于类和方法对 Invoker 列表进行路由操作。第三是对 Invoker 列表进行排序,并转成不可变列表。关于 toMethodInvokers 方法就先分析到这,我们继续向下分析,这次要分析的多组服务的合并逻辑。 private Map<String, List<Invoker<T>>> toMergeMethodInvokerMap(Map<String, List<Invoker<T>>> methodMap) { Map<String, List<Invoker<T>>> result = new HashMap<String, List<Invoker<T>>>(); // 遍历入参 for (Map.Entry<String, List<Invoker<T>>> entry : methodMap.entrySet()) { String method = entry.getKey(); List<Invoker<T>> invokers = entry.getValue(); // group -> Invoker 列表 Map<String, List<Invoker<T>>> groupMap = new HashMap<String, List<Invoker<T>>>(); // 遍历 Invoker 列表 for (Invoker<T> invoker : invokers) { // 获取分组配置 String group = invoker.getUrl().getParameter(Constants.GROUP_KEY, ""); List<Invoker<T>> groupInvokers = groupMap.get(group); if (groupInvokers == null) { groupInvokers = new ArrayList<Invoker<T>>(); // 缓存 <group, List<Invoker>> 到 groupMap 中 groupMap.put(group, groupInvokers); } // 存储 invoker 到 groupInvokers groupInvokers.add(invoker); } if (groupMap.size() == 1) { // 如果 groupMap 中仅包含一组键值对,此时直接取出该键值对的值即可 result.put(method, groupMap.values().iterator().next()); // groupMap 中包含多组键值对,比如: // { // "dubbo": [invoker1, invoker2, invoker3, ...], // "hello": [invoker4, invoker5, invoker6, ...] // } } else if (groupMap.size() > 1) { List<Invoker<T>> groupInvokers = new ArrayList<Invoker<T>>(); for (List<Invoker<T>> groupList : groupMap.values()) { // 通过集群类合并每个分组对应的 Invoker 列表 groupInvokers.add(cluster.join(new StaticDirectory<T>(groupList))); } // 缓存结果 result.put(method, groupInvokers); } else { result.put(method, invokers); } } return result; } 上面方法首先是生成 group 到 Invoker 类比的映射关系表,若关系表中的映射关系数量大于1,表示有多组服务。此时通过集群类合并每组 Invoker,并将合并结果存储到 groupInvokers 中。之后将方法名与 groupInvokers 存到到 result 中,并返回,整个逻辑结束。 接下来我们再来看一下 Invoker 列表刷新逻辑的最后一个动作 -- 删除无用 Invoker。如下: private void destroyUnusedInvokers(Map<String, Invoker<T>> oldUrlInvokerMap, Map<String, Invoker<T>> newUrlInvokerMap) { if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) { destroyAllInvokers(); return; } List<String> deleted = null; if (oldUrlInvokerMap != null) { // 获取新生成的 Invoker 列表 Collection<Invoker<T>> newInvokers = newUrlInvokerMap.values(); // 遍历老的 <url, Invoker> 映射表 for (Map.Entry<String, Invoker<T>> entry : oldUrlInvokerMap.entrySet()) { // 检测 newInvokers 中是否包含老的 Invoker if (!newInvokers.contains(entry.getValue())) { if (deleted == null) { deleted = new ArrayList<String>(); } // 若不包含,则将老的 Invoker 对应的 url 存入 deleted 列表中 deleted.add(entry.getKey()); } } } if (deleted != null) { // 遍历 deleted 集合,并到老的 <url, Invoker> 映射关系表查出 Invoker,销毁之 for (String url : deleted) { if (url != null) { // 从 oldUrlInvokerMap 中移除 url 对应的 Invoker Invoker<T> invoker = oldUrlInvokerMap.remove(url); if (invoker != null) { try { // 销毁 Invoker invoker.destroy(); } catch (Exception e) { logger.warn("destroy invoker..."); } } } } } } destroyUnusedInvokers 方法的主要逻辑是通过 newUrlInvokerMap 找出待删除 Invoker 对应的 url,并将 url 存入到 deleted 列表中。然后再遍历 deleted 列表,并从 oldUrlInvokerMap 中移除相应的 Invoker,销毁之。整个逻辑大致如此,不是很难理解。 到此关于 Invoker 列表的刷新逻辑就分析了,这里对整个过程进行简单总结。如下: 检测入参是否仅包含一个 url,且 url 协议头为 empty 若第一步检测结果为 true,表示禁用所有服务,此时销毁所有的 Invoker 若第一步检测结果为 false,此时将入参转为 Invoker 列表 对将上一步逻辑删除的结果进行进一步处理,得到方法名到 Invoker 的映射关系表 合并多组 Invoker 销毁无用 Invoker Invoker 的刷新逻辑还是比较复杂的,大家在看的过程中多写点 demo 进行调试。好了,本节就到这。 4. 总结 本篇文章对 Dubbo 服务目录进行了较为详细的分析,篇幅主要集中在 RegistryDirectory 的源码分析上。分析下来,不由得感叹,想让本地服务目录和注册中心保持一致还是需要做很多事情的,并不简单。服务目录是 Dubbo 集群容错的一部分,也是比较基础的部分,所以大家务必搞懂。 好了,本篇文章就先到这了。感谢大家阅读。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处作者:田小波本文同步发布在我的个人博客:http://www.tianxiaobo.com 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 在上一篇文章中,我详细的分析了服务导出的原理。本篇文章我们趁热打铁,继续分析服务引用的原理。在 Dubbo 中,我们可以通过两种方式引用远程服务。第一种是使用服务直联的方式引用服务,第二种方式是基于注册中心进行引用。服务直联的方式仅适合在调试或测试服务的场景下使用,不适合在线上环境使用。因此,本文我将重点分析通过注册中心引用服务的过程。从注册中心中获取服务配置只是服务引用过程中的一环,除此之外,服务消费者还需要经历 Invoker 创建、代理类创建等步骤。这些步骤,我将在后续章节中一一进行分析。 2.服务引用原理 Dubbo 服务引用的时机有两个,第一个是在 Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务,第二个是在 ReferenceBean 对应的服务被注入到其他类中时引用。这两个引用服务的时机区别在于,第一个是饿汉式的,第二个是懒汉式的。默认情况下,Dubbo 使用懒汉式引用服务。如果需要使用饿汉式,可通过配置 <dubbo:reference> 的 init 属性开启。下面我们按照 Dubbo 默认配置进行分析,整个分析过程从 ReferenceBean 的 getObject 方法开始。当我们的服务被注入到其他类中时,Spring 会第一时间调用 getObject 方法,并由该方法执行服务引用逻辑。按照惯例,在进行具体工作之前,需先进行配置检查与收集工作。接着根据收集到的信息决定服务用的方式,有三种,第一种是引用本地 (JVM) 服务,第二是通过直联方式引用远程服务,第三是通过注册中心引用远程服务。不管是哪种引用方式,最后都会得到一个 Invoker 实例。如果有多个注册中心,多个服务提供者,这个时候会得到一组 Invoker 实例,此时需要通过集群管理类 Cluster 将多个 Invoker 合并成一个实例。合并后的 Invoker 实例已经具备调用本地或远程服务的能力了,但并不能将此实例暴露给用户使用,这会对用户业务代码造成侵入。此时框架还需要通过代理工厂类 (ProxyFactory) 为服务接口生成代理类,并让代理类去调用 Invoker 逻辑。避免了 Dubbo 框架代码对业务代码的侵入,同时也让框架更容易使用。 以上就是 Dubbo 引用服务的大致原理,下面我们深入到代码中,详细分析服务引用细节。 3.源码分析 服务引用的入口方法为 ReferenceBean 的 getObject 方法,该方法定义在 Spring 的 FactoryBean 接口中,ReferenceBean 实现了这个方法。实现代码如下: public Object getObject() throws Exception { return get(); } public synchronized T get() { if (destroyed) { throw new IllegalStateException("Already destroyed!"); } // 检测 ref 是否为空,为空则通过 init 方法创建 if (ref == null) { // init 方法主要用于处理配置,以及调用 createProxy 生成代理类 init(); } return ref; } 这里两个方法代码都比较简短,并不难理解。不过这里需要特别说明一下,如果大家从 getObject 方法进行代码调试时,会碰到比较诧异的问题。这里假设你使用 IDEA,且保持了 IDEA 的默认配置。当你面调试到 get 方法的if (ref == null)时,你会惊奇的发现 ref 不为空,导致你无法进入到 init 方法中继续调试。导致这个现象的原因是 Dubbo 框架本身有点小问题,这个小问题会引发一些让人诧异的现象。关于这个问题,我进行了将近两个小时的排查。查明问题后,我给 Dubbo 提交了一个 pull request (#2754 ) 介绍这个问题,有兴趣的朋友可以去看看。大家如果想规避这个问题,可以修改一下 IDEA 的配置。在配置面板中搜索 toString,然后取消Enable 'toString' object view前的对号。具体如下: 讲完需要注意的点,我们继续向下分析,接下来将分析配置的处理过程。 3.1 处理配置 Dubbo 提供了丰富的配置,用于调整和优化框架行为,性能等。Dubbo 在引用或导出服务时,首先会对这些配置进行检查和处理,以保证配置到正确性。如果大家不是很熟悉 Dubbo 配置,建议先阅读以下官方文档。配置解析的方法为 ReferenceConfig 的 init 方法,下面来看一下方法逻辑。 private void init() { if (initialized) { return; } initialized = true; if (interfaceName == null || interfaceName.length() == 0) { throw new IllegalStateException("interface not allow null!"); } // 检测 consumer 变量是否为空,为空则创建 checkDefault(); appendProperties(this); if (getGeneric() == null && getConsumer() != null) { // 设置 generic setGeneric(getConsumer().getGeneric()); } // 检测是否为泛化接口 if (ProtocolUtils.isGeneric(getGeneric())) { interfaceClass = GenericService.class; } else { try { // 加载类 interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() .getContextClassLoader()); } catch (ClassNotFoundException e) { throw new IllegalStateException(e.getMessage(), e); } checkInterfaceAndMethods(interfaceClass, methods); } // ------------------------------- 分割线1 ------------------------------ // 从系统变量中获取与接口名对应的属性值 String resolve = System.getProperty(interfaceName); String resolveFile = null; if (resolve == null || resolve.length() == 0) { // 从系统属性中获取解析文件路径 resolveFile = System.getProperty("dubbo.resolve.file"); if (resolveFile == null || resolveFile.length() == 0) { // 从指定位置加载配置文件 File userResolveFile = new File(new File(System.getProperty("user.home")), "dubbo-resolve.properties"); if (userResolveFile.exists()) { // 获取文件绝对路径 resolveFile = userResolveFile.getAbsolutePath(); } } if (resolveFile != null && resolveFile.length() > 0) { Properties properties = new Properties(); FileInputStream fis = null; try { fis = new FileInputStream(new File(resolveFile)); // 从文件中加载配置 properties.load(fis); } catch (IOException e) { throw new IllegalStateException("Unload ..., cause:..."); } finally { try { if (null != fis) fis.close(); } catch (IOException e) { logger.warn(e.getMessage(), e); } } // 获取与接口名对应的配置 resolve = properties.getProperty(interfaceName); } } if (resolve != null && resolve.length() > 0) { // 将 resolve 赋值给 url url = resolve; } // ------------------------------- 分割线2 ------------------------------ if (consumer != null) { if (application == null) { // 从 consumer 中获取 Application 实例,下同 application = consumer.getApplication(); } if (module == null) { module = consumer.getModule(); } if (registries == null) { registries = consumer.getRegistries(); } if (monitor == null) { monitor = consumer.getMonitor(); } } if (module != null) { if (registries == null) { registries = module.getRegistries(); } if (monitor == null) { monitor = module.getMonitor(); } } if (application != null) { if (registries == null) { registries = application.getRegistries(); } if (monitor == null) { monitor = application.getMonitor(); } } // 检测本地 Application 和本地存根配置合法性 checkApplication(); checkStubAndMock(interfaceClass); // ------------------------------- 分割线3 ------------------------------ Map<String, String> map = new HashMap<String, String>(); Map<Object, Object> attributes = new HashMap<Object, Object>(); // 添加 side、协议版本信息、时间戳和进程号等信息到 map 中 map.put(Constants.SIDE_KEY, Constants.CONSUMER_SIDE); map.put(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion()); map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis())); if (ConfigUtils.getPid() > 0) { map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid())); } if (!isGeneric()) { // 非泛化服务 // 获取版本 String revision = Version.getVersion(interfaceClass, version); if (revision != null && revision.length() > 0) { map.put("revision", revision); } // 获取接口方法列表,并添加到 map 中 String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); if (methods.length == 0) { map.put("methods", Constants.ANY_VALUE); } else { map.put("methods", StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ",")); } } map.put(Constants.INTERFACE_KEY, interfaceName); // 将 ApplicationConfig、ConsumerConfig、ReferenceConfig 等对象的字段信息添加到 map 中 appendParameters(map, application); appendParameters(map, module); appendParameters(map, consumer, Constants.DEFAULT_KEY); appendParameters(map, this); // ------------------------------- 分割线4 ------------------------------ String prefix = StringUtils.getServiceKey(map); if (methods != null && !methods.isEmpty()) { // 遍历 MethodConfig 列表 for (MethodConfig method : methods) { appendParameters(map, method, method.getName()); String retryKey = method.getName() + ".retry"; // 检测 map 是否包含 methodName.retry if (map.containsKey(retryKey)) { String retryValue = map.remove(retryKey); if ("false".equals(retryValue)) { // 添加重试次数配置 methodName.retries map.put(method.getName() + ".retries", "0"); } } // 添加 MethodConfig 中的“属性”字段到 attributes // 比如 onreturn、onthrow、oninvoke 等 appendAttributes(attributes, method, prefix + "." + method.getName()); checkAndConvertImplicitConfig(method, map, attributes); } } // ------------------------------- 分割线5 ------------------------------ // 获取服务消费者 ip 地址 String hostToRegistry = ConfigUtils.getSystemProperty(Constants.DUBBO_IP_TO_REGISTRY); if (hostToRegistry == null || hostToRegistry.length() == 0) { hostToRegistry = NetUtils.getLocalHost(); } else if (isInvalidLocalHost(hostToRegistry)) { throw new IllegalArgumentException("Specified invalid registry ip from property..." ); } map.put(Constants.REGISTER_IP_KEY, hostToRegistry); // 存储 attributes 到系统上下文中 StaticContext.getSystemContext().putAll(attributes); // 创建代理类 ref = createProxy(map); // 根据服务名,ReferenceConfig,代理类构建 ConsumerModel, // 并将 ConsumerModel 存入到 ApplicationModel 中 ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods()); ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel); } 上面的代码很长,做的事情比较多。这里我根据代码逻辑,对代码进行了分块,下面我们一起来看一下。 首先是方法开始到分割线1之间的代码。这段代码主要用于检测 ConsumerConfig 实例是否存在,如不存在则创建一个新的实例,然后通过系统变量或 dubbo.properties 配置文件填充 ConsumerConfig 的字段。接着是检测泛化配置,并根据配置设置 interfaceClass 的值。本段代码逻辑大致就是这些,接着来看分割线1到分割线2之间的逻辑。这段逻辑用于从系统属性或配置文件中加载与接口名相对应的配置,并将解析结果赋值给 url 字段。url 字段的作用一般是用于点对点调用。继续向下看,分割线2和分割线3之间的代码用于检测几个核心配置类是否为空,为空则尝试从其他配置类中获取。分割线3与分割线4之间的代码主要是用于收集各种配置,并将配置存储到 map 中。分割线4和分割线5之间的代码用于处理 MethodConfig 实例。该实例包含了事件通知配置,比如 onreturn、onthrow、oninvoke 等。分割线5到方法结尾的代码主要用于解析服务消费者 ip,以及调用 createProxy 创建代理对象。关于该方法的详细分析,将会在接下来的章节中展开。 到这里,关于配置的检查与处理过长就分析完了。这部分逻辑不是很难理解,但比较繁杂,大家需要耐心看一下。好了,本节先到这,接下来分析服务引用的过程。 3.2 引用服务 本节我们要从 createProxy 开始看起。createProxy 这个方法表面上看起来只是用于创建代理对象,但实际上并非如此。该方法还会调用其他方法构建以及合并 Invoker 实例。具体细节如下。 private T createProxy(Map<String, String> map) { URL tmpUrl = new URL("temp", "localhost", 0, map); final boolean isJvmRefer; if (isInjvm() == null) { // url 配置被指定,则不做本地引用 if (url != null && url.length() > 0) { isJvmRefer = false; // 根据 url 的协议、scope 以及 injvm 等参数检测是否需要本地引用 // 比如如果用户显式配置了 scope=local,此时 isInjvmRefer 返回 true } else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) { isJvmRefer = true; } else { isJvmRefer = false; } } else { // 获取 injvm 配置值 isJvmRefer = isInjvm().booleanValue(); } // 本地引用 if (isJvmRefer) { // 生成本地引用 URL,协议为 injvm URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map); // 调用 refer 方法构建 InjvmInvoker 实例 invoker = refprotocol.refer(interfaceClass, url); // 远程引用 } else { // url 不为空,表明用户可能想进行点对点调用 if (url != null && url.length() > 0) { // 当需要配置多个 url 时,可用分号进行分割,这里会进行切分 String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url); if (us != null && us.length > 0) { for (String u : us) { URL url = URL.valueOf(u); if (url.getPath() == null || url.getPath().length() == 0) { // 设置接口全限定名为 url 路径 url = url.setPath(interfaceName); } // 检测 url 协议是否为 registry,若是,表明用户想使用指定的注册中心 if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) { // 将 map 转换为查询字符串,并作为 refer 参数的值添加到 url 中 urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map))); } else { // 合并 url,移除服务提供者的一些配置(这些配置来源于用户配置的 url 属性), // 比如线程池相关配置。并保留服务提供者的部分配置,比如版本,group,时间戳等 // 最后将合并后的配置设置为 url 查询字符串中。 urls.add(ClusterUtils.mergeUrl(url, map)); } } } } else { // 加载注册中心 url List<URL> us = loadRegistries(false); if (us != null && !us.isEmpty()) { for (URL u : us) { URL monitorUrl = loadMonitor(u); if (monitorUrl != null) { map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString())); } // 添加 refer 参数到 url 中,并将 url 添加到 urls 中 urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map))); } } // 未配置注册中心,抛出异常 if (urls.isEmpty()) { throw new IllegalStateException("No such any registry to reference..."); } } // 单个注册中心或服务提供者(服务直联,下同) if (urls.size() == 1) { // 调用 RegistryProtocol 的 refer 构建 Invoker 实例 invoker = refprotocol.refer(interfaceClass, urls.get(0)); // 多个注册中心或多个服务提供者,或者两者混合 } else { List<Invoker<?>> invokers = new ArrayList<Invoker<?>>(); URL registryURL = null; // 获取所有的 Invoker for (URL url : urls) { // 通过 refprotocol 调用 refer 构建 Invoker,refprotocol 会在运行时 // 根据 url 协议头加载指定的 Protocol 实例,并调用实例的 refer 方法 invokers.add(refprotocol.refer(interfaceClass, url)); if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) { registryURL = url; } } if (registryURL != null) { // 如果注册中心链接不为空,则将使用 AvailableCluster URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME); // 创建 StaticDirectory 实例,并由 Cluster 对多个 Invoker 进行合并 invoker = cluster.join(new StaticDirectory(u, invokers)); } else { invoker = cluster.join(new StaticDirectory(invokers)); } } } Boolean c = check; if (c == null && consumer != null) { c = consumer.isCheck(); } if (c == null) { c = true; } // invoker 可用性检查 if (c && !invoker.isAvailable()) { throw new IllegalStateException("No provider available for the service..."); } // 生成代理类 return (T) proxyFactory.getProxy(invoker); } 上面代码很多,不过逻辑比较清晰。首先根据配置检查是否为本地调用,若是,则调用 InjvmProtocol 的 refer 方法生成 InjvmInvoker 实例。若不是,则读取直联配置项,或注册中心 url,并将读取到的 url 存储到 urls 中。然后,根据 urls 元素数量进行后续操作。若 urls 元素数量为1,则直接通过 Protocol 自适应拓展构建 Invoker 实例接口。若 urls 元素数量大于1,即存在多个注册中心或服务直联 url,此时先根据 url 构建 Invoker。然后再通过 Cluster 合并多个 Invoker,最后调用 ProxyFactory 生成代理类。这里,Invoker 的构建过程以及代理类的过程比较重要,因此我将分两小节对这两个过程进行分析。 3.2.1 创建 Invoker Invoker 是 Dubbo 的核心模型,代表一个可执行体。在服务提供方,Invoker 用于调用服务提供类。在服务消费方,Invoker 用于执行远程调用。Invoker 在 Dubbo 中的位置十分重要,因此我们有必要去搞懂它。从前面的代码中可知,Invoker 是由 Protocol 实现类构建的。Protocol 实现类有很多,这里我会分析最常用的两个,分别是 RegistryProtocol 和 DubboProtocol,其他的大家自行分析。下面先来分析 DubboProtocol 的 refer 方法源码。如下: public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException { optimizeSerialization(url); // 创建 DubboInvoker DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers); invokers.add(invoker); return invoker; } 上面方法看起来比较简单,不过这里有一个调用需要我们注意一下,即 getClients。这个方法用于获取客户端实例,实例类型为 ExchangeClient。ExchangeClient 实际上并不具备通信能力,因此它需要更底层的客户端实例进行通信。比如 NettyClient、MinaClient 等,默认情况下,Dubbo 使用 NettyClient 进行通信。接下来,我们简单看一下 getClients 方法的逻辑。 private ExchangeClient[] getClients(URL url) { // 是否共享连接 boolean service_share_connect = false; // 获取连接数,默认为0,表示未配置 int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0); // 如果未配置 connections,则共享连接 if (connections == 0) { service_share_connect = true; connections = 1; } ExchangeClient[] clients = new ExchangeClient[connections]; for (int i = 0; i < clients.length; i++) { if (service_share_connect) { // 获取共享客户端 clients[i] = getSharedClient(url); } else { // 初始化新的客户端 clients[i] = initClient(url); } } return clients; } 这里根据 connections 数量决定是获取共享客户端还是创建新的客户端实例,默认情况下,使用共享客户端实例。不过 getSharedClient 方法中也会调用 initClient 方法,因此下面我们一起看一下这两个方法。 private ExchangeClient getSharedClient(URL url) { String key = url.getAddress(); // 获取带有“引用计数”功能的 ExchangeClient ReferenceCountExchangeClient client = referenceClientMap.get(key); if (client != null) { if (!client.isClosed()) { // 增加引用计数 client.incrementAndGetCount(); return client; } else { referenceClientMap.remove(key); } } locks.putIfAbsent(key, new Object()); synchronized (locks.get(key)) { if (referenceClientMap.containsKey(key)) { return referenceClientMap.get(key); } // 创建 ExchangeClient 客户端 ExchangeClient exchangeClient = initClient(url); // 将 ExchangeClient 实例传给 ReferenceCountExchangeClient,这里明显用了装饰模式 client = new ReferenceCountExchangeClient(exchangeClient, ghostClientMap); referenceClientMap.put(key, client); ghostClientMap.remove(key); locks.remove(key); return client; } } 上面方法先访问缓存,若缓存未命中,则通过 initClient 方法创建新的 ExchangeClient 实例,并将该实例传给 ReferenceCountExchangeClient 构造方法创建一个带有引用技术功能的 ExchangeClient 实例。ReferenceCountExchangeClient 内部实现比较简单,就不分析了。下面我们再来看一下 initClient 方法的代码。 private ExchangeClient initClient(URL url) { // 获取客户端类型,默认为 netty String str = url.getParameter(Constants.CLIENT_KEY, url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_CLIENT)); // 添加编解码和心跳包参数到 url 中 url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME); url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT)); // 检测客户端类型是否存在,不存在则抛出异常 if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) { throw new RpcException("Unsupported client type: ..."); } ExchangeClient client; try { // 获取 lazy 配置,并根据配置值决定创建的客户端类型 if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) { // 创建懒加载 ExchangeClient 实例 client = new LazyConnectExchangeClient(url, requestHandler); } else { // 创建普通 ExchangeClient 实例 client = Exchangers.connect(url, requestHandler); } } catch (RemotingException e) { throw new RpcException("Fail to create remoting client for service..."); } return client; } initClient 方法首先获取用户配置的客户端类型,默认为 netty。然后检测用户配置的客户端类型是否存在,不存在则抛出异常。最后根据 lazy 配置决定创建什么类型的客户端。这里的 LazyConnectExchangeClient 代码并不是很复杂,该类会在 request 方法被调用时通过 Exchangers 的 connect 方法创建 ExchangeClient 客户端,这里就不分析 LazyConnectExchangeClient 的代码了。下面我们分析一下 Exchangers 的 connect 方法。 public static ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException { if (url == null) { throw new IllegalArgumentException("url == null"); } if (handler == null) { throw new IllegalArgumentException("handler == null"); } url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange"); // 获取 Exchanger 实例,默认为 HeaderExchangeClient return getExchanger(url).connect(url, handler); } 如上,getExchanger 会通过 SPI 加载 HeaderExchangeClient 实例,这个方法比较简单,大家自己看一下吧。接下来分析 HeaderExchangeClient 的实现。 public ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException { // 这里包含了多个调用,分别如下: // 1. 创建 HeaderExchangeHandler 对象 // 2. 创建 DecodeHandler 对象 // 3. 通过 Transporters 构建 Client 实例 // 4. 创建 HeaderExchangeClient 对象 return new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))), true); } 这里的调用比较多,我们这里重点看一下 Transporters 的 connect 方法。如下: public static Client connect(URL url, ChannelHandler... handlers) throws RemotingException { if (url == null) { throw new IllegalArgumentException("url == null"); } ChannelHandler handler; if (handlers == null || handlers.length == 0) { handler = new ChannelHandlerAdapter(); } else if (handlers.length == 1) { handler = handlers[0]; } else { // 如果 handler 数量大于1,则创建一个 ChannelHandler 分发器 handler = new ChannelHandlerDispatcher(handlers); } // 获取 Transporter 自适应拓展类,并调用 connect 方法生成 Client 实例 return getTransporter().connect(url, handler); } 这里,getTransporter 方法返回的是自适应拓展类,该类会在运行时根据客户端类型加载指定的 Transporter 实现类。若用户未显示配置客户端类型,则默认加载 NettyTransporter,并调用该类的 connect 方法。如下: public Client connect(URL url, ChannelHandler listener) throws RemotingException { // 创建 NettyClient 对象 return new NettyClient(url, listener); } 到这里就不继续跟下去了,在往下就是通过 Netty 提供的接口构建 Netty 客户端了,大家有兴趣自己看看。到这里,关于 DubboProtocol 的 refer 方法就分析完了。接下来,继续分析 RegistryProtocol 的 refer 方法逻辑。 public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException { // 取 registry 参数值,并将其设置为协议头 url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY); // 获取注册中心实例 Registry registry = registryFactory.getRegistry(url); // 这个判断暂时不知道有什么意图,为什么要给 RegistryService 类型生成 Invoker ? if (RegistryService.class.equals(type)) { return proxyFactory.getInvoker((T) registry, type, url); } // 将 url 查询字符串转为 Map Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY)); // 获取 group 配置 String group = qs.get(Constants.GROUP_KEY); if (group != null && group.length() > 0) { if ((Constants.COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) { // 通过 SPI 加载 MergeableCluster 实例,并调用 doRefer 继续执行引用服务逻辑 return doRefer(getMergeableCluster(), registry, type, url); } } // 调用 doRefer 继续执行引用服务逻辑 return doRefer(cluster, registry, type, url); } 上面代码首先为 url 设置协议头,然后根据 url 参数加载注册中心实例。接下来对 RegistryService 继续针对性处理,这个处理逻辑我不是很明白,不知道为什么要为 RegistryService 类型生成 Invoker,有知道同学麻烦告知一下。然后就是获取 group 配置,根据 group 配置决定 doRefer 第一个参数的类型。这里的重点是 doRefer 方法,如下: private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) { // 创建 RegistryDirectory 实例 RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url); // 设置注册中心和协议 directory.setRegistry(registry); directory.setProtocol(protocol); Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters()); // 生成服务消费者链接 URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters); // 注册服务消费者,在 consumers 目录下新节点 if (!Constants.ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(Constants.REGISTER_KEY, true)) { registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY, Constants.CHECK_KEY, String.valueOf(false))); } // 订阅 providers、configurators、routers 等节点数据 directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY, Constants.PROVIDERS_CATEGORY + "," + Constants.CONFIGURATORS_CATEGORY + "," + Constants.ROUTERS_CATEGORY)); // 一个注册中心可能有多个服务提供者,因此这里需要将多个服务提供者合并为一个 Invoker invoker = cluster.join(directory); ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory); return invoker; } 如上,doRefer 方法创建一个 RegistryDirectory 实例,然后生成服务者消费者链接,并向注册中心进行注册。注册完毕后,紧接着订阅 providers、configurators、routers 等节点下的数据。完成订阅后,RegistryDirectory 会收到这几个节点下的子节点信息,比如可以获取到服务提供者的配置信息。由于一个服务可能部署在多台服务器上,这样就会在 providers 产生多个节点,这个时候就需要 Cluster 将多个服务节点合并为一个,并生成一个 Invoker。关于 RegistryDirectory 和 Cluster,本文不打算进行分析,相关分析将会在随后的文章中展开。 好了,关于 Invoker 的创建的逻辑就先分析到这。逻辑比较多,大家耐心看一下。 3.2.2 创建代理 Invoker 创建完毕后,接下来要做的事情是为服务接口生成代理对象。有了代理对象,我们就可以通过代理对象进行远程调用。代理对象生成的入口方法为在 ProxyFactory 的 getProxy,接下来进行分析。 public <T> T getProxy(Invoker<T> invoker) throws RpcException { // 调用重载方法 return getProxy(invoker, false); } public <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException { Class<?>[] interfaces = null; // 获取接口列表 String config = invoker.getUrl().getParameter("interfaces"); if (config != null && config.length() > 0) { // 切分接口列表 String[] types = Constants.COMMA_SPLIT_PATTERN.split(config); if (types != null && types.length > 0) { interfaces = new Class<?>[types.length + 2]; // 设置服务接口类和 EchoService.class 到 interfaces 中 interfaces[0] = invoker.getInterface(); interfaces[1] = EchoService.class; for (int i = 0; i < types.length; i++) { // 加载接口类 interfaces[i + 1] = ReflectUtils.forName(types[i]); } } } if (interfaces == null) { interfaces = new Class<?>[]{invoker.getInterface(), EchoService.class}; } // 为 http 和 hessian 协议提供泛化调用支持,参考 pull request #1827 if (!invoker.getInterface().equals(GenericService.class) && generic) { int len = interfaces.length; Class<?>[] temp = interfaces; // 创建新的 interfaces 数组 interfaces = new Class<?>[len + 1]; System.arraycopy(temp, 0, interfaces, 0, len); // 设置 GenericService.class 到数组中 interfaces[len] = GenericService.class; } // 调用重载方法 return getProxy(invoker, interfaces); } public abstract <T> T getProxy(Invoker<T> invoker, Class<?>[] types); 如上,上面大段代码都是用来获取 interfaces 数组的,因此我们需要继续往下看。getProxy(Invoker, Class<?>[]) 这个方法是一个抽象方法,下面我们到 JavassistProxyFactory 类中看一下该方法的实现代码。 public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) { // 生成 Proxy 子类(Proxy 是抽象类)。并调用Proxy 子类的 newInstance 方法生成 Proxy 实例 return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker)); } 上面代码并不多,首先是通过 Proxy 的 getProxy 方法获取 Proxy 子类,然后创建 InvokerInvocationHandler 对象,并将该对象传给 newInstance 生成 Proxy 实例。InvokerInvocationHandler 实现自 JDK 的 InvocationHandler 接口,具体的用途是拦截接口类调用。该类逻辑比较简单,这里就不分析了。下面我们重点关注一下 Proxy 的 getProxy 方法,如下。 public static Proxy getProxy(Class<?>... ics) { // 调用重载方法 return getProxy(ClassHelper.getClassLoader(Proxy.class), ics); } public static Proxy getProxy(ClassLoader cl, Class<?>... ics) { if (ics.length > 65535) throw new IllegalArgumentException("interface limit exceeded"); StringBuilder sb = new StringBuilder(); // 遍历接口列表 for (int i = 0; i < ics.length; i++) { String itf = ics[i].getName(); // 检测类型是否为接口 if (!ics[i].isInterface()) throw new RuntimeException(itf + " is not a interface."); Class<?> tmp = null; try { // 重新加载接口类 tmp = Class.forName(itf, false, cl); } catch (ClassNotFoundException e) { } // 检测接口是否相同,这里 tmp 有可能为空 if (tmp != ics[i]) throw new IllegalArgumentException(ics[i] + " is not visible from class loader"); // 拼接接口全限定名,分隔符为 ; sb.append(itf).append(';'); } // 使用拼接后接口名作为 key String key = sb.toString(); Map<String, Object> cache; synchronized (ProxyCacheMap) { cache = ProxyCacheMap.get(cl); if (cache == null) { cache = new HashMap<String, Object>(); ProxyCacheMap.put(cl, cache); } } Proxy proxy = null; synchronized (cache) { do { // 从缓存中获取 Reference<Proxy> 实例 Object value = cache.get(key); if (value instanceof Reference<?>) { proxy = (Proxy) ((Reference<?>) value).get(); if (proxy != null) { return proxy; } } // 多线程控制,保证只有一个线程可以进行后续操作 if (value == PendingGenerationMarker) { try { // 其他线程在此处进行等待 cache.wait(); } catch (InterruptedException e) { } } else { // 放置标志位到缓存中,并跳出 while 循环进行后续操作 cache.put(key, PendingGenerationMarker); break; } } while (true); } long id = PROXY_CLASS_COUNTER.getAndIncrement(); String pkg = null; ClassGenerator ccp = null, ccm = null; try { // 创建 ClassGenerator 对象 ccp = ClassGenerator.newInstance(cl); Set<String> worked = new HashSet<String>(); List<Method> methods = new ArrayList<Method>(); for (int i = 0; i < ics.length; i++) { // 检测接口访问级别是否为 protected 或 privete if (!Modifier.isPublic(ics[i].getModifiers())) { // 获取接口包名 String npkg = ics[i].getPackage().getName(); if (pkg == null) { pkg = npkg; } else { if (!pkg.equals(npkg)) // 非 public 级别的接口必须在同一个包下,否者抛出异常 throw new IllegalArgumentException("non-public interfaces from different packages"); } } // 添加接口到 ClassGenerator 中 ccp.addInterface(ics[i]); // 遍历接口方法 for (Method method : ics[i].getMethods()) { // 获取方法描述,可理解为方法签名 String desc = ReflectUtils.getDesc(method); // 如果已包含在 worked 中,则忽略。考虑这种情况, // A 接口和 B 接口中包含一个完全相同的方法 if (worked.contains(desc)) continue; worked.add(desc); int ix = methods.size(); // 获取方法返回值类型 Class<?> rt = method.getReturnType(); // 获取参数列表 Class<?>[] pts = method.getParameterTypes(); // 生成 Object[] args = new Object[1...N] StringBuilder code = new StringBuilder("Object[] args = new Object[").append(pts.length).append("];"); for (int j = 0; j < pts.length; j++) // 生成 args[1...N] = ($w)$1...N; code.append(" args[").append(j).append("] = ($w)$").append(j + 1).append(";"); // 生成 InvokerHandler 接口的 invoker 方法调用语句,如下: // Object ret = handler.invoke(this, methods[1...N], args); code.append(" Object ret = handler.invoke(this, methods[" + ix + "], args);"); // 返回值不为 void if (!Void.TYPE.equals(rt)) // 生成返回语句,形如 return (java.lang.String) ret; code.append(" return ").append(asArgument(rt, "ret")).append(";"); methods.add(method); // 添加方法名、访问控制符、参数列表、方法代码等信息到 ClassGenerator 中 ccp.addMethod(method.getName(), method.getModifiers(), rt, pts, method.getExceptionTypes(), code.toString()); } } if (pkg == null) pkg = PACKAGE_NAME; // 构建接口代理类名称:pkg + ".proxy" + id,比如 com.tianxiaobo.proxy0 String pcn = pkg + ".proxy" + id; ccp.setClassName(pcn); ccp.addField("public static java.lang.reflect.Method[] methods;"); // 生成 private java.lang.reflect.InvocationHandler handler; ccp.addField("private " + InvocationHandler.class.getName() + " handler;"); // 为接口代理类添加带有 InvocationHandler 参数的构造方法,比如: // porxy0(java.lang.reflect.InvocationHandler arg0) { // handler=$1; // } ccp.addConstructor(Modifier.PUBLIC, new Class<?>[]{InvocationHandler.class}, new Class<?>[0], "handler=$1;"); // 为接口代理类添加默认构造方法 ccp.addDefaultConstructor(); // 生成接口代理类 Class<?> clazz = ccp.toClass(); clazz.getField("methods").set(null, methods.toArray(new Method[0])); // 构建 Proxy 子类名称,比如 Proxy1,Proxy2 等 String fcn = Proxy.class.getName() + id; ccm = ClassGenerator.newInstance(cl); ccm.setClassName(fcn); ccm.addDefaultConstructor(); ccm.setSuperClass(Proxy.class); // 为 Proxy 的抽象方法 newInstance 生成实现代码,形如: // public Object newInstance(java.lang.reflect.InvocationHandler h) { // return new com.tianxiaobo.proxy0($1); // } ccm.addMethod("public Object newInstance(" + InvocationHandler.class.getName() + " h){ return new " + pcn + "($1); }"); // 生成 Proxy 实现类 Class<?> pc = ccm.toClass(); // 通过反射创建 Proxy 实例 proxy = (Proxy) pc.newInstance(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } finally { if (ccp != null) // 释放资源 ccp.release(); if (ccm != null) ccm.release(); synchronized (cache) { if (proxy == null) cache.remove(key); else // 写缓存 cache.put(key, new WeakReference<Proxy>(proxy)); // 唤醒其他等待线程 cache.notifyAll(); } } return proxy; } 上面代码比较复杂,我也写了很多注释。大家在阅读这段代码时,要搞清楚 ccp 和 ccm 的用途,不然会被搞晕。ccp 用于为服务接口生成代理类,比如我们有一个 DemoService 接口,这个接口代理类就是由 ccp 生成的。ccm 则是用于为 org.apache.dubbo.common.bytecode.Proxy 抽象类生成子类,主要是实现 Proxy 的抽象方法。下面以 org.apache.dubbo.demo.DemoService 这个接口为例,来看一下该接口代理类代码大致是怎样的(忽略 EchoService 接口)。 package org.apache.dubbo.common.bytecode; public class proxy0 implements org.apache.dubbo.demo.DemoService { public static java.lang.reflect.Method[] methods; private java.lang.reflect.InvocationHandler handler; public proxy0() { } public proxy0(java.lang.reflect.InvocationHandler arg0) { handler = $1; } public java.lang.String sayHello(java.lang.String arg0) { Object[] args = new Object[1]; args[0] = ($w) $1; Object ret = handler.invoke(this, methods[0], args); return (java.lang.String) ret; } } 好了,到这里代理类生成逻辑就分析完了。整个过程比较复杂,大家需要耐心看一下,本节点到这里。 4.总结 本篇文章对服务引用的过程进行了较为详尽的分析,之所以说是较为详尽,是因为还有一些地方没有分析到。比如 Directory、Cluster 等实现类的代码并未进行详细分析,由于这些类功能比较独立,因此我打算后续单独成文进行分析。暂时我们可以先把这些类看成黑盒,只要知道这些类的用途即可。引用服务过程涉及到的调用也非常多,大家在阅读相关代码的中耐心些,并多进行调试。 好了,本篇文章就先到这里了。谢谢阅读。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处作者:田小波本文同步发布在我的个人博客:http://www.tianxiaobo.com 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.服务导出过程 本篇文章,我们来研究一下 Dubbo 导出服务的过程。Dubbo 服务导出过程始于 Spring 容器发布刷新事件,Dubbo 在接收到事件后,会立即执行服务导出逻辑。整个逻辑大致可分为三个部分,第一是前置工作,主要用于检查参数,组装 URL。第二是导出服务,包含导出服务到本地 (JVM),和导出服务到远程两个过程。第三是向注册中心注册服务,用于服务发现。本篇文章将会对这三个部分代码进行详细的分析,在分析之前,我们先来了解一下服务的导出过程。 Dubbo 支持两种服务导出方式,分别延迟导出和立即导出。延迟导出的入口是 ServiceBean 的 afterPropertiesSet 方法,立即导出的入口是 ServiceBean 的 onApplicationEvent 方法。本文打算分析服务延迟导出过程,因此不会分析 afterPropertiesSet 方法。下面从 onApplicationEvent 方法说起,该方法收到 Spring 容器的刷新事件后,会调用 export 方法执行服务导出操作。服务导出之前,要进行对一系列的配置进行检查,以及生成 URL。准备工作做完,随后开始导出服务。首先导出到本地,然后再导出到远程。导出到本地就是将服务导出到 JVM 中,此过程比较简单。导出到远程的过程则要复杂的多,以 dubbo 协议为例,DubboProtocol 类的 export 方法将会被调用。该方法主要用于创建 Exporter 和 ExchangeServer。ExchangeServer 本身并不具备通信能力,需要借助更底层的 Server 实现通信功能。因此,在创建 ExchangeServer 实例时,需要先创建 NettyServer 或者 MinaServer 实例,并将实例作为参数传给 ExchangeServer 实现类的构造方法。ExchangeServer 实例创建完成后,导出服务到远程的过程也就接近尾声了。服务导出结束后,服务消费者即可通过直联的方式消费服务。当然,一般我们不会使用直联的方式消费服务。所以,在服务导出结束后,紧接着要做的事情是向注册中心注册服务。此时,客户端即可从注册中心发现服务。 以上就是 Dubbo 服务导出的过程,比较复杂。下面开始分析源码,从源码的角度展现整个过程。 2.源码分析 一场 Dubbo 源码分析的马拉松比赛即将开始,现在我们站在赛道的起点进行热身准备。本次比赛的起点位置位于 ServiceBean 的 onApplicationEvent 方法处。好了,发令枪响了,我将和一些朋友从 onApplicationEvent 方法处出发,探索 Dubbo 服务导出的全过程。下面我们来看一下 onApplicationEvent 方法的源码。 public void onApplicationEvent(ContextRefreshedEvent event) { // 是否有延迟导出 && 是否已导出 && 是不是已被取消导出 if (isDelay() && !isExported() && !isUnexported()) { // 导出服务 export(); } } onApplicationEvent 是一个事件响应方法,该方法会在收到 Spring 上下文刷新事件后执行。这个方法首先会根据条件决定是否导出服务,比如有些服务设置了延时导出,那么此时就不应该在此处导出。还有一些服务已经被导出了,或者当前服务被取消导出了,此时也不能再次导出相关服务。注意这里的 isDelay 方法,这个方法字面意思是“是否延迟导出服务”,返回 true 表示延迟导出,false 表示不延迟导出。但是该方法真实意思却并非如此,当方法返回 true 时,表示无需延迟导出。返回 false 时,表示需要延迟导出。与字面意思恰恰相反,让人觉得很奇怪。下面我们来看一下这个方法的逻辑。 // -- ServiceBean private boolean isDelay() { // 获取 delay Integer delay = getDelay(); ProviderConfig provider = getProvider(); if (delay == null && provider != null) { // 如果前面获取的 delay 为空,这里继续获取 delay = provider.getDelay(); } // 判断 delay 是否为空,或者等于 -1 return supportedApplicationListener && (delay == null || delay == -1); } 暂时忽略 supportedApplicationListener 这个条件,当 delay 为空,或者等于-1时,该方法返回 true,而不是 false。这个方法的返回值让人有点困惑,因此我重构了该方法的代码,并给 Dubbo 提了一个 Pull Request,最终这个 PR 被合到了 Dubbo 主分支中。详细请参见 Dubbo #2686。 现在解释一下 supportedApplicationListener 变量含义,该变量用于表示当前的 Spring 容器是否支持 ApplicationListener,这个值初始为 false。在 Spring 容器将自己设置到 ServiceBean 中时,ServiceBean 的 setApplicationContext 方法会检测 Spring 容器是否支持 ApplicationListener。若支持,则将 supportedApplicationListener 置为 true。代码就不分析了,大家自行查阅了解。 ServiceBean 是 Dubbo 与 Spring 框架进行整合的关键,可以看做是两个框架之间的桥梁。具有同样作用的类还有 ReferenceBean。ServiceBean 实现了 Spring 的一些拓展接口,有 FactoryBean、ApplicationContextAware、ApplicationListener、DisposableBean 和 BeanNameAware。这些接口我在 Spring 源码分析系列文章中介绍过,大家可以参考一下,这里就不赘述了。 现在我们知道了 Dubbo 服务导出过程的起点。那么接下来,我们快马加鞭,继续进行比赛。赛程预告,下一站是“服务导出的前置工作”。 2.1 前置工作 前置工作主要包含两个部分,分别是配置检查,以及 URL 装配。在导出服务之前,Dubbo 需要检查用户的配置是否合理,或者为用户补充缺省配置。配置检查完成后,接下来需要根据这些配置组装 URL。在 Dubbo 中,URL 的作用十分重要。Dubbo 使用 URL 作为配置载体,所有的拓展点都是通过 URL 获取配置。这一点,官方文档中有所说明。 采用 URL 作为配置信息的统一格式,所有扩展点都通过传递 URL 携带配置信息。 接下来,我们先来分析配置检查部分的源码,随后再来分析 URL 组装部分的源码。 2.1.1 检查配置 本节我们接着前面的源码向下分析,前面说过 onApplicationEvent 方法在经过一些判断后,会决定是否调用 export 方法导出服务。那么下面我们从 export 方法开始进行分析,如下: public synchronized void export() { if (provider != null) { // 获取 export 和 delay 配置 if (export == null) { export = provider.getExport(); } if (delay == null) { delay = provider.getDelay(); } } // 如果 export 为 false,则不导出服务 if (export != null && !export) { return; } if (delay != null && delay > 0) { // delay > 0,延时导出服务 delayExportExecutor.schedule(new Runnable() { @Override public void run() { doExport(); } }, delay, TimeUnit.MILLISECONDS); } else { // 立即导出服务 doExport(); } } export 对两个配置进行了检查,并配置执行相应的动作。首先是 export,这个配置决定了是否导出服务。有时候我们只是想本地启动服务进行一些调试工作,这个时候我们并不希望把本地启动的服务暴露出去给别人调用。此时,我们就可以通过配置 export 禁止服务导出,比如: <dubbo:provider export="false" /> delay 见名知意了,用于延迟导出服务。下面,我们继续分析源码,这次要分析的是 doExport 方法。 protected synchronized void doExport() { if (unexported) { throw new IllegalStateException("Already unexported!"); } if (exported) { return; } exported = true; // 检测 interfaceName 是否合法 if (interfaceName == null || interfaceName.length() == 0) { throw new IllegalStateException("interface not allow null!"); } // 检测 provider 是否为空,为空则新建一个,并通过系统变量为其初始化 checkDefault(); // 下面几个 if 语句用于检测 provider、application 等核心配置类对象是否为空, // 若为空,则尝试从其他配置类对象中获取相应的实例。 if (provider != null) { if (application == null) { application = provider.getApplication(); } if (module == null) { module = provider.getModule(); } if (registries == null) {...} if (monitor == null) {...} if (protocols == null) {...} } if (module != null) { if (registries == null) { registries = module.getRegistries(); } if (monitor == null) {...} } if (application != null) { if (registries == null) { registries = application.getRegistries(); } if (monitor == null) {...} } // 检测 ref 是否泛化服务类型 if (ref instanceof GenericService) { // 设置 interfaceClass 为 GenericService.class interfaceClass = GenericService.class; if (StringUtils.isEmpty(generic)) { // 设置 generic = "true" generic = Boolean.TRUE.toString(); } } else { // ref 非 GenericService 类型 try { interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() .getContextClassLoader()); } catch (ClassNotFoundException e) { throw new IllegalStateException(e.getMessage(), e); } // 对 interfaceClass,以及 <dubbo:method> 必要字段进行检查 checkInterfaceAndMethods(interfaceClass, methods); // 对 ref 合法性进行检测 checkRef(); // 设置 generic = "false" generic = Boolean.FALSE.toString(); } // local 属性 Dubbo 官方文档中没有说明,不过 local 和 stub 在功能应该是一致的,用于配置本地存根 if (local != null) { if ("true".equals(local)) { local = interfaceName + "Local"; } Class<?> localClass; try { // 获取本地存根类 localClass = ClassHelper.forNameWithThreadContextClassLoader(local); } catch (ClassNotFoundException e) { throw new IllegalStateException(e.getMessage(), e); } // 检测本地存根类是否可赋值给接口类,若不可赋值则会抛出异常,提醒使用者本地存根类类型不合法 if (!interfaceClass.isAssignableFrom(localClass)) { throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName); } } // stub 和 local 均用于配置本地存根 if (stub != null) { // 此处的代码和上一个 if 分支的代码基本一致,这里省略了 } // 检测各种对象是否为空,为空则新建,或者抛出异常 checkApplication(); checkRegistry(); checkProtocol(); appendProperties(this); checkStubAndMock(interfaceClass); if (path == null || path.length() == 0) { path = interfaceName; } // 导出服务 doExportUrls(); // ProviderModel 表示服务提供者模型,此对象中存储了和服务提供者相关的信息。 // 比如服务的配置信息,服务实例等。每个被导出的服务对应一个 ProviderModel。 // ApplicationModel 持有所有的 ProviderModel。 ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), this, ref); ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel); } 以上就是配置检查的相关分析,代码比较多,需要大家耐心看一下。下面对配置检查的逻辑进行简单的总结,如下: 检测 <dubbo:service> 标签的 interface 属性合法性,不合法则抛出异常 检测 ProviderConfig、ApplicationConfig 等核心配置类对象是否为空,若为空,则尝试从其他配置类对象中获取相应的实例。 检测并处理泛化服务和普通服务类 检测本地存根配置,并进行相应的处理 对 ApplicationConfig、RegistryConfig 等配置类进行检测,为空则尝试创建,若无法创建则抛出异常 配置检查并非本文重点,因此我不打算对 doExport 方法所调用的方法进行分析(doExportUrls 方法除外)。在这些方法中,除了 appendProperties 方法稍微复杂一些,其他方法都还好。因此,大家可自行进行分析。好了,其他的就不多说了,继续向下分析。 2.1.2 多协议多注册中心导出服务 Dubbo 允许我们使用不同的协议导出服务,也允许我们向多个注册中心注册服务。Dubbo 在 doExportUrls 方法中对多协议,多注册中心进行了支持。相关代码如下: private void doExportUrls() { // 加载注册中心链接 List<URL> registryURLs = loadRegistries(true); // 遍历 protocols,导出每个服务 for (ProtocolConfig protocolConfig : protocols) { doExportUrlsFor1Protocol(protocolConfig, registryURLs); } } 上面代码比较简单,首先是通过 loadRegistries 加载注册中心链接,然后再遍历 ProtocolConfig 集合导出每个服务。并在导出服务的过程中,将服务注册到注册中心处。下面,我们先来看一下 loadRegistries 方法的逻辑。 protected List<URL> loadRegistries(boolean provider) { // 检测是否存在注册中心配置类,不存在则抛出异常 checkRegistry(); List<URL> registryList = new ArrayList<URL>(); if (registries != null && !registries.isEmpty()) { for (RegistryConfig config : registries) { String address = config.getAddress(); if (address == null || address.length() == 0) { // 若 address 为空,则将其设为 0.0.0.0 address = Constants.ANYHOST_VALUE; } // 从系统属性中加载注册中心地址 String sysaddress = System.getProperty("dubbo.registry.address"); if (sysaddress != null && sysaddress.length() > 0) { address = sysaddress; } // 判断 address 是否合法 if (address.length() > 0 && !RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) { Map<String, String> map = new HashMap<String, String>(); // 添加 ApplicationConfig 中的字段信息到 map 中 appendParameters(map, application); // 添加 RegistryConfig 字段信息到 map 中 appendParameters(map, config); map.put("path", RegistryService.class.getName()); map.put("dubbo", Version.getProtocolVersion()); map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis())); if (ConfigUtils.getPid() > 0) { map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid())); } if (!map.containsKey("protocol")) { if (ExtensionLoader.getExtensionLoader(RegistryFactory.class).hasExtension("remote")) { map.put("protocol", "remote"); } else { map.put("protocol", "dubbo"); } } // 解析得到 URL 列表,address 可能包含多个注册中心 ip, // 因此解析得到的是一个 URL 列表 List<URL> urls = UrlUtils.parseURLs(address, map); for (URL url : urls) { url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol()); // 将 URL 协议头设置为 registry url = url.setProtocol(Constants.REGISTRY_PROTOCOL); // 通过判断条件,决定是否添加 url 到 registryList 中,条件如下: // (服务提供者 && register = true 或 null) // || (非服务提供者 && subscribe = true 或 null) if ((provider && url.getParameter(Constants.REGISTER_KEY, true)) || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) { registryList.add(url); } } } } } return registryList; } 上面代码不是很复杂,包含如下逻辑: 检测是否存在注册中心配置类,不存在则抛出异常 构建参数映射集合,也就是 map 构建注册中心链接列表 遍历链接列表,并根据条件决定是否将其添加到 registryList 中 关于多协议多注册中心导出服务就先分析到这,代码不是很多,就不过多叙述了。接下来分析 URL 组装过程。 2.1.3 组装 URL 配置检查完毕后,紧接着要做的事情是根据配置,以及其他一些信息组装 URL。前面说过,URL 是 Dubbo 配置的载体,通过 URL 可让 Dubbo 的各种配置在各个模块之间传递。URL 之于 Dubbo,犹如水之于鱼,非常重要。大家在阅读 Dubbo 服务导出相关源码的过程中,要注意 URL 内容的变化。既然 URL 如此重要,那么下面我们来了解一下 URL 组装的过程。 private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) { String name = protocolConfig.getName(); // 如果协议名为空,或空串,则将协议名变量设置为 dubbo if (name == null || name.length() == 0) { name = "dubbo"; } Map<String, String> map = new HashMap<String, String>(); // 添加 side、版本、时间戳以及进程号等信息到 map 中 map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE); map.put(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion()); map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis())); if (ConfigUtils.getPid() > 0) { map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid())); } // 通过反射将对象的字段信息到 map 中 appendParameters(map, application); appendParameters(map, module); appendParameters(map, provider, Constants.DEFAULT_KEY); appendParameters(map, protocolConfig); appendParameters(map, this); // methods 为 MethodConfig 集合,MethodConfig 中存储了 <dubbo:method> 标签的配置信息 if (methods != null && !methods.isEmpty()) { // 这段代码用于添加 Callback 配置到 map 中,代码太长,待会单独分析 } // 检测 generic 是否为 "true",并根据检测结果向 map 中添加不同的信息 if (ProtocolUtils.isGeneric(generic)) { map.put(Constants.GENERIC_KEY, generic); map.put(Constants.METHODS_KEY, Constants.ANY_VALUE); } else { String revision = Version.getVersion(interfaceClass, version); if (revision != null && revision.length() > 0) { map.put("revision", revision); } // 为接口生成包裹类 Wrapper,Wrapper 中包含了接口的详细信息,比如接口方法名数组,字段信息等 String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); // 添加方法名到 map 中,如果包含多个方法名,则用逗号隔开,比如 method = init,destroy if (methods.length == 0) { logger.warn("NO method found in service interface ..."); map.put(Constants.METHODS_KEY, Constants.ANY_VALUE); } else { // 将逗号作为分隔符连接方法名,并将连接后的字符串放入 map 中 map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ",")); } } // 添加 token 到 map 中 if (!ConfigUtils.isEmpty(token)) { if (ConfigUtils.isDefault(token)) { map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString()); } else { map.put(Constants.TOKEN_KEY, token); } } // 判断协议名是否为 injvm if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) { protocolConfig.setRegister(false); map.put("notify", "false"); } // 获取上下文路径 String contextPath = protocolConfig.getContextpath(); if ((contextPath == null || contextPath.length() == 0) && provider != null) { contextPath = provider.getContextpath(); } // 获取 host 和 port String host = this.findConfigedHosts(protocolConfig, registryURLs, map); Integer port = this.findConfigedPorts(protocolConfig, name, map); // 组装 URL URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map); // 省略无关代码 } 上面的代码首先是将一些信息,比如版本、时间戳、方法名以及各种配置对象的字段信息放入到 map 中,map 中的内容将作为 URL 的查询字符串。构建好 map 后,紧接着是获取上下文路径、主机名以及端口号等信息。最后将 map 和主机名等数据传给 URL 构造方法创建 URL 对象。需要注意的是,这里出现的 URL 并非 java.net.URL,而是 com.alibaba.dubbo.common.URL。 上面省略了一段代码,这里简单分析一下。这段代码用于检测 <dubbo:argument> 标签中的配置信息,并将相关配置添加到 map 中。代码如下: private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) { // ... // methods 为 MethodConfig 集合,MethodConfig 中存储了 <dubbo:method> 标签的配置信息 if (methods != null && !methods.isEmpty()) { for (MethodConfig method : methods) { // 添加 MethodConfig 对象的字段信息到 map 中,键 = 方法名.属性名。 // 比如存储 <dubbo:method name="sayHello" retries="2"> 对应的 MethodConfig, // 键 = sayHello.retries,map = {"sayHello.retries": 2, "xxx": "yyy"} appendParameters(map, method, method.getName()); String retryKey = method.getName() + ".retry"; if (map.containsKey(retryKey)) { String retryValue = map.remove(retryKey); // 检测 MethodConfig retry 是否为 false,若是,则设置重试次数为0 if ("false".equals(retryValue)) { map.put(method.getName() + ".retries", "0"); } } // 获取 ArgumentConfig 列表 List<ArgumentConfig> arguments = method.getArguments(); if (arguments != null && !arguments.isEmpty()) { for (ArgumentConfig argument : arguments) { // 检测 type 属性是否为空,或者空串(分支1 ⭐️) if (argument.getType() != null && argument.getType().length() > 0) { Method[] methods = interfaceClass.getMethods(); if (methods != null && methods.length > 0) { for (int i = 0; i < methods.length; i++) { String methodName = methods[i].getName(); // 比对方法名,查找目标方法 if (methodName.equals(method.getName())) { Class<?>[] argtypes = methods[i].getParameterTypes(); if (argument.getIndex() != -1) { // 检测 ArgumentConfig 中的 type 属性与方法参数列表 // 中的参数名称是否一致,不一致则抛出异常(分支2 ⭐️) if (argtypes[argument.getIndex()].getName().equals(argument.getType())) { // 添加 ArgumentConfig 字段信息到 map 中, // 键前缀 = 方法名.index,比如: // map = {"sayHello.3": true} appendParameters(map, argument, method.getName() + "." + argument.getIndex()); } else { throw new IllegalArgumentException("argument config error: ..."); } } else { // 分支3 ⭐️ for (int j = 0; j < argtypes.length; j++) { Class<?> argclazz = argtypes[j]; // 从参数类型列表中查找类型名称为 argument.type 的参数 if (argclazz.getName().equals(argument.getType())) { appendParameters(map, argument, method.getName() + "." + j); if (argument.getIndex() != -1 && argument.getIndex() != j) { throw new IllegalArgumentException("argument config error: ..."); } } } } } } } // 用户未配置 type 属性,但配置了 index 属性,且 index != -1 } else if (argument.getIndex() != -1) { // 分支4 ⭐️ // 添加 ArgumentConfig 字段信息到 map 中 appendParameters(map, argument, method.getName() + "." + argument.getIndex()); } else { throw new IllegalArgumentException("argument config must set index or type"); } } } } } // ... } 上面这段代码 for 循环和 if else 分支嵌套太多,导致层次太深,不利于阅读,需要耐心看一下。大家在看这段代码时,注意把几个重要的条件分支找出来。只要理解了这几个分支的意图,就可以弄懂这段代码。我在上面代码中用⭐️符号标识出了4个重要的分支,下面用伪代码解释一下这几个分支的含义。 // 获取 ArgumentConfig 列表 for (遍历 ArgumentConfig 列表) { if (type 不为 null,也不为空串) { // 分支1 1. 通过反射获取 interfaceClass 的方法列表 for (遍历方法列表) { 1. 比对方法名,查找目标方法 2. 通过反射获取目标方法的参数类型数组 argtypes if (index != -1) { // 分支2 1. 从 argtypes 数组中获取下标 index 处的元素 argType 2. 检测 argType 的名称与 ArgumentConfig 中的 type 属性是否一致 3. 添加 ArgumentConfig 字段信息到 map 中,或抛出异常 } else { // 分支3 1. 遍历参数类型数组 argtypes,查找 argument.type 类型的参数 2. 添加 ArgumentConfig 字段信息到 map 中 } } } else if (index != -1) { // 分支4 1. 添加 ArgumentConfig 字段信息到 map 中 } } 在本节分析的源码中,appendParameters 这个方法出现的次数比较多,该方法用于将对象字段信息添加到 map 中。实现上则是通过反射获取目标对象的 getter 方法,并调用该方法获取属性值。然后再通过 getter 方法名解析出属性名,比如从方法名 getName 中可解析出属性 name。如果用户传入了属性名前缀,此时需要将属性名加入前缀内容。最后将 <属性名,属性值> 键值对存入到 map 中就行了。限于篇幅原因,这里就不分析 appendParameters 方法的源码了,大家请自行分析。 2.2 导出 Dubbo 服务 前置工作做完,接下来就可以进行服务导出工作。服务导出,分为导出到本地 (JVM),和导出到远程。在深入分析服务导出源码前,我们先来从宏观层面上看一下服务导出逻辑。如下: private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) { // 省略无关代码 if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) .hasExtension(url.getProtocol())) { // 加载 ConfiguratorFactory,并生成 Configurator 配置 url url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) .getExtension(url.getProtocol()).getConfigurator(url).configure(url); } String scope = url.getParameter(Constants.SCOPE_KEY); // 如果 scope = none,则什么都不做 if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) { // scope != remote,导出到本地 if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) { exportLocal(url); } // scope != local,导出到远程 if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) { if (registryURLs != null && !registryURLs.isEmpty()) { for (URL registryURL : registryURLs) { url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY)); // 加载监视器链接 URL monitorUrl = loadMonitor(registryURL); if (monitorUrl != null) { // 将监视器链接作为参数添加到 url 中 url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString()); } String proxy = url.getParameter(Constants.PROXY_KEY); if (StringUtils.isNotEmpty(proxy)) { registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy); } // 为服务提供类(ref)生成 Invoker Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())); // DelegateProviderMetaDataInvoker 仅用于持有 Invoker 和 ServiceConfig DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); // 导出服务,并生成 Exporter Exporter<?> exporter = protocol.export(wrapperInvoker); exporters.add(exporter); } } else { // 不存在注册中心,仅导出服务 Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url); DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); Exporter<?> exporter = protocol.export(wrapperInvoker); exporters.add(exporter); } } } this.urls.add(url); } 上面代码根据 url 中的 scope 参数决定服务导出方式,分别如下: scope = none,不导出服务 scope != remote,导出到本地 scope != local,导出到远程 不管是导出到本地,还是远程。进行服务导出之前,均需要先创建 Invoker。这是一个很重要的步骤,因此接下来我会先分析 Invoker 的创建过程。 2.2.1 Invoker 创建过程 在 Dubbo 中,Invoker 是一个非常重要的模型。在服务提供端,以及服务引用端均会出现 Invoker。Dubbo 官方文档中对 Invoker 进行了说明,这里引用一下。 Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。 既然 Invoker 如此重要,那么我们很有必要搞清楚 Invoker 的用途。Invoker 是由 ProxyFactory 创建而来,Dubbo 默认的 ProxyFactory 实现类是 JavassistProxyFactory。下面我们到 JavassistProxyFactory 代码中,探索 Invoker 的创建过程。如下: -- JavassistProxyFactory public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) { // 为目标类创建 Wrapper final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type); // 创建匿名 Invoker 类对象,并实现 doInvoke 方法。 return new AbstractProxyInvoker<T>(proxy, type, url) { @Override protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable { // 调用 Wrapper 的 invokeMethod 方法,invokeMethod 最终会调用目标方法 return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments); } }; } 如上,JavassistProxyFactory 创建了一个继承自 AbstractProxyInvoker 类的匿名对象,并覆写了抽象方法 doInvoke。覆写后的 doInvoke 逻辑比较简单,仅是将调用请求转发给了 Wrapper 类的 invokeMethod 方法。Wrapper 用于“包裹”目标类,Wrapper 是一个抽象类,仅可通过 getWrapper(Class) 方法创建子类。在创建 Wrapper 子类的过程中,子类代码生成逻辑会对 getWrapper 方法传入的 Class 对象进行解析,拿到诸如类方法,类成员变量等信息。以及生成 invokeMethod 方法代码,和其他一些方法代码。代码生成完毕后,通过 Javassist 生成 Class 对象,最后再通过反射创建 Wrapper 实例。相关的代码如下: public static Wrapper getWrapper(Class<?> c) { while (ClassGenerator.isDynamicClass(c)) c = c.getSuperclass(); if (c == Object.class) return OBJECT_WRAPPER; // 访存 Wrapper ret = WRAPPER_MAP.get(c); if (ret == null) { // 缓存未命中,创建 Wrapper ret = makeWrapper(c); // 写入缓存 WRAPPER_MAP.put(c, ret); } return ret; } getWrapper 方法只是包含了一些缓存操作逻辑,非重点。下面我们重点关注 makeWrapper 方法。 private static Wrapper makeWrapper(Class<?> c) { // 检测 c 是否为私有类型,若是则抛出异常 if (c.isPrimitive()) throw new IllegalArgumentException("Can not create wrapper for primitive type: " + c); String name = c.getName(); ClassLoader cl = ClassHelper.getClassLoader(c); // c1 用于存储 setPropertyValue 方法代码 StringBuilder c1 = new StringBuilder("public void setPropertyValue(Object o, String n, Object v){ "); // c2 用于存储 getPropertyValue 方法代码 StringBuilder c2 = new StringBuilder("public Object getPropertyValue(Object o, String n){ "); // c3 用于存储 invokeMethod 方法代码 StringBuilder c3 = new StringBuilder("public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws " + InvocationTargetException.class.getName() + "{ "); // 生成类型转换代码及异常捕捉代码,比如: // DemoService w; try { w = ((DemoServcie) $1); }}catch(Throwable e){ throw new IllegalArgumentException(e); } c1.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }"); c2.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }"); c3.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }"); // pts 用于存储成员变量名和类型 Map<String, Class<?>> pts = new HashMap<String, Class<?>>(); // ms 用于存储方法描述信息(可理解为方法签名)及 Method 实例 Map<String, Method> ms = new LinkedHashMap<String, Method>(); // mns 为方法名列表 List<String> mns = new ArrayList<String>(); // dmns 用于存储定义在当前类中的方法的名称 List<String> dmns = new ArrayList<String>(); // -------------------------------- 分割线1 ------------------------------------- // 获取 public 访问级别的字段,并为所有字段生成条件判断语句 for (Field f : c.getFields()) { String fn = f.getName(); Class<?> ft = f.getType(); if (Modifier.isStatic(f.getModifiers()) || Modifier.isTransient(f.getModifiers())) // 忽略关键字 static 或 transient 修饰的变量 continue; // 生成条件判断及赋值语句,比如: // if( $2.equals("name") ) { w.name = (java.lang.String) $3; return;} // if( $2.equals("age") ) { w.age = ((Number) $3).intValue(); return;} c1.append(" if( $2.equals(\"").append(fn).append("\") ){ w.").append(fn).append("=").append(arg(ft, "$3")).append("; return; }"); // 生成条件判断及返回语句,比如: // if( $2.equals("name") ) { return ($w)w.name; } c2.append(" if( $2.equals(\"").append(fn).append("\") ){ return ($w)w.").append(fn).append("; }"); // 存储 <字段名, 字段类型> 键值对到 pts 中 pts.put(fn, ft); } // -------------------------------- 分割线2 ------------------------------------- Method[] methods = c.getMethods(); // 检测 c 中是否包含在当前类中声明的方法 boolean hasMethod = hasMethods(methods); if (hasMethod) { c3.append(" try{"); } for (Method m : methods) { if (m.getDeclaringClass() == Object.class) // 忽略 Object 中定义的方法 continue; String mn = m.getName(); // 生成方法名判断语句,示例如下: // if ( "sayHello".equals( $2 ) c3.append(" if( \"").append(mn).append("\".equals( $2 ) "); int len = m.getParameterTypes().length; // 生成运行时传入参数的数量与方法的参数列表长度判断语句,示例如下: // && $3.length == 2 c3.append(" && ").append(" $3.length == ").append(len); boolean override = false; for (Method m2 : methods) { // 检测方法是否存在重载情况,条件为:方法对象不同 && 方法名相同 if (m != m2 && m.getName().equals(m2.getName())) { override = true; break; } } // 对重载方法进行处理,考虑下面的方法: // 1. void sayHello(Integer, String) // 2. void sayHello(Integer, Integer) // 方法名相同,参数列表长度也相同,因此不能仅通过这两项判断两个方法是否相等。 // 需要进一步判断方法的参数类型 if (override) { if (len > 0) { for (int l = 0; l < len; l++) { // && $3[0].getName().equals("java.lang.Integer") // && $3[1].getName().equals("java.lang.String") c3.append(" && ").append(" $3[").append(l).append("].getName().equals(\"") .append(m.getParameterTypes()[l].getName()).append("\")"); } } } // 添加 ) {,完成方法判断语句,此时生成的方法可能如下(已格式化): // if ("sayHello".equals($2) // && $3.length == 2 // && $3[0].getName().equals("java.lang.Integer") // && $3[1].getName().equals("java.lang.String")) { c3.append(" ) { "); // 根据返回值类型生成目标方法调用语句 if (m.getReturnType() == Void.TYPE) // w.sayHello((java.lang.Integer)$4[0], (java.lang.String)$4[1]); return null; c3.append(" w.").append(mn).append('(').append(args(m.getParameterTypes(), "$4")).append(");").append(" return null;"); else // return w.sayHello((java.lang.Integer)$4[0], (java.lang.String)$4[1]); c3.append(" return ($w)w.").append(mn).append('(').append(args(m.getParameterTypes(), "$4")).append(");"); // 添加 }, 当前”方法判断条件“代码生成完毕,示例代码如下(已格式化): // if ("sayHello".equals($2) // && $3.length == 2 // && $3[0].getName().equals("java.lang.Integer") // && $3[1].getName().equals("java.lang.String")) { // // w.sayHello((java.lang.Integer)$4[0], (java.lang.String)$4[1]); // return null; // } c3.append(" }"); // 添加方法名到 mns 集合中 mns.add(mn); // 检测当前方法是否在 c 中被声明的 if (m.getDeclaringClass() == c) // 若是,则将当前方法名添加到 dmns 中 dmns.add(mn); ms.put(ReflectUtils.getDesc(m), m); } if (hasMethod) { // 添加异常捕捉语句 c3.append(" } catch(Throwable e) { "); c3.append(" throw new java.lang.reflect.InvocationTargetException(e); "); c3.append(" }"); } // 添加 NoSuchMethodException 异常抛出代码 c3.append(" throw new " + NoSuchMethodException.class.getName() + "(\"Not found method \\\"\"+$2+\"\\\" in class " + c.getName() + ".\"); }"); // -------------------------------- 分割线3 ------------------------------------- Matcher matcher; // 处理 get/set 方法 for (Map.Entry<String, Method> entry : ms.entrySet()) { String md = entry.getKey(); Method method = (Method) entry.getValue(); // 匹配以 get 开头的方法 if ((matcher = ReflectUtils.GETTER_METHOD_DESC_PATTERN.matcher(md)).matches()) { // 获取属性名 String pn = propertyName(matcher.group(1)); // 生成属性判断以及返回语句,示例如下: // if( $2.equals("name") ) { return ($w).w.getName(); } c2.append(" if( $2.equals(\"").append(pn).append("\") ){ return ($w)w.").append(method.getName()).append("(); }"); pts.put(pn, method.getReturnType()); // 匹配以 is/has/can 开头的方法 } else if ((matcher = ReflectUtils.IS_HAS_CAN_METHOD_DESC_PATTERN.matcher(md)).matches()) { String pn = propertyName(matcher.group(1)); // 生成属性判断以及返回语句,示例如下: // if( $2.equals("dream") ) { return ($w).w.hasDream(); } c2.append(" if( $2.equals(\"").append(pn).append("\") ){ return ($w)w.").append(method.getName()).append("(); }"); pts.put(pn, method.getReturnType()); // 匹配以 set 开头的方法 } else if ((matcher = ReflectUtils.SETTER_METHOD_DESC_PATTERN.matcher(md)).matches()) { Class<?> pt = method.getParameterTypes()[0]; String pn = propertyName(matcher.group(1)); // 生成属性判断以及 setter 调用语句,示例如下: // if( $2.equals("name") ) { w.setName((java.lang.String)$3); return; } c1.append(" if( $2.equals(\"").append(pn).append("\") ){ w.").append(method.getName()).append("(").append(arg(pt, "$3")).append("); return; }"); pts.put(pn, pt); } } // 添加 NoSuchPropertyException 异常抛出代码 c1.append(" throw new " + NoSuchPropertyException.class.getName() + "(\"Not found property \\\"\"+$2+\"\\\" filed or setter method in class " + c.getName() + ".\"); }"); c2.append(" throw new " + NoSuchPropertyException.class.getName() + "(\"Not found property \\\"\"+$2+\"\\\" filed or setter method in class " + c.getName() + ".\"); }"); // -------------------------------- 分割线4 ------------------------------------- long id = WRAPPER_CLASS_COUNTER.getAndIncrement(); // 创建类生成器 ClassGenerator cc = ClassGenerator.newInstance(cl); // 设置类名及超类 cc.setClassName((Modifier.isPublic(c.getModifiers()) ? Wrapper.class.getName() : c.getName() + "$sw") + id); cc.setSuperClass(Wrapper.class); // 添加默认构造方法 cc.addDefaultConstructor(); // 添加字段 cc.addField("public static String[] pns;"); cc.addField("public static " + Map.class.getName() + " pts;"); cc.addField("public static String[] mns;"); cc.addField("public static String[] dmns;"); for (int i = 0, len = ms.size(); i < len; i++) cc.addField("public static Class[] mts" + i + ";"); // 添加方法代码 cc.addMethod("public String[] getPropertyNames(){ return pns; }"); cc.addMethod("public boolean hasProperty(String n){ return pts.containsKey($1); }"); cc.addMethod("public Class getPropertyType(String n){ return (Class)pts.get($1); }"); cc.addMethod("public String[] getMethodNames(){ return mns; }"); cc.addMethod("public String[] getDeclaredMethodNames(){ return dmns; }"); cc.addMethod(c1.toString()); cc.addMethod(c2.toString()); cc.addMethod(c3.toString()); try { // 生成类 Class<?> wc = cc.toClass(); // 设置字段值 wc.getField("pts").set(null, pts); wc.getField("pns").set(null, pts.keySet().toArray(new String[0])); wc.getField("mns").set(null, mns.toArray(new String[0])); wc.getField("dmns").set(null, dmns.toArray(new String[0])); int ix = 0; for (Method m : ms.values()) wc.getField("mts" + ix++).set(null, m.getParameterTypes()); // 创建 Wrapper 实例 return (Wrapper) wc.newInstance(); } catch (RuntimeException e) { throw e; } catch (Throwable e) { throw new RuntimeException(e.getMessage(), e); } finally { cc.release(); ms.clear(); mns.clear(); dmns.clear(); } } 上面代码很长,大家耐心看一下。我在上面代码中做了大量的注释,并按功能对代码进行了分块,以帮助大家理解代码逻辑。下面对这段代码进行讲解。首先我们把目光移到分割线1之上的代码,这段代码主要用于进行一些初始化操作。比如创建 c1、c2、c3 以及 pts、ms、mns 等变量,以及向 c1、c2、c3 中添加方法定义和类型类型转换代码。接下来是分割线1到分割线2之间的代码,这段代码用于为 public 级别的字段生成条件判断取值与赋值代码。这段代码不是很难看懂,就不多说了。继续向下看,分割线2和分隔线3之间的代码用于为定义在当前类中的方法生成判断语句,和方法调用语句。因为需要对方法重载进行校验,因此到这这段代码看起来有点复杂。不过耐心开一下,也不是很难理解。接下来是分割线3和分隔线4之间的代码,这段代码用于处理 getter、setter 以及以 is/has/can 开头的方法。处理方式是通过正则表达式获取方法类型(get/set/is/...),以及属性名。之后为属性名生成判断语句,然后为方法生成调用语句。最后我们再来看一下分隔线4以下的代码,这段代码通过 ClassGenerator 为刚刚生成的代码构建 Class 类,并通过反射创建对象。ClassGenerator 是 Dubbo 自己封装的,该类的核心是 toClass() 的重载方法 toClass(ClassLoader, ProtectionDomain),该方法通过 javassist 构建 Class。这里就不分析 toClass 方法了,大家请自行分析。 阅读 Wrapper 类代码需要对 javassist 框架有所了解。关于 javassist,大家如果不熟悉,请自行查阅资料,本节不打算介绍 javassist 相关内容。 好了,关于 Wrapper 类生成过程就分析到这。如果大家看的不是很明白,可以单独为 Wrapper 创建单元测试,然后单步调试。并将生成的代码拷贝出来,格式化后再进行观察和理解。好了,本节先到这。 2.2.2 导出服务到本地 本节我们来看一下服务导出相关的代码,按照代码执行顺序,本节先来分析导出服务到本地的过程。相关代码如下: private void exportLocal(URL url) { // 如果 URL 的协议头等于 injvm,说明已经导出到本地了,无需再次导出 if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) { URL local = URL.valueOf(url.toFullString()) .setProtocol(Constants.LOCAL_PROTOCOL) // 设置协议头为 injvm .setHost(LOCALHOST) .setPort(0); ServiceClassHolder.getInstance().pushServiceClass(getServiceClass(ref)); // 创建 Invoker,并导出服务,这里的 protocol 会在运行时调用 InjvmProtocol 的 export 方法 Exporter<?> exporter = protocol.export( proxyFactory.getInvoker(ref, (Class) interfaceClass, local)); exporters.add(exporter); } } exportLocal 方法比较简单,首先根据 URL 协议头决定是否导出服务。若需导出,则创建一个新的 URL 并将协议头、主机名以及端口设置成新的值。然后创建 Invoker,并调用 InjvmProtocol 的 export 方法导出服务。下面我们来看一下 InjvmProtocol 的 export 方法都做了哪些事情。 public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { // 创建 InjvmExporter return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap); } 如上,InjvmProtocol 的 export 方法仅创建了一个 InjvmExporter,无其他逻辑。到此导出服务到本地就分析完了,接下来,我们继续分析导出服务到远程的过程。 2.2.3 导出服务到远程 与导出服务到本地相比,导出服务到远程的过程要复杂不少,其包含了服务导出与服务注册两个过程。这两个过程涉及到了大量的调用,因此比较复杂。不过不管再难,我们都要看一下,万一看懂了呢。按照代码执行顺序,本节先来分析服务导出逻辑,服务注册逻辑将在下一节进行分析。下面开始分析,我们把目光移动到 RegistryProtocol 的 export 方法上。 public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException { // 导出服务 final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker); // 获取注册中心 URL,以 zookeeper 注册中心为例,得到的示例 URL 如下: // zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo%3A%2F%2F172.17.48.52%3A20880%2Fcom.alibaba.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider URL registryUrl = getRegistryUrl(originInvoker); // 根据 URL 加载 Registry 实现类,比如 ZookeeperRegistry final Registry registry = getRegistry(originInvoker); // 获取已注册的服务提供者 URL,比如: // dubbo://172.17.48.52:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker); // 获取 register 参数 boolean register = registeredProviderUrl.getParameter("register", true); // 向服务提供者与消费者注册表中注册服务提供者 ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl); // 根据 register 的值决定是否注册服务 if (register) { // 向注册中心注册服务 register(registryUrl, registeredProviderUrl); ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true); } // 获取订阅 URL,比如: // provider://172.17.48.52:20880/com.alibaba.dubbo.demo.DemoService?category=configurators&check=false&anyhost=true&application=demo-provider&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl); // 创建监听器 final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker); overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener); // 向注册中心进行订阅 override 数据 registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); // 创建并返回 DestroyableExporter return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl); } 上面代码看起来比较复杂,主要做如下一些操作: 调用 doLocalExport 导出服务 向注册中心注册服务 向注册中心进行订阅 override 数据 创建并返回 DestroyableExporter 在以上操作中,除了创建并返回 DestroyableExporter 没啥难度外,其他几步操作都不是很简单。这其中,导出服务和注册服务是本章要重点分析的逻辑。 订阅 override 数据这个是非重点内容,后面会简单介绍一下。下面开始本节的分析,先来分析 doLocalExport 方法的逻辑,如下: private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) { String key = getCacheKey(originInvoker); // 访问缓存 ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key); if (exporter == null) { synchronized (bounds) { exporter = (ExporterChangeableWrapper<T>) bounds.get(key); if (exporter == null) { // 创建 Invoker 为委托类对象 final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker)); // 调用 protocol 的 export 方法导出服务 exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker); // 写缓存 bounds.put(key, exporter); } } } return exporter; } 上面的代码是典型的双重检查,这个大家应该都知道。接下来,我们把重点放在 Protocol 的 export 方法上。假设运行时协议为 dubbo,此处的 protocol 会在运行时加载 DubboProtocol,并调用 DubboProtocol 的 export 方法。我们目光转移到 DubboProtocol 的 export 方法上,相关分析如下: public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { URL url = invoker.getUrl(); // 获取服务标识,理解成服务坐标也行。由服务组名,服务名,服务版本号以及端口组成。比如: // demoGroup/com.alibaba.dubbo.demo.DemoService:1.0.1:20880 String key = serviceKey(url); // 创建 DubboExporter DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap); // 将 <key, exporter> 键值对放入缓存中 exporterMap.put(key, exporter); // 以下代码应该和本地存根有关,代码不难看懂,但具体用途暂时不清楚,先忽略 Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT); Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false); if (isStubSupportEvent && !isCallbackservice) { String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY); if (stubServiceMethods == null || stubServiceMethods.length() == 0) { // 省略日志打印代码 } else { stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods); } } // 启动服务器 openServer(url); // 优化序列化 optimizeSerialization(url); return exporter; } 如上,我们重点关注 DubboExporter 的创建以及 openServer 方法,其他逻辑看不懂也没关系,不影响理解服务导出过程。另外,DubboExporter 的代码比较简单,就不分析了。下面分析 openServer 方法。 private void openServer(URL url) { // 获取 host:port,并将其作为服务器实例的 key,用于标识当前的服务器实例 String key = url.getAddress(); boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true); if (isServer) { // 访问缓存 ExchangeServer server = serverMap.get(key); if (server == null) { // 创建服务器实例 serverMap.put(key, createServer(url)); } else { // 服务器已创建,则根据 url 中的配置重置服务器 server.reset(url); } } } 如上,在同一台机器上(单网卡),同一个端口上仅允许启动一个服务器实例。若某个端口上已有服务器实例,此时则调用 reset 方法重置服务器的一些配置。考虑到篇幅问题,关于服务器实例重置的代码就不分析了。接下来分析服务器实例的创建过程。如下: private ExchangeServer createServer(URL url) { url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, // 添加心跳检测配置到 url 中 url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT)); // 获取 server 参数,默认为 netty String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER); // 通过 SPI 检测是否存在 server 参数所代表的 Transporter 拓展,不存在则抛出异常 if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) throw new RpcException("Unsupported server type: " + str + ", url: " + url); // 添加编码解码器参数 url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME); ExchangeServer server; try { // 创建 ExchangeServer server = Exchangers.bind(url, requestHandler); } catch (RemotingException e) { throw new RpcException("Fail to start server..."); } // 获取 client 参数,可指定 netty,mina str = url.getParameter(Constants.CLIENT_KEY); if (str != null && str.length() > 0) { // 获取所有的 Transporter 实现类名称集合,比如 supportedTypes = [netty, mina] Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions(); // 检测当前 Dubbo 所支持的 Transporter 实现类名称列表中, // 是否包含 client 所表示的 Transporter,若不包含,则抛出异常 if (!supportedTypes.contains(str)) { throw new RpcException("Unsupported client type..."); } } return server; } 如上,createServer 包含三个核心的操作。第一是检测是否存在 server 参数所代表的 Transporter 拓展,不存在则抛出异常。第二是创建服务器实例。第三是检测是否支持 client 参数所表示的 Transporter 拓展,不存在也是抛出异常。两次检测操作所对应的代码比较直白了,无需多说。但创建服务器的操作目前还不是很清晰,我们继续往下看。 public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException { if (url == null) { throw new IllegalArgumentException("url == null"); } if (handler == null) { throw new IllegalArgumentException("handler == null"); } url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange"); // 获取 Exchanger,默认为 HeaderExchanger。 // 紧接着调用 HeaderExchanger 的 bind 方法创建 ExchangeServer 实例 return getExchanger(url).bind(url, handler); } 上面代码比较简单,就不多说了。下面看一下 HeaderExchanger 的 bind 方法。 public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException { // 创建 HeaderExchangeServer 实例,该方法包含了多步操作,本别如下: // 1. new HeaderExchangeHandler(handler) // 2. new DecodeHandler(new HeaderExchangeHandler(handler)) // 3. Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))) return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))); } HeaderExchanger 的 bind 方法包含的逻辑比较多,但目前我们仅需关心 Transporters 的 bind 方法逻辑即可。该方法的代码如下: public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException { if (url == null) { throw new IllegalArgumentException("url == null"); } if (handlers == null || handlers.length == 0) { throw new IllegalArgumentException("handlers == null"); } ChannelHandler handler; if (handlers.length == 1) { handler = handlers[0]; } else { // 如果 handlers 元素数量大于1,则创建 ChannelHandler 分发器 handler = new ChannelHandlerDispatcher(handlers); } // 获取自适应 Transporter 实例,并调用实例方法 return getTransporter().bind(url, handler); } 如上,getTransporter() 方法获取的 Transporter 是在运行时动态创建的,类名为 Transporter$Adaptive,也就是自适应拓展类。我在上一篇文章中详细分析了自适应拓展类的生成过程,对自适应拓展类不了解的同学可以参考我之前的文章,这里不再赘述。Transporter$Adaptive 会在运行时根据传入的 URL 参数决定加载什么类型的 Transporter,默认为 NettyTransporter。下面我们继续跟下去,这次分析的是 NettyTransporter 的 bind 方法。 public Server bind(URL url, ChannelHandler listener) throws RemotingException { // 创建 NettyServer return new NettyServer(url, listener); } 这里仅有一句创建 NettyServer 的代码,没啥好讲的,我们继续向下看。 public class NettyServer extends AbstractServer implements Server { public NettyServer(URL url, ChannelHandler handler) throws RemotingException { // 调用父类构造方法 super(url, ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME))); } } public abstract class AbstractServer extends AbstractEndpoint implements Server { public AbstractServer(URL url, ChannelHandler handler) throws RemotingException { // 调用父类构造方法,这里就不用跟进去了,没什么复杂逻辑 super(url, handler); localAddress = getUrl().toInetSocketAddress(); // 获取 ip 和端口 String bindIp = getUrl().getParameter(Constants.BIND_IP_KEY, getUrl().getHost()); int bindPort = getUrl().getParameter(Constants.BIND_PORT_KEY, getUrl().getPort()); if (url.getParameter(Constants.ANYHOST_KEY, false) || NetUtils.isInvalidLocalHost(bindIp)) { // 设置 ip 为 0.0.0.0 bindIp = NetUtils.ANYHOST; } bindAddress = new InetSocketAddress(bindIp, bindPort); // 获取最大可接受连接数 this.accepts = url.getParameter(Constants.ACCEPTS_KEY, Constants.DEFAULT_ACCEPTS); this.idleTimeout = url.getParameter(Constants.IDLE_TIMEOUT_KEY, Constants.DEFAULT_IDLE_TIMEOUT); try { // 调用模板方法 doOpen 启动服务器 doOpen(); } catch (Throwable t) { throw new RemotingException("Failed to bind "); } DataStore dataStore = ExtensionLoader.getExtensionLoader(DataStore.class).getDefaultExtension(); executor = (ExecutorService) dataStore.get(Constants.EXECUTOR_SERVICE_COMPONENT_KEY, Integer.toString(url.getPort())); } protected abstract void doOpen() throws Throwable; protected abstract void doClose() throws Throwable; } 上面多数代码为赋值代码,不需要多讲。我们重点关注 doOpen 抽象方法,该方法需要子类实现。下面回到 NettyServer 中。 protected void doOpen() throws Throwable { NettyHelper.setNettyLoggerFactory(); // 创建 boss 和 worker 线程池 ExecutorService boss = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerBoss", true)); ExecutorService worker = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerWorker", true)); ChannelFactory channelFactory = new NioServerSocketChannelFactory(boss, worker, getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS)); // 创建 ServerBootstrap bootstrap = new ServerBootstrap(channelFactory); final NettyHandler nettyHandler = new NettyHandler(getUrl(), this); channels = nettyHandler.getChannels(); bootstrap.setOption("child.tcpNoDelay", true); // 设置 PipelineFactory bootstrap.setPipelineFactory(new ChannelPipelineFactory() { @Override public ChannelPipeline getPipeline() { NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this); ChannelPipeline pipeline = Channels.pipeline(); pipeline.addLast("decoder", adapter.getDecoder()); pipeline.addLast("encoder", adapter.getEncoder()); pipeline.addLast("handler", nettyHandler); return pipeline; } }); // 绑定到指定的 ip 和端口上 channel = bootstrap.bind(getBindAddress()); } 以上就是 NettyServer 创建的过程,dubbo 默认使用的 NettyServer 是基于 netty 3.x 版本实现的,比较老了。因此 Dubbo 中另外提供了 netty 4.x 版本的 NettyServer,大家可在使用 Dubbo 的过程中按需进行配置。 到此,关于服务导出的过程就分析完了。整个过程比较复杂,大家在分析的过程中耐心一些。并且多写 Demo 进行进行调试,以便能够更好的理解代码逻辑。好了,本节内容先到这里,接下来分析服务导出的另一块逻辑 -- 服务注册。 2.2.4 服务注册 本节我们来分析服务注册过程,服务注册操作对于 Dubbo 来说不是必需的,通过服务直连的方式就可以绕过注册中心。但通常我们不会这么做,直连方式不利于服务治理,仅推荐在测试环境测试服务时使用。对于 Dubbo 来说,注册中心虽不是必需,但却是必要的。因此,关于注册中心以及服务注册相关逻辑,我们也需要搞懂。 本节内容以 Zookeeper 注册中心作为分析目标,其他类型注册中心大家可自行分析。下面从服务注册的入口方法开始分析,我们把目光再次移到 RegistryProtocol 的 export 方法上。如下: public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException { // ${导出服务} // 省略其他代码 boolean register = registeredProviderUrl.getParameter("register", true); if (register) { // 注册服务 register(registryUrl, registeredProviderUrl); ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true); } final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl); final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker); overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener); // 订阅 override 数据 registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); // 省略部分代码 } RegistryProtocol 的 export 方法包含了服务导出,注册,以及数据订阅等逻辑。其中服务导出逻辑上一节已经分析过了,本节将分析服务注册逻辑,数据订阅逻辑将在下一节进行分析。下面开始本节的分析,相关代码如下: public void register(URL registryUrl, URL registedProviderUrl) { // 获取 Registry Registry registry = registryFactory.getRegistry(registryUrl); // 注册服务 registry.register(registedProviderUrl); } register 方法包含两步操作,第一步是获取注册中心实例,第二步是向注册中心注册服务。接下来,我分两节内容对这两步操作进行分析。按照顺序,先来分析获取注册中心的逻辑。 2.2.4.1 创建注册中心 本节内容以 Zookeeper 注册中心为例进行分析。下面先来看一下 getRegistry 方法的源码,这个方法由 AbstractRegistryFactory 实现。如下: public Registry getRegistry(URL url) { url = url.setPath(RegistryService.class.getName()) .addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName()) .removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY); String key = url.toServiceString(); LOCK.lock(); try { // 访问缓存 Registry registry = REGISTRIES.get(key); if (registry != null) { return registry; } // 缓存未命中,创建 Registry 实例 registry = createRegistry(url); if (registry == null) { throw new IllegalStateException("Can not create registry..."); } // 写入缓存 REGISTRIES.put(key, registry); return registry; } finally { LOCK.unlock(); } } protected abstract Registry createRegistry(URL url); 如上,getRegistry 方法先访问缓存,缓存未命中则调用 createRegistry 创建 Registry,然后写入缓存。这里的 createRegistry 是一个模板方法,由具体的子类实现。因此,下面我们到 ZookeeperRegistryFactory 中探究一番。 public class ZookeeperRegistryFactory extends AbstractRegistryFactory { // zookeeperTransporter 由 SPI 在运行时注入,类型为 ZookeeperTransporter$Adaptive private ZookeeperTransporter zookeeperTransporter; public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) { this.zookeeperTransporter = zookeeperTransporter; } @Override public Registry createRegistry(URL url) { // 创建 ZookeeperRegistry return new ZookeeperRegistry(url, zookeeperTransporter); } } ZookeeperRegistryFactory 的 createRegistry 方法仅包含一句代码,无需解释,继续跟下去。 public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) { super(url); if (url.isAnyHost()) { throw new IllegalStateException("registry address == null"); } // 获取组名,默认为 dubbo String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT); if (!group.startsWith(Constants.PATH_SEPARATOR)) { // group = "/" + group group = Constants.PATH_SEPARATOR + group; } this.root = group; // 创建 Zookeeper 客户端,默认为 CuratorZookeeperTransporter zkClient = zookeeperTransporter.connect(url); // 添加状态监听器 zkClient.addStateListener(new StateListener() { @Override public void stateChanged(int state) { if (state == RECONNECTED) { try { recover(); } catch (Exception e) { logger.error(e.getMessage(), e); } } } }); } 在上面的代码代码中,我们重点关注 ZookeeperTransporter 的 connect 方法调用,这个方法用于创建 Zookeeper 客户端。创建好 Zookeeper 客户端,意味着注册中心的创建过程就结束了。不过,显然我们不能就此停止,难道大家没有兴趣了解一下 Zookeeper 客户端的创建过程吗?如果有,那么继续向下看。没有的话,直接跳到下一节。那我接着分析了。 前面说过,这里的 zookeeperTransporter 类型为自适应拓展类,因此 connect 方法会在被调用时决定加载什么类型的 ZookeeperTransporter 拓展,默认为 CuratorZookeeperTransporter。下面我们到 CuratorZookeeperTransporter 中看一看。 public ZookeeperClient connect(URL url) { // 创建 CuratorZookeeperClient return new CuratorZookeeperClient(url); } 上面方法仅用于创建 CuratorZookeeperClient 实例,没什么好说的,继续往下看。 public class CuratorZookeeperClient extends AbstractZookeeperClient<CuratorWatcher> { private final CuratorFramework client; public CuratorZookeeperClient(URL url) { super(url); try { // 创建 CuratorFramework 构造器 CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder() .connectString(url.getBackupAddress()) .retryPolicy(new RetryNTimes(1, 1000)) .connectionTimeoutMs(5000); String authority = url.getAuthority(); if (authority != null && authority.length() > 0) { builder = builder.authorization("digest", authority.getBytes()); } // 构建 CuratorFramework 实例 client = builder.build(); // 添加监听器 client.getConnectionStateListenable().addListener(new ConnectionStateListener() { @Override public void stateChanged(CuratorFramework client, ConnectionState state) { if (state == ConnectionState.LOST) { CuratorZookeeperClient.this.stateChanged(StateListener.DISCONNECTED); } else if (state == ConnectionState.CONNECTED) { CuratorZookeeperClient.this.stateChanged(StateListener.CONNECTED); } else if (state == ConnectionState.RECONNECTED) { CuratorZookeeperClient.this.stateChanged(StateListener.RECONNECTED); } } }); // 启动客户端 client.start(); } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); } } } CuratorZookeeperClient 构造方法主要用于创建和启动 CuratorFramework 实例。以上基本上都是 Curator 框架的代码,大家如果对 Curator 框架不是很了解,可以参考 Curator 官方文档,并写点 Demo 跑跑。 本节分析了 ZookeeperRegistry 实例的创建过程,整个过程并不是很复杂。大家在看完分析后,可以自行调试,以加深印象。现在注册中心实例创建好了,接下来要做的事情是向注册中心注册服务,我们继续往下看。 2.2.4.2 节点创建 以 Zookeeper 为例,所谓的服务注册,本质上是将服务配置数据写入到 Zookeeper 的某个路径的节点下。为了验证这个说法,下面我们将 Dobbo 官方提供提供的实例跑起来,然后通过 Zookeeper 可视化客户端 ZooInspector 查看节点数据。如下: 从上图中可以看到 com.alibaba.dubbo.demo.DemoService 这个服务对应的配置信息(存储在 URL 中)最终被注册到了 /dubbo/com.alibaba.dubbo.demo.DemoService/providers/ 节点下。搞懂了服务注册的本质,那么接下来我们就可以去阅读服务注册的代码了。服务注册的接口为 register(URL),这个方法定义在 FailbackRegistry 抽象类中。方法代码如下: public void register(URL url) { super.register(url); failedRegistered.remove(url); failedUnregistered.remove(url); try { // 模板方法,由子类实现 doRegister(url); } catch (Exception e) { Throwable t = e; // 获取 check 参数,若 check = true 将会直接抛出异常 boolean check = getUrl().getParameter(Constants.CHECK_KEY, true) && url.getParameter(Constants.CHECK_KEY, true) && !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol()); boolean skipFailback = t instanceof SkipFailbackWrapperException; if (check || skipFailback) { if (skipFailback) { t = t.getCause(); } throw new IllegalStateException("Failed to register"); } else { logger.error("Failed to register"); } // 记录注册失败的链接 failedRegistered.add(url); } } protected abstract void doRegister(URL url); 如上,我们重点关注 doRegister 方法调用即可,其他的代码先忽略。doRegister 方法是一个模板方法,因此我们到 FailbackRegistry 子类 ZookeeperRegistry 中进行分析。如下: protected void doRegister(URL url) { try { // 通过 Zookeeper 客户端创建节点,节点路径由 toUrlPath 方法生成,路径格式如下: // /${group}/${serviceInterface}/providers/${url} // 比如 // /dubbo/com.tianxiaobo.DemoService/providers/dubbo%3A%2F%2F127.0.0.1...... zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true)); } catch (Throwable e) { throw new RpcException("Failed to register..."); } } 如上,ZookeeperRegistry 在 doRegister 中调用了 Zookeeper 客户端创建服务节点。节点路径由 toUrlPath 方法生成,该方法逻辑不难理解,就不分析了。接下来分析 create 方法,如下: public void create(String path, boolean ephemeral) { if (!ephemeral) { // 如果要创建的节点类型非临时节点,那么这里要检测节点是否存在 if (checkExists(path)) { return; } } int i = path.lastIndexOf('/'); if (i > 0) { create(path.substring(0, i), false); // 递归创建上一级路径 } // 根据 ephemeral 的值创建临时或持久节点 if (ephemeral) { createEphemeral(path); } else { createPersistent(path); } } 上面方法先是通过递归创建当前节点的上一级路径,然后再根据 ephemeral 的值决定创建临时还是持久节点。createEphemeral 和 createPersistent 这两个方法都比较简单,这里简单分析其中的一个。如下: public void createEphemeral(String path) { try { // 通过 Curator 框架创建节点 client.create().withMode(CreateMode.EPHEMERAL).forPath(path); } catch (NodeExistsException e) { } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); } } 好了,到此关于服务注册的过程就分析完了。整个过程可简单总结为:先创建注册中心实例,之后再通过注册中心实例注册服务。本节先到这,接下来分析数据订阅过程。 2.2.5 订阅 override 数据 订阅 override 数据对应的代码我粗略看了一遍,这部分代码的主要目的是为了在服务配置发生变化时,重新导出服务。具体的使用场景应该当我们通过 Dubbo 管理后台修改了服务配置后,Dubbo 得到服务配置被修改的通知,然后重新导出服务。这个使用场景只是猜测,我并未进行过验证。如果大家有兴趣可以自行验证。 override 数据订阅相关代码也不是很少,考虑到文章篇幅问题以及重要性,遂决定不对此逻辑进行详细的分析。如果大家有兴趣,可自行分析。 3.总结 本篇文章详细分析了 Dubbo 服务导出过程,包括配置检测,URL 组装,Invoker 创建过程、导出服务以及注册服务等等。篇幅比较大,需要大家耐心阅读。对于这篇文章,我建议大家当成一个工具书使用。需要的时候跳到指定章节看一下,通读可能会有点累。由于文章篇幅比较大,因此可能会隐藏一些我没意识到的错误。若大家在阅读的过程中发现了错误,还请指出。如果能够不吝赐教,那就更好了,先在这里说声谢谢。 好了,本篇文章就到这了。谢谢阅读。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处作者:田小波本文同步发布在我的个人博客:http://www.tianxiaobo.com 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.原理 我在上一篇文章中分析了 Dubbo 的 SPI 机制,Dubbo SPI 是 Dubbo 框架的核心。Dubbo 中的很多拓展都是通过 SPI 机制进行加载的,比如 Protocol、Cluster、LoadBalance 等。有时,有些拓展并非想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。这听起来有些矛盾。拓展未被加载,那么拓展方法就无法被调用(静态方法除外)。拓展方法未被调用,就无法进行加载,这似乎是个死结。不过好在也有相应的解决办法,通过代理模式就可以解决这个问题,这里我们将具有代理功能的拓展称之为自适应拓展。Dubbo 并未直接通过代理模式实现自适应拓展,而是代理代理模式基础上,封装了一个更炫的实现方式。Dubbo 首先会为拓展接口生成具有代理功能的代码,然后通过 javassist 或 jdk 编译这段代码,得到 Class 类,最后在通过反射创建代理类。整个过程比较复杂、炫丽。如此复杂的过程最终的目的是为拓展生成代理对象,但实际上每个代理对象的代理逻辑基本一致,均是从 URL 中获取欲加载实现类的名称。因此,我们完全可以把代理逻辑抽出来,并通过动态代理的方式实现自适应拓展。这样做的好处显而易见,方便维护,也方便源码学习者学习和调试代码。本文将在随后实现一个动态代理版的自适应拓展,有兴趣的同学可以继续往下读。 接下来,我们通过一个示例演示自适应拓展类。这个示例取自 Dubbo 官方文档,我这里进行了一定的拓展。这是一个与汽车相关的例子,我们有一个车轮制造厂接口 WheelMaker: public interface WheelMaker { Wheel makeWheel(URL url); } WheelMaker 接口的 Adaptive 实现类如下: public class AdaptiveWheelMaker implements WheelMaker { public Wheel makeWheel(URL url) { if (url == null) { throw new IllegalArgumentException("url == null"); } // 1.从 URL 中获取 WheelMaker 名称 String wheelMakerName = url.getParameter("Wheel.maker"); if (name == null) { throw new IllegalArgumentException("wheelMakerName == null"); } // 2.通过 SPI 加载具体的 WheelMaker WheelMaker wheelMaker = ExtensionLoader .getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName); // 3.调用目标方法 return wheelMaker.makeWheel(URL url); } } AdaptiveWheelMaker 是一个代理类,它主要做了三件事情: 从 URL 中获取 WheelMaker 名称 通过 SPI 加载具体的 WheelMaker 调用目标方法 接下来,我们来看看汽车制造厂 CarMaker 接口与其实现类。 public interface CarMaker { Car makeCar(URL url); } public class RaceCarMaker implements CarMaker { WheelMaker wheelMaker; // 通过 setter 注入 AdaptiveWheelMaker public setWheelMaker(WheelMaker wheelMaker) { this.wheelMaker = wheelMaker; } public Car makeCar(URL url) { Wheel wheel = wheelMaker.makeWheel(url); return new RaceCar(wheel, ...); } } RaceCarMaker 持有一个 WheelMaker 类型从成员变量,在程序启动时,我们可以将 AdaptiveWheelMaker 通过 setter 方法注入到 RaceCarMaker 中。在运行时,假设有这样一个 URL 类型的参数: dubbo://192.168.0.101:20880/XxxService?wheel.maker=MichelinWheelMaker RaceCarMaker 的 makeCar 方法将上面的 url 作为参数传给 AdaptiveWheelMaker 的 makeWheel 方法,makeWheel 方法从 url 中提取 wheel.maker 参数,得到 MichelinWheelMaker。之后再通过 SPI 加载名为 MichelinWheelMaker 的实现类,得到具体的 WheelMaker 实例。 上面这个示例展示了自适应拓展类的核心实现 -- 在组件方法被调用时,通过代理的方式加载指定的实现类,并调用被代理的方法。 经过以上说明,大家应该搞懂了自适应拓展的原理。接下来,我们深入到源码中,探索自适应拓展生成的过程。 2.源码分析 在对自适应拓展生成过程进行深入分析之前,我们先来看一下与自适应拓展息息相关的一个注解,即 Adaptive 注解。该注解的定义如下: @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Adaptive { String[] value() default {}; } 从上面的代码中可知,Adaptive 可注解在类或方法上。注解在类上时,Dubbo 不会为该类生成代理类。注解上方法(接口方法)上时,Dubbo 会为为该方法生成代理逻辑。Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory。此种情况表示拓展的加载逻辑由人工编码完成。更多时候,Adaptive 是注解在接口方法上的,表示拓展的加载逻辑需由框架自动生成。Adaptive 注解的地方不同,相应的处理逻辑也是不同的。注解在类上时,处理逻辑比较简单,本文就不分析了。注解在接口方法上时,处理逻辑较为复杂,本章将会重点分析此块逻辑。接下来,我们从 getAdaptiveExtension 方法进行分析。代码如下: 2.1 获取自适应拓展 public T getAdaptiveExtension() { // 从缓存中获取自适应拓展 Object instance = cachedAdaptiveInstance.get(); if (instance == null) { // 缓存未命中 if (createAdaptiveInstanceError == null) { synchronized (cachedAdaptiveInstance) { instance = cachedAdaptiveInstance.get(); if (instance == null) { try { // 创建自适应拓展 instance = createAdaptiveExtension(); // 设置拓展到缓存中 cachedAdaptiveInstance.set(instance); } catch (Throwable t) { createAdaptiveInstanceError = t; throw new IllegalStateException("..."); } } } } else { throw new IllegalStateException("..."); } } return (T) instance; } getAdaptiveExtension 方法首先会检查缓存,缓存未命中,则调用 createAdaptiveExtension 方法创建自适应拓展。下面,我们看一下 createAdaptiveExtension 方法的代码。 private T createAdaptiveExtension() { try { // 获取自适应拓展类,并通过反射实例化 return injectExtension((T) getAdaptiveExtensionClass().newInstance()); } catch (Exception e) { throw new IllegalStateException("..."); } } createAdaptiveExtension 方法代码比较少,但却包含了三个动作,分别如下: 调用 getAdaptiveExtensionClass 方法获取自适应拓展 Class 对象 通过反射进行实例化 调用 injectExtension 方法向拓展实例中注入依赖 前两个动作比较好理解,第三个动作不好理解,这里简单说明一下。injectExtension 方法通过 setter 方法向目标对象中注入依赖,可以看做是一个简单 IOC 的实现。前面说过,Dubbo 中有两种类型的自适应拓展,一种是手工编码的,一种是自动生成的。手工编码的 Adaptive 拓展中可能存在着一些依赖,而自动生成的 Adaptive 拓展则不会依赖其他类。这里调用 injectExtension 方法的目的是为手工编码的自适应拓展注入依赖,这一点需要大家注意一下。关于 injectExtension 方法,我在[上一篇文章]()中已经分析过了,这里不再赘述。接下来,分析 getAdaptiveExtensionClass 方法的逻辑。 private Class<?> getAdaptiveExtensionClass() { // 通过 SPI 获取所有的拓展类 getExtensionClasses(); // 检查缓存,若缓存不为空,则直接返回缓存 if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } // 创建自适应拓展类 return cachedAdaptiveClass = createAdaptiveExtensionClass(); } getAdaptiveExtensionClass 方法也包含了三个步骤,如下: 调用 getExtensionClasses 获取所有的拓展类 检查缓存,若缓存不为空,则返回缓存 若缓存为空,则调用 createAdaptiveExtensionClass 创建自适应拓展类 这三个步骤看起来平淡无奇,似乎没有多讲的必要。但是这些平淡无奇的代码中隐藏了一些细节,需要说明一下。首先从第一个步骤说起,getExtensionClasses 这个方法用于获取某个接口的所有实现类。比如该方法可以获取 Protocol 接口的 DubboProtocol、HttpProtocol、InjvmProtocol 等实现类。在获取实现类的过程中,如果某个某个实现类被 Adaptive 注解修饰了,那么该类就会被赋值给 cachedAdaptiveClass 变量。此时,上面步骤中的第二步条件成立(缓存不为空),直接返回 cachedAdaptiveClass 即可。如果所有的实现类均未被 Adaptive 注解修饰,那么执行第三步逻辑,创建自适应拓展类。相关代码如下: private Class<?> createAdaptiveExtensionClass() { // 构建自适应拓展代码 String code = createAdaptiveExtensionClassCode(); ClassLoader classLoader = findClassLoader(); // 获取编译器实现类 com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); // 编译代码,生成 Class return compiler.compile(code, classLoader); } createAdaptiveExtensionClass 方法用于生成自适应拓展类,该方法首先会生成自适应拓展类的源码,然后通过 Compiler 实例(Dubbo 默认使用 javassist 作为编译器)编译源码,得到代理类 Class 实例。接下来,我将重点分析代理类代码生成逻辑。至于代码编译的过程,并非本文范畴,这里就不分析了,大家有兴趣可以自己看看。下面,我们把目光聚焦在 createAdaptiveExtensionClassCode 方法上。 2.2 自适应拓展类代码生成 createAdaptiveExtensionClassCode 方法代码略多,约有两百行代码。因此在本节中,我将会对该方法的代码进行拆分分析,以帮助大家更好的理解代码含义。 2.2.1 Adaptive 注解检测 在生成代理类源码之前,createAdaptiveExtensionClassCode 方法首先会通过反射检测接口方法是否包含 Adaptive 注解。对于要生成自适应拓展的接口,Dubbo 要求该接口至少有一个方法被 Adaptive 注解修饰。若不满足此条件,就会抛出运行时异常。相关代码如下: // 通过反射获取所有的方法 Method[] methods = type.getMethods(); boolean hasAdaptiveAnnotation = false; // 遍历方法列表 for (Method m : methods) { // 检测方法上是否有 Adaptive 注解 if (m.isAnnotationPresent(Adaptive.class)) { hasAdaptiveAnnotation = true; break; } } if (!hasAdaptiveAnnotation) // 若所有的方法上均无 Adaptive 注解,则抛出异常 throw new IllegalStateException("..."); 2.2.2 生成类 通过 Adaptive 注解检测后,即可开始生成代码。代码生成的顺序与 Java 文件内容顺序一致,首先会生成 package 语句,然后生成 import 语句,紧接着生成类名等代码。整个逻辑如下: // 生成 package 代码:package + type 所在包 codeBuilder.append("package ").append(type.getPackage().getName()).append(";"); // 生成 import 代码:import + ExtensionLoader 全限定名 codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";"); // 生成类代码:public class + type简单名称 + $Adaptive + implements + type全限定名 + { codeBuilder.append("\npublic class ") .append(type.getSimpleName()) .append("$Adaptive") .append(" implements ") .append(type.getCanonicalName()) .append(" {"); // ${生成方法} codeBuilder.append("\n}"); 这里,我用 ${...} 占位符代表其他代码的生成逻辑,该部分逻辑我将在随后进行分析。上面代码不是很难理解,这里我直接通过一个例子展示该段代码所生成的内容。以 Dubbo 的 Protocol 接口为例,生成的代码如下: package com.alibaba.dubbo.rpc; import com.alibaba.dubbo.common.extension.ExtensionLoader; public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol { // 省略方法代码 } 2.2.3 生成方法 一个方法可以被 Adaptive 注解修饰,也可以不被修饰。这里将未被 Adaptive 注解修饰的方法称为“无 Adaptive 注解方法”,下面我们先来看看此种方法的代码生成逻辑是怎样的。 2.2.3.1 无 Adaptive 注解方法代码生成 对于接口方法,我们可以按照需求标注 Adaptive 注解。以 Protocol 接口为例,该接口的 destroy 和 getDefaultPort 未标注 Adaptive 注解,其他方法均标注了 Adaptive 注解。Dubbo 不会为没有标注 Adaptive 注解的方法生成代理逻辑,对于该种类型的方法,仅会生成一句抛出异常的代码。生成逻辑如下: for (Method method : methods) { // 省略无关逻辑 Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); StringBuilder code = new StringBuilder(512); // 如果方法上无 Adaptive 注解,则生成 throw new UnsupportedOperationException(...) 代码 if (adaptiveAnnotation == null) { // 生成规则: // throw new UnsupportedOperationException( // "method " + 方法签名 + of interface + 全限定接口名 + is not adaptive method!”) code.append("throw new UnsupportedOperationException(\"method ") .append(method.toString()).append(" of interface ") .append(type.getName()).append(" is not adaptive method!\");"); } else { // 省略无关逻辑 } // 省略无关逻辑 } 以 Protocol 接口的 destroy 方法为例,上面代码生成的内容如下: throw new UnsupportedOperationException( "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!"); 2.2.3.2 获取 URL 数据 前面说过方法代理逻辑会从 URL 中提取目标拓展的名称,因此代码生成逻辑的一个重要的任务是从方法的参数列表获取其他参数中获取 URL 数据。举个例子说明一下,我们要为 Protocol 接口的 refer 和 export 方法生成代理逻辑。在运行时,通过反射得到的方法定义大致如下: Invoker refer(Class<T> arg0, URL arg1) throws RpcException; Exporter export(Invoker<T> arg0) throws RpcException; 对于 refer 方法,通过遍历 refer 的参数列表即可获取 URL 数据,这个还比较简单。对于 export 方法,获取 URL 数据则要麻烦一些。export 参数列表中没有 URL 参数,因此需要从 Invoker 参数中获取 URL 数据。获取方式是调用 Invoker 中可返回 URL 的 getter 方法,比如 getUrl。如果 Invoker 中无相关 getter 方法,此时则会抛出异常。整个逻辑如下: for (Method method : methods) { Class<?> rt = method.getReturnType(); Class<?>[] pts = method.getParameterTypes(); Class<?>[] ets = method.getExceptionTypes(); Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); StringBuilder code = new StringBuilder(512); if (adaptiveAnnotation == null) { // ${无 Adaptive 注解方法代码生成} } else { int urlTypeIndex = -1; // 遍历参数列表,确定 URL 参数位置 for (int i = 0; i < pts.length; ++i) { if (pts[i].equals(URL.class)) { urlTypeIndex = i; break; } } if (urlTypeIndex != -1) { // 参数列表中存在 URL 参数 // 为 URL 类型参数生成判空代码,格式如下: // if (arg + urlTypeIndex == null) // throw new IllegalArgumentException("url == null"); String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");", urlTypeIndex); code.append(s); // 为 URL 类型参数生成赋值代码,即 URL url = arg1 或 arg2,或 argN s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex); code.append(s); } else { // 参数列表中不存在 URL 类型参数 String attribMethod = null; LBL_PTS: // 遍历方法的参数类型列表 for (int i = 0; i < pts.length; ++i) { // 获取某一类型参数的全部方法 Method[] ms = pts[i].getMethods(); // 遍历方法列表,寻找可返回 URL 的 getter 方法 for (Method m : ms) { String name = m.getName(); // 1. 方法名以 get 开头,或方法名大于3个字符 // 2. 方法的访问权限为 public // 3. 方法非静态类型 // 4. 方法参数数量为0 // 5. 方法返回值类型为 URL if ((name.startsWith("get") || name.length() > 3) && Modifier.isPublic(m.getModifiers()) && !Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 0 && m.getReturnType() == URL.class) { urlTypeIndex = i; attribMethod = name; // 结束 for (int i = 0; i < pts.length; ++i) 循环 break LBL_PTS; } } } if (attribMethod == null) { // 如果所有参数中均不包含可返回 URL 的 getter 方法,则抛出异常 throw new IllegalStateException("..."); } // 为包含可返回 URL 的参数生成判空代码,格式如下: // if (arg + urlTypeIndex == null) // throw new IllegalArgumentException("参数全限定名 + argument == null"); String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");", urlTypeIndex, pts[urlTypeIndex].getName()); code.append(s); // 为 getter 方法返回的 URL 生成判空代码,格式如下: // if (argN.getter方法名() == null) // throw new IllegalArgumentException(参数全限定名 + argument getUrl() == null); s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");", urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod); code.append(s); // 生成赋值语句,格式如下: // URL全限定名 url = argN.getter方法名(),比如 // com.alibaba.dubbo.common.URL url = invoker.getUrl(); s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod); code.append(s); } // 省略无关代码 } // 省略无关代码 } 上面代码有点多,但并不是很难看懂。这段代码主要是为了获取 URL 数据,并为之生成判空和赋值代码。以 Protocol 的 refer 和 export 方法为例,上面代码会为它们生成如下内容(代码已格式化): refer: if (arg1 == null) throw new IllegalArgumentException("url == null"); com.alibaba.dubbo.common.URL url = arg1; export: if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null"); if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null"); com.alibaba.dubbo.common.URL url = arg0.getUrl(); 2.2.3.3 获取 Adaptive 注解值 Adaptive 注解值 value 类型为 String[],可填写多个值,默认情况下为空数组。若 value 为非空数组,直接获取数组内容即可。若 value 为空数组,则需进行额外处理。处理的过程是将类名转换为字符数组,然后遍历字符数组,并将字符加入到 StringBuilder 中。若字符为大写字母,则向 StringBuilder 中添加点号,随后将字符变为小写存入 StringBuilder 中。比如 LoadBalance 经过处理后,得到 load.balance。 for (Method method : methods) { Class<?> rt = method.getReturnType(); Class<?>[] pts = method.getParameterTypes(); Class<?>[] ets = method.getExceptionTypes(); Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); StringBuilder code = new StringBuilder(512); if (adaptiveAnnotation == null) { // ${无 Adaptive 注解方法代码生成} } else { // ${获取 URL 数据} String[] value = adaptiveAnnotation.value(); // value 为空数组 if (value.length == 0) { // 获取类名,并将类名转换为字符数组 char[] charArray = type.getSimpleName().toCharArray(); StringBuilder sb = new StringBuilder(128); // 遍历字节数组 for (int i = 0; i < charArray.length; i++) { // 检测当前字符是否为大写字母 if (Character.isUpperCase(charArray[i])) { if (i != 0) { // 向 sb 中添加点号 sb.append("."); } // 将字符变为小写,并添加到 sb 中 sb.append(Character.toLowerCase(charArray[i])); } else { // 添加字符到 sb 中 sb.append(charArray[i]); } } value = new String[]{sb.toString()}; } // 省略无关代码 } // 省略无关逻辑 } 2.2.3.4 检测 Invocation 参数 此段逻辑是检测方法列表中是否存在 Invocation 类型的参数,若存在,则为其生成判空代码和其他一些代码。相应的逻辑如下: for (Method method : methods) { Class<?> rt = method.getReturnType(); Class<?>[] pts = method.getParameterTypes(); // 获取参数类型列表 Class<?>[] ets = method.getExceptionTypes(); Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); StringBuilder code = new StringBuilder(512); if (adaptiveAnnotation == null) { // ${无 Adaptive 注解方法代码生成} } else { // ${获取 URL 数据} // ${获取 Adaptive 注解值} boolean hasInvocation = false; // 遍历参数类型列表 for (int i = 0; i < pts.length; ++i) { // 判断当前参数名称是否等于 com.alibaba.dubbo.rpc.Invocation if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) { // 为 Invocation 类型参数生成判空代码 String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i); code.append(s); // 生成 getMethodName 方法调用代码,格式为: // String methodName = argN.getMethodName(); s = String.format("\nString methodName = arg%d.getMethodName();", i); code.append(s); // 设置 hasInvocation 为 true hasInvocation = true; break; } } } // 省略无关逻辑 } 2.2.3.5 生成拓展名获取逻辑 本段逻辑用于根据 SPI 和 Adaptive 注解值生成“拓展名获取逻辑”,同时生成逻辑也受 Invocation 类型参数影响,综合因素导致本段逻辑相对复杂。本段逻辑可以会生成但不限于下面的代码: String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); 或 String extName = url.getMethodParameter(methodName, "loadbalance", "random"); 亦或是 String extName = url.getParameter("client", url.getParameter("transporter", "netty")); 本段逻辑复杂指出在于条件分支比较多,大家在阅读源码时需要知道每个条件分支的意义是什么,否则不太容易看懂相关代码。好了,其他的就不多说了,开始分析本段逻辑。 for (Method method : methods) { Class<?> rt = method.getReturnType(); Class<?>[] pts = method.getParameterTypes(); Class<?>[] ets = method.getExceptionTypes(); Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); StringBuilder code = new StringBuilder(512); if (adaptiveAnnotation == null) { // $无 Adaptive 注解方法代码生成} } else { // ${获取 URL 数据} // ${获取 Adaptive 注解值} // ${检测 Invocation 参数} // 设置默认拓展名,cachedDefaultName = SPI 注解值,比如 Protocol 接口上标注的 // SPI 注解值为 dubbo。默认情况下,SPI 注解值为空串,此时 cachedDefaultName = null String defaultExtName = cachedDefaultName; String getNameCode = null; // 遍历 value,这里的 value 是 Adaptive 的注解值,2.2.3.3 节分析过 value 变量的获取过程。 // 此处循环目的是生成从 URL 中获取拓展名的代码,生成的代码会赋值给 getNameCode 变量。注意这 // 个循环的遍历顺序是由后向前遍历的。 for (int i = value.length - 1; i >= 0; --i) { if (i == value.length - 1) { // 当 i 为最后一个元素的坐标时 if (null != defaultExtName) { // 默认拓展名非空 // protocol 是 url 的一部分,可通过 getProtocol 方法获取,其他的则是从 // URL 参数中获取。所以这里要判断 value[i] 是否为 protocol if (!"protocol".equals(value[i])) // hasInvocation 用于标识方法参数列表中是否有 Invocation 类型参数 if (hasInvocation) // 生成的代码功能等价于下面的代码: // url.getMethodParameter(methodName, value[i], defaultExtName) // 以 LoadBalance 接口的 select 方法为例,最终生成的代码如下: // url.getMethodParameter(methodName, "loadbalance", "random") getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); else // 生成的代码功能等价于下面的代码: // url.getParameter(value[i], defaultExtName) getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName); else // 生成的代码功能等价于下面的代码: // ( url.getProtocol() == null ? defaultExtName : url.getProtocol() ) getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName); } else { // 默认拓展名为空 if (!"protocol".equals(value[i])) if (hasInvocation) // 生成代码格式同上 getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); else // 生成的代码功能等价于下面的代码: // url.getParameter(value[i]) getNameCode = String.format("url.getParameter(\"%s\")", value[i]); else // 生成从 url 中获取协议的代码,比如 "dubbo" getNameCode = "url.getProtocol()"; } } else { if (!"protocol".equals(value[i])) if (hasInvocation) // 生成代码格式同上 getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); else // 生成的代码功能等价于下面的代码: // url.getParameter(value[i], getNameCode) // 以 Transporter 接口的 connect 方法为例,最终生成的代码如下: // url.getParameter("client", url.getParameter("transporter", "netty")) getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode); else // 生成的代码功能等价于下面的代码: // url.getProtocol() == null ? getNameCode : url.getProtocol() // 以 Protocol 接口的 connect 方法为例,最终生成的代码如下: // url.getProtocol() == null ? "dubbo" : url.getProtocol() getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode); } } // 生成 extName 赋值代码 code.append("\nString extName = ").append(getNameCode).append(";"); // 生成 extName 判空代码 String s = String.format("\nif(extName == null) " + "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");", type.getName(), Arrays.toString(value)); code.append(s); } // 省略无关逻辑 } 上面代码已经进行了大量的注释,不过看起来任然不是很好理解。既然如此,那么建议大家写点测试代码,对 Protocol、LoadBalance 以及 Transporter 等接口的自适应拓展类代码生成过程进行调试。这里我以 Transporter 接口的自适应拓展类代码生成过程进行分析。首先看一下 Transporter 接口的定义,如下: @SPI("netty") public interface Transporter { // @Adaptive({server, transporter}) @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY}) Server bind(URL url, ChannelHandler handler) throws RemotingException; // @Adaptive({client, transporter}) @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY}) Client connect(URL url, ChannelHandler handler) throws RemotingException; } 下面对 connect 方法代理逻辑生成的过程进行分析,此时生成代理逻辑所用到的变量和值如下: String defaultExtName = "netty"; boolean hasInvocation = false; String getNameCode = null; String[] value = ["client", "transporter"]; 下面对 value 数组进行遍历,此时 i = 1, value[i] = "transporter",生成的代码如下: getNameCode = url.getParameter("transporter", "netty"); 接下来,for 循环继续执行,此时 i = 0, value[i] = "client",生成的代码如下: getNameCode = url.getParameter("client", url.getParameter("transporter", "netty")); for 循环结束运行,现在生成 extName 变量及判空代码,如下: String extName = url.getParameter("client", url.getParameter("transporter", "netty")); if (extName == null) { throw new IllegalStateException( "Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString() + ") use keys([client, transporter])"); } 到此,connect 方法的拓展名获取代码就生成好了。如果大家不是很明白,建议自己调试走一遍。好了,本节先到这里。 2.2.3.6 生成拓展加载与目标方法调用逻辑 上一节的逻辑生成拓展名 extName 获取逻辑,接下来要做的是根据拓展名加载拓展实例,并调用拓展实例的目标方法。相关逻辑如下: for (Method method : methods) { Class<?> rt = method.getReturnType(); Class<?>[] pts = method.getParameterTypes(); Class<?>[] ets = method.getExceptionTypes(); Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); StringBuilder code = new StringBuilder(512); if (adaptiveAnnotation == null) { // $无 Adaptive 注解方法代码生成} } else { // ${获取 URL 数据} // ${获取 Adaptive 注解值} // ${检测 Invocation 参数} // ${生成拓展名获取逻辑} // 生成拓展获取代码,格式如下: // type全限定名 extension = (type全限定名)ExtensionLoader全限定名 // .getExtensionLoader(type全限定名.class).getExtension(extName); // Tips: 格式化字符串中的 %<s 表示使用前一个转换符所描述的参数,即 type 全限定名 s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);", type.getName(), ExtensionLoader.class.getSimpleName(), type.getName()); code.append(s); // 如果方法有返回值类型非 void,则生成 return 语句。 if (!rt.equals(void.class)) { code.append("\nreturn "); } // 生成目标方法调用逻辑,格式为: // extension.方法名(arg0, arg2, ..., argN); s = String.format("extension.%s(", method.getName()); code.append(s); for (int i = 0; i < pts.length; i++) { if (i != 0) code.append(", "); code.append("arg").append(i); } code.append(");"); } // 省略无关逻辑 } 以 Protocol 接口举例说明,上面代码生成的内容如下: com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader .getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName); return extension.refer(arg0, arg1); 2.2.3.7 生成完整的方法 本节进行代码生成的收尾工作,主要用于生成方法定义的代码。相关逻辑如下: for (Method method : methods) { Class<?> rt = method.getReturnType(); Class<?>[] pts = method.getParameterTypes(); Class<?>[] ets = method.getExceptionTypes(); Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); StringBuilder code = new StringBuilder(512); if (adaptiveAnnotation == null) { // $无 Adaptive 注解方法代码生成} } else { // ${获取 URL 数据} // ${获取 Adaptive 注解值} // ${检测 Invocation 参数} // ${生成拓展名获取逻辑} // ${生成拓展加载与目标方法调用逻辑} } } // public + 返回值全限定名 + 方法名 + ( codeBuilder.append("\npublic ") .append(rt.getCanonicalName()) .append(" ") .append(method.getName()) .append("("); // 添加参数列表代码 for (int i = 0; i < pts.length; i++) { if (i > 0) { codeBuilder.append(", "); } codeBuilder.append(pts[i].getCanonicalName()); codeBuilder.append(" "); codeBuilder.append("arg").append(i); } codeBuilder.append(")"); // 添加异常抛出代码 if (ets.length > 0) { codeBuilder.append(" throws "); for (int i = 0; i < ets.length; i++) { if (i > 0) { codeBuilder.append(", "); } codeBuilder.append(ets[i].getCanonicalName()); } } codeBuilder.append(" {"); codeBuilder.append(code.toString()); codeBuilder.append("\n}"); 以 Protocol 的 refer 方法为例,上面代码生成的内容如下: public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) { // 方法体 } 3.基于动态代理实现知识与拓展 我在第一章介绍自适应拓展原理时说过,Dubbo 通过生成和编译代码实现自适应拓展的方式有点复杂,不利于维护。另外,这样做对源码学习读者来说,也不是很友好。我敢肯定,有同学会像我一样,在开始调试 Dubbo 源码时,不知道如何调试各种自适应拓展类,比如 Protocol$Adaptive。如果你也有类似的困惑,这里教大家一个方法。如下: 在 createAdaptiveExtensionClass 方法的第一行打个断点 启动测试代码,代码运行到端点处,单步越过断点,此时可以得到生成的代码。 拷贝出刚刚获取到的代码,到指定的包下创建同名类,并将代码拷过去,格式化一下即可 以 Protocol 接口为例,当代码越过断点后,调试信息如下: 从调试信息中可知,Protocol$Adaptive 所在包为 com.alibaba.dubbo.rpc。因此接下来到 com.alibaba.dubbo.rpc 包下创建 Protocol$Adaptive 类,并把 code 变量值拷贝到刚创建的文件中。当我们再次进行调试时,就能进入内部了。比如: 既然 Dubbo 实现的 Adaptive 机制不利于调试,那么我们可以对其进行改造。改造后的代码如下: public class AdaptiveInvokeHandler implements InvocationHandler { private String defaultExtName; public AdaptiveInvokeHandler(String defaultExtName) { this.defaultExtName = defaultExtName; } public Object getProxy(Class clazz) { if (!clazz.isInterface()) { throw new IllegalStateException("Only create the proxy for interface."); } return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Class<?> type = method.getDeclaringClass(); if (type.equals(Object.class)) { throw new UnsupportedOperationException("Cannot invoke the method of Object"); } Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); if (adaptiveAnnotation == null) { throw new UnsupportedOperationException("method " + method.toString() + " of interface " + type.getName() + " is not adaptive method!"); } // 获取 URL 数据 URL url = getUrlData(method, args); // 获取 Adaptive 注解值 String[] value = getAdaptiveAnnotationValue(method); // 获取 Invocation 参数 Object invocation = getInvocationArgument(method, args); // 获取拓展名 String extName = getExtensionName(url, value, invocation); if (StringUtils.isEmpty(extName)) { throw new IllegalStateException( "Fail to get extension(" + type.getName() + ") name from url(" + url.toString() + ") use keys(" + Arrays.toString(value) +")"); } // 获取拓展实例 Object extension = ExtensionLoader.getExtensionLoader(type).getExtension(extName); Class<?> extType = extension.getClass(); Method targetMethod = extType.getMethod(method.getName(), method.getParameterTypes()); // 通过反射调用目标方法 return targetMethod.invoke(extension, args); } } 这样看起来是不是简单了一些,不过这并不是全部的代码。我将 URL 数据以及 Adaptive 注解值的获取逻辑封装在了私有方法中,相应的代码如下: private URL getUrlData(Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { URL url = null; Class<?>[] pts = method.getParameterTypes(); for (int i = 0; i < pts.length; i++) { if (pts[i].equals(URL.class)) { url = (URL) args[i]; if (url == null) { throw new IllegalArgumentException("url == null"); } break; } } if (url == null) { int urlTypeIndex = -1; Method getter = null; LBL_PTS: for (int i = 0; i < pts.length; ++i) { Method[] ms = pts[i].getMethods(); for (Method m : ms) { String name = m.getName(); if ((name.startsWith("get") || name.length() > 3) && Modifier.isPublic(m.getModifiers()) && !Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 0 && m.getReturnType() == URL.class) { urlTypeIndex = i; getter = m; break LBL_PTS; } } } if (urlTypeIndex == -1) { throw new IllegalArgumentException("Cannot find URL argument."); } if (args[urlTypeIndex] == null) { throw new IllegalArgumentException(pts[urlTypeIndex].getName() + " argument == null"); } url = (URL) getter.invoke(args[urlTypeIndex]); if (url == null) { throw new IllegalArgumentException(pts[urlTypeIndex].getName() + " argument " + getter.getName() + "() == null"); } } return url; } private String[] getAdaptiveAnnotationValue(Method method) { Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); Class type = method.getDeclaringClass(); if (adaptiveAnnotation == null) { throw new IllegalArgumentException("method " + method.toString() + " of interface " + type.getName() + " is not adaptive method!"); } String[] value = adaptiveAnnotation.value(); if (value.length == 0) { char[] charArray = type.getSimpleName().toCharArray(); StringBuilder sb = new StringBuilder(128); for (int i = 0; i < charArray.length; i++) { if (Character.isUpperCase(charArray[i])) { if (i != 0) { sb.append("."); } sb.append(Character.toLowerCase(charArray[i])); } else { sb.append(charArray[i]); } } value = new String[]{sb.toString()}; } return value; } private Object getInvocationArgument(Method method, Object[] args) { Class<?>[] pts = method.getParameterTypes(); for (int i = 0; i < pts.length; ++i) { if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) { Object invocation = args[i]; if (invocation == null) { throw new IllegalArgumentException("invocation == null"); } return invocation; } } return null; } private String getExtensionName(URL url, String[] value, Invocation invocation) { String methodName = null; boolean hasInvocation = invocation != null; if (hasInvocation) { Class<?> clazz = invocation.getClass(); Method method = clazz.getMethod("getMethodName"); methodName = (String) method.invoke(invocation); } String extName = null; for (int i = 0; i < value.length; i++) { if (!"protocol".equals(value[i])) { if (hasInvocation) { extName = url.getMethodParameter(methodName, value[i], defaultExtName); } else { extName = url.getParameter(value[i]); } } else { extName = url.getProtocol(); } if (StringUtils.isNotEmpty(extName)) { break; } if (i == value.length -1 && StringUtils.isEmpty(extName)) { extName = defaultExtName; } } return extName; } 现在我们将 AdaptiveInvokeHandler 放置到 ExtensionLoader 所在包下,并对 ExtensionLoader 的 createAdaptiveExtension 方法代码进行改造。如下: private T createAdaptiveExtension() { try { getExtensionClasses(); T extension = null; if (cachedAdaptiveClass != null) { extension = (T) cachedAdaptiveClass.newInstance(); } if (extension == null) { extension = (T) new AdaptiveInvokeHandler(cachedDefaultName).getProxy(type); } return injectExtension(extension); } catch (Exception e) { throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e); } } 以上就是改造后的代码,需要特别说明的是,上面的代码仅供演示使用,代码逻辑并不是十分严谨。如果你有更好的写法,欢迎分享。 4.总结 到此,关于自适应拓展的原理,实现以及改造过程就分析完了。总的来说自适应拓展整个逻辑还是很复杂的,并不是很容易弄懂。因此,大家在阅读该部分源码时,耐心一些,同时多进行调试。亦或是通过生成好的代码思考生成逻辑。当然,大家也可以将代码生成逻辑看成一个黑盒,不懂细节也没关系,只要知道自适应拓展原理即可。 好了,本篇文章先到这里,感谢大家的阅读。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处作者:田小波本文同步发布在我的个人博客:http://www.tianxiaobo.com 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 SPI 全称为 Service Provider Interface,是 Java 提供的一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。下面,我们先来了解一下 Java SPI 与 Dubbo SPI 的使用方法,然后再来分析 Dubbo SPI 的源码。 2.SPI 示例 2.1 Java SPI 示例 前面简单介绍了 SPI 机制的原理,本节通过一个示例来演示 JAVA SPI 的使用方法。首先,我们定义一个接口,名称为 Robot。 public interface Robot { void sayHello(); } 接下来定义两个实现类,分别为擎天柱 OptimusPrime 和大黄蜂 Bumblebee。 public class OptimusPrime implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Optimus Prime."); } } public class Bumblebee implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Bumblebee."); } } 接下来 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 com.tianxiaobo.spi.Robot。文件内容为实现类的全限定的类名,如下: com.tianxiaobo.spi.OptimusPrime com.tianxiaobo.spi.Bumblebee 做好了所需的准备工作,接下来编写代码进行测试。 public class JavaSPITest { @Test public void sayHello() throws Exception { ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class); System.out.println("Java SPI"); serviceLoader.forEach(Robot::sayHello); } } 最后来看一下测试结果,如下: 从测试结果可以看出,我们的两个实现类被成功的加载,并输出了相应的内容。关于 Java SPI 的演示先到这,接下来演示 Dubbo SPI。 2.2 Dubbo SPI 示例 Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 的实现类配置放置在 META-INF/dubbo 路径下,下面来看一下配置内容。 optimusPrime = com.tianxiaobo.spi.OptimusPrime bumblebee = com.tianxiaobo.spi.Bumblebee 与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们就可以按需加载指定的实现类了。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。下面来演示一下 Dubbo SPI 的使用方式: public class DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); } } 测试结果如下: 演示完 Dubbo SPI,下面来看看 Dubbo SPI 对 Java SPI 做了哪些改进,以下内容引用至 Dubbo 官方文档。 JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。 增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。 在以上改进项中,第一个改进项比较好理解。第二个改进项没有进行验证,就不多说了。第三个改进项是增加了对 IOC 和 AOP 的支持,这是什么意思呢?这里简单解释一下,Dubbo SPI 加载完拓展实例后,会通过该实例的 setter 方法解析出实例依赖项的名称。比如通过 setProtocol 方法名,可知道目标实例依赖 Protocal。知道了具体的依赖,接下来即可到 IOC 容器中寻找或生成一个依赖对象,并通过 setter 方法将依赖注入到目标实例中。说完 Dubbo IOC,接下来说说 Dubbo AOP。Dubbo AOP 是指使用 Wrapper 类(可自定义实现)对拓展对象进行包装,Wrapper 类中包含了一些自定义逻辑,这些逻辑可在目标方法前行前后被执行,类似 AOP。Dubbo AOP 实现的很简单,其实就是个代理模式。这个官方文档中有所说明,大家有兴趣可以查阅一下。 关于 Dubbo SPI 的演示,以及与 Java SPI 的对比就先这么多,接下来加入源码分析阶段。 3. Dubbo SPI 源码分析 上一章,我简单演示了 Dubbo SPI 的使用方法。我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。该方法的逻辑比较简单,本章就不就行分析了。下面我们从 ExtensionLoader 的 getExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。 public T getExtension(String name) { if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null"); if ("true".equals(name)) { // 获取默认的拓展实现类 return getDefaultExtension(); } // Holder 仅用于持有目标对象,没其他什么逻辑 Holder<Object> holder = cachedInstances.get(name); if (holder == null) { cachedInstances.putIfAbsent(name, new Holder<Object>()); holder = cachedInstances.get(name); } Object instance = holder.get(); if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { // 创建拓展实例,并设置到 holder 中 instance = createExtension(name); holder.set(instance); } } } return (T) instance; } 上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建拓展对象的过程是怎样的。 private T createExtension(String name) { // 从配置文件中加载所有的拓展类,形成配置项名称到配置类的映射关系 Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { // 通过反射创建实例 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } // 向实例中注入依赖 injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (wrapperClasses != null && !wrapperClasses.isEmpty()) { // 循环创建 Wrapper 实例 for (Class<?> wrapperClass : wrapperClasses) { // 将当前 instance 作为参数创建 Wrapper 实例,然后向 Wrapper 实例中注入属性值, // 并将 Wrapper 实例赋值给 instance instance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException("..."); } } createExtension 方法的逻辑稍复杂一下,包含了如下的步骤: 通过 getExtensionClasses 获取所有的拓展类 通过反射创建拓展对象 向拓展对象中注入依赖 将拓展对象包裹在相应的 Wrapper 对象中 以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。在接下来的章节中,我将会重点分析 getExtensionClasses 方法的逻辑,以及简单分析 Dubbo IOC 的具体实现。 3.1 获取所有的拓展类 我们在通过名称获取拓展类之前,首先需要根据配置文件解析出名称到拓展类的映射,也就是 Map<名称, 拓展类>。之后再从 Map 中取出相应的拓展类即可。相关过程的代码分析如下: private Map<String, Class<?>> getExtensionClasses() { // 从缓存中获取已加载的拓展类 Map<String, Class<?>> classes = cachedClasses.get(); if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { // 加载拓展类 classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes; } 这里也是先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则加载拓展类。以上代码的写法是典型的双重检查锁,前面所分析的 getExtension 方法中有相似的代码。关于双重检查就说这么多,下面分析 loadExtensionClasses 方法的逻辑。 private Map<String, Class<?>> loadExtensionClasses() { // 获取 SPI 注解,这里的 type 是在调用 getExtensionLoader 方法时传入的 final SPI defaultAnnotation = type.getAnnotation(SPI.class); if (defaultAnnotation != null) { String value = defaultAnnotation.value(); if ((value = value.trim()).length() > 0) { // 对 SPI 注解内容进行切分 String[] names = NAME_SEPARATOR.split(value); // 检测 SPI 注解内容是否合法,不合法则抛出异常 if (names.length > 1) { throw new IllegalStateException("..."); } // 设置默认名称,cachedDefaultName 用于加载默认实现,参考 getDefaultExtension 方法 if (names.length == 1) { cachedDefaultName = names[0]; } } } Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>(); // 加载指定文件夹配置文件 loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); loadDirectory(extensionClasses, DUBBO_DIRECTORY); loadDirectory(extensionClasses, SERVICES_DIRECTORY); return extensionClasses; } loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。 private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) { // fileName = 文件夹路径 + type 全限定名 String fileName = dir + type.getName(); try { Enumeration<java.net.URL> urls; ClassLoader classLoader = findClassLoader(); if (classLoader != null) { // 根据文件名加载所有的同名文件 urls = classLoader.getResources(fileName); } else { urls = ClassLoader.getSystemResources(fileName); } if (urls != null) { while (urls.hasMoreElements()) { java.net.URL resourceURL = urls.nextElement(); // 加载资源 loadResource(extensionClasses, classLoader, resourceURL); } } } catch (Throwable t) { logger.error("..."); } } loadDirectory 方法代码不多,理解起来不难。该方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。 private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { try { BufferedReader reader = new BufferedReader( new InputStreamReader(resourceURL.openStream(), "utf-8")); try { String line; // 按行读取配置内容 while ((line = reader.readLine()) != null) { final int ci = line.indexOf('#'); if (ci >= 0) { // 截取 # 之前的字符串,# 之后的内容为注释 line = line.substring(0, ci); } line = line.trim(); if (line.length() > 0) { try { String name = null; int i = line.indexOf('='); if (i > 0) { // 以 = 为界,截取键与值。比如 dubbo=com.alibaba....DubboProtocol name = line.substring(0, i).trim(); line = line.substring(i + 1).trim(); } if (line.length() > 0) { // 加载解析出来的限定类名 loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); } } catch (Throwable t) { IllegalStateException e = new IllegalStateException("..."); } } } } finally { reader.close(); } } catch (Throwable t) { logger.error("..."); } } loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法有点名不副实,它的功能只是操作缓存,而非加载类。该方法的逻辑如下: private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { if (!type.isAssignableFrom(clazz)) { throw new IllegalStateException("..."); } if (clazz.isAnnotationPresent(Adaptive.class)) { // 检测目标类上是否有 Adaptive 注解 if (cachedAdaptiveClass == null) { // 设置 cachedAdaptiveClass缓存 cachedAdaptiveClass = clazz; } else if (!cachedAdaptiveClass.equals(clazz)) { throw new IllegalStateException("..."); } } else if (isWrapperClass(clazz)) { // 检测 clazz 是否是 Wrapper 类型 Set<Class<?>> wrappers = cachedWrapperClasses; if (wrappers == null) { cachedWrapperClasses = new ConcurrentHashSet<Class<?>>(); wrappers = cachedWrapperClasses; } // 存储 clazz 到 cachedWrapperClasses 缓存中 wrappers.add(clazz); } else { // 程序进入此分支,表明是一个普通的拓展类 // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常 clazz.getConstructor(); if (name == null || name.length() == 0) { // 如果 name 为空,则尝试从 Extension 注解获取 name,或使用小写的类名作为 name name = findAnnotationName(clazz); if (name.length() == 0) { throw new IllegalStateException("..."); } } // 切分 name String[] names = NAME_SEPARATOR.split(name); if (names != null && names.length > 0) { Activate activate = clazz.getAnnotation(Activate.class); if (activate != null) { // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键, // 存储 name 到 Activate 注解对象的映射关系 cachedActivates.put(names[0], activate); } for (String n : names) { if (!cachedNames.containsKey(clazz)) { // 存储 Class 到名称的映射关系 cachedNames.put(clazz, n); } Class<?> c = extensionClasses.get(n); if (c == null) { // 存储名称到 Class 的映射关系 extensionClasses.put(n, clazz); } else if (c != clazz) { throw new IllegalStateException("..."); } } } } } 如上,loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,该方法没有其他什么逻辑了,就不多说了。 到此,关于缓存类加载的过程就分析完了。整个过程没什么特别复杂的地方,大家按部就班的分析就行了,不懂的地方可以调试一下。接下来,我们来聊聊 Dubbo IOC 方面的内容。 3.2 Dubbo IOC Dubbo IOC 是基于 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。整个过程对应的代码如下: private T injectExtension(T instance) { try { if (objectFactory != null) { // 遍历目标类的所有方法 for (Method method : instance.getClass().getMethods()) { // 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) { // 获取 setter 方法参数类型 Class<?> pt = method.getParameterTypes()[0]; try { // 获取属性名 String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : ""; // 从 ObjectFactory 中获取依赖对象 Object object = objectFactory.getExtension(pt, property); if (object != null) { // 通过反射调用 setter 方法设置依赖 method.invoke(instance, object); } } catch (Exception e) { logger.error("..."); } } } } } catch (Exception e) { logger.error(e.getMessage(), e); } return instance; } 在上面代码中,objectFactory 变量的类型为 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部维护了一个 ExtensionFactory 列表,用于存储其他类型的 ExtensionFactory。Dubbo 目前提供了两种 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于创建自适应的拓展,关于自适应拓展,我将会在下一篇文章中进行说明。SpringExtensionFactory 则是到 Spring 的 IOC 容器中获取所需拓展,该类的实现并不复杂,大家自行分析源码,这里就不多说了。 Dubbo IOC 的实现比较简单,仅支持 setter 方式注入。总的来说,逻辑简单易懂。 4.总结 本篇文章简单介绍了 Java SPI 与 Dubbo SPI 用法与区别,并对 Dubbo SPI 的部分源码进行了分析。在 Dubbo SPI 中还有一块重要的逻辑没有进行分析,那就是 Dubbo SPI 的扩展点自适应机制。该机制的逻辑较为复杂,我将会在下一篇文章中进行分析。好了,其他的就不多说了,本篇文件就先到这里了。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处作者:田小波本文同步发布在我的个人博客:http://www.tianxiaobo.com 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章。起初,我只是打算通过博客的形式进行分享。但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大。在这7篇文章中,有4篇文章字数超过了1万,最长的一篇文章约有2.7万字(含代码)。考虑到超长文章对读者不太友好,以及拆分文章工作量也不小等问题。遂决定将博文整理成电子书,方便大家阅读。 经过两周紧张的排版,《一本小小的MyBatis源码分析书》诞生了。本书共7章,约300页。本书以电子书的形式发布,大家可自由的下载。下载地址如下: 百度网盘:点击下载 百度文库:审核中,这里先放上我的个人主页 CSDN: 点击下载 测试代码:GitHub 下面来看看本书章节的缩略图。 2.目录 第1章 MyBatis入门 1.1 MyBatis是什么 1.2 为什么要使用MyBatis 1.2.1 使用MyBatis访问数据库 1.2.2 使用JDBC访问数据库 1.2.3 使用SpringJDBC访问数据库 1.2.4 使用Hibernate访问数据库 1.3如何使用MyBatis 1.3.1 单独使用MyBatis 1.3.2 在Spring中使用MyBatis 1.4 本章小结 第2章 配置文件解析过程 2.1 配置文件解析过程分析 2.1.1 解析节点 2.1.2 解析节点 2.1.3 设置内容到Configuration中 2.1.4 解析节点 2.1.5 解析节点 2.1.6 解析节点 2.1.7 解析节点 2.2 本章小结 第3章 映射文件解析过程 3.1 映射文件解析解析入口 3.2 解析映射文件 3.2.1 解析节点 3.2.2 解析节点 3.2.3 解析节点 3.2.4 解析节点 3.2.5 解析SQL语句节点 3.3 Mapper接口绑定过程分析 3.4 处理未完成解析的节点 3.5 本章小结 第4章 SQL执行流程 4.1 SQL执行入口 4.1.1 为Mapper接口创建代理对象 4.1.2 执行代理逻辑 4.2 查询语句的执行过程 4.2.1 selectOne方法分析 4.2.2 获取BoundSql 4.2.3 创建StatementHandler 4.2.4 设置运行时参数到SQL中 4.2.5 #{}占位符的解析与参数的设置过程梳理 4.2.6 处理查询结果 4.3 更新语句的执行过程 4.3.1 更新语句执行过程全貌 4.3.2 KeyGenerator 4.3.3 处理更新结果 4.4 SQL执行过程总结 4.5 本章小结 第5章 内置数据源 5.1 内置数据源初始化过程 5.2 UnpooledDataSource 5.2.1 初始化数据库驱动 5.2.2 获取数据库连接 5.3 PooledDataSource 5.3.1 辅助类介绍 5.3.2 获取连接 5.3.3 回收连接 5.4 本章小结 第6章 缓存机制 6.1 缓存类介绍 6.1.1 PerpetualCache 6.1.2 LruCache 6.1.3 BlockingCache 6.2 CacheKey 6.3 一级缓存 6.4 二级缓存 6.5 本章小结 第7章 插件机制 7.1 插件机制原理 7.1.1 植入插件逻辑 7.1.2 执行插件逻辑 7.2 实现一个分页插件 7.3 本章小结 附录 MyBatis源码分析系列文章列表 3.写在最后 本书的排版工作耗时两周,其中40%的时间用在了内容的修改上,另外40%用在了代码的整理与排版上,最后的20%则是花在了图片和小修小改上。总的来说,整个过程还是有点辛苦的。当然,在完成排版后,成就感也是满满的。经过这次排版,深感写书不易。所以大家在日常学习过程中,应尽量买正版书予以支持。我在写MyBatis系列文章中,买了一本书作为参考,这本书是《MyBatis技术内幕》。这本书在我阅读源码的过程中,给予了不少的帮助,这里感谢该书的作者。同时,也向大家推荐这本书。另外,感谢清华出版社的王金柱编辑提供的书籍排版样例,使得我在排版的过程中可以有所参照。 最后需要说明的是,我个人工作刚满两年,不管是技术能力,还是工作经验,均处于入门水平。同时这也是我写的第一本电子书,经验不足。因此对于书中写的不好的地方,还请大家见谅,同时也希望大家多多指导。 好了,本文到此结束,感谢大家的阅读。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处作者:田小波本文同步发布在我的个人博客:http://www.tianxiaobo.com 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 在上一篇文章中,我向大家介绍了 Spring MVC 是如何处理 HTTP 请求的。Spring MVC 可对外提供服务时,说明其已经处于了就绪状态。再次之前,Spring MVC 需要进行一系列的初始化操作。正所谓兵马未动,粮草先行。这些操作包括创建容器,加载 DispatcherServlet 中用到的各种组件等。本篇文章就来和大家讨论一下这些初始化操作中的容器创建操作,容器的创建是其他一些初始化过程的基础。那其他的就不多说了,我们直入主题吧。 2.容器的创建过程 一般情况下,我们会在一个 Web 应用中配置两个容器。一个容器用于加载 Web 层的类,比如我们的接口 Controller、HandlerMapping、ViewResolver 等。在本文中,我们把这个容器叫做 web 容器。另一个容器用于加载业务逻辑相关的类,比如 service、dao 层的一些类。在本文中,我们把这个容器叫做业务容器。在容器初始化的过程中,业务容器会先于 web 容器进行初始化。web 容器初始化时,会将业务容器作为父容器。这样做的原因是,web 容器中的一些 bean 会依赖于业务容器中的 bean。比如我们的 controller 层接口通常会依赖 service 层的业务逻辑类。下面举个例子进行说明: 如上,我们将 dao 层的类配置在 application-dao.xml 文件中,将 service 层的类配置在 application-service.xml 文件中。然后我们将这两个配置文件通过 标签导入到 application.xml 文件中。此时,我们可以让业务容器去加载 application.xml 配置文件即可。另一方面,我们将 Web 相关的配置放在 application-web.xml 文件中,并将该文件交给 Web 容器去加载。 这里我们把配置文件进行分层,结构上看起来清晰了很多,也便于维护。这个其实和代码分层是一个道理,如果我们把所有的代码都放在同一个包下,那看起来会多难受啊。同理,我们用业务容器和 Web 容器去加载不同的类也是一种分层的体现吧。当然,如果应用比较简单,仅用 Web 容器去加载所有的类也不是不可以。 2.1 业务容器的创建过程 前面说了一些背景知识作为铺垫,那下面我们开始分析容器的创建过程吧。按照创建顺序,我们先来分析业务容器的创建过程。业务容器的创建入口是 ContextLoaderListener 的 contextInitialized 方法。顾名思义,ContextLoaderListener 是用来监听 ServletContext 加载事件的。当 ServletContext 被加载后,监听器的 contextInitialized 方法就会被 Servlet 容器调用。ContextLoaderListener Spring 框架提供的,它的配置方法如下: <web-app> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:application.xml</param-value> </context-param> <!-- 省略其他配置 --> </web-app> 如上,ContextLoaderListener 可通过 ServletContext 获取到 contextConfigLocation 配置。这样,业务容器就可以加载 application.xml 配置文件了。那下面我们来分析一下 ContextLoaderListener 的源码吧。 public class ContextLoaderListener extends ContextLoader implements ServletContextListener { // 省略部分代码 @Override public void contextInitialized(ServletContextEvent event) { // 初始化 WebApplicationContext initWebApplicationContext(event.getServletContext()); } } public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { /* * 如果 ServletContext 中 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 属性值 * 不为空时,表明有其他监听器设置了这个属性。Spring 认为不能替换掉别的监听器设置 * 的属性值,所以这里抛出异常。 */ if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { throw new IllegalStateException( "Cannot initialize context because there is already a root application context present - " + "check whether you have multiple ContextLoader* definitions in your web.xml!"); } Log logger = LogFactory.getLog(ContextLoader.class); servletContext.log("Initializing Spring root WebApplicationContext"); if (logger.isInfoEnabled()) {...} long startTime = System.currentTimeMillis(); try { if (this.context == null) { // 创建 WebApplicationContext this.context = createWebApplicationContext(servletContext); } if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; if (!cwac.isActive()) { if (cwac.getParent() == null) { /* * 加载父 ApplicationContext,一般情况下,业务容器不会有父容器, * 除非进行配置 */ ApplicationContext parent = loadParentContext(servletContext); cwac.setParent(parent); } // 配置并刷新 WebApplicationContext configureAndRefreshWebApplicationContext(cwac, servletContext); } } // 设置 ApplicationContext 到 servletContext 中 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { currentContextPerThread.put(ccl, this.context); } if (logger.isDebugEnabled()) {...} if (logger.isInfoEnabled()) {...} return this.context; } catch (RuntimeException ex) { logger.error("Context initialization failed", ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex; } catch (Error err) { logger.error("Context initialization failed", err); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); throw err; } } 如上,我们看一下上面的创建过程。首先 Spring 会检测 ServletContext 中 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 属性有没有被设置,若被设置过,则抛出异常。若未设置,则调用 createWebApplicationContext 方法创建容器。创建好后,再调用 configureAndRefreshWebApplicationContext 方法配置并刷新容器。最后,调用 setAttribute 方法将容器设置到 ServletContext 中。经过以上几步,整个创建流程就结束了。流程并不复杂,可简单总结为创建容器 → 配置并刷新容器 → 设置容器到 ServletContext 中。这三步流程中,最后一步就不进行分析,接下来分析一下第一步和第二步流程对应的源码。如下: protected WebApplicationContext createWebApplicationContext(ServletContext sc) { // 判断创建什么类型的容器,默认类型为 XmlWebApplicationContext Class<?> contextClass = determineContextClass(sc); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]"); } // 通过反射创建容器 return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); } protected Class<?> determineContextClass(ServletContext servletContext) { /* * 读取用户自定义配置,比如: * <context-param> * <param-name>contextClass</param-name> * <param-value>XXXConfigWebApplicationContext</param-value> * </context-param> */ String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM); if (contextClassName != null) { try { return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader()); } catch (ClassNotFoundException ex) { throw new ApplicationContextException( "Failed to load custom context class [" + contextClassName + "]", ex); } } else { /* * 若无自定义配置,则获取默认的容器类型,默认类型为 XmlWebApplicationContext。 * defaultStrategies 读取的配置文件为 ContextLoader.properties, * 该配置文件内容如下: * org.springframework.web.context.WebApplicationContext = * org.springframework.web.context.support.XmlWebApplicationContext */ contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName()); try { return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader()); } catch (ClassNotFoundException ex) { throw new ApplicationContextException( "Failed to load default context class [" + contextClassName + "]", ex); } } } 简单说一下 createWebApplicationContext 方法的流程,该方法首先会调用 determineContextClass 判断创建什么类型的容器,默认为 XmlWebApplicationContext。然后调用 instantiateClass 方法通过反射的方式创建容器实例。instantiateClass 方法就不跟进去分析了,大家可以自己去看看,比较简单。 继续往下分析,接下来分析一下 configureAndRefreshWebApplicationContext 方法的源码。如下: protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // 从 ServletContext 中获取用户配置的 contextId 属性 String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); if (idParam != null) { // 设置容器 id wac.setId(idParam); } else { // 用户未配置 contextId,则设置一个默认的容器 id wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } wac.setServletContext(sc); // 获取 contextConfigLocation 配置 String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM); if (configLocationParam != null) { wac.setConfigLocation(configLocationParam); } ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(sc, null); } customizeContext(sc, wac); // 刷新容器 wac.refresh(); } 上面的源码不是很长,逻辑不是很复杂。下面简单总结 configureAndRefreshWebApplicationContext 方法主要做了事情,如下: 设置容器 id 获取 contextConfigLocation 配置,并设置到容器中 刷新容器 到此,关于业务容器的创建过程就分析完了,下面我们继续分析 Web 容器的创建过程。 2.2 Web 容器的创建过程 前面说了业务容器的创建过程,业务容器是通过 ContextLoaderListener。那 Web 容器是通过什么创建的呢?答案是通过 DispatcherServlet。我在上一篇文章介绍 HttpServletBean 抽象类时,说过该类覆写了父类 HttpServlet 中的 init 方法。这个方法就是创建 Web 容器的入口,那下面我们就从这个方法入手。如下: // -- org.springframework.web.servlet.HttpServletBean public final void init() throws ServletException { if (logger.isDebugEnabled()) {...} // 获取 ServletConfig 中的配置信息 PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); if (!pvs.isEmpty()) { try { /* * 为当前对象(比如 DispatcherServlet 对象)创建一个 BeanWrapper, * 方便读/写对象属性。 */ BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); initBeanWrapper(bw); // 设置配置信息到目标对象中 bw.setPropertyValues(pvs, true); } catch (BeansException ex) { if (logger.isErrorEnabled()) {...} throw ex; } } // 进行后续的初始化 initServletBean(); if (logger.isDebugEnabled()) {...} } protected void initServletBean() throws ServletException { } 上面的源码主要做的事情是将 ServletConfig 中的配置信息设置到 HttpServletBean 的子类对象中(比如 DispatcherServlet),我们并未从上面的源码中发现创建容器的痕迹。不过如果大家注意看源码的话,会发现 initServletBean 这个方法稍显奇怪,是个空方法。这个方法的访问级别为 protected,子类可进行覆盖。HttpServletBean 子类 FrameworkServlet 覆写了这个方法,下面我们到 FrameworkServlet 中探索一番。 // -- org.springframework.web.servlet.FrameworkServlet protected final void initServletBean() throws ServletException { getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'"); if (this.logger.isInfoEnabled()) {...} long startTime = System.currentTimeMillis(); try { // 初始化容器 this.webApplicationContext = initWebApplicationContext(); initFrameworkServlet(); } catch (ServletException ex) { this.logger.error("Context initialization failed", ex); throw ex; } catch (RuntimeException ex) { this.logger.error("Context initialization failed", ex); throw ex; } if (this.logger.isInfoEnabled()) {...} } protected WebApplicationContext initWebApplicationContext() { // 从 ServletContext 中获取容器,也就是 ContextLoaderListener 创建的容器 WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; /* * 若下面的条件成立,则需要从外部设置 webApplicationContext。有两个途径可以设置 * webApplicationContext,以 DispatcherServlet 为例: * 1. 通过 DispatcherServlet 有参构造方法传入 WebApplicationContext 对象 * 2. 将 DispatcherServlet 配置到其他容器中,由其他容器通过 * setApplicationContext 方法进行设置 * * 途径1 可参考 AbstractDispatcherServletInitializer 中的 * registerDispatcherServlet 方法源码。一般情况下,代码执行到此处, * this.webApplicationContext 为 null,大家可自行调试进行验证。 */ if (this.webApplicationContext != null) { wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { if (cwac.getParent() == null) { // 设置 rootContext 为父容器 cwac.setParent(rootContext); } // 配置并刷新容器 configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // 尝试从 ServletContext 中获取容器 wac = findWebApplicationContext(); } if (wac == null) { // 创建容器,并将 rootContext 作为父容器 wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { onRefresh(wac); } if (this.publishContext) { String attrName = getServletContextAttributeName(); // 将创建好的容器设置到 ServletContext 中 getServletContext().setAttribute(attrName, wac); if (this.logger.isDebugEnabled()) {...} } return wac; } protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) { // 获取容器类型,默认为 XmlWebApplicationContext.class Class<?> contextClass = getContextClass(); if (this.logger.isDebugEnabled()) {...} if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException( "Fatal initialization error in servlet with name '" + getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext"); } // 通过反射实例化容器 ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment()); wac.setParent(parent); wac.setConfigLocation(getContextConfigLocation()); // 配置并刷新容器 configureAndRefreshWebApplicationContext(wac); return wac; } protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // 设置容器 id if (this.contextId != null) { wac.setId(this.contextId); } else { // 生成默认 id wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName()); } } wac.setServletContext(getServletContext()); wac.setServletConfig(getServletConfig()); wac.setNamespace(getNamespace()); wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener())); ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig()); } // 后置处理,子类可以覆盖进行一些自定义操作。在 Spring MVC 未使用到,是个空方法。 postProcessWebApplicationContext(wac); applyInitializers(wac); // 刷新容器 wac.refresh(); } 以上就是创建 Web 容器的源码,下面总结一下该容器创建的过程。如下: 从 ServletContext 中获取 ContextLoaderListener 创建的容器 若 this.webApplicationContext != null 条件成立,仅设置父容器和刷新容器即可 尝试从 ServletContext 中获取容器,若容器不为空,则无需执行步骤4 创建容器,并将 rootContext 作为父容器 设置容器到 ServletContext 中 到这里,关于 Web 容器的创建过程就讲完了。总的来说,Web 容器的创建过程和业务容器的创建过程大致相同,但是差异也是有的,不能忽略。 3.总结 本篇文章对 Spring MVC 两种容器的创建过程进行了较为详细的分析,总的来说两种容器的创建过程并不是很复杂。大家在分析这两种容器的创建过程时,看的不明白的地方,可以进行调试,这对于理解代码逻辑还是很有帮助的。当然阅读 Spring MVC 部分的源码最好有 Servlet 和 Spring IOC 容器方面的知识,这些是基础,Spring MVC 就是在这些基础上构建的。 限于个人能力,文章叙述有误,还望大家指明。也请多多指教,在这里说声谢谢。好了,本篇文章就到这里了。感谢大家的阅读。 参考 《看透Spring MVC》- 韩路彪 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 在前面的文章中,我较为详细的分析了 Spring IOC 和 AOP 部分的源码,并写成了文章。为了让我的 Spring 源码分析系列文章更为丰富一些,所以从本篇文章开始,我将来向大家介绍一下 Spring MVC 的一些原理。在本篇文章中,你将会了解到 Spring MVC 处理请求的过程。同时,你也会了解到 Servlet 相关的知识。以及 Spring MVC 的核心 DispatcherServlet 类的源码分析。在掌握以上内容后,相信大家会对 Spring MVC 的原理有更深的认识。 如果大家对上面介绍的知识点感兴趣的话,那下面不妨和我一起来去探索 Spring MVC 的原理。Let`s Go。 2.一个请求的旅行过程 在探索更深层次的原理之前,我们先来了解一下 Spring MVC 是怎么处理请求的。弄懂了这个流程后,才能更好的理解具体的源码。这里我把 Spring MVC 处理请求的流程图画了出来,一起看一下吧: 如上,每一个重要的步骤上面都有编号。我先来简单分析一下上面的流程,然后再向大家介绍图中出现的一些组件。我们从第一步开始,首先,用户的浏览器发出了一个请求,这个请求经过互联网到达了我们的服务器。Servlet 容器首先接待了这个请求,并将该请求委托给 DispatcherServlet 进行处理。接着 DispatcherServlet 将该请求传给了处理器映射组件 HandlerMapping,并获取到适合该请求的拦截器和处理器。在获取到处理器后,DispatcherServlet 还不能直接调用处理器的逻辑,需要进行对处理器进行适配。处理器适配成功后,DispatcherServlet 通过处理器适配器 HandlerAdapter 调用处理器的逻辑,并获取返回值 ModelAndView。之后,DispatcherServlet 需要根据 ModelAndView 解析视图。解析视图的工作由 ViewResolver 完成,若能解析成功,ViewResolver 会返回相应的视图对象 View。在获取到具体的 View 对象后,最后一步要做的事情就是由 View 渲染视图,并将渲染结果返回给用户。 以上就是 Spring MVC 处理请求的全过程,上面的流程进行了一定的简化,比如拦截器的执行时机就没说。不过这并不影响大家对主过程的理解。下来来简单介绍一下图中出现的一些组件: 组件 说明 DispatcherServlet Spring MVC 的核心组件,是请求的入口,负责协调各个组件工作 HandlerMapping 内部维护了一些 <访问路径, 处理器> 映射,负责为请求找到合适的处理器 HandlerAdapter 处理器的适配器。Spring 中的处理器的实现多变,比如用户处理器可以实现 Controller 接口,也可以用 @RequestMapping 注解将方法作为一个处理器等,这就导致 Spring 不止到怎么调用用户的处理器逻辑。所以这里需要一个处理器适配器,由处理器适配器去调用处理器的逻辑 ViewResolver 视图解析器的用途不难理解,用于将视图名称解析为视图对象 View。 View 视图对象用于将模板渲染成 html 或其他类型的文件。比如 InternalResourceView 可将 jsp 渲染成 html。 从上面的流程中可以看出,Spring MVC 对各个组件的职责划分的比较清晰。DispatcherServlet 负责协调,其他组件则各自做分内之事,互不干扰。经过这样的职责划分,代码会便于维护。同时对于源码阅读者来说,也会很友好。可以降低理解源码的难度,使大家能够快速理清主逻辑。这一点值得我们学习。 3.知其然,更要知其所以然 3.1 追根溯源之 Servlet 本章要向大家介绍一下 Servlet,为什么要介绍 Servlet 呢?原因不难理解,Spring MVC 是基于 Servlet 实现的。所以要分析 Spring MVC,首先应追根溯源,弄懂 Servlet。Servlet 是 J2EE 规范之一,在遵守该规范的前提下,我们可将 Web 应用部署在 Servlet 容器下。这样做的好处是什么呢?我觉得可使开发者聚焦业务逻辑,而不用去关心 HTTP 协议方面的事情。比如,普通的 HTTP 请求就是一段有格式的文本,服务器需要去解析这段文本才能知道用户请求的内容是什么。比如我对个人网站的 80 端口抓包,然后获取到的 HTTP 请求头如下: 如果我们为了写一个 Web 应用,还要去解析 HTTP 协议相关的内容,那会增加很多工作量。有兴趣的朋友可以考虑使用 Java socket 编写实现一个 HTTP 服务器,体验一下解析部分 HTTP 协议的过程。也可以参考我之前写的文章 - 基于 Java NIO 实现简单的 HTTP 服务器。 如果我们写的 Web 应用不大,不夸张的说,项目中对 HTTP 提供支持的代码会比业务代码还要多,这岂不是得不偿失。当然,在现实中,有现成的框架可用,并不需要自己造轮子。如果我们基于 Servlet 规范实现 Web 应用的话,HTTP 协议的处理过程就不需要我们参与了。这些工作交给 Servlet 容器就行了,我们只需要关心业务逻辑怎么实现即可。 下面,我们先来看看 Servlet 接口及其实现类结构,然后再进行更进一步的说明。 如上图,我们接下来按照从上到下顺序进行分析。先来看看最顶层的两个接口是怎么定义的。 3.1.1 Servlet 与 ServletConfig 先来看看 Servlet 接口的定义,如下: public interface Servlet { public void init(ServletConfig config) throws ServletException; public ServletConfig getServletConfig(); public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public String getServletInfo(); public void destroy(); } init 方法会在容器启动时由容器调用,也可能会在 Servlet 第一次被使用时调用,调用时机取决 load-on-start 的配置。容器调用 init 方法时,会向其传入一个 ServletConfig 参数。ServletConfig 是什么呢?顾名思义,ServletConfig 是一个和 Servlet 配置相关的接口。举个例子说明一下,我们在配置 Spring MVC 的 DispatcherServlet 时,会通过 ServletConfig 将配置文件的位置告知 DispatcherServlet。比如: <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:application-web.xml</param-value> </init-param> </servlet> 如上, 标签内的配置信息最终会被放入 ServletConfig 实现类对象中。DispatcherServlet 通过 ServletConfig 接口中的方法,就能获取到 contextConfigLocation 对应的值。 Servlet 中的 service 方法用于处理请求。当然,一般情况下我们不会直接实现 Servlet 接口,通常是通过继承 HttpServlet 抽象类编写业务逻辑的。Servlet 中接口不多,也不难理解,这里就不多说了。下面我们来看看 ServletConfig 接口定义,如下: public interface ServletConfig { public String getServletName(); public ServletContext getServletContext(); public String getInitParameter(String name); public Enumeration<String> getInitParameterNames(); } 先来看看 getServletName 方法,该方法用于获取 servlet 名称,也就是 标签中配置的内容。getServletContext 方法用于获取 Servlet 上下文。如果说一个 ServletConfig 对应一个 Servlet,那么一个 ServletContext 则是对应所有的 Servlet。ServletContext 代表当前的 Web 应用,可用于记录一些全局变量,当然它的功能不局限于记录变量。我们可通过 标签向 ServletContext 中配置信息,比如在配置 Spring 监听器(ContextLoaderListener)时,就可以通过该标签配置 contextConfigLocation。如下: <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:application.xml</param-value> </context-param> 关于 ServletContext 就先说这么多了,继续介绍 ServletConfig 中的其他方法。getInitParameter 方法用于获取 标签中配置的参数值,getInitParameterNames 则是获取所有配置的名称集合,这两个方法用途都不难理解。 以上是 Servlet 与 ServletConfig 两个接口的说明,比较简单。说完这两个接口,我们继续往下看,接下来是 GenericServlet。 3.1.2 GenericServlet GenericServlet 实现了 Servlet 和 ServletConfig 两个接口,为这两个接口中的部分方法提供了简单的实现。比如该类实现了 Servlet 接口中的 void init(ServletConfig) 方法,并在方法体内调用了内部提供了一个无参的 init 方法,子类可覆盖该无参 init 方法。除此之外,GenericServlet 还实现了 ServletConfig 接口中的 getInitParameter 方法,用户可直接调用该方法获取到配置信息。而不用先获取 ServletConfig,然后再调用 ServletConfig 的 getInitParameter 方法获取。下面我们来看看 GenericServlet 部分方法的源码: public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable { // 省略部分代码 private transient ServletConfig config; public GenericServlet() { } /** 有参 init 方法 */ public void init(ServletConfig config) throws ServletException { this.config = config; // 调用内部定义的无参 init 方法 this.init(); } /** 无参 init 方法,子类可覆盖该方法 */ public void init() throws ServletException { } /** 未给 service 方法提供具体的实现 */ public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public void destroy() { } /** 通过 getInitParameter 可直接从 ServletConfig 实现类中获取配置信息 */ public String getInitParameter(String name) { ServletConfig sc = getServletConfig(); if (sc == null) { throw new IllegalStateException( lStrings.getString("err.servlet_config_not_initialized")); } return sc.getInitParameter(name); } public ServletConfig getServletConfig() { return config; } // 省略部分代码 } 如上,GenericServlet 代码比较简单,配合着我写注释,很容易看懂。 GenericServlet 是一个协议无关的 servlet,是一个比较原始的实现,通常我们不会直接继承该类。一般情况下,我们都是继承 GenericServlet 的子类 HttpServlet,该类是一个和 HTTP 协议相关的 Servlet。那下面我们来看一下这个类。 3.1.3 HttpServlet HttpServlet,从名字上就可看出,这个类是和 HTTP 协议相关。该类的关注点在于怎么处理 HTTP 请求,比如其定义了 doGet 方法处理 GET 类型的请求,定义了 doPost 方法处理 POST 类型的请求等。我们若需要基于 Servlet 写 Web 应用,应继承该类,并覆盖指定的方法。doGet 和 doPost 等方法并不是处理的入口方法,所以这些方法需要由其他方法调用才行。其他方法是哪个方法呢?当然是 service 方法了。下面我们看一下这个方法的实现。如下: @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) { throw new ServletException("non-HTTP request or response"); } request = (HttpServletRequest) req; response = (HttpServletResponse) res; // 调用重载方法,该重载方法接受 HttpServletRequest 和 HttpServletResponse 类型的参数 service(request, response); } protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); // 处理 GET 请求 if (method.equals(METHOD_GET)) { long lastModified = getLastModified(req); if (lastModified == -1) { // 调用 doGet 方法 doGet(req, resp); } else { long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); if (ifModifiedSince < lastModified) { maybeSetLastModified(resp, lastModified); doGet(req, resp); } else { resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } } // 处理 HEAD 请求 } else if (method.equals(METHOD_HEAD)) { long lastModified = getLastModified(req); maybeSetLastModified(resp, lastModified); doHead(req, resp); // 处理 POST 请求 } else if (method.equals(METHOD_POST)) { // 调用 doPost 方法 doPost(req, resp); } else if (method.equals(METHOD_PUT)) { doPut(req, resp); } else if (method.equals(METHOD_DELETE)) { doDelete(req, resp); } else if (method.equals(METHOD_OPTIONS)) { doOptions(req,resp); } else if (method.equals(METHOD_TRACE)) { doTrace(req,resp); } else { String errMsg = lStrings.getString("http.method_not_implemented"); Object[] errArgs = new Object[1]; errArgs[0] = method; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); } } 如上,第一个 service 方法覆盖父类中的抽象方法,并没什么太多逻辑。所有的逻辑集中在第二个 service 方法中,该方法根据请求类型分发请求。我们可以根据需要覆盖指定的处理方法。 以上所述只是 Servlet 规范中的一部分内容,这些内容是和本文相关的内容。对于 Servlet 规范中的其他内容,大家有兴趣可以自己去探索。好了,关于 Servlet 方面的内容,这里先说这么多。 3.2 DispatcherServlet 族谱 我在前面说到,DispatcherServlet 是 Spring MVC 的核心。所以在分析这个类的源码前,我们有必要了解一下它的族谱,也就是继承关系图。如下: 如上图,红色框是 Servlet 中的接口和类,蓝色框中则是 Spring 中的接口和类。关于 Servlet 内容前面已经说过,下面来简单介绍一下蓝色框中的接口和类,我们从最顶层的接口开始。 ● Aware 在 Spring 中,Aware 类型的接口用于向 Spring “索要”一些框架中的信息。比如当某个 bean 实现了 ApplicationContextAware 接口时,Spring 在运行时会将当前的 ApplicationContext 实例通过接口方法 setApplicationContext 传给该 bean。下面举个例子说明,这里我写一个 SystemInfo API,通过该 API 返回一些系统信息。代码如下: @RestController @RequestMapping("/systeminfo") public class SystemInfo implements ApplicationContextAware, EnvironmentAware { private ApplicationContext applicationContext; private Environment environment; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println(applicationContext.getClass()); this.applicationContext = applicationContext; } @Override public void setEnvironment(Environment environment) { this.environment = environment; } @RequestMapping("/env") public String environment() { StandardServletEnvironment sse = (StandardServletEnvironment) environment; Map<String, Object> envs = sse.getSystemEnvironment(); StringBuilder sb = new StringBuilder(); sb.append("-------------------------++ System Environment ++-------------------------\n"); List<String> list = new ArrayList<>(); list.addAll(envs.keySet()); for (int i = 0; i < 5 && i < list.size(); i++) { String key = list.get(i); Object val = envs.get(key); sb.append(String.format("%s = %s\n", key, val.toString())); } Map<String, Object> props = sse.getSystemProperties(); sb.append("\n-------------------------++ System Properties ++-------------------------\n"); list.clear(); list.addAll(props.keySet()); for (int i = 0; i < 5 && i < list.size(); i++) { String key = list.get(i); Object val = props.get(key); sb.append(String.format("%s = %s\n", key, val.toString())); } return sb.toString(); } @RequestMapping("/beans") public String listBeans() { ListableBeanFactory lbf = applicationContext; String[] beanNames = lbf.getBeanDefinitionNames(); StringBuilder sb = new StringBuilder(); sb.append("-------------------------++ Bean Info ++-------------------------\n"); Arrays.stream(beanNames).forEach(beanName -> { Object bean = lbf.getBean(beanName); sb.append(String.format("beanName = %s\n", beanName)); sb.append(String.format("beanClass = %s\n\n", bean.getClass().toString())); }); return sb.toString(); } } 如上,SystemInfo 分别实现了 ApplicationContextAware 和 EnvironmentAware 接口,因此它可以在运行时获取到 ApplicationContext 和 Environment 实例。下面我们调一下接口看看结果吧: 如上,我们通过接口拿到了环境变量、配置信息以及容器中所有 bean 的数据。这说明,Spring 在运行时向 SystemInfo 中注入了 ApplicationContext 和 Environment 实例。 ● EnvironmentCapable EnvironmentCapable 仅包含一个方法定义 getEnvironment,通过该方法可以获取到环境变量对象。我们可以将 EnvironmentCapable 和 EnvironmentAware 接口配合使用,比如下面的实例: public class EnvironmentHolder implements EnvironmentCapable, EnvironmentAware { private Environment environment; @Override public void setEnvironment(Environment environment) { this.environment = environment; } @Override public Environment getEnvironment() { return environment; } } ● HttpServletBean HttpServletBean 是 HttpServlet 抽象类的简单拓展。HttpServletBean 覆写了父类中的无参 init 方法,并在该方法中将 ServletConfig 里的配置信息设置到子类对象中,比如 DispatcherServlet。 ● FrameworkServlet FrameworkServlet 是 Spring Web 框架中的一个基础类,该类会在初始化时创建一个容器。同时该类覆写了 doGet、doPost 等方法,并将所有类型的请求委托给 doService 方法去处理。doService 是一个抽象方法,需要子类实现。 ● DispatcherServlet DispatcherServlet 主要的职责相信大家都比较清楚了,即协调各个组件工作。除此之外,DispatcherServlet 还有一个重要的事情要做,即初始化各种组件,比如 HandlerMapping、HandlerAdapter 等。 3.3 DispatcherServlet 源码简析 在第二章中,我们知道了一个 HTTP 请求是怎么样被 DispatcherServlet 处理的。本节,我们从源码的角度对第二章的内容进行补充说明。这里,我们直入主题,直接分析 DispatcherServlet 中的 doDispatch 方法。这里我把请求的处理流程图再贴一遍,大家可以对着流程图阅读源码。 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // 获取可处理当前请求的处理器 Handler,对应流程图中的步骤② mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // 获取可执行处理器逻辑的适配器 HandlerAdapter,对应步骤③ HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 处理 last-modified 消息头 String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } // 执行拦截器 preHandle 方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 调用处理器逻辑,对应步骤④ mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } // 如果 controller 未返回 view 名称,这里生成默认的 view 名称 applyDefaultViewName(processedRequest, mv); // 执行拦截器 preHandle 方法 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { dispatchException = new NestedServletException("Handler dispatch failed", err); } // 解析并渲染视图 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } } private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception { boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } if (mv != null && !mv.wasCleared()) { // 渲染视图 render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isDebugEnabled()) {... } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { return; } if (mappedHandler != null) { mappedHandler.triggerAfterCompletion(request, response, null); } } protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { Locale locale = this.localeResolver.resolveLocale(request); response.setLocale(locale); View view; /* * 若 mv 中的 view 是 String 类型,即处理器返回的是模板名称, * 这里将其解析为具体的 View 对象 */ if (mv.isReference()) { // 解析视图,对应步骤⑤ view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'"); } } else { view = mv.getView(); if (view == null) { throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'"); } } if (logger.isDebugEnabled()) {...} try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } // 渲染视图,并将结果返回给用户。对应步骤⑥和⑦ view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) {...} throw ex; } } 以上就是 doDispatch 方法的分析过程,我已经做了较为详细的注释,这里就不多说了。需要说明的是,以上只是进行了简单分析,并没有深入分析每个方法调用。大家若有兴趣,可以自己去分析一下 doDispatch 所调用的一些方法,比如 getHandler 和 getHandlerAdapter,这两个方法比较简单。从我最近所分析的源码来看,我个人觉得处理器适配器 RequestMappingHandlerAdapter 应该是 Spring MVC 中最为复杂的一个类。该类用于对 @RequestMapping 注解的方法进行适配。该类的逻辑我暂时没看懂,就不多说了,十分尴尬。关于该类比较详细的分析,大家可以参考《看透Spring MVC》一书。 4.总结 到此,本篇文章的主体内容就说完了。本篇文章从一个请求的旅行过程进行分析,并在分析的过程中补充了 Servlet 和 DispatcherServlet 方面的知识。在最后,从源码的角度分析了 DispatcherServlet 处理请求的过程。总的来算,算是做到了循序渐进。当然,限于个人能力,以上内容可能会有一些讲的不好的地方,这里请大家见谅。同时,也希望大家多多指教。 好了,本篇文章先到这里。谢谢大家的阅读。 参考 《看透Spring MVC》- 韩路彪 《JSP & Servlet学习笔记》- 林信良 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 本篇文章是 AOP 源码分析系列文章的最后一篇文章,在前面的两篇文章中,我分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器,以及如何创建代理对象的过程。现在我们的得到了 bean 的代理对象,且通知也以合适的方式插在了目标方法的前后。接下来要做的事情,就是执行通知逻辑了。通知可能在目标方法前执行,也可能在目标方法后执行。具体的执行时机,取决于用户的配置。当目标方法被多个通知匹配到时,Spring 通过引入拦截器链来保证每个通知的正常执行。在本文中,我们将会通过源码了解到 Spring 是如何支持 expose-proxy 属性的,以及通知与拦截器之间的关系,拦截器链的执行过程等。和上一篇文章一样,在进行源码分析前,我们先来了解一些背景知识。好了,下面进入正题吧。 2.背景知识 关于 expose-proxy,我们先来说说它有什么用,然后再来说说怎么用。Spring 引入 expose-proxy 特性是为了解决目标方法调用同对象中其他方法时,其他方法的切面逻辑无法执行的问题。这个解释可能不好理解,不直观。那下面我来演示一下它的用法,大家就知道是怎么回事了。我们先来看看 expose-proxy 是怎样配置的,如下: <bean id="hello" class="xyz.coolblog.aop.Hello"/> <bean id="aopCode" class="xyz.coolblog.aop.AopCode"/> <aop:aspectj-autoproxy expose-proxy="true" /> <aop:config expose-proxy="true"> <aop:aspect id="myaspect" ref="aopCode"> <aop:pointcut id="helloPointcut" expression="execution(* xyz.coolblog.aop.*.hello*(..))" /> <aop:before method="before" pointcut-ref="helloPointcut" /> </aop:aspect> </aop:config> 如上,expose-proxy 可配置在 <aop:config/> 和 <aop:aspectj-autoproxy /> 标签上。在使用 expose-proxy 时,需要对内部调用进行改造,比如: public class Hello implements IHello { @Override public void hello() { System.out.println("hello"); this.hello("world"); } @Override public void hello(String hello) { System.out.println("hello " + hello); } } hello()方法调用了同类中的另一个方法hello(String),此时hello(String)上的切面逻辑就无法执行了。这里,我们要对hello()方法进行改造,强制它调用代理对象中的hello(String)。改造结果如下: public class Hello implements IHello { @Override public void hello() { System.out.println("hello"); ((IHello) AopContext.currentProxy()).hello("world"); } @Override public void hello(String hello) { System.out.println("hello " + hello); } } 如上,AopContext.currentProxy()用于获取当前的代理对象。当 expose-proxy 被配置为 true 时,该代理对象会被放入 ThreadLocal 中。关于 expose-proxy,这里先说这么多,后面分析源码时会再次提及。 3.源码分析 本章所分析的源码来自 JdkDynamicAopProxy,至于 CglibAopProxy 中的源码,大家若有兴趣可以自己去看一下。 3.1 JDK 动态代理逻辑分析 本节,我来分析一下 JDK 动态代理逻辑。对于 JDK 动态代理,代理逻辑封装在 InvocationHandler 接口实现类的 invoke 方法中。JdkDynamicAopProxy 实现了 InvocationHandler 接口,下面我们就来分析一下 JdkDynamicAopProxy 的 invoke 方法。如下: public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodInvocation invocation; Object oldProxy = null; boolean setProxyContext = false; TargetSource targetSource = this.advised.targetSource; Class<?> targetClass = null; Object target = null; try { // 省略部分代码 Object retVal; // 如果 expose-proxy 属性为 true,则暴露代理对象 if (this.advised.exposeProxy) { // 向 AopContext 中设置代理对象 oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } // 获取适合当前方法的拦截器 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); // 如果拦截器链为空,则直接执行目标方法 if (chain.isEmpty()) { Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); // 通过反射执行目标方法 retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { // 创建一个方法调用器,并将拦截器链传入其中 invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); // 执行拦截器链 retVal = invocation.proceed(); } // 获取方法返回值类型 Class<?> returnType = method.getReturnType(); if (retVal != null && retVal == target && returnType != Object.class && returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { // 如果方法返回值为 this,即 return this; 则将代理对象 proxy 赋值给 retVal retVal = proxy; } // 如果返回值类型为基础类型,比如 int,long 等,当返回值为 null,抛出异常 else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } return retVal; } finally { if (target != null && !targetSource.isStatic()) { targetSource.releaseTarget(target); } if (setProxyContext) { AopContext.setCurrentProxy(oldProxy); } } } 如上,上面的代码我做了比较详细的注释。下面我们来总结一下 invoke 方法的执行流程,如下: 检测 expose-proxy 是否为 true,若为 true,则暴露代理对象 获取适合当前方法的拦截器 如果拦截器链为空,则直接通过反射执行目标方法 若拦截器链不为空,则创建方法调用 ReflectiveMethodInvocation 对象 调用 ReflectiveMethodInvocation 对象的 proceed() 方法启动拦截器链 处理返回值,并返回该值 在以上6步中,我们重点关注第2步和第5步中的逻辑。第2步用于获取拦截器链,第5步则是启动拦截器链。下面先来分析获取拦截器链的过程。 3.2 获取所有的拦截器 所谓的拦截器,顾名思义,是指用于对目标方法的调用进行拦截的一种工具。拦截器的源码比较简单,所以我们直接看源码好了。下面以前置通知拦截器为例,如下: public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable { /** 前置通知 */ private MethodBeforeAdvice advice; public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { Assert.notNull(advice, "Advice must not be null"); this.advice = advice; } @Override public Object invoke(MethodInvocation mi) throws Throwable { // 执行前置通知逻辑 this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); // 通过 MethodInvocation 调用下一个拦截器,若所有拦截器均执行完,则调用目标方法 return mi.proceed(); } } 如上,前置通知的逻辑在目标方法执行前被执行。这里先简单向大家介绍一下拦截器是什么,关于拦截器更多的描述将放在下一节中。本节我们先来看看如何如何获取拦截器,如下: public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, Class<?> targetClass) { MethodCacheKey cacheKey = new MethodCacheKey(method); // 从缓存中获取 List<Object> cached = this.methodCache.get(cacheKey); // 缓存未命中,则进行下一步处理 if (cached == null) { // 获取所有的拦截器 cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice( this, method, targetClass); // 存入缓存 this.methodCache.put(cacheKey, cached); } return cached; } public List<Object> getInterceptorsAndDynamicInterceptionAdvice( Advised config, Method method, Class<?> targetClass) { List<Object> interceptorList = new ArrayList<Object>(config.getAdvisors().length); Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass()); boolean hasIntroductions = hasMatchingIntroductions(config, actualClass); // registry 为 DefaultAdvisorAdapterRegistry 类型 AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); // 遍历通知器列表 for (Advisor advisor : config.getAdvisors()) { if (advisor instanceof PointcutAdvisor) { PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor; /* * 调用 ClassFilter 对 bean 类型进行匹配,无法匹配则说明当前通知器 * 不适合应用在当前 bean 上 */ if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) { // 将 advisor 中的 advice 转成相应的拦截器 MethodInterceptor[] interceptors = registry.getInterceptors(advisor); MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher(); // 通过方法匹配器对目标方法进行匹配 if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) { // 若 isRuntime 返回 true,则表明 MethodMatcher 要在运行时做一些检测 if (mm.isRuntime()) { for (MethodInterceptor interceptor : interceptors) { interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm)); } } else { interceptorList.addAll(Arrays.asList(interceptors)); } } } } else if (advisor instanceof IntroductionAdvisor) { IntroductionAdvisor ia = (IntroductionAdvisor) advisor; // IntroductionAdvisor 类型的通知器,仅需进行类级别的匹配即可 if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) { Interceptor[] interceptors = registry.getInterceptors(advisor); interceptorList.addAll(Arrays.asList(interceptors)); } } else { Interceptor[] interceptors = registry.getInterceptors(advisor); interceptorList.addAll(Arrays.asList(interceptors)); } } return interceptorList; } public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException { List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3); Advice advice = advisor.getAdvice(); /* * 若 advice 是 MethodInterceptor 类型的,直接添加到 interceptors 中即可。 * 比如 AspectJAfterAdvice 就实现了 MethodInterceptor 接口 */ if (advice instanceof MethodInterceptor) { interceptors.add((MethodInterceptor) advice); } /* * 对于 AspectJMethodBeforeAdvice 等类型的通知,由于没有实现 MethodInterceptor * 接口,所以这里需要通过适配器进行转换 */ for (AdvisorAdapter adapter : this.adapters) { if (adapter.supportsAdvice(advice)) { interceptors.add(adapter.getInterceptor(advisor)); } } if (interceptors.isEmpty()) { throw new UnknownAdviceTypeException(advisor.getAdvice()); } return interceptors.toArray(new MethodInterceptor[interceptors.size()]); } 以上就是获取拦截器的过程,代码有点长,不过好在逻辑不是很复杂。这里简单总结一下以上源码的执行过程,如下: 从缓存中获取当前方法的拦截器链 若缓存未命中,则调用 getInterceptorsAndDynamicInterceptionAdvice 获取拦截器链 遍历通知器列表 对于 PointcutAdvisor 类型的通知器,这里要调用通知器所持有的切点(Pointcut)对类和方法进行匹配,匹配成功说明应向当前方法织入通知逻辑 调用 getInterceptors 方法对非 MethodInterceptor 类型的通知进行转换 返回拦截器数组,并在随后存入缓存中 这里需要说明一下,部分通知器是没有实现 MethodInterceptor 接口的,比如 AspectJMethodBeforeAdvice。我们可以看一下前置通知适配器是如何将前置通知转为拦截器的,如下: class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { @Override public boolean supportsAdvice(Advice advice) { return (advice instanceof MethodBeforeAdvice); } @Override public MethodInterceptor getInterceptor(Advisor advisor) { MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); // 创建 MethodBeforeAdviceInterceptor 拦截器 return new MethodBeforeAdviceInterceptor(advice); } } 如上,适配器的逻辑比较简单,这里就不多说了。 现在我们已经获得了拦截器链,那接下来要做的事情就是启动拦截器了。所以接下来,我们一起去看看 Sring 是如何让拦截器链运行起来的。 3.3 启动拦截器链 3.3.1 执行拦截器链 本节的开始,我们先来说说 ReflectiveMethodInvocation。ReflectiveMethodInvocation 贯穿于拦截器链执行的始终,可以说是核心。该类的 proceed 方法用于启动启动拦截器链,下面我们去看看这个方法的逻辑。 public class ReflectiveMethodInvocation implements ProxyMethodInvocation { private int currentInterceptorIndex = -1; public Object proceed() throws Throwable { // 拦截器链中的最后一个拦截器执行完后,即可执行目标方法 if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { // 执行目标方法 return invokeJoinpoint(); } Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; /* * 调用具有三个参数(3-args)的 matches 方法动态匹配目标方法, * 两个参数(2-args)的 matches 方法用于静态匹配 */ if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { // 调用拦截器逻辑 return dm.interceptor.invoke(this); } else { // 如果匹配失败,则忽略当前的拦截器 return proceed(); } } else { // 调用拦截器逻辑,并传递 ReflectiveMethodInvocation 对象 return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); } } } 如上,proceed 根据 currentInterceptorIndex 来确定当前应执行哪个拦截器,并在调用拦截器的 invoke 方法时,将自己作为参数传给该方法。前面的章节中,我们看过了前置拦截器的源码,这里来看一下后置拦截器源码。如下: public class AspectJAfterAdvice extends AbstractAspectJAdvice implements MethodInterceptor, AfterAdvice, Serializable { public AspectJAfterAdvice( Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { super(aspectJBeforeAdviceMethod, pointcut, aif); } @Override public Object invoke(MethodInvocation mi) throws Throwable { try { // 调用 proceed return mi.proceed(); } finally { // 调用后置通知逻辑 invokeAdviceMethod(getJoinPointMatch(), null, null); } } //... } 如上,由于后置通知需要在目标方法返回后执行,所以 AspectJAfterAdvice 先调用 mi.proceed() 执行下一个拦截器逻辑,等下一个拦截器返回后,再执行后置通知逻辑。如果大家不太理解的话,先看个图。这里假设目标方法 method 在执行前,需要执行两个前置通知和一个后置通知。下面我们看一下由三个拦截器组成的拦截器链是如何执行的,如下: 注:这里用 advice.after() 表示执行后置通知 本节的最后,插播一个拦截器,即 ExposeInvocationInterceptor。为啥要在这里介绍这个拦截器呢,原因是我在Spring AOP 源码分析 - 筛选合适的通知器一文中,在介绍 extendAdvisors 方法时,有一个点没有详细说明。现在大家已经知道拦截器的概念了,就可以把之前没法详细说明的地方进行补充说明。这里再贴一下 extendAdvisors 方法的源码,如下: protected void extendAdvisors(List<Advisor> candidateAdvisors) { AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors); } public static boolean makeAdvisorChainAspectJCapableIfNecessary(List<Advisor> advisors) { if (!advisors.isEmpty()) { // 省略部分代码 if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) { // 向通知器列表中添加 ExposeInvocationInterceptor.ADVISOR advisors.add(0, ExposeInvocationInterceptor.ADVISOR); return true; } } return false; } 如上,extendAdvisors 所调用的方法会向通知器列表首部添加 ExposeInvocationInterceptor.ADVISOR。现在我们再来看看 ExposeInvocationInterceptor 的源码,如下: public class ExposeInvocationInterceptor implements MethodInterceptor, PriorityOrdered, Serializable { public static final ExposeInvocationInterceptor INSTANCE = new ExposeInvocationInterceptor(); // 创建 DefaultPointcutAdvisor 匿名对象 public static final Advisor ADVISOR = new DefaultPointcutAdvisor(INSTANCE) { @Override public String toString() { return ExposeInvocationInterceptor.class.getName() +".ADVISOR"; } }; private static final ThreadLocal<MethodInvocation> invocation = new NamedThreadLocal<MethodInvocation>("Current AOP method invocation"); public static MethodInvocation currentInvocation() throws IllegalStateException { MethodInvocation mi = invocation.get(); if (mi == null) throw new IllegalStateException( "No MethodInvocation found: Check that an AOP invocation is in progress, and that the " + "ExposeInvocationInterceptor is upfront in the interceptor chain. Specifically, note that " + "advices with order HIGHEST_PRECEDENCE will execute before ExposeInvocationInterceptor!"); return mi; } // 私有构造方法 private ExposeInvocationInterceptor() { } @Override public Object invoke(MethodInvocation mi) throws Throwable { MethodInvocation oldInvocation = invocation.get(); // 将 mi 设置到 ThreadLocal 中 invocation.set(mi); try { // 调用下一个拦截器 return mi.proceed(); } finally { invocation.set(oldInvocation); } } //... } 如上,ExposeInvocationInterceptor.ADVISOR 经过 registry.getInterceptors 方法(前面已分析过)处理后,即可得到 ExposeInvocationInterceptor。ExposeInvocationInterceptor 的作用是用于暴露 MethodInvocation 对象到 ThreadLocal 中,其名字也体现出了这一点。如果其他地方需要当前的 MethodInvocation 对象,直接通过调用 currentInvocation 方法取出。至于哪些地方需要 MethodInvocation,这个大家自己去探索吧。最后,建议大家写点代码调试一下。我在一开始阅读代码时,并没有注意到 ExposeInvocationInterceptor,而是在调试代码的过程中才发现的。比如: 好了,关于拦截器链的执行过程这里就讲完了。下一节,我们来看一下目标方法的执行过程。大家再忍忍,源码很快分析完了。 3.3.2 执行目标方法 与前面的大部头相比,本节的源码比较短,也很简单。本节我们来看一下目标方法的执行过程,如下: protected Object invokeJoinpoint() throws Throwable { return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments); } public abstract class AopUtils { public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args) throws Throwable { try { ReflectionUtils.makeAccessible(method); // 通过反射执行目标方法 return method.invoke(target, args); } catch (InvocationTargetException ex) {...} catch (IllegalArgumentException ex) {...} catch (IllegalAccessException ex) {...} } } 目标方法时通过反射执行的,比较简单的吧。好了,就不多说了,over。 4.总结 到此,本篇文章的就要结束了。本篇文章是Spring AOP 源码分析系列文章的最后一篇,从阅读源码到写完本系列的4篇文章总共花了约两周的时间。总的来说还是有点累的,但是也有很大的收获和成就感,值了。需要说明的是,Spring IOC 和 AOP 部分的源码我分析的并不是非常详细,也有很多地方没弄懂。这一系列的文章,是作为自己工作两年的一个总结。由于工作时间不长,工作经验和技术水平目前都还处于入门阶段。所以暂时很难把 Spring IOC 和 AOP 模块的源码分析的很出彩,这个请见谅。如果大家在阅读文章的过程中发现了错误,可以指出来,也希望多多指教,这里先说说谢谢。 好了,本篇文章到这里就结束了。谢谢大家的阅读。 参考 《Spring 源码深度解析》- 郝佳 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 在上一篇文章中,我分析了 Spring 是如何为目标 bean 筛选合适的通知器的。现在通知器选好了,接下来就要通过代理的方式将通知器(Advisor)所持有的通知(Advice)织入到 bean 的某些方法前后。与筛选合适的通知器相比,创建代理对象的过程则要简单不少,本文所分析的源码不过100行,相对比较简单。在接下里的章节中,我将会首先向大家介绍一些背景知识,然后再去分析源码。那下面,我们先来了解一下背景知识。 2.背景知识 2.1 proxy-target-class 在 Spring AOP 配置中,proxy-target-class 属性可影响 Spring 生成的代理对象的类型。以 XML 配置为例,可进行如下配置: <aop:aspectj-autoproxy proxy-target-class="true"/> <aop:config proxy-target-class="true"> <aop:aspect id="xxx" ref="xxxx"> <!-- 省略 --> </aop:aspect> </aop:config> 如上,默认情况下 proxy-target-class 属性为 false。当目标 bean 实现了接口时,Spring 会基于 JDK 动态代理为目标 bean 创建代理对象。若未实现任何接口,Spring 则会通过 CGLIB 创建代理。而当 proxy-target-class 属性设为 true 时,则会强制 Spring 通过 CGLIB 的方式创建代理对象,即使目标 bean 实现了接口。 关于 proxy-target-class 属性的用途这里就说完了,下面我们来看看两种不同创建动态代理的方式。 2.2 动态代理 2.2.1 基于 JDK 的动态代理 基于 JDK 的动态代理主要是通过 JDK 提供的代理创建类 Proxy 为目标对象创建代理,下面我们来看一下 Proxy 中创建代理的方法声明。如下: public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 简单说一下上面的参数列表: loader - 类加载器 interfaces - 目标类所实现的接口列表 h - 用于封装代理逻辑 JDK 动态代理对目标类是有一定要求的,即要求目标类必须实现了接口,JDK 动态代理只能为实现了接口的目标类生成代理对象。至于 InvocationHandler,是一个接口类型,定义了一个 invoke 方法。使用者需要实现该方法,并在其中封装代理逻辑。 关于 JDK 动态代理的介绍,就先说到这。下面我来演示一下 JDK 动态代理的使用方式,如下: 目标类定义: public interface UserService { void save(User user); void update(User user); } public class UserServiceImpl implements UserService { @Override public void save(User user) { System.out.println("save user info"); } @Override public void update(User user) { System.out.println("update user info"); } } 代理创建者定义: public interface ProxyCreator { Object getProxy(); } public class JdkProxyCreator implements ProxyCreator, InvocationHandler { private Object target; public JdkProxyCreator(Object target) { assert target != null; Class<?>[] interfaces = target.getClass().getInterfaces(); if (interfaces.length == 0) { throw new IllegalArgumentException("target class don`t implement any interface"); } this.target = target; } @Override public Object getProxy() { Class<?> clazz = target.getClass(); // 生成代理对象 return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(System.currentTimeMillis() + " - " + method.getName() + " method start"); // 调用目标方法 Object retVal = method.invoke(target, args); System.out.println(System.currentTimeMillis() + " - " + method.getName() + " method over"); return retVal; } } 如上,invoke 方法中的代理逻辑主要用于记录目标方法的调用时间,和结束时间。下面写点测试代码简单验证一下,如下: public class JdkProxyCreatorTest { @Test public void getProxy() throws Exception { ProxyCreator proxyCreator = new JdkProxyCreator(new UserServiceImpl()); UserService userService = (UserService) proxyCreator.getProxy(); System.out.println("proxy type = " + userService.getClass()); System.out.println(); userService.save(null); System.out.println(); userService.update(null); } } 测试结果如下: 如上,从测试结果中。我们可以看出,我们的代理逻辑正常执行了。另外,注意一下 userService 指向对象的类型,并非是 xyz.coolblog.proxy.UserServiceImpl,而是 com.sun.proxy.$Proxy4。 关于 JDK 动态代理,这里先说这么多。下一节,我来演示一下 CGLIB 动态代理,继续往下看吧。 2.2.2 基于 CGLIB 的动态代理 当我们要为未实现接口的类生成代理时,就无法使用 JDK 动态代理了。那么此类的目标对象生成代理时应该怎么办呢?当然是使用 CGLIB 了。在 CGLIB 中,代理逻辑是封装在 MethodInterceptor 实现类中的,代理对象则是通过 Enhancer 类的 create 方法进行创建。下面我来演示一下 CGLIB 创建代理对象的过程,如下: 本节的演示环节,打算调侃(无贬低之意)一下59式坦克,这是我们国家大量装备过的一款坦克。59式坦克有很多种改款,一般把改款统称为59改,59改这个梗也正是源于此。下面我们先来一览59式坦克的风采: 图片来源:百度图片搜索 下面我们的工作就是为咱们的 59 创建一个代理,即 59改。好了,开始我们的魔改吧。 目标类,59式坦克: public class Tank59 { void run() { System.out.println("极速前行中...."); } void shoot() { System.out.println("轰...轰...轰...轰..."); } } CGLIB 代理创建者 public class CglibProxyCreator implements ProxyCreator { private Object target; private MethodInterceptor methodInterceptor; public CglibProxyCreator(Object target, MethodInterceptor methodInterceptor) { assert (target != null && methodInterceptor != null); this.target = target; this.methodInterceptor = methodInterceptor; } @Override public Object getProxy() { Enhancer enhancer = new Enhancer(); // 设置代理类的父类 enhancer.setSuperclass(target.getClass()); // 设置代理逻辑 enhancer.setCallback(methodInterceptor); // 创建代理对象 return enhancer.create(); } } 方法拦截器 - 坦克再制造: public class TankRemanufacture implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { if (method.getName().equals("run")) { System.out.println("正在重造59坦克..."); System.out.println("重造成功,已获取 59改 之 超音速飞行版"); System.out.print("已起飞,正在突破音障。"); methodProxy.invokeSuper(o, objects); System.out.println("已击落黑鸟 SR-71,正在返航..."); return null; } return methodProxy.invokeSuper(o, objects); } } 好了,下面开始演示,测试代码如下: public class CglibProxyCreatorTest { @Test public void getProxy() throws Exception { ProxyCreator proxyCreator = new CglibProxyCreator(new Tank59(), new TankRemanufacture()); Tank59 tank59 = (Tank59) proxyCreator.getProxy(); System.out.println("proxy class = " + tank59.getClass() + "\n"); tank59.run(); System.out.println(); System.out.print("射击测试:"); tank59.shoot(); } } 测试结果如下: 如上,"极速前行中...." 和 "轰...轰...轰...轰..." 这两行字符串是目标对象中的方法打印出来的,其他的则是由代理逻辑打印的。由此可知,我们的代理逻辑生效了。 好了,最后我们来看一下,经过魔改后的 59,也就是超音速59改的效果图: 图片来源:未知 本节用59式坦克举例,仅是调侃,并无恶意。作为年轻的一代,我们应感谢那些为国防事业做出贡献的科技人员们。没有他们贡献,我们怕是不会有像今天这样安全的环境了(尽管不完美)。 到此,背景知识就介绍完了。下一章,我将开始分析源码。源码不是很长,主逻辑比较容易懂,所以一起往下看吧。 3.源码分析 为目标 bean 创建代理对象前,需要先创建 AopProxy 对象,然后再调用该对象的 getProxy 方法创建实际的代理类。我们先来看看 AopProxy 这个接口的定义,如下: public interface AopProxy { /** 创建代理对象 */ Object getProxy(); Object getProxy(ClassLoader classLoader); } 在 Spring 中,有两个类实现了 AopProxy,如下: Spring 在为目标 bean 创建代理的过程中,要根据 bean 是否实现接口,以及一些其他配置来决定使用 AopProxy 何种实现类为目标 bean 创建代理对象。下面我们就来看一下代理创建的过程,如下: protected Object createProxy( Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) { if (this.beanFactory instanceof ConfigurableListableBeanFactory) { AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); } ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); /* * 默认配置下,或用户显式配置 proxy-target-class = "false" 时, * 这里的 proxyFactory.isProxyTargetClass() 也为 false */ if (!proxyFactory.isProxyTargetClass()) { if (shouldProxyTargetClass(beanClass, beanName)) { proxyFactory.setProxyTargetClass(true); } else { /* * 检测 beanClass 是否实现了接口,若未实现,则将 * proxyFactory 的成员变量 proxyTargetClass 设为 true */ evaluateProxyInterfaces(beanClass, proxyFactory); } } // specificInterceptors 中若包含有 Advice,此处将 Advice 转为 Advisor Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); customizeProxyFactory(proxyFactory); proxyFactory.setFrozen(this.freezeProxy); if (advisorsPreFiltered()) { proxyFactory.setPreFiltered(true); } // 创建代理 return proxyFactory.getProxy(getProxyClassLoader()); } public Object getProxy(ClassLoader classLoader) { // 先创建 AopProxy 实现类对象,然后再调用 getProxy 为目标 bean 创建代理对象 return createAopProxy().getProxy(classLoader); } getProxy 这里有两个方法调用,一个是调用 createAopProxy 创建 AopProxy 实现类对象,然后再调用 AopProxy 实现类对象中的 getProxy 创建代理对象。这里我们先来看一下创建 AopProxy 实现类对象的过程,如下: protected final synchronized AopProxy createAopProxy() { if (!this.active) { activate(); } return getAopProxyFactory().createAopProxy(this); } public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { @Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { /* * 下面的三个条件简单分析一下: * * 条件1:config.isOptimize() - 是否需要优化,这个属性没怎么用过, * 细节我不是很清楚 * 条件2:config.isProxyTargetClass() - 检测 proxyTargetClass 的值, * 前面的代码会设置这个值 * 条件3:hasNoUserSuppliedProxyInterfaces(config) * - 目标 bean 是否实现了接口 */ if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } // 创建 CGLIB 代理,ObjenesisCglibAopProxy 继承自 CglibAopProxy return new ObjenesisCglibAopProxy(config); } else { // 创建 JDK 动态代理 return new JdkDynamicAopProxy(config); } } } 如上,DefaultAopProxyFactory 根据一些条件决定生成什么类型的 AopProxy 实现类对象。生成好 AopProxy 实现类对象后,下面就要为目标 bean 创建代理对象了。这里以 JdkDynamicAopProxy 为例,我们来看一下,该类的 getProxy 方法的逻辑是怎样的。如下: public Object getProxy() { return getProxy(ClassUtils.getDefaultClassLoader()); } public Object getProxy(ClassLoader classLoader) { if (logger.isDebugEnabled()) { logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource()); } Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); // 调用 newProxyInstance 创建代理对象 return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); } 如上,请把目光移至最后一行有效代码上,会发现 JdkDynamicAopProxy 最终调用 Proxy.newProxyInstance 方法创建代理对象。到此,创建代理对象的整个过程也就分析完了,不知大家看懂了没。好了,关于创建代理的源码分析,就先说到这里吧。 4.总结 本篇文章对 Spring AOP 创建代理对象的过程进行了较为详细的分析,并在分析源码前介绍了相关的背景知识。总的来说,本篇文章涉及的技术点不是很复杂,相信大家都能看懂。限于个人能力,若文中有错误的地方,欢迎大家指出来。好了,本篇文章到此结束,谢谢阅读。 参考 《Spring 源码深度解析》- 郝佳 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 从本篇文章开始,我将会对 Spring AOP 部分的源码进行分析。本文是 Spring AOP 源码分析系列文章的第二篇,本文主要分析 Spring AOP 是如何为目标 bean 筛选出合适的通知器(Advisor)。在上一篇AOP 源码分析导读一文中,我简单介绍了 AOP 中的一些术语及其对应的源码,部分术语和源码将会在本篇文章中出现。如果大家不熟悉这些术语和源码,不妨去看看。 关于 Spring AOP,我个人在日常开发中用过一些,也参照过 tiny-spring 过写过一个玩具版的 AOP 框架,并写成了文章。正因为前面做了一些准备工作,最近再看 Spring AOP 源码时,觉得也没那么难了。所以如果大家打算看 AOP 源码的话,这里建议大家多做一些准备工作。比如熟悉 AOP 的中的术语,亦或是实现一个简单的 IOC 和 AOP,并将两者整合在一起。经过如此准备,相信大家会对 AOP 会有更多的认识。 好了,其他的就不多说了,下面进入源码分析阶段。 2.源码分析 2.1 AOP 入口分析 在导读一文中,我已经说过 Spring AOP 是在何处向目标 bean 中织入通知(Advice)的。也说过 Spring 是如何将 AOP 和 IOC 模块整合到一起的,即通过拓展点 BeanPostProcessor 接口。Spring AOP 抽象代理创建器实现了 BeanPostProcessor 接口,并在 bean 初始化后置处理过程中向 bean 中织入通知。下面我们就来看看相关源码,如下: public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware { @Override /** bean 初始化后置处理方法 */ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (!this.earlyProxyReferences.contains(cacheKey)) { // 如果需要,为 bean 生成代理对象 return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; } protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { if (beanName != null && this.targetSourcedBeans.contains(beanName)) { return bean; } if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { return bean; } /* * 如果是基础设施类(Pointcut、Advice、Advisor 等接口的实现类),或是应该跳过的类, * 则不应该生成代理,此时直接返回 bean */ if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { // 将 <cacheKey, FALSE> 键值对放入缓存中,供上面的 if 分支使用 this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } // 为目标 bean 查找合适的通知器 Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); /* * 若 specificInterceptors != null,即 specificInterceptors != DO_NOT_PROXY, * 则为 bean 生成代理对象,否则直接返回 bean */ if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); // 创建代理 Object proxy = createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); /* * 返回代理对象,此时 IOC 容器输入 bean,得到 proxy。此时, * beanName 对应的 bean 是代理对象,而非原始的 bean */ return proxy; } this.advisedBeans.put(cacheKey, Boolean.FALSE); // specificInterceptors = null,直接返回 bean return bean; } } 以上就是 Spring AOP 创建代理对象的入口方法分析,过程比较简单,这里简单总结一下: 若 bean 是 AOP 基础设施类型,则直接返回 为 bean 查找合适的通知器 如果通知器数组不为空,则为 bean 生成代理对象,并返回该对象 若数组为空,则返回原始 bean 上面的流程看起来并不复杂,不过不要被表象所迷糊,以上流程不过是冰山一角。 图片来源:无版权图片网站 pixabay.com 在本文,以及后续的文章中,我将会对步骤2和步骤3对应的源码进行分析。在本篇文章先来分析步骤2对应的源码。 2.2 筛选合适的通知器 在向目标 bean 中织入通知之前,我们先要为 bean 筛选出合适的通知器(通知器持有通知)。如何筛选呢?方式由很多,比如我们可以通过正则表达式匹配方法名,当然更多的时候用的是 AspectJ 表达式进行匹配。那下面我们就来看一下使用 AspectJ 表达式筛选通知器的过程,如下: protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) { // 查找合适的通知器 List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName); if (advisors.isEmpty()) { return DO_NOT_PROXY; } return advisors.toArray(); } protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { // 查找所有的通知器 List<Advisor> candidateAdvisors = findCandidateAdvisors(); /* * 筛选可应用在 beanClass 上的 Advisor,通过 ClassFilter 和 MethodMatcher * 对目标类和方法进行匹配 */ List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); // 拓展操作 extendAdvisors(eligibleAdvisors); if (!eligibleAdvisors.isEmpty()) { eligibleAdvisors = sortAdvisors(eligibleAdvisors); } return eligibleAdvisors; } 如上,Spring 先查询出所有的通知器,然后再调用 findAdvisorsThatCanApply 对通知器进行筛选。在下面几节中,我将分别对 findCandidateAdvisors 和 findAdvisorsThatCanApply 两个方法进行分析,继续往下看吧。 2.2.1 查找通知器 Spring 提供了两种配置 AOP 的方式,一种是通过 XML 进行配置,另一种是注解。对于两种配置方式,Spring 的处理逻辑是不同的。对于 XML 类型的配置,比如下面的配置: <!-- 目标 bean --> <bean id="hello" class="xyz.coolblog.aop.Hello"/> <aop:aspectj-autoproxy/> <!-- 普通 bean,包含 AOP 切面逻辑 --> <bean id="aopCode" class="xyz.coolblog.aop.AopCode"/> <!-- 由 @Aspect 注解修饰的切面类 --> <bean id="annotationAopCode" class="xyz.coolblog.aop.AnnotationAopCode"/> <aop:config> <aop:aspect ref="aopCode"> <aop:pointcut id="helloPointcut" expression="execution(* xyz.coolblog.aop.*.hello*(..))" /> <aop:before method="before" pointcut-ref="helloPointcut"/> <aop:after method="after" pointcut-ref="helloPointcut"/> </aop:aspect> </aop:config> Spring 会将上的配置解析为下面的结果: 如上图所示,红框中对应的是普通的 bean 定义,比如 <bean id="hello" .../>、<bean id="annotationAopCode" .../>、<bean id="appCode" .../> 等配置。黄色框中的则是切点的定义,类型为 AspectJExpressionPointcut,对应 <aop:pointcut id="helloPointcut" .../> 配置。那绿色框中的结果对应的是什么配置呢?目前仅剩下两个配置没说,所以对应 <aop:before .../> 和 <aop:after .../> 配置,类型为 AspectJPointcutAdvisor。这里请大家注意,由 @Aspect 注解修饰的 AnnotationAopCode 也是普通类型的 bean,该 bean 会在查找通知器的过程中被解析,并被构建为一个或多个 Advisor。 上面讲解了 Spring AOP 两种配置的处理方式,算是为下面的源码分析做铺垫。现在铺垫完毕,我们就来分析一下源码吧。如下: public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator { //... @Override protected List<Advisor> findCandidateAdvisors() { // 调用父类方法从容器中查找所有的通知器 List<Advisor> advisors = super.findCandidateAdvisors(); // 解析 @Aspect 注解,并构建通知器 advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); return advisors; } //... } AnnotationAwareAspectJAutoProxyCreator 覆写了父类的方法 findCandidateAdvisors,并增加了一步操作,即解析 @Aspect 注解,并构建成通知器。下面我先来分析一下父类中的 findCandidateAdvisors 方法的逻辑,然后再来分析 buildAspectJAdvisors 方法逻的辑。 2.2.1.1 findCandidateAdvisors 方法分析 我们先来看一下 AbstractAdvisorAutoProxyCreator 中 findCandidateAdvisors 方法的定义,如下: public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator { private BeanFactoryAdvisorRetrievalHelper advisorRetrievalHelper; //... protected List<Advisor> findCandidateAdvisors() { return this.advisorRetrievalHelper.findAdvisorBeans(); } //... } 从上面的源码中可以看出,AbstractAdvisorAutoProxyCreator 中的 findCandidateAdvisors 是个空壳方法,所有逻辑封装在了一个 BeanFactoryAdvisorRetrievalHelper 的 findAdvisorBeans 方法中。这里大家可以仔细看一下类名 BeanFactoryAdvisorRetrievalHelper 和方法 findAdvisorBeans,两个名字其实已经描述出他们的职责了。BeanFactoryAdvisorRetrievalHelper 可以理解为从 bean 容器中获取 Advisor 的帮助类,findAdvisorBeans 则可理解为查找 Advisor 类型的 bean。所以即使不看 findAdvisorBeans 方法的源码,我们也可从方法名上推断出它要做什么,即从 bean 容器中将 Advisor 类型的 bean 查找出来。下面我来分析一下这个方法的源码,如下: public List<Advisor> findAdvisorBeans() { String[] advisorNames = null; synchronized (this) { // cachedAdvisorBeanNames 是 advisor 名称的缓存 advisorNames = this.cachedAdvisorBeanNames; /* * 如果 cachedAdvisorBeanNames 为空,这里到容器中查找, * 并设置缓存,后续直接使用缓存即可 */ if (advisorNames == null) { // 从容器中查找 Advisor 类型 bean 的名称 advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this.beanFactory, Advisor.class, true, false); // 设置缓存 this.cachedAdvisorBeanNames = advisorNames; } } if (advisorNames.length == 0) { return new LinkedList<Advisor>(); } List<Advisor> advisors = new LinkedList<Advisor>(); // 遍历 advisorNames for (String name : advisorNames) { if (isEligibleBean(name)) { // 忽略正在创建中的 advisor bean if (this.beanFactory.isCurrentlyInCreation(name)) { if (logger.isDebugEnabled()) { logger.debug("Skipping currently created advisor '" + name + "'"); } } else { try { /* * 调用 getBean 方法从容器中获取名称为 name 的 bean, * 并将 bean 添加到 advisors 中 */ advisors.add(this.beanFactory.getBean(name, Advisor.class)); } catch (BeanCreationException ex) { Throwable rootCause = ex.getMostSpecificCause(); if (rootCause instanceof BeanCurrentlyInCreationException) { BeanCreationException bce = (BeanCreationException) rootCause; if (this.beanFactory.isCurrentlyInCreation(bce.getBeanName())) { if (logger.isDebugEnabled()) { logger.debug("Skipping advisor '" + name + "' with dependency on currently created bean: " + ex.getMessage()); } continue; } } throw ex; } } } } return advisors; } 以上就是从容器中查找 Advisor 类型的 bean 所有的逻辑,代码虽然有点长,但并不复杂。主要做了两件事情: 从容器中查找所有类型为 Advisor 的 bean 对应的名称 遍历 advisorNames,并从容器中获取对应的 bean 看完上面的分析,我们继续来分析一下 @Aspect 注解的解析过程。 2.2.1.2 buildAspectJAdvisors 方法分析 与上一节的内容相比,解析 @Aspect 注解的过程还是比较复杂的,需要一些耐心去看。下面我们开始分析 buildAspectJAdvisors 方法的源码,如下: public List<Advisor> buildAspectJAdvisors() { List<String> aspectNames = this.aspectBeanNames; if (aspectNames == null) { synchronized (this) { aspectNames = this.aspectBeanNames; if (aspectNames == null) { List<Advisor> advisors = new LinkedList<Advisor>(); aspectNames = new LinkedList<String>(); // 从容器中获取所有 bean 的名称 String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this.beanFactory, Object.class, true, false); // 遍历 beanNames for (String beanName : beanNames) { if (!isEligibleBean(beanName)) { continue; } // 根据 beanName 获取 bean 的类型 Class<?> beanType = this.beanFactory.getType(beanName); if (beanType == null) { continue; } // 检测 beanType 是否包含 Aspect 注解 if (this.advisorFactory.isAspect(beanType)) { aspectNames.add(beanName); AspectMetadata amd = new AspectMetadata(beanType, beanName); if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { MetadataAwareAspectInstanceFactory factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); // 获取通知器 List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory); if (this.beanFactory.isSingleton(beanName)) { this.advisorsCache.put(beanName, classAdvisors); } else { this.aspectFactoryCache.put(beanName, factory); } advisors.addAll(classAdvisors); } else { if (this.beanFactory.isSingleton(beanName)) { throw new IllegalArgumentException("Bean with name '" + beanName + "' is a singleton, but aspect instantiation model is not singleton"); } MetadataAwareAspectInstanceFactory factory = new PrototypeAspectInstanceFactory(this.beanFactory, beanName); this.aspectFactoryCache.put(beanName, factory); advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } } this.aspectBeanNames = aspectNames; return advisors; } } } if (aspectNames.isEmpty()) { return Collections.emptyList(); } List<Advisor> advisors = new LinkedList<Advisor>(); for (String aspectName : aspectNames) { List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName); if (cachedAdvisors != null) { advisors.addAll(cachedAdvisors); } else { MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } return advisors; } 上面就是 buildAspectJAdvisors 的代码,看起来比较长。代码比较多,我们关注重点的方法调用即可。在进行后续的分析前,这里先对 buildAspectJAdvisors 方法的执行流程做个总结。如下: 获取容器中所有 bean 的名称(beanName) 遍历上一步获取到的 bean 名称数组,并获取当前 beanName 对应的 bean 类型(beanType) 根据 beanType 判断当前 bean 是否是一个的 Aspect 注解类,若不是则不做任何处理 调用 advisorFactory.getAdvisors 获取通知器 下面我们来重点分析advisorFactory.getAdvisors(factory)这个调用,如下: public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) { // 获取 aspectClass 和 aspectName Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName(); validate(aspectClass); MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory); List<Advisor> advisors = new LinkedList<Advisor>(); // getAdvisorMethods 用于返回不包含 @Pointcut 注解的方法 for (Method method : getAdvisorMethods(aspectClass)) { // 为每个方法分别调用 getAdvisor 方法 Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName); if (advisor != null) { advisors.add(advisor); } } // If it's a per target aspect, emit the dummy instantiating aspect. if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); advisors.add(0, instantiationAdvisor); } // Find introduction fields. for (Field field : aspectClass.getDeclaredFields()) { Advisor advisor = getDeclareParentsAdvisor(field); if (advisor != null) { advisors.add(advisor); } } return advisors; } public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrderInAspect, String aspectName) { validate(aspectInstanceFactory.getAspectMetadata().getAspectClass()); // 获取切点实现类 AspectJExpressionPointcut expressionPointcut = getPointcut( candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass()); if (expressionPointcut == null) { return null; } // 创建 Advisor 实现类 return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, this, aspectInstanceFactory, declarationOrderInAspect, aspectName); } 如上,getAdvisor 方法包含两个主要步骤,一个是获取 AspectJ 表达式切点,另一个是创建 Advisor 实现类。在第二个步骤中,包含一个隐藏步骤 -- 创建 Advice。下面我将按顺序依次分析这两个步骤,先看获取 AspectJ 表达式切点的过程,如下: private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) { // 获取方法上的 AspectJ 相关注解,包括 @Before,@After 等 AspectJAnnotation<?> aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); if (aspectJAnnotation == null) { return null; } // 创建一个 AspectJExpressionPointcut 对象 AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]); // 设置切点表达式 ajexp.setExpression(aspectJAnnotation.getPointcutExpression()); ajexp.setBeanFactory(this.beanFactory); return ajexp; } protected static AspectJAnnotation<?> findAspectJAnnotationOnMethod(Method method) { // classesToLookFor 中的元素是大家熟悉的 Class<?>[] classesToLookFor = new Class<?>[] { Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class}; for (Class<?> c : classesToLookFor) { // 查找注解 AspectJAnnotation<?> foundAnnotation = findAnnotation(method, (Class<Annotation>) c); if (foundAnnotation != null) { return foundAnnotation; } } return null; } 获取切点的过程并不复杂,不过需要注意的是,目前获取到的切点可能还只是个半成品,需要再次处理一下才行。比如下面的代码: @Aspect public class AnnotationAopCode { @Pointcut("execution(* xyz.coolblog.aop.*.world*(..))") public void pointcut() {} @Before("pointcut()") public void before() { System.out.println("AnnotationAopCode`s before"); } } @Before 注解中的表达式是pointcut(),也就是说 ajexp 设置的表达式只是一个中间值,不是最终值,即execution(* xyz.coolblog.aop.*.world*(..))。所以后续还需要将 ajexp 中的表达式进行转换,关于这个转换的过程,我就不说了。有点复杂,我暂时没怎么看懂。 说完切点的获取过程,下面再来看看 Advisor 实现类的创建过程。如下: public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut, Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { this.declaredPointcut = declaredPointcut; this.declaringClass = aspectJAdviceMethod.getDeclaringClass(); this.methodName = aspectJAdviceMethod.getName(); this.parameterTypes = aspectJAdviceMethod.getParameterTypes(); this.aspectJAdviceMethod = aspectJAdviceMethod; this.aspectJAdvisorFactory = aspectJAdvisorFactory; this.aspectInstanceFactory = aspectInstanceFactory; this.declarationOrder = declarationOrder; this.aspectName = aspectName; if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { Pointcut preInstantiationPointcut = Pointcuts.union( aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut); this.pointcut = new PerTargetInstantiationModelPointcut( this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory); this.lazy = true; } else { this.pointcut = this.declaredPointcut; this.lazy = false; // 按照注解解析 Advice this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut); } } 上面是 InstantiationModelAwarePointcutAdvisorImpl 的构造方法,不过我们无需太关心这个方法中的一些初始化逻辑。我们把目光移到构造方法的最后一行代码中,即 instantiateAdvice(this.declaredPointcut),这个方法用于创建通知 Advice。在上一篇文章中我已经说过,通知器 Advisor 是通知 Advice 的持有者,所以在 Advisor 实现类的构造方法中创建通知也是合适的。那下面我们就来看看构建通知的过程是怎样的,如下: private Advice instantiateAdvice(AspectJExpressionPointcut pcut) { return this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pcut, this.aspectInstanceFactory, this.declarationOrder, this.aspectName); } public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); validate(candidateAspectClass); // 获取 Advice 注解 AspectJAnnotation<?> aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); if (aspectJAnnotation == null) { return null; } if (!isAspect(candidateAspectClass)) { throw new AopConfigException("Advice must be declared inside an aspect type: " + "Offending method '" + candidateAdviceMethod + "' in class [" + candidateAspectClass.getName() + "]"); } if (logger.isDebugEnabled()) { logger.debug("Found AspectJ method: " + candidateAdviceMethod); } AbstractAspectJAdvice springAdvice; // 按照注解类型生成相应的 Advice 实现类 switch (aspectJAnnotation.getAnnotationType()) { case AtBefore: // @Before -> AspectJMethodBeforeAdvice springAdvice = new AspectJMethodBeforeAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); break; case AtAfter: // @After -> AspectJAfterAdvice springAdvice = new AspectJAfterAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); break; case AtAfterReturning: // @AfterReturning -> AspectJAfterAdvice springAdvice = new AspectJAfterReturningAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation(); if (StringUtils.hasText(afterReturningAnnotation.returning())) { springAdvice.setReturningName(afterReturningAnnotation.returning()); } break; case AtAfterThrowing: // @AfterThrowing -> AspectJAfterThrowingAdvice springAdvice = new AspectJAfterThrowingAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation(); if (StringUtils.hasText(afterThrowingAnnotation.throwing())) { springAdvice.setThrowingName(afterThrowingAnnotation.throwing()); } break; case AtAround: // @Around -> AspectJAroundAdvice springAdvice = new AspectJAroundAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); break; /* * 什么都不做,直接返回 null。从整个方法的调用栈来看, * 并不会出现注解类型为 AtPointcut 的情况 */ case AtPointcut: if (logger.isDebugEnabled()) { logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'"); } return null; default: throw new UnsupportedOperationException( "Unsupported advice type on method: " + candidateAdviceMethod); } springAdvice.setAspectName(aspectName); springAdvice.setDeclarationOrder(declarationOrder); /* * 获取方法的参数列表名称,比如方法 int sum(int numX, int numY), * getParameterNames(sum) 得到 argNames = [numX, numY] */ String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); if (argNames != null) { // 设置参数名 springAdvice.setArgumentNamesFromStringArray(argNames); } springAdvice.calculateArgumentBindings(); return springAdvice; } 上面的代码逻辑不是很复杂,主要的逻辑就是根据注解类型生成与之对应的通知对象。下面来总结一下获取通知器(getAdvisors)整个过程的逻辑,如下: 从目标 bean 中获取不包含 Pointcut 注解的方法列表 遍历上一步获取的方法列表,并调用 getAdvisor 获取当前方法对应的 Advisor 创建 AspectJExpressionPointcut 对象,并从方法中的注解中获取表达式,最后设置到切点对象中 创建 Advisor 实现类对象 InstantiationModelAwarePointcutAdvisorImpl 调用 instantiateAdvice 方法构建通知 调用 getAdvice 方法,并根据注解类型创建相应的通知 如上所示,上面的步骤做了一定的简化。总的来说,获取通知器的过程还是比较复杂的,并不是很容易看懂。大家在阅读的过程中,还要写一些测试代码进行调试才行。调试的过程中,一些不关心的调用就别跟进去了,不然会陷入很深的调用栈中,影响对源码主流程的理解。 现在,大家知道了通知是怎么创建的。那我们难道不要去看看这些通知的实现源码吗?显然,我们应该看一下。那接下里,我们一起来分析一下 AspectJMethodBeforeAdvice,也就是 @Before 注解对应的通知实现类。看看它的逻辑是什么样的。 2.2.1.3 AspectJMethodBeforeAdvice 分析 public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice { public AspectJMethodBeforeAdvice( Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { super(aspectJBeforeAdviceMethod, pointcut, aif); } @Override public void before(Method method, Object[] args, Object target) throws Throwable { // 调用通知方法 invokeAdviceMethod(getJoinPointMatch(), null, null); } @Override public boolean isBeforeAdvice() { return true; } @Override public boolean isAfterAdvice() { return false; } } protected Object invokeAdviceMethod(JoinPointMatch jpMatch, Object returnValue, Throwable ex) throws Throwable { // 调用通知方法,并向其传递参数 return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex)); } protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable { Object[] actualArgs = args; if (this.aspectJAdviceMethod.getParameterTypes().length == 0) { actualArgs = null; } try { ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); // 通过反射调用通知方法 return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); } catch (IllegalArgumentException ex) { throw new AopInvocationException("Mismatch on arguments to advice method [" + this.aspectJAdviceMethod + "]; pointcut expression [" + this.pointcut.getPointcutExpression() + "]", ex); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } 如上,AspectJMethodBeforeAdvice 的源码比较简单,这里我们仅关注 before 方法。这个方法调用了父类中的 invokeAdviceMethod,然后 invokeAdviceMethod 在调用 invokeAdviceMethodWithGivenArgs,最后在 invokeAdviceMethodWithGivenArgs 通过反射执行通知方法。是不是很简单? 关于 AspectJMethodBeforeAdvice 就简单介绍到这里吧,至于剩下的几种实现,大家可以自己去看看。好了,关于 AspectJMethodBeforeAdvice 的源码分析,就分析到这里了。我们继续往下看吧。 2.2.2 筛选合适的通知器 查找出所有的通知器,整个流程还没算完,接下来我们还要对这些通知器进行筛选。适合应用在当前 bean 上的通知器留下,不适合的就让它自生自灭吧。那下面我们来分析一下通知器筛选的过程,如下: protected List<Advisor> findAdvisorsThatCanApply( List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) { ProxyCreationContext.setCurrentProxiedBeanName(beanName); try { // 调用重载方法 return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass); } finally { ProxyCreationContext.setCurrentProxiedBeanName(null); } } public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) { if (candidateAdvisors.isEmpty()) { return candidateAdvisors; } List<Advisor> eligibleAdvisors = new LinkedList<Advisor>(); for (Advisor candidate : candidateAdvisors) { // 筛选 IntroductionAdvisor 类型的通知器 if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { eligibleAdvisors.add(candidate); } } boolean hasIntroductions = !eligibleAdvisors.isEmpty(); for (Advisor candidate : candidateAdvisors) { if (candidate instanceof IntroductionAdvisor) { continue; } // 筛选普通类型的通知器 if (canApply(candidate, clazz, hasIntroductions)) { eligibleAdvisors.add(candidate); } } return eligibleAdvisors; } public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) { if (advisor instanceof IntroductionAdvisor) { /* * 从通知器中获取类型过滤器 ClassFilter,并调用 matchers 方法进行匹配。 * ClassFilter 接口的实现类 AspectJExpressionPointcut 为例,该类的 * 匹配工作由 AspectJ 表达式解析器负责,具体匹配细节这个就没法分析了,我 * AspectJ 表达式的工作流程不是很熟 */ return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); } else if (advisor instanceof PointcutAdvisor) { PointcutAdvisor pca = (PointcutAdvisor) advisor; // 对于普通类型的通知器,这里继续调用重载方法进行筛选 return canApply(pca.getPointcut(), targetClass, hasIntroductions); } else { return true; } } public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) { Assert.notNull(pc, "Pointcut must not be null"); // 使用 ClassFilter 匹配 class if (!pc.getClassFilter().matches(targetClass)) { return false; } MethodMatcher methodMatcher = pc.getMethodMatcher(); if (methodMatcher == MethodMatcher.TRUE) { return true; } IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; if (methodMatcher instanceof IntroductionAwareMethodMatcher) { introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; } /* * 查找当前类及其父类(以及父类的父类等等)所实现的接口,由于接口中的方法是 public, * 所以当前类可以继承其父类,和父类的父类中所有的接口方法 */ Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); classes.add(targetClass); for (Class<?> clazz : classes) { // 获取当前类的方法列表,包括从父类中继承的方法 Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); for (Method method : methods) { // 使用 methodMatcher 匹配方法,匹配成功即可立即返回 if ((introductionAwareMethodMatcher != null && introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) || methodMatcher.matches(method, targetClass)) { return true; } } } return false; } 以上是通知器筛选的过程,筛选的工作主要由 ClassFilter 和 MethodMatcher 完成。关于 ClassFilter 和 MethodMatcher 我在导读一文中已经说过了,这里再说一遍吧。在 AOP 中,切点 Pointcut 是用来匹配连接点的,以 AspectJExpressionPointcut 类型的切点为例。该类型切点实现了ClassFilter 和 MethodMatcher 接口,匹配的工作则是由 AspectJ 表达式解析器复杂。除了使用 AspectJ 表达式进行匹配,Spring 还提供了基于正则表达式的切点类,以及更简单的根据方法名进行匹配的切点类。大家有兴趣的话,可以自己去了解一下,这里就不多说了。 在完成通知器的查找和筛选过程后,还需要进行最后一步处理 -- 对通知器列表进行拓展。怎么拓展呢?我们一起到下一节中一探究竟吧。 2.2.3 拓展筛选出通知器列表 拓展方法 extendAdvisors 做的事情并不多,逻辑也比较简单。我们一起来看一下,如下: protected void extendAdvisors(List<Advisor> candidateAdvisors) { AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors); } public static boolean makeAdvisorChainAspectJCapableIfNecessary(List<Advisor> advisors) { // 如果通知器列表是一个空列表,则啥都不做 if (!advisors.isEmpty()) { boolean foundAspectJAdvice = false; /* * 下面的 for 循环用于检测 advisors 列表中是否存在 * AspectJ 类型的 Advisor 或 Advice */ for (Advisor advisor : advisors) { if (isAspectJAdvice(advisor)) { foundAspectJAdvice = true; } } /* * 向 advisors 列表的首部添加 DefaultPointcutAdvisor, * 至于为什么这样做,我会在后续的文章中进行说明 */ if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) { advisors.add(0, ExposeInvocationInterceptor.ADVISOR); return true; } } return false; } private static boolean isAspectJAdvice(Advisor advisor) { return (advisor instanceof InstantiationModelAwarePointcutAdvisor || advisor.getAdvice() instanceof AbstractAspectJAdvice || (advisor instanceof PointcutAdvisor && ((PointcutAdvisor) advisor).getPointcut() instanceof AspectJExpressionPointcut)); } 如上,上面的代码比较少,也不复杂。由源码可以看出 extendAdvisors 是一个空壳方法,除了调用makeAdvisorChainAspectJCapableIfNecessary,该方法没有其他更多的逻辑了。至于 makeAdvisorChainAspectJCapableIfNecessary 这个方法,该方法主要的目的是向通知器列表首部添加 DefaultPointcutAdvisor 类型的通知器,也就是 ExposeInvocationInterceptor.ADVISOR。至于添加此种类型通知器的意图,我会在后面文章里说明,这里不便展开。关于 extendAdvisors 这个方法,这里就先说到这了。 3.总结 到这里,本篇文章就接近尾声了。这篇文章有点长,大家看下来应该蛮累的吧。由于个人能力问题,暂时未能做到对本篇文章中所贴的源码进行更为细致的分析,有点遗憾。不过好在目前把主逻辑分析弄清楚了,总的来说还算合格吧,给个及格分。大家在阅读的过程中,如果发现文章中出现错误或不妥之处,这里还请指明,也请多多指教。大家共同学习,一起进步。 好了,本篇文章就到这里了。谢谢大家的阅读。 参考 《Spring 源码深度解析》- 郝佳 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解。在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了3天时间阅读了 AOP 方面的源码。开始以为 AOP 部分的源码也会比较复杂,所以原计划投入一周的时间用于阅读源码。但在我大致理清 AOP 源码逻辑后,发现没想的那么复杂,所以目前进度算是超前了。从今天(5.15)开始,我将对 AOP 部分的源码分析系列文章进行更新。包括本篇文章在内,本系列大概会有4篇文章,我将会在接下来一周时间内陆续进行更新。在本系列文章中,我将会分析 Spring AOP 是如何为 bean 筛选合适的通知器(Advisor),以及代理对象生成的过程。除此之外,还会对拦截器的调用过程进行分析。与前面的文章一样,本系列文章不会对 AOP 的 XML 配置解析过程进行分析。 下面来讲讲本篇文章的内容,在本篇文章中,我将会向大家介绍一下 AOP 的原理,以及 AOP 中的一些术语及其对应的源码。我觉得,大家在阅读 AOP 源码时,一定要弄懂这些术语和源码。不然,在阅读 AOP 源码的过程中,可能会有点晕。好了,其他的就不多说了,下面进入正题吧。 2. AOP 原理 关于 AOP 的原理,想必大家都知道了。无非是通过代理模式为目标对象生产代理对象,并将横切逻辑插入到目标方法执行的前后。这样一说,本章确实没什么好说的了,毕竟原理就是这么简单。不过原理归原理,在具体的实现上,很多事情并没想象的那么简单。比如,我们需要确定是否应该为某个 bean 生成代理,如果应该的话,还要进一步确定将横切逻辑插入到哪些方法上。说到横切逻辑,这里简单介绍一下。横切逻辑其实就是通知(Advice),Spring 提供了5种通知,Spring 需要为每种通知提供相应的实现类。除了以上说的这些,在具体的实现过程中,还要考虑如何将 AOP 和 IOC 整合在一起,毕竟 IOC 是 Spring 框架的根基。除此之外,还有其他一些需要考虑的地方,这里就不一一列举了。总之 AOP 原理说起来容易,但做起来却不简单,尤其是实现一个业界认可的,久经考验的框架。所以,在随后的文章中,让我们带着对代码的敬畏之心,去学习 Spring AOP 模块的源码吧。 3. AOP 术语及相应的实现 本章我来向大家介绍一下 AOP 中的一些术语,并会把这些术语对应的代码也贴出来。在介绍这些术语之前,我们先来了解一下 AOP 吧。AOP 全称是 Aspect Oriented Programming,即面向切面的编程,AOP 是一种开发理念。通过 AOP,我们可以把一些非业务逻辑的代码,比如安全检查,监控等代码从业务方法中抽取出来,以非侵入的方式与原方法进行协同。这样可以使原方法更专注于业务逻辑,代码结构会更加清晰,便于维护。 这里特别说明一下,AOP 并非是 Spring 独创,AOP 有自己的标准,也有机构在维护这个标准。Spring AOP 目前也遵循相关标准,所以别认为 AOP 是 Spring 独创的。 3.1 连接点 - Joinpoint 连接点是指程序执行过程中的一些点,比如方法调用,异常处理等。在 Spring AOP 中,仅支持方法级别的连接点。上面是比较官方的说明,下面举个例子说明一下。现在我们有一个用户服务 UserService 接口,该接口定义如下: public interface UserService { void save(User user); void update(User user); void delete(String userId); User findOne(String userId); List<User> findAll(); boolean exists(String userId); } 该接口的实现类是 UserServiceImpl,假设该类的方法调用如下: 如上所示,每个方法调用都是一个连接点。接下来,我们来看看连接点的定义: public interface Joinpoint { /** 用于执行拦截器链中的下一个拦截器逻辑 */ Object proceed() throws Throwable; Object getThis(); AccessibleObject getStaticPart(); } 这个 Joinpoint 接口中,proceed 方法是核心,该方法用于执行拦截器逻辑。关于拦截器这里简单说一下吧,以前置通知拦截器为例。在执行目标方法前,该拦截器首先会执行前置通知逻辑,如果拦截器链中还有其他的拦截器,则继续调用下一个拦截器逻辑。直到拦截器链中没有其他的拦截器后,再去调用目标方法。关于拦截器这里先说这么多,在后续文章中,我会进行更为详细的说明。 上面说到一个方法调用就是一个连接点,那下面我们不妨看一下方法调用这个接口的定义。如下: public interface Invocation extends Joinpoint { Object[] getArguments(); } public interface MethodInvocation extends Invocation { Method getMethod(); } 如上所示,方法调用接口 MethodInvocation 继承自 Invocation,Invocation 接口又继承自 Joinpoint。看了上面的代码,我想大家现在对连接点应该有更多的一些认识了。接下面,我们来继续看一下 Joinpoint 接口的一个实现类 ReflectiveMethodInvocation。当然不是看源码,而是看它的继承体系图。如下: 关于连接点的相关知识,我们先了解到这里。有了这些连接点,接下来要做的事情是对我们感兴趣连接点进行一些横切操作。在操作之前,我们首先要把我们所感兴趣的连接点选中,怎么选中的呢?这就是切点 Pointcut 要做的事情了,继续往下看。 3.2 切点 - Pointcut 刚刚说到切点是用于选择连接点的,那么应该怎么选呢?在回答这个问题前,我们不妨先去看看 Pointcut 接口的定义。如下: public interface Pointcut { /** 返回一个类型过滤器 */ ClassFilter getClassFilter(); /** 返回一个方法匹配器 */ MethodMatcher getMethodMatcher(); Pointcut TRUE = TruePointcut.INSTANCE; } Pointcut 接口中定义了两个接口,分别用于返回类型过滤器和方法匹配器。下面我们再来看一下类型过滤器和方法匹配器接口的定义: public interface ClassFilter { boolean matches(Class<?> clazz); ClassFilter TRUE = TrueClassFilter.INSTANCE; } public interface MethodMatcher { boolean matches(Method method, Class<?> targetClass); boolean matches(Method method, Class<?> targetClass, Object... args); boolean isRuntime(); MethodMatcher TRUE = TrueMethodMatcher.INSTANCE; } 上面的两个接口均定义了 matches 方法,用户只要实现了 matches 方法,即可对连接点进行选择。在日常使用中,大家通常是用 AspectJ 表达式对连接点进行选择。Spring 中提供了一个 AspectJ 表达式切点类 - AspectJExpressionPointcut,下面我们来看一下这个类的继承体系图: 如上所示,这个类最终实现了 Pointcut、ClassFilter 和 MethodMatcher 接口,因此该类具备了通过 AspectJ 表达式对连接点进行选择的能力。那下面我们不妨写一个表达式对上一节的连接点进行选择,比如下面这个表达式: execution(* *.find*(..)) 该表达式用于选择以 find 的开头的方法,选择结果如下: 通过上面的表达式,我们可以就可以选中 findOne 和 findAll 两个方法了。那选中方法之后呢?当然是要搞点事情。so,接下来通知(Advice)就该上场了。 3.3 通知 - Advice 通知 Advice 即我们定义的横切逻辑,比如我们可以定义一个用于监控方法性能的通知,也可以定义一个安全检查的通知等。如果说切点解决了通知在哪里调用的问题,那么现在还需要考虑了一个问题,即通知在何时被调用?是在目标方法前被调用,还是在目标方法返回后被调用,还在两者兼备呢?Spring 帮我们解答了这个问题,Spring 中定义了以下几种通知类型: 前置通知(Before advice)- 在目标方便调用前执行通知 后置通知(After advice)- 在目标方法完成后执行通知 返回通知(After returning advice)- 在目标方法执行成功后,调用通知 异常通知(After throwing advice)- 在目标方法抛出异常后,执行通知 环绕通知(Around advice)- 在目标方法调用前后均可执行自定义逻辑 上面是对通知的一些介绍,下面我们来看一下通知的源码吧。如下: public interface Advice { } 如上,通知接口里好像什么都没定义。不过别慌,我们再去到它的子类接口中一探究竟。 /** BeforeAdvice */ public interface BeforeAdvice extends Advice { } public interface MethodBeforeAdvice extends BeforeAdvice { void before(Method method, Object[] args, Object target) throws Throwable; } /** AfterAdvice */ public interface AfterAdvice extends Advice { } public interface AfterReturningAdvice extends AfterAdvice { void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable; } 从上面的代码中可以看出,Advice 接口的子类接口里还是定义了一些东西的。下面我们再来看看 Advice 接口的具体实现类 AspectJMethodBeforeAdvice 的继承体系图,如下: 现在我们有了切点 Pointcut 和通知 Advice,由于这两个模块目前还是分离的,我们需要把它们整合在一起。这样切点就可以为通知进行导航,然后由通知逻辑实施精确打击。那怎么整合两个模块呢?答案是,切面。好的,是时候来介绍切面 Aspect 这个概念了。 3.4 切面 - Aspect 切面 Aspect 整合了切点和通知两个模块,切点解决了 where 问题,通知解决了 when 和 how 问题。切面把两者整合起来,就可以解决 对什么方法(where)在何时(when - 前置还是后置,或者环绕)执行什么样的横切逻辑(how)的三连发问题。在 AOP 中,切面只是一个概念,并没有一个具体的接口或类与此对应。不过 Spring 中倒是有一个接口的用途和切面很像,我们不妨了解一下,这个接口就是切点通知器 PointcutAdvisor。我们先来看看这个接口的定义,如下: public interface Advisor { Advice getAdvice(); boolean isPerInstance(); } public interface PointcutAdvisor extends Advisor { Pointcut getPointcut(); } 简单来说一下 PointcutAdvisor 及其父接口 Advisor,Advisor 中有一个 getAdvice 方法,用于返回通知。PointcutAdvisor 在 Advisor 基础上,新增了 getPointcut 方法,用于返回切点对象。因此 PointcutAdvisor 的实现类即可以返回切点,也可以返回通知,所以说 PointcutAdvisor 和切面的功能相似。不过他们之间还是有一些差异的,比如看下面的配置: <bean id="aopCode" class="xyz.coolblog.aop.AopCode"/> <aop:config expose-proxy="true"> <aop:aspect ref="aopCode"> <!-- pointcut --> <aop:pointcut id="helloPointcut" expression="execution(* xyz.coolblog.aop.*.hello*(..))" /> <!-- advoce --> <aop:before method="before" pointcut-ref="helloPointcut"/> <aop:after method="after" pointcut-ref="helloPointcut"/> </aop:aspect> </aop:config> 如上,一个切面中配置了一个切点和两个通知,两个通知均引用了同一个切点,即 pointcut-ref="helloPointcut"。这里在一个切面中,一个切点对应多个通知,是一对多的关系(可以配置多个 pointcut,形成多对多的关系)。而在 PointcutAdvisor 的实现类中,切点和通知是一一对应的关系。上面的通知最终会被转换成两个 PointcutAdvisor,这里我把源码调试的结果贴在下面: 在本节的最后,我们再来看看 PointcutAdvisor 的实现类 AspectJPointcutAdvisor 的继承体系图。如下: 3.5 织入 - Weaving 现在我们有了连接点、切点、通知,以及切面等,可谓万事俱备,但是还差了一股东风。这股东风是什么呢?没错,就是织入。所谓织入就是在切点的引导下,将通知逻辑插入到方法调用上,使得我们的通知逻辑在方法调用时得以执行。说完织入的概念,现在来说说 Spring 是通过何种方式将通知织入到目标方法上的。先来说说以何种方式进行织入,这个方式就是通过实现后置处理器 BeanPostProcessor 接口。该接口是 Spring 提供的一个拓展接口,通过实现该接口,用户可在 bean 初始化前后做一些自定义操作。那 Spring 是在何时进行织入操作的呢?答案是在 bean 初始化完成后,即 bean 执行完初始化方法(init-method)。Spring通过切点对 bean 类中的方法进行匹配。若匹配成功,则会为该 bean 生成代理对象,并将代理对象返回给容器。容器向后置处理器输入 bean 对象,得到 bean 对象的代理,这样就完成了织入过程。关于后置处理器的细节,这里就不多说了.大家若有兴趣,可以参考我之前写的Spring IOC 容器源码分析系列文章。 4.总结 本篇文章作为 AOP 源码分析系列文章的导读,简单介绍了 AOP 中的一些术语,及其对应的源码。总的来说,没有什么特别之处。毕竟对于 AOP,大家都有所了解。因此,若文中有不妥错误之处,还请大家指明。当然,也希望多多指教。 好了,本篇文章先到这里。感谢大家的阅读。 参考 Spring Framework Reference Documentation 《Spring 实战》第4版 - Craig Walls 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 本篇文章是“Spring IOC 容器源码分析”系列文章的最后一篇文章,本篇文章所分析的对象是 initializeBean 方法,该方法用于对已完成属性填充的 bean 做最后的初始化工作。相较于之前几篇文章所分析的源码,initializeBean 的源码相对比较简单,大家可以愉快的阅读。好了,其他的不多说了,我们直入主题吧。 2. 源码分析 本章我们来分析一下 initializeBean 方法的源码。在完成分析后,还是像往常一样,把方法的执行流程列出来。好了,看源码吧: protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) { if (System.getSecurityManager() != null) { AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { invokeAwareMethods(beanName, bean); return null; } }, getAccessControlContext()); } else { // 若 bean 实现了 BeanNameAware、BeanFactoryAware、BeanClassLoaderAware 等接口,则向 bean 中注入相关对象 invokeAwareMethods(beanName, bean); } Object wrappedBean = bean; if (mbd == null || !mbd.isSynthetic()) { // 执行 bean 初始化前置操作 wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); } try { /* * 调用初始化方法: * 1. 若 bean 实现了 InitializingBean 接口,则调用 afterPropertiesSet 方法 * 2. 若用户配置了 bean 的 init-method 属性,则调用用户在配置中指定的方法 */ invokeInitMethods(beanName, wrappedBean, mbd); } catch (Throwable ex) { throw new BeanCreationException( (mbd != null ? mbd.getResourceDescription() : null), beanName, "Invocation of init method failed", ex); } if (mbd == null || !mbd.isSynthetic()) { // 执行 bean 初始化后置操作,AOP 会在此处向目标对象中织入切面逻辑 wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); } return wrappedBean; } 以上就是 initializeBean 方法的逻辑,很简单是不是。该方法做了如下几件事情: 检测 bean 是否实现了 *Aware 类型接口,若实现,则向 bean 中注入相应的对象 执行 bean 初始化前置操作 执行初始化操作 执行 bean 初始化后置操作 在上面的流程中,我们又发现了后置处理器的踪影。如果大家阅读过 Spring 的源码,会发现后置处理器在 Spring 源码中多次出现过。后置处理器是 Spring 拓展点之一,通过实现后置处理器 BeanPostProcessor 接口,我们就可以插手 bean 的初始化过程。比如大家所熟悉的 AOP 就是在后置处理 postProcessAfterInitialization 方法中向目标对象中织如切面逻辑的。关于“前置处理”和“后置处理”相关的源码,这里就不分析了,大家有兴趣自己去看一下。接下来分析一下 invokeAwareMethods 和 invokeInitMethods 方法,如下: private void invokeAwareMethods(final String beanName, final Object bean) { if (bean instanceof Aware) { if (bean instanceof BeanNameAware) { // 注入 beanName 字符串 ((BeanNameAware) bean).setBeanName(beanName); } if (bean instanceof BeanClassLoaderAware) { // 注入 ClassLoader 对象 ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader()); } if (bean instanceof BeanFactoryAware) { // 注入 BeanFactory 对象 ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this); } } } invokeAwareMethods 方法的逻辑很简单,一句话总结:根据 bean 所实现的 Aware 的类型,向 bean 中注入不同类型的对象。 protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable { // 检测 bean 是否是 InitializingBean 类型的 boolean isInitializingBean = (bean instanceof InitializingBean); if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { if (logger.isDebugEnabled()) { logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'"); } if (System.getSecurityManager() != null) { try { AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() { @Override public Object run() throws Exception { ((InitializingBean) bean).afterPropertiesSet(); return null; } }, getAccessControlContext()); } catch (PrivilegedActionException pae) { throw pae.getException(); } } else { // 如果 bean 实现了 InitializingBean,则调用 afterPropertiesSet 方法执行初始化逻辑 ((InitializingBean) bean).afterPropertiesSet(); } } if (mbd != null) { String initMethodName = mbd.getInitMethodName(); if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && !mbd.isExternallyManagedInitMethod(initMethodName)) { // 调用用户自定义的初始化方法 invokeCustomInitMethod(beanName, bean, mbd); } } } invokeInitMethods 方法用于执行初始化方法,也不复杂,就不多说了。 3. 总结 本篇文章到这里差不多就分析完了,总的来说本文的内容比较简单,很容易看懂。正如简介一章中所说,本篇文章是我的“Spring IOC 容器源码分析”系列文章的最后一篇文章。写完这本篇文章,有种如释重负的感觉。我在5月15号写完 Java CAS 原理分析 文章后,次日开始阅读 Spring IOC 部分的源码,阅读该部分源码花了大概两周的时间。然后在5月30号发布了“Spring IOC 容器源码分析”系列文章的第一篇文章 Spring IOC 容器源码分析系列文章导读。在写完第一篇文章后,就开启了快速更新模式,以平均2天一篇的速度进行更新。终于在今天,也就是6月11号写完了最后一篇。这一段时间写文章写的很累,经常熬夜。主要的原因在于,在自己看懂源码的同时,通过写文章的方式尽量保证别人也能看懂的话,这个就比较难了。比如我在阅读源码的时候,在源码上面写了一些简单的注释。这些注释我可以看懂,但如果想写成文章,则需要把注释写的尽量详细,必要的背景知识也要介绍一下。总的来说,认真写一篇技术文章还是不容易的。写文章尚如此,那写书呢,想必更加辛苦了。我在阅读源码和写文章的过程中,也参考了一些资料(相关资料在“导读”一文中指明了出处,本文就不再次说明)。在这里,向这些资料的作者表示感谢! 好了,本篇文章就到这里了,感谢大家的阅读。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 本篇文章,我们来一起了解一下 Spring 是如何将配置文件中的属性值填充到 bean 对象中的。我在前面几篇文章中介绍过 Spring 创建 bean 的流程,即 Spring 先通过反射创建一个原始的 bean 对象,然后再向这个原始的 bean 对象中填充属性。对于填充属性这个过程,简单点来说,JavaBean 的每个属性通常都有 getter/setter 方法,我们可以直接调用 setter 方法将属性值设置进去。当然,这样做还是太简单了,填充属性的过程中还有许多事情要做。比如在 Spring 配置中,所有属性值都是以字符串的形式进行配置的,我们在将这些属性值赋值给对象的成员变量时,要根据变量类型进行相应的类型转换。对于一些集合类的配置,比如 关于属性填充的一些知识,本章先介绍这里。接下来,我们深入到源码中,从源码中了解属性填充的整个过程。 2. 源码分析 2.1 populateBean 源码一览 本节,我们先来看一下填充属性的方法,即 populateBean。该方法并不复杂,但它所调用的一些方法比较复杂。不过好在我们这里只需要知道这些方法都有什么用就行了,暂时不用纠结细节。好了,下面看源码吧。 protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { // 获取属性列表 PropertyValues pvs = mbd.getPropertyValues(); if (bw == null) { if (!pvs.isEmpty()) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance"); } else { return; } } boolean continueWithPropertyPopulation = true; /* * 在属性被填充前,给 InstantiationAwareBeanPostProcessor 类型的后置处理器一个修改 * bean 状态的机会。关于这段后置引用,官方的解释是:让用户可以自定义属性注入。比如用户实现一 * 个 InstantiationAwareBeanPostProcessor 类型的后置处理器,并通过 * postProcessAfterInstantiation 方法向 bean 的成员变量注入自定义的信息。当然,如果无 * 特殊需求,直接使用配置中的信息注入即可。另外,Spring 并不建议大家直接实现 * InstantiationAwareBeanPostProcessor 接口,如果想实现这种类型的后置处理器,更建议 * 通过继承 InstantiationAwareBeanPostProcessorAdapter 抽象类实现自定义后置处理器。 */ if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { continueWithPropertyPopulation = false; break; } } } } /* * 如果上面设置 continueWithPropertyPopulation = false,表明用户可能已经自己填充了 * bean 的属性,不需要 Spring 帮忙填充了。此时直接返回即可 */ if (!continueWithPropertyPopulation) { return; } // 根据名称或类型注入依赖 if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { MutablePropertyValues newPvs = new MutablePropertyValues(pvs); // 通过属性名称注入依赖 if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { autowireByName(beanName, mbd, bw, newPvs); } // 通过属性类型注入依赖 if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { autowireByType(beanName, mbd, bw, newPvs); } pvs = newPvs; } boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE); /* * 这里又是一种后置处理,用于在 Spring 填充属性到 bean 对象前,对属性的值进行相应的处理, * 比如可以修改某些属性的值。这时注入到 bean 中的值就不是配置文件中的内容了, * 而是经过后置处理器修改后的内容 */ if (hasInstAwareBpps || needsDepCheck) { PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); if (hasInstAwareBpps) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; // 对属性进行后置处理 pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); if (pvs == null) { return; } } } } if (needsDepCheck) { checkDependencies(beanName, mbd, filteredPds, pvs); } } // 应用属性值到 bean 对象中 applyPropertyValues(beanName, mbd, bw, pvs); } 上面的源码注释的比较详细了,下面我们来总结一下这个方法的执行流程。如下: 获取属性列表 pvs 在属性被填充到 bean 前,应用后置处理自定义属性填充 根据名称或类型解析相关依赖 再次应用后置处理,用于动态修改属性列表 pvs 的内容 将属性应用到 bean 对象中 注意第3步,也就是根据名称或类型解析相关依赖(autowire)。该逻辑只会解析依赖,并不会将解析出的依赖立即注入到 bean 对象中。所有的属性值是在 applyPropertyValues 方法中统一被注入到 bean 对象中的。 在下面的章节中,我将会对 populateBean 方法中比较重要的几个方法调用进行分析,也就是第3步和第5步中的三个方法。好了,本节先到这里。 2.2 autowireByName 方法分析 本节来分析一下 autowireByName 方法的代码,其实这个方法根据方法名,大家应该知道它有什么用了。所以我也就不啰嗦了,咱们直奔主题,直接分析源码: protected void autowireByName( String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { /* * 获取非简单类型属性的名称,且该属性未被配置在配置文件中。这里从反面解释一下什么是"非简单类型" * 属性,我们先来看看 Spring 认为的"简单类型"属性有哪些,如下: * 1. CharSequence 接口的实现类,比如 String * 2. Enum * 3. Date * 4. URI/URL * 5. Number 的继承类,比如 Integer/Long * 6. byte/short/int... 等基本类型 * 7. Locale * 8. 以上所有类型的数组形式,比如 String[]、Date[]、int[] 等等 * * 除了要求非简单类型的属性外,还要求属性未在配置文件中配置过,也就是 pvs.contains(pd.getName()) = false。 */ String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); for (String propertyName : propertyNames) { // 检测是否存在与 propertyName 相关的 bean 或 BeanDefinition。若存在,则调用 BeanFactory.getBean 方法获取 bean 实例 if (containsBean(propertyName)) { // 从容器中获取相应的 bean 实例 Object bean = getBean(propertyName); // 将解析出的 bean 存入到属性值列表(pvs)中 pvs.add(propertyName, bean); registerDependentBean(propertyName, beanName); if (logger.isDebugEnabled()) { logger.debug("Added autowiring by name from bean name '" + beanName + "' via property '" + propertyName + "' to bean named '" + propertyName + "'"); } } else { if (logger.isTraceEnabled()) { logger.trace("Not autowiring property '" + propertyName + "' of bean '" + beanName + "' by name: no matching bean found"); } } } } autowireByName 方法的逻辑比较简单,该方法首先获取非简单类型属性的名称,然后再根据名称到容器中获取相应的 bean 实例,最后再将获取到的 bean 添加到属性列表中即可。既然这个方法比较简单,那我也就不多说了,继续下面的分析。 2.3 autowireByType 方法分析 本节我们来分析一下 autowireByName 的孪生兄弟 autowireByType,相较于 autowireByName,autowireByType 则要复杂一些,复杂之处在于解析依赖的过程。不过也没关系,如果我们不过于纠结细节,我们完全可以把一些复杂的地方当做一个黑盒,我们只需要要知道这个黑盒有什么用即可。这样可以在很大程度上降低源码分析的难度。好了,其他的就不多说了,咱们来分析源码吧。 protected void autowireByType( String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { TypeConverter converter = getCustomTypeConverter(); if (converter == null) { converter = bw; } Set<String> autowiredBeanNames = new LinkedHashSet<String>(4); // 获取非简单类型的属性 String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); for (String propertyName : propertyNames) { try { PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName); // 如果属性类型为 Object,则忽略,不做解析 if (Object.class != pd.getPropertyType()) { /* * 获取 setter 方法(write method)的参数信息,比如参数在参数列表中的 * 位置,参数类型,以及该参数所归属的方法等信息 */ MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd); // Do not allow eager init for type matching in case of a prioritized post-processor. boolean eager = !PriorityOrdered.class.isAssignableFrom(bw.getWrappedClass()); // 创建依赖描述对象 DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager); /* * 下面的方法用于解析依赖。过程比较复杂,先把这里看成一个黑盒,我们只要知道这 * 个方法可以帮我们解析出合适的依赖即可。 */ Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter); if (autowiredArgument != null) { // 将解析出的 bean 存入到属性值列表(pvs)中 pvs.add(propertyName, autowiredArgument); } for (String autowiredBeanName : autowiredBeanNames) { registerDependentBean(autowiredBeanName, beanName); if (logger.isDebugEnabled()) { logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" + propertyName + "' to bean named '" + autowiredBeanName + "'"); } } autowiredBeanNames.clear(); } } catch (BeansException ex) { throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex); } } } 如上所示,autowireByType 的代码本身并不复杂。和 autowireByName 一样,autowireByType 首先也是获取非简单类型属性的名称。然后再根据属性名获取属性描述符,并由属性描述符获取方法参数对象 MethodParameter,随后再根据 MethodParameter 对象获取依赖描述符对象,整个过程为 beanName → PropertyDescriptor → MethodParameter → DependencyDescriptor。在获取到依赖描述符对象后,再根据依赖描述符解析出合适的依赖。最后将解析出的结果存入属性列表 pvs 中即可。 关于 autowireByType 方法中出现的几种描述符对象,大家自己去看一下他们的实现吧,我就不分析了。接下来,我们来分析一下解析依赖的方法 resolveDependency。如下: public Object resolveDependency(DependencyDescriptor descriptor, String requestingBeanName, Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException { descriptor.initParameterNameDiscovery(getParameterNameDiscoverer()); if (javaUtilOptionalClass == descriptor.getDependencyType()) { return new OptionalDependencyFactory().createOptionalDependency(descriptor, requestingBeanName); } else if (ObjectFactory.class == descriptor.getDependencyType() || ObjectProvider.class == descriptor.getDependencyType()) { return new DependencyObjectProvider(descriptor, requestingBeanName); } else if (javaxInjectProviderClass == descriptor.getDependencyType()) { return new Jsr330ProviderFactory().createDependencyProvider(descriptor, requestingBeanName); } else { Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary( descriptor, requestingBeanName); if (result == null) { // 解析依赖 result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); } return result; } } public Object doResolveDependency(DependencyDescriptor descriptor, String beanName, Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException { InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor); try { // 该方法最终调用了 beanFactory.getBean(String, Class),从容器中获取依赖 Object shortcut = descriptor.resolveShortcut(this); // 如果容器中存在所需依赖,这里进行断路操作,提前结束依赖解析逻辑 if (shortcut != null) { return shortcut; } Class<?> type = descriptor.getDependencyType(); // 处理 @value 注解 Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); if (value != null) { if (value instanceof String) { String strVal = resolveEmbeddedValue((String) value); BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null); value = evaluateBeanDefinitionString(strVal, bd); } TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); return (descriptor.getField() != null ? converter.convertIfNecessary(value, type, descriptor.getField()) : converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); } // 解析数组、list、map 等类型的依赖 Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter); if (multipleBeans != null) { return multipleBeans; } /* * 按类型查找候选列表,如果某个类型已经被实例化,则返回相应的实例。 * 比如下面的配置: * * <bean name="mongoDao" class="xyz.coolblog.autowire.MongoDao" primary="true"/> * <bean name="service" class="xyz.coolblog.autowire.Service" autowire="byType"/> * <bean name="mysqlDao" class="xyz.coolblog.autowire.MySqlDao"/> * * MongoDao 和 MySqlDao 均实现自 Dao 接口,Service 对象(不是接口)中有一个 Dao * 类型的属性。现在根据类型自动注入 Dao 的实现类。这里有两个候选 bean,一个是 * mongoDao,另一个是 mysqlDao,其中 mongoDao 在 service 之前实例化, * mysqlDao 在 service 之后实例化。此时 findAutowireCandidates 方法会返回如下的结果: * * matchingBeans = [ <mongoDao, Object@MongoDao>, <mysqlDao, Class@MySqlDao> ] * * 注意 mysqlDao 还未实例化,所以返回的是 MySqlDao.class。 * * findAutowireCandidates 这个方法逻辑比较复杂,我简单说一下它的工作流程吧,如下: * 1. 从 BeanFactory 中获取某种类型 bean 的名称,比如上面的配置中 * mongoDao 和 mysqlDao 均实现了 Dao 接口,所以他们是同一种类型的 bean。 * 2. 遍历上一步得到的名称列表,并判断 bean 名称对应的 bean 是否是合适的候选项, * 若合适则添加到候选列表中,并在最后返回候选列表 * * findAutowireCandidates 比较复杂,我并未完全搞懂,就不深入分析了。见谅 */ Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor); if (matchingBeans.isEmpty()) { if (isRequired(descriptor)) { // 抛出 NoSuchBeanDefinitionException 异常 raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor); } return null; } String autowiredBeanName; Object instanceCandidate; if (matchingBeans.size() > 1) { /* * matchingBeans.size() > 1,则表明存在多个可注入的候选项,这里判断使用哪一个 * 候选项。比如下面的配置: * * <bean name="mongoDao" class="xyz.coolblog.autowire.MongoDao" primary="true"/> * <bean name="mysqlDao" class="xyz.coolblog.autowire.MySqlDao"/> * * mongoDao 的配置中存在 primary 属性,所以 mongoDao 会被选为最终的候选项。如 * 果两个 bean 配置都没有 primary 属性,则需要根据优先级选择候选项。优先级这一块 * 的逻辑没细看,不多说了。 */ autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); if (autowiredBeanName == null) { if (isRequired(descriptor) || !indicatesMultipleBeans(type)) { // 抛出 NoUniqueBeanDefinitionException 异常 return descriptor.resolveNotUnique(type, matchingBeans); } else { return null; } } // 根据解析出的 autowiredBeanName,获取相应的候选项 instanceCandidate = matchingBeans.get(autowiredBeanName); } else { // 只有一个候选项,直接取出来即可 Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next(); autowiredBeanName = entry.getKey(); instanceCandidate = entry.getValue(); } if (autowiredBeanNames != null) { autowiredBeanNames.add(autowiredBeanName); } // 返回候选项实例,如果实例是 Class 类型,则调用 beanFactory.getBean(String, Class) 获取相应的 bean。否则直接返回即可 return (instanceCandidate instanceof Class ? descriptor.resolveCandidate(autowiredBeanName, type, this) : instanceCandidate); } finally { ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint); } } 由上面的代码可以看出,doResolveDependency 这个方法还是挺复杂的。这里我就不继续分析 doResolveDependency 所调用的方法了,对于这些方法,我也是似懂非懂。好了,本节的最后我们来总结一下 doResolveDependency 的执行流程吧,如下: 首先将 beanName 和 requiredType 作为参数,并尝试从 BeanFactory 中获取与此对于的 bean。若获取成功,就可以提前结束 doResolveDependency 的逻辑。 处理 @value 注解 解析数组、List、Map 等类型的依赖,如果解析结果不为空,则返回结果 根据类型查找合适的候选项 如果候选项的数量为0,则抛出异常。为1,直接从候选列表中取出即可。若候选项数量 > 1,则在多个候选项中确定最优候选项,若无法确定则抛出异常 若候选项是 Class 类型,表明候选项还没实例化,此时通过 BeanFactory.getBean 方法对其进行实例化。若候选项是非 Class 类型,则表明已经完成了实例化,此时直接返回即可。 好了,本节的内容先到这里。如果有分析错的地方,欢迎大家指出来。 2.4 applyPropertyValues 方法分析 经过了上面的流程,现在终于可以将属性值注入到 bean 对象中了。当然,这里还不能立即将属性值注入到对象中,因为在 Spring 配置文件中属性值都是以 String 类型进行配置的,所以 Spring 框架需要对 String 类型进行转换。除此之外,对于 ref 属性,这里还需要根据 ref 属性值解析依赖。还有一些其他操作,这里就不多说了,更多的信息我们一起在源码探寻。 protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { if (pvs == null || pvs.isEmpty()) { return; } if (System.getSecurityManager() != null && bw instanceof BeanWrapperImpl) { ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); } MutablePropertyValues mpvs = null; List<PropertyValue> original; if (pvs instanceof MutablePropertyValues) { mpvs = (MutablePropertyValues) pvs; // 如果属性列表 pvs 被转换过,则直接返回即可 if (mpvs.isConverted()) { try { bw.setPropertyValues(mpvs); return; } catch (BeansException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Error setting property values", ex); } } original = mpvs.getPropertyValueList(); } else { original = Arrays.asList(pvs.getPropertyValues()); } TypeConverter converter = getCustomTypeConverter(); if (converter == null) { converter = bw; } BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter); List<PropertyValue> deepCopy = new ArrayList<PropertyValue>(original.size()); boolean resolveNecessary = false; // 遍历属性列表 for (PropertyValue pv : original) { // 如果属性值被转换过,则就不需要再次转换 if (pv.isConverted()) { deepCopy.add(pv); } else { String propertyName = pv.getName(); Object originalValue = pv.getValue(); /* * 解析属性值。举例说明,先看下面的配置: * * <bean id="macbook" class="MacBookPro"> * <property name="manufacturer" value="Apple"/> * <property name="width" value="280"/> * <property name="cpu" ref="cpu"/> * <property name="interface"> * <list> * <value>USB</value> * <value>HDMI</value> * <value>Thunderbolt</value> * </list> * </property> * </bean> * * 上面是一款电脑的配置信息,每个 property 配置经过下面的方法解析后,返回如下结果: * propertyName = "manufacturer", resolvedValue = "Apple" * propertyName = "width", resolvedValue = "280" * propertyName = "cpu", resolvedValue = "CPU@1234" 注:resolvedValue 是一个对象 * propertyName = "interface", resolvedValue = ["USB", "HDMI", "Thunderbolt"] * * 如上所示,resolveValueIfNecessary 会将 ref 解析为具体的对象,将 <list> * 标签转换为 List 对象等。对于 int 类型的配置,这里并未做转换,所以 * width = "280",还是字符串。除了解析上面几种类型,该方法还会解析 <set/>、 * <map/>、<array/> 等集合配置 */ Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); Object convertedValue = resolvedValue; /* * convertible 表示属性值是否可转换,由两个条件合成而来。第一个条件不难理解,解释 * 一下第二个条件。第二个条件用于检测 propertyName 是否是 nested 或者 indexed, * 直接举例说明吧: * * public class Room { * private Door door = new Door(); * } * * room 对象里面包含了 door 对象,如果我们想向 door 对象中注入属性值,则可以这样配置: * * <bean id="room" class="xyz.coolblog.Room"> * <property name="door.width" value="123"/> * </bean> * * isNestedOrIndexedProperty 会根据 propertyName 中是否包含 . 或 [ 返回 * true 和 false。包含则返回 true,否则返回 false。关于 nested 类型的属性,我 * 没在实践中用过,所以不知道上面举的例子是不是合理。若不合理,欢迎指正,也请多多指教。 * 关于 nested 类型的属性,大家还可以参考 Spring 的官方文档: * https://docs.spring.io/spring/docs/4.3.17.RELEASE/spring-framework-reference/htmlsingle/#beans-beans-conventions */ boolean convertible = bw.isWritableProperty(propertyName) && !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); // 对于一般的属性,convertible 通常为 true if (convertible) { // 对属性值的类型进行转换,比如将 String 类型的属性值 "123" 转为 Integer 类型的 123 convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); } /* * 如果 originalValue 是通过 autowireByType 或 autowireByName 解析而来, * 那么此处条件成立,即 (resolvedValue == originalValue) = true */ if (resolvedValue == originalValue) { if (convertible) { // 将 convertedValue 设置到 pv 中,后续再次创建同一个 bean 时,就无需再次进行转换了 pv.setConvertedValue(convertedValue); } deepCopy.add(pv); } /* * 如果原始值 originalValue 是 TypedStringValue,且转换后的值 * convertedValue 不是 Collection 或数组类型,则将转换后的值存入到 pv 中。 */ else if (convertible && originalValue instanceof TypedStringValue && !((TypedStringValue) originalValue).isDynamic() && !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) { pv.setConvertedValue(convertedValue); deepCopy.add(pv); } else { resolveNecessary = true; deepCopy.add(new PropertyValue(pv, convertedValue)); } } } if (mpvs != null && !resolveNecessary) { mpvs.setConverted(); } try { // 将所有的属性值设置到 bean 实例中 bw.setPropertyValues(new MutablePropertyValues(deepCopy)); } catch (BeansException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Error setting property values", ex); } } 以上就是 applyPropertyValues 方法的源码,配合着我写的注释,应该可以理解这个方法的流程。这个方法也调用了很多其他的方法,如果大家跟下去的话,会发现这些方法的调用栈也是很深的,比较复杂。这里说一下 bw.setPropertyValues 这个方法,如果大家跟到这个方法的调用栈的最底部,会发现这个方法是通过调用对象的 setter 方法进行属性设置的。这里贴一下简化后的代码: public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements BeanWrapper { // 省略部分代码 private class BeanPropertyHandler extends PropertyHandler { @Override public void setValue(final Object object, Object valueToApply) throws Exception { // 获取 writeMethod,也就是 setter 方法 final Method writeMethod = this.pd.getWriteMethod(); if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers()) && !writeMethod.isAccessible()) { writeMethod.setAccessible(true); } final Object value = valueToApply; // 调用 setter 方法,getWrappedInstance() 返回的是 bean 对象 writeMethod.invoke(getWrappedInstance(), value); } } } 好了,本节的最后来总结一下 applyPropertyValues 方法的执行流程吧,如下: 检测属性值列表是否已转换过的,若转换过,则直接填充属性,无需再次转换 遍历属性值列表 pvs,解析原始值 originalValue,得到解析值 resolvedValue 对解析后的属性值 resolvedValue 进行类型转换 将类型转换后的属性值设置到 PropertyValue 对象中,并将 PropertyValue 对象存入 deepCopy 集合中 将 deepCopy 中的属性信息注入到 bean 对象中 3. 总结 本文对 populateBean 方法及其所调用的 autowireByName、autowireByType 和 applyPropertyValues 做了较为详细的分析,不知道大家看完后感觉如何。我说一下我的感受吧,从我看 Spring IOC 部分的源码到现在写了5篇关于 IOC 部分的源码分析文章,总体感觉 Spring 的源码还是很复杂的,调用层次很深。如果想对源码有一个比较好的理解,需要不少的时间去分析,调试源码。总的来说,不容易。当然,我的水平有限。如果大家自己去阅读源码,可能会觉得也没这么难啊。 好了,其他的就不多说了。如果本文中有分析错的地方,欢迎大家指正。最后感谢大家的阅读。 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 本文,我们来看一下 Spring 是如何解决循环依赖问题的。在本篇文章中,我会首先向大家介绍一下什么是循环依赖。然后,进入源码分析阶段。为了更好的说明 Spring 解决循环依赖的办法,我将会从获取 bean 的方法getBean(String)开始,把整个调用过程梳理一遍。梳理完后,再来详细分析源码。通过这几步的讲解,希望让大家能够弄懂什么是循环依赖,以及如何解循环依赖。 循环依赖相关的源码本身不是很复杂,不过这里要先介绍大量的前置知识。不然这些源码看起来很简单,但读起来可能却也不知所云。那下面我们先来了解一下什么是循环依赖。 2. 背景知识 2.1 什么是循环依赖 所谓的循环依赖是指,A 依赖 B,B 又依赖 A,它们之间形成了循环依赖。或者是 A 依赖 B,B 依赖 C,C 又依赖 A。它们之间的依赖关系如下: 这里以两个类直接相互依赖为例,他们的实现代码可能如下: public class BeanB { private BeanA beanA; // 省略 getter/setter } public class BeanA { private BeanB beanB; } 配置信息如下: <bean id="beanA" class="xyz.coolblog.BeanA"> <property name="beanB" ref="beanB"/> </bean> <bean id="beanB" class="xyz.coolblog.BeanB"> <property name="beanA" ref="beanA"/> </bean> IOC 容器在读到上面的配置时,会按照顺序,先去实例化 beanA。然后发现 beanA 依赖于 beanB,接在又去实例化 beanB。实例化 beanB 时,发现 beanB 又依赖于 beanA。如果容器不处理循环依赖的话,容器会无限执行上面的流程,直到内存溢出,程序崩溃。当然,Spring 是不会让这种情况发生的。在容器再次发现 beanB 依赖于 beanA 时,容器会获取 beanA 对象的一个早期的引用(early reference),并把这个早期引用注入到 beanB 中,让 beanB 先完成实例化。beanB 完成实例化,beanA 就可以获取到 beanB 的引用,beanA 随之完成实例化。这里大家可能不知道“早期引用”是什么意思,这里先别着急,我会在下一章进行说明。 好了,本章先到这里,我们继续往下看。 2.2 一些缓存的介绍 在进行源码分析前,我们先来看一组缓存的定义。如下: /** Cache of singleton objects: bean name --> bean instance */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256); /** Cache of singleton factories: bean name --> ObjectFactory */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16); /** Cache of early singleton objects: bean name --> bean instance */ private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16); 根据缓存变量上面的注释,大家应该能大致了解他们的用途。我这里简单说明一下吧: 缓存 用途 singletonObjects 用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用 earlySingletonObjects 存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 singletonFactories 存放 bean 工厂对象,用于解决循环依赖 上一章提到了”早期引用“,所谓的”早期引用“是指向原始对象的引用。所谓的原始对象是指刚创建好的对象,但还未填充属性。这样讲大家不知道大家听明白了没,不过没听明白也不要紧。简单做个实验就知道了,这里我们先定义一个对象 Room: /** Room 包含了一些电器 */ public class Room { private String television; private String airConditioner; private String refrigerator; private String washer; // 省略 getter/setter } 配置如下: <bean id="room" class="xyz.coolblog.demo.Room"> <property name="television" value="Xiaomi"/> <property name="airConditioner" value="Gree"/> <property name="refrigerator" value="Haier"/> <property name="washer" value="Siemens"/> </bean> 我们先看一下完全实例化好后的 bean 长什么样的。如下: 从调试信息中可以看得出,Room 的每个成员变量都被赋上值了。然后我们再来看一下“原始的 bean 对象”长的是什么样的,如下: 结果比较明显了,所有字段都是 null。这里的 bean 和上面的 bean 指向的是同一个对象Room@1567,但现在这个对象所有字段都是 null,我们把这种对象成为原始的对象。形象点说,上面的 bean 对象是一个装修好的房子,可以拎包入住了。而这里的 bean 对象还是个毛坯房,还要装修一下(填充属性)才行。 2.3 回顾获取 bean 的过程 本节,我们来了解从 Spring IOC 容器中获取 bean 实例的流程(简化版),这对我们后续的源码分析会有比较大的帮助。先看图: 先来简单介绍一下这张图,这张图是一个简化后的流程图。开始流程图中只有一条执行路径,在条件 sharedInstance != null 这里出现了岔路,形成了绿色和红色两条路径。在上图中,读取/添加缓存的方法我用蓝色的框和标注了出来。至于虚线的箭头,和虚线框里的路径,这个下面会说到。 我来按照上面的图,分析一下整个流程的执行顺序。这个流程从 getBean 方法开始,getBean 是个空壳方法,所有逻辑都在 doGetBean 方法中。doGetBean 首先会调用 getSingleton(beanName) 方法获取 sharedInstance,sharedInstance 可能是完全实例化好的 bean,也可能是一个原始的 bean,当然也有可能是 null。如果不为 null,则走绿色的那条路径。再经 getObjectForBeanInstance 这一步处理后,绿色的这条执行路径就结束了。 我们再来看一下红色的那条执行路径,也就是 sharedInstance = null 的情况。在第一次获取某个 bean 的时候,缓存中是没有记录的,所以这个时候要走创建逻辑。上图中的 getSingleton(beanName, new ObjectFactory<Object>() {...}) 方法会创建一个 bean 实例,上图虚线路径指的是 getSingleton 方法内部调用的两个方法,其逻辑如下: public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { // 省略部分代码 singletonObject = singletonFactory.getObject(); // ... addSingleton(beanName, singletonObject); } 如上所示,getSingleton 会在内部先调用 getObject 方法创建 singletonObject,然后再调用 addSingleton 将 singletonObject 放入缓存中。getObject 在内部代用了 createBean 方法,createBean 方法基本上也属于空壳方法,更多的逻辑是写在 doCreateBean 方法中的。doCreateBean 方法中的逻辑很多,其首先调用了 createBeanInstance 方法创建了一个原始的 bean 对象,随后调用 addSingletonFactory 方法向缓存中添加单例 bean 工厂,从该工厂可以获取原始对象的引用,也就是所谓的“早期引用”。再之后,继续调用 populateBean 方法向原始 bean 对象中填充属性,并解析依赖。getObject 执行完成后,会返回完全实例化好的 bean。紧接着再调用 addSingleton 把完全实例化好的 bean 对象放入缓存中。到这里,红色执行路径差不多也就要结束的。 我这里没有把 getObject、addSingleton 方法和 getSingleton(String, ObjectFactory) 并列画在红色的路径里,目的是想简化一下方法的调用栈(都画进来有点复杂)。我们可以进一步简化上面的调用流程,比如下面: 这个流程看起来是不是简单多了,命中缓存走绿色路径,未命中走红色的创建路径。好了,本节先到这。 3. 源码分析 好了,经过前面的铺垫,现在我们终于可以深入源码一探究竟了,想必大家已等不及了。那我不卖关子了,下面我们按照方法的调用顺序,依次来看一下循环依赖相关的代码。如下: protected <T> T doGetBean( final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException { // ...... // 从缓存中获取 bean 实例 Object sharedInstance = getSingleton(beanName); // ...... } public Object getSingleton(String beanName) { return getSingleton(beanName, true); } protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 从 singletonObjects 获取实例,singletonObjects 中的实例都是准备好的 bean 实例,可以直接使用 Object singletonObject = this.singletonObjects.get(beanName); // 判断 beanName 对应的 bean 是否正在创建中 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 从 earlySingletonObjects 中获取提前曝光的 bean singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { // 获取相应的 bean 工厂 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 提前曝光 bean 实例(raw bean),用于解决循环依赖 singletonObject = singletonFactory.getObject(); // 将 singletonObject 放入缓存中,并将 singletonFactory 从缓存中移除 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return (singletonObject != NULL_OBJECT ? singletonObject : null); } 上面的源码中,doGetBean 所调用的方法 getSingleton(String) 是一个空壳方法,其主要逻辑在 getSingleton(String, boolean) 中。该方法逻辑比较简单,首先从 singletonObjects 缓存中获取 bean 实例。若未命中,再去 earlySingletonObjects 缓存中获取原始 bean 实例。如果仍未命中,则从 singletonFactory 缓存中获取 ObjectFactory 对象,然后再调用 getObject 方法获取原始 bean 实例的应用,也就是早期引用。获取成功后,将该实例放入 earlySingletonObjects 缓存中,并将 ObjectFactory 对象从 singletonFactories 移除。看完这个方法,我们再来看看 getSingleton(String, ObjectFactory) 方法,这个方法也是在 doGetBean 中被调用的。这次我会把 doGetBean 的代码多贴一点出来,如下: protected <T> T doGetBean( final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException { // ...... Object bean; // 从缓存中获取 bean 实例 Object sharedInstance = getSingleton(beanName); // 这里先忽略 args == null 这个条件 if (sharedInstance != null && args == null) { // 进行后续的处理 bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else { // ...... // mbd.isSingleton() 用于判断 bean 是否是单例模式 if (mbd.isSingleton()) { // 再次获取 bean 实例 sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { try { // 创建 bean 实例,createBean 返回的 bean 是完全实例化好的 return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } } }); // 进行后续的处理 bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } // ...... } // ...... // 返回 bean return (T) bean; } 这里的代码逻辑和我在 2.3 回顾获取 bean 的过程 一节的最后贴的主流程图已经很接近了,对照那张图和代码中的注释,大家应该可以理解 doGetBean 方法了。继续往下看: public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { synchronized (this.singletonObjects) { // ...... // 调用 getObject 方法创建 bean 实例 singletonObject = singletonFactory.getObject(); newSingleton = true; if (newSingleton) { // 添加 bean 到 singletonObjects 缓存中,并从其他集合中将 bean 相关记录移除 addSingleton(beanName, singletonObject); } // ...... // 返回 singletonObject return (singletonObject != NULL_OBJECT ? singletonObject : null); } } protected void addSingleton(String beanName, Object singletonObject) { synchronized (this.singletonObjects) { // 将 <beanName, singletonObject> 映射存入 singletonObjects 中 this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT)); // 从其他缓存中移除 beanName 相关映射 this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } 上面的代码中包含两步操作,第一步操作是调用 getObject 创建 bean 实例,第二步是调用 addSingleton 方法将创建好的 bean 放入缓存中。代码逻辑并不复杂,相信大家都能看懂。那么接下来我们继续往下看,这次分析的是 doCreateBean 中的一些逻辑。如下: protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException { BeanWrapper instanceWrapper = null; // ...... // 创建 bean 对象,并将 bean 对象包裹在 BeanWrapper 对象中返回 instanceWrapper = createBeanInstance(beanName, mbd, args); // 从 BeanWrapper 对象中获取 bean 对象,这里的 bean 指向的是一个原始的对象 final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null); /* * earlySingletonExposure 用于表示是否”提前暴露“原始对象的引用,用于解决循环依赖。 * 对于单例 bean,该变量一般为 true。更详细的解释可以参考我之前的文章 */ boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 添加 bean 工厂对象到 singletonFactories 缓存中 addSingletonFactory(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { /* * 获取原始对象的早期引用,在 getEarlyBeanReference 方法中,会执行 AOP * 相关逻辑。若 bean 未被 AOP 拦截,getEarlyBeanReference 原样返回 * bean,所以大家可以把 * return getEarlyBeanReference(beanName, mbd, bean) * 等价于: * return bean; */ return getEarlyBeanReference(beanName, mbd, bean); } }); } Object exposedObject = bean; // ...... // 填充属性,解析依赖 populateBean(beanName, mbd, instanceWrapper); // ...... // 返回 bean 实例 return exposedObject; } protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { // 将 singletonFactory 添加到 singletonFactories 缓存中 this.singletonFactories.put(beanName, singletonFactory); // 从其他缓存中移除相关记录,即使没有 this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } } 上面的代码简化了不少,不过看起来仍有点复杂。好在,上面代码的主线逻辑比较简单,由三个方法组成。如下: 1. 创建原始 bean 实例 → createBeanInstance(beanName, mbd, args) 2. 添加原始对象工厂对象到 singletonFactories 缓存中 → addSingletonFactory(beanName, new ObjectFactory<Object>{...}) 3. 填充属性,解析依赖 → populateBean(beanName, mbd, instanceWrapper) 到这里,本节涉及到的源码就分析完了。可是看完源码后,我们似乎仍然不知道这些源码是如何解决循环依赖问题的。难道本篇文章就到这里了吗?答案是否。下面我来解答这个问题,这里我还是以 BeanA 和 BeanB 两个类相互依赖为例。在上面的方法调用中,有几个关键的地方,下面一一列举出来: 1. 创建原始 bean 对象 instanceWrapper = createBeanInstance(beanName, mbd, args); final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null); 假设 beanA 先被创建,创建后的原始对象为 BeanA@1234,上面代码中的 bean 变量指向就是这个对象。 2. 暴露早期引用 addSingletonFactory(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { return getEarlyBeanReference(beanName, mbd, bean); } }); beanA 指向的原始对象创建好后,就开始把指向原始对象的引用通过 ObjectFactory 暴露出去。getEarlyBeanReference 方法的第三个参数 bean 指向的正是 createBeanInstance 方法创建出原始 bean 对象 BeanA@1234。 3. 解析依赖 populateBean(beanName, mbd, instanceWrapper); populateBean 用于向 beanA 这个原始对象中填充属性,当它检测到 beanA 依赖于 beanB 时,会首先去实例化 beanB。beanB 在此方法处也会解析自己的依赖,当它检测到 beanA 这个依赖,于是调用 BeanFactry.getBean("beanA") 这个方法,从容器中获取 beanA。 4. 获取早期引用 protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 从缓存中获取早期引用 singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 从 SingletonFactory 中获取早期引用 singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return (singletonObject != NULL_OBJECT ? singletonObject : null); } 接着上面的步骤讲,populateBean 调用 BeanFactry.getBean("beanA") 以获取 beanB 的依赖。getBean("beanA") 会先调用 getSingleton("beanA"),尝试从缓存中获取 beanA。此时由于 beanA 还没完全实例化好,于是 this.singletonObjects.get("beanA") 返回 null。接着 this.earlySingletonObjects.get("beanA") 也返回空,因为 beanA 早期引用还没放入到这个缓存中。最后调用 singletonFactory.getObject() 返回 singletonObject,此时 singletonObject != null。singletonObject 指向 BeanA@1234,也就是 createBeanInstance 创建的原始对象。此时 beanB 获取到了这个原始对象的引用,beanB 就能顺利完成实例化。beanB 完成实例化后,beanA 就能获取到 beanB 所指向的实例,beanA 随之也完成了实例化工作。由于 beanB.beanA 和 beanA 指向的是同一个对象 BeanA@1234,所以 beanB 中的 beanA 此时也处于可用状态了。 以上的过程对应下面的流程图: 4. 总结 到这里,本篇文章差不多就快写完了,不知道大家看懂了没。这篇文章在前面做了大量的铺垫,然后再进行源码分析。相比于我之前写的几篇文章,本篇文章所对应的源码难度上比之前简单一些。但说实话也不好写,我本来只想简单介绍一下背景知识,然后直接进行源码分析。但是又怕有的朋友看不懂,所以还是用了大篇幅介绍的背景知识。这样写,可能有的朋友觉得比较啰嗦。但是考虑到大家的水平不一,为了保证让大家能够更好的理解,所以还是尽量写的详细一点。本篇文章总的来说写的还是有点累的,花了一些心思思考怎么安排章节顺序,怎么简化代码和画图。如果大家看完这篇文章,觉得还不错的话,不妨给个赞吧,也算是对我的鼓励吧。 由于个人的技术能力有限,若文章有错误不妥之处,欢迎大家指出来。好了,本篇文章到此结束,谢谢大家的阅读。 参考: 《Spring 源码深度解析》- 郝佳 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 本篇文章是上一篇文章(创建单例 bean 的过程)的延续。在上一篇文章中,我们从战略层面上领略了doCreateBean方法的全过程。本篇文章,我们就从战术的层面上,详细分析doCreateBean方法中的一个重要的调用,即createBeanInstance方法。在本篇文章中,你将看到三种不同的构造 bean 对象的方式。你也会了解到构造 bean 对象的两种策略。如果你对这些内容感兴趣,那么不妨继续往下读。我会在代码进行大量的注解,相信能帮助你理解代码逻辑。好了,其他的就不多说了,进入正题吧。 2. 源码分析 2.1 创建 bean 对象的过程 本节,我们一起来来分析一下本篇文章的主角createBeanInstance方法。按照惯例,我们还是先分析一下方法的大致脉络,然后我们再按照这个脉络去分析一些重要的调用。So. Let`s go → ↓ protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) { Class<?> beanClass = resolveBeanClass(mbd, beanName); /* * 检测类的访问权限。默认情况下,对于非 public 的类,是允许访问的。 * 若禁止访问,这里会抛出异常 */ if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Bean class isn't public, and non-public access not allowed: " + beanClass.getName()); } /* * 如果工厂方法不为空,则通过工厂方法构建 bean 对象。这种构建 bean 的方式 * 就不深入分析了,有兴趣的朋友可以自己去看一下。 */ if (mbd.getFactoryMethodName() != null) { // 通过“工厂方法”的方式构建 bean 对象 return instantiateUsingFactoryMethod(beanName, mbd, args); } /* * 当多次构建同一个 bean 时,可以使用此处的快捷路径,即无需再次推断应该使用哪种方式构造实例, * 以提高效率。比如在多次构建同一个 prototype 类型的 bean 时,就可以走此处的捷径。 * 这里的 resolved 和 mbd.constructorArgumentsResolved 将会在 bean 第一次实例 * 化的过程中被设置,在后面的源码中会分析到,先继续往下看。 */ boolean resolved = false; boolean autowireNecessary = false; if (args == null) { synchronized (mbd.constructorArgumentLock) { if (mbd.resolvedConstructorOrFactoryMethod != null) { resolved = true; autowireNecessary = mbd.constructorArgumentsResolved; } } } if (resolved) { if (autowireNecessary) { // 通过“构造方法自动注入”的方式构造 bean 对象 return autowireConstructor(beanName, mbd, null, null); } else { // 通过“默认构造方法”的方式构造 bean 对象 return instantiateBean(beanName, mbd); } } // 由后置处理器决定返回哪些构造方法,这里不深入分析了 Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); /* * 下面的条件分支条件用于判断使用什么方式构造 bean 实例,有两种方式可选 - 构造方法自动 * 注入和默认构造方法。判断的条件由4部分综合而成,如下: * * 条件1:ctors != null -> 后置处理器返回构造方法数组是否为空 * * 条件2:mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR * -> bean 配置中的 autowire 属性是否为 constructor * 条件3:mbd.hasConstructorArgumentValues() * -> constructorArgumentValues 是否存在元素,即 bean 配置文件中 * 是否配置了 <construct-arg/> * 条件4:!ObjectUtils.isEmpty(args) * -> args 数组是否存在元素,args 是由用户调用 * getBean(String name, Object... args) 传入的 * * 上面4个条件,只要有一个为 true,就会通过构造方法自动注入的方式构造 bean 实例 */ if (ctors != null || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR || mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { // 通过“构造方法自动注入”的方式构造 bean 对象 return autowireConstructor(beanName, mbd, ctors, args); } // 通过“默认构造方法”的方式构造 bean 对象 return instantiateBean(beanName, mbd); } 以上就是 createBeanInstance 方法的源码,不是很长。配合着注释,应该不是很难懂。下面我们来总结一下这个方法的执行流程,如下: 检测类的访问权限,若禁止访问,则抛出异常 若工厂方法不为空,则通过工厂方法构建 bean 对象,并返回结果 若构造方式已解析过,则走快捷路径构建 bean 对象,并返回结果 如第三步不满足,则通过组合条件决定使用哪种方式构建 bean 对象 这里有三种构造 bean 对象的方式,如下: 通过“工厂方法”的方式构造 bean 对象 通过“构造方法自动注入”的方式构造 bean 对象 通过“默认构造方法”的方式构造 bean 对象 下面我将会分析第2和第3种构造 bean 对象方式的实现源码。至于第1种方式,实现逻辑和第2种方式较为相似。所以就不分析了,大家有兴趣可以自己看一下。 2.2 通过构造方法自动注入的方式创建 bean 实例 本节,我将会分析构造方法自动注入的实现逻辑。代码逻辑较为复杂,需要大家耐心阅读。代码如下: protected BeanWrapper autowireConstructor( String beanName, RootBeanDefinition mbd, Constructor<?>[] ctors, Object[] explicitArgs) { // 创建 ConstructorResolver 对象,并调用其 autowireConstructor 方法 return new ConstructorResolver(this).autowireConstructor(beanName, mbd, ctors, explicitArgs); } public BeanWrapper autowireConstructor(final String beanName, final RootBeanDefinition mbd, Constructor<?>[] chosenCtors, final Object[] explicitArgs) { // 创建 BeanWrapperImpl 对象 BeanWrapperImpl bw = new BeanWrapperImpl(); this.beanFactory.initBeanWrapper(bw); Constructor<?> constructorToUse = null; ArgumentsHolder argsHolderToUse = null; Object[] argsToUse = null; // 确定参数值列表(argsToUse) if (explicitArgs != null) { argsToUse = explicitArgs; } else { Object[] argsToResolve = null; synchronized (mbd.constructorArgumentLock) { // 获取已解析的构造方法 constructorToUse = (Constructor<?>) mbd.resolvedConstructorOrFactoryMethod; if (constructorToUse != null && mbd.constructorArgumentsResolved) { // 获取已解析的构造方法参数列表 argsToUse = mbd.resolvedConstructorArguments; if (argsToUse == null) { // 若 argsToUse 为空,则获取未解析的构造方法参数列表 argsToResolve = mbd.preparedConstructorArguments; } } } if (argsToResolve != null) { // 解析参数列表 argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve); } } if (constructorToUse == null) { boolean autowiring = (chosenCtors != null || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); ConstructorArgumentValues resolvedValues = null; int minNrOfArgs; if (explicitArgs != null) { minNrOfArgs = explicitArgs.length; } else { ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues(); resolvedValues = new ConstructorArgumentValues(); /* * 确定构造方法参数数量,比如下面的配置: * <bean id="persion" class="xyz.coolblog.autowire.Person"> * <constructor-arg index="0" value="xiaoming"/> * <constructor-arg index="1" value="1"/> * <constructor-arg index="2" value="man"/> * </bean> * * 此时 minNrOfArgs = maxIndex + 1 = 2 + 1 = 3,除了计算 minNrOfArgs, * 下面的方法还会将 cargs 中的参数数据转存到 resolvedValues 中 */ minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues); } // 获取构造方法列表 Constructor<?>[] candidates = chosenCtors; if (candidates == null) { Class<?> beanClass = mbd.getBeanClass(); try { candidates = (mbd.isNonPublicAccessAllowed() ? beanClass.getDeclaredConstructors() : beanClass.getConstructors()); } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Resolution of declared constructors on bean Class [" + beanClass.getName() + "] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex); } } // 按照构造方法的访问权限级别和参数数量进行排序 AutowireUtils.sortConstructors(candidates); int minTypeDiffWeight = Integer.MAX_VALUE; Set<Constructor<?>> ambiguousConstructors = null; LinkedList<UnsatisfiedDependencyException> causes = null; for (Constructor<?> candidate : candidates) { Class<?>[] paramTypes = candidate.getParameterTypes(); /* * 下面的 if 分支的用途是:若匹配到到合适的构造方法了,提前结束 for 循环 * constructorToUse != null 这个条件比较好理解,下面分析一下条件 argsToUse.length > paramTypes.length: * 前面说到 AutowireUtils.sortConstructors(candidates) 用于对构造方法进行 * 排序,排序规则如下: * 1. 具有 public 访问权限的构造方法排在非 public 构造方法前 * 2. 参数数量多的构造方法排在前面 * * 假设现在有一组构造方法按照上面的排序规则进行排序,排序结果如下(省略参数名称): * * 1. public Hello(Object, Object, Object) * 2. public Hello(Object, Object) * 3. public Hello(Object) * 4. protected Hello(Integer, Object, Object, Object) * 5. protected Hello(Integer, Object, Object) * 6. protected Hello(Integer, Object) * * argsToUse = [num1, obj2],可以匹配上的构造方法2和构造方法6。由于构造方法2有 * 更高的访问权限,所以没理由不选他(尽管后者在参数类型上更加匹配)。由于构造方法3 * 参数数量 < argsToUse.length,参数数量上不匹配,也不应该选。所以 * argsToUse.length > paramTypes.length 这个条件用途是:在条件 * constructorToUse != null 成立的情况下,通过判断参数数量与参数值数量 * (argsToUse.length)是否一致,来决定是否提前终止构造方法匹配逻辑。 */ if (constructorToUse != null && argsToUse.length > paramTypes.length) { break; } /* * 构造方法参数数量低于配置的参数数量,则忽略当前构造方法,并重试。比如 * argsToUse = [obj1, obj2, obj3, obj4],上面的构造方法列表中, * 构造方法1、2和3显然不是合适选择,忽略之。 */ if (paramTypes.length < minNrOfArgs) { continue; } ArgumentsHolder argsHolder; if (resolvedValues != null) { try { /* * 判断否则方法是否有 ConstructorProperties 注解,若有,则取注解中的 * 值。比如下面的代码: * * public class Persion { * private String name; * private Integer age; * * @ConstructorProperties(value = {"coolblog", "20"}) * public Persion(String name, Integer age) { * this.name = name; * this.age = age; * } * } */ String[] paramNames = ConstructorPropertiesChecker.evaluate(candidate, paramTypes.length); if (paramNames == null) { ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer(); if (pnd != null) { /* * 获取构造方法参数名称列表,比如有这样一个构造方法: * public Person(String name, int age, String sex) * * 调用 getParameterNames 方法返回 paramNames = [name, age, sex] */ paramNames = pnd.getParameterNames(candidate); } } /* * 创建参数值列表,返回 argsHolder 会包含进行类型转换后的参数值,比如下 * 面的配置: * * <bean id="persion" class="xyz.coolblog.autowire.Person"> * <constructor-arg name="name" value="xiaoming"/> * <constructor-arg name="age" value="1"/> * <constructor-arg name="sex" value="man"/> * </bean> * * Person 的成员变量 age 是 Integer 类型的,但由于在 Spring 配置中 * 只能配成 String 类型,所以这里要进行类型转换。 */ argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames, getUserDeclaredConstructor(candidate), autowiring); } catch (UnsatisfiedDependencyException ex) { if (this.beanFactory.logger.isTraceEnabled()) { this.beanFactory.logger.trace( "Ignoring constructor [" + candidate + "] of bean '" + beanName + "': " + ex); } if (causes == null) { causes = new LinkedList<UnsatisfiedDependencyException>(); } causes.add(ex); continue; } } else { if (paramTypes.length != explicitArgs.length) { continue; } argsHolder = new ArgumentsHolder(explicitArgs); } /* * 计算参数值(argsHolder.arguments)每个参数类型与构造方法参数列表 * (paramTypes)中参数的类型差异量,差异量越大表明参数类型差异越大。参数类型差异 * 越大,表明当前构造方法并不是一个最合适的候选项。引入差异量(typeDiffWeight) * 变量目的:是将候选构造方法的参数列表类型与参数值列表类型的差异进行量化,通过量化 * 后的数值筛选出最合适的构造方法。 * * 讲完差异量,再来说说 mbd.isLenientConstructorResolution() 条件。 * 官方的解释是:返回构造方法的解析模式,有宽松模式(lenient mode)和严格模式 * (strict mode)两种类型可选。具体的细节没去研究,就不多说了。 */ int typeDiffWeight = (mbd.isLenientConstructorResolution() ? argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes)); if (typeDiffWeight < minTypeDiffWeight) { constructorToUse = candidate; argsHolderToUse = argsHolder; argsToUse = argsHolder.arguments; minTypeDiffWeight = typeDiffWeight; ambiguousConstructors = null; } /* * 如果两个构造方法与参数值类型列表之间的差异量一致,那么这两个方法都可以作为 * 候选项,这个时候就出现歧义了,这里先把有歧义的构造方法放入 * ambiguousConstructors 集合中 */ else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) { if (ambiguousConstructors == null) { ambiguousConstructors = new LinkedHashSet<Constructor<?>>(); ambiguousConstructors.add(constructorToUse); } ambiguousConstructors.add(candidate); } } // 若上面未能筛选出合适的构造方法,这里将抛出 BeanCreationException 异常 if (constructorToUse == null) { if (causes != null) { UnsatisfiedDependencyException ex = causes.removeLast(); for (Exception cause : causes) { this.beanFactory.onSuppressedException(cause); } throw ex; } throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Could not resolve matching constructor " + "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)"); } /* * 如果 constructorToUse != null,且 ambiguousConstructors 也不为空,表明解析 * 出了多个的合适的构造方法,此时就出现歧义了。Spring 不会擅自决定使用哪个构造方法, * 所以抛出异常。 */ else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Ambiguous constructor matches found in bean '" + beanName + "' " + "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities): " + ambiguousConstructors); } if (explicitArgs == null) { /* * 缓存相关信息,比如: * 1. 已解析出的构造方法对象 resolvedConstructorOrFactoryMethod * 2. 构造方法参数列表是否已解析标志 constructorArgumentsResolved * 3. 参数值列表 resolvedConstructorArguments 或 preparedConstructorArguments * * 这些信息可用在其他地方,用于进行快捷判断 */ argsHolderToUse.storeCache(mbd, constructorToUse); } } try { Object beanInstance; if (System.getSecurityManager() != null) { final Constructor<?> ctorToUse = constructorToUse; final Object[] argumentsToUse = argsToUse; beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { return beanFactory.getInstantiationStrategy().instantiate( mbd, beanName, beanFactory, ctorToUse, argumentsToUse); } }, beanFactory.getAccessControlContext()); } else { /* * 调用实例化策略创建实例,默认情况下使用反射创建实例。如果 bean 的配置信息中 * 包含 lookup-method 和 replace-method,则通过 CGLIB 增强 bean 实例 */ beanInstance = this.beanFactory.getInstantiationStrategy().instantiate( mbd, beanName, this.beanFactory, constructorToUse, argsToUse); } // 设置 beanInstance 到 BeanWrapperImpl 对象中 bw.setBeanInstance(beanInstance); return bw; } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Bean instantiation via constructor failed", ex); } } 上面的方法逻辑比较复杂,做了不少事情,该方法的核心逻辑是根据参数值类型筛选合适的构造方法。解析出合适的构造方法后,剩下的工作就是构建 bean 对象了,这个工作交给了实例化策略去做。下面罗列一下这个方法的工作流程吧: 创建 BeanWrapperImpl 对象 解析构造方法参数,并算出 minNrOfArgs 获取构造方法列表,并排序 遍历排序好的构造方法列表,筛选合适的构造方法 获取构造方法参数列表中每个参数的名称 再次解析参数,此次解析会将 计算构造方法参数列表与参数值列表之间的类型差异量,以筛选出更为合适的构造方法 缓存已筛选出的构造方法以及参数值列表,若再次创建 bean 实例时,可直接使用,无需再次进行筛选 使用初始化策略创建 bean 对象 将 bean 对象放入 BeanWrapperImpl 对象中,并返回该对象 由上面的流程可以看得出,通过构造方法自动注入的方式构造 bean 对象的过程还是很复杂的。为了看懂这个流程,我进行了多次调试,算是勉强弄懂大致逻辑。由于时间有限,我并未能详细分析 autowireConstructor 方法及其所调用的一些方法,比如 resolveConstructorArguments、 autowireConstructor 等。关于这些方法,这里只写了个大概,有兴趣的朋友自己去探索吧。 2.3 通过默认构造方法创建 bean 对象 看完了上面冗长的逻辑,本节来看点轻松的吧 - 通过默认构造方法创建 bean 对象。如下: protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { try { Object beanInstance; final BeanFactory parent = this; // if 条件分支里的一大坨是 Java 安全相关的代码,可以忽略,直接看 else 分支 if (System.getSecurityManager() != null) { beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { return getInstantiationStrategy().instantiate(mbd, beanName, parent); } }, getAccessControlContext()); } else { /* * 调用实例化策略创建实例,默认情况下使用反射创建对象。如果 bean 的配置信息中 * 包含 lookup-method 和 replace-method,则通过 CGLIB 创建 bean 对象 */ beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); } // 创建 BeanWrapperImpl 对象 BeanWrapper bw = new BeanWrapperImpl(beanInstance); initBeanWrapper(bw); return bw; } catch (Throwable ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex); } } public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) { // 检测 bean 配置中是否配置了 lookup-method 或 replace-method,若配置了,则需使用 CGLIB 构建 bean 对象 if (bd.getMethodOverrides().isEmpty()) { Constructor<?> constructorToUse; synchronized (bd.constructorArgumentLock) { constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod; if (constructorToUse == null) { final Class<?> clazz = bd.getBeanClass(); if (clazz.isInterface()) { throw new BeanInstantiationException(clazz, "Specified class is an interface"); } try { if (System.getSecurityManager() != null) { constructorToUse = AccessController.doPrivileged(new PrivilegedExceptionAction<Constructor<?>>() { @Override public Constructor<?> run() throws Exception { return clazz.getDeclaredConstructor((Class[]) null); } }); } else { // 获取默认构造方法 constructorToUse = clazz.getDeclaredConstructor((Class[]) null); } // 设置 resolvedConstructorOrFactoryMethod bd.resolvedConstructorOrFactoryMethod = constructorToUse; } catch (Throwable ex) { throw new BeanInstantiationException(clazz, "No default constructor found", ex); } } } // 通过无参构造方法创建 bean 对象 return BeanUtils.instantiateClass(constructorToUse); } else { // 使用 GCLIG 创建 bean 对象 return instantiateWithMethodInjection(bd, beanName, owner); } } 上面就是通过默认构造方法创建 bean 对象的过程,比较简单,就不多说了。最后我们再来看看简单看看通过无参构造方法刚创建 bean 对象的代码(通过 CGLIB 创建 bean 对象的方式就不看了)是怎样的,如下: public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException { Assert.notNull(ctor, "Constructor must not be null"); try { // 设置构造方法为可访问 ReflectionUtils.makeAccessible(ctor); // 通过反射创建 bean 实例,这里的 args 是一个没有元素的空数组 return ctor.newInstance(args); } catch (InstantiationException ex) { throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex); } catch (IllegalAccessException ex) { throw new BeanInstantiationException(ctor, "Is the constructor accessible?", ex); } catch (IllegalArgumentException ex) { throw new BeanInstantiationException(ctor, "Illegal arguments for constructor", ex); } catch (InvocationTargetException ex) { throw new BeanInstantiationException(ctor, "Constructor threw exception", ex.getTargetException()); } } 到这里,终于看到了创建 bean 对象的代码了。在经历层层调用后,我们总算是追到了调用栈的最深处。看到这里,大家可以休息一下了,本文也差不多要结束了。好了,最后再容我多啰嗦一会,往下看。 3.写在最后 写到这里,我也算是松了一口气,终于快写完了。这篇文章写起来感觉挺不容易的,原因是 createBeanInstance 及其调用的方法是在太多了,而且很多方法逻辑还是比较复杂的,尤其是 autowireConstructor 中调用的一些方法。autowireConstructor 中调用的方法我基本上都看了一遍,但并非全部都弄懂了,有些方法只是知道个大概。所以,这篇文章写的我挺纠结的,生怕有些地方分析的不对。由于我后续还有很多东西要看,以至于我暂时没法抽出大量的时间去详细阅读 Spring 的源码。所以如果上面的分析有不对的地方,欢迎指正,我会虚心听之。如果这些不对的地方给你造成了困扰,实在很抱歉,抱歉。 好了,本篇文章先到这里。谢谢阅读! 参考 《Spring 源码深度解析》- 郝佳 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑。对于已实例化好的单例 bean,getBean(String) 方法并不会再一次去创建,而是从缓存中获取。如果某个 bean 还未实例化,这个时候就无法命中缓存。此时,就要根据 bean 的配置信息去创建这个 bean 了。相较于getBean(String)方法的实现逻辑,创建 bean 的方法createBean(String, RootBeanDefinition, Object[])及其所调用的方法逻辑上更为复杂一些。关于创建 bean 实例的过程,我将会分几篇文章进行分析。本篇文章会先从大体上分析 createBean(String, RootBeanDefinition, Object[])方法的代码逻辑,至于其所调用的方法将会在随后的文章中进行分析。 好了,其他的不多说,直接进入正题吧。 2. 源码分析 2.1 创建 bean 实例的入口 在正式分析createBean(String, RootBeanDefinition, Object[])方法前,我们先来看看 createBean 方法是在哪里被调用的。如下: public T doGetBean(...) { // 省略不相关代码 if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } // 省略不相关代码 } 上面是 doGetBean 方法的代码片段,从中可以发现 createBean 方法。createBean 方法被匿名工厂类的 getObject 方法包裹,但这个匿名工厂类对象并未直接调用 getObject 方法。而是将自身作为参数传给了getSingleton(String, ObjectFactory)方法,那么我们接下来再去看看一下getSingleton(String, ObjectFactory) 方法的实现。如下: public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(beanName, "'beanName' must not be null"); synchronized (this.singletonObjects) { // 从缓存中获取单例 bean,若不为空,则直接返回,不用再初始化 Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { if (this.singletonsCurrentlyInDestruction) { throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction " + "(Do not request a bean from a BeanFactory in a destroy method implementation!)"); } if (logger.isDebugEnabled()) { logger.debug("Creating shared instance of singleton bean '" + beanName + "'"); } /* * 将 beanName 添加到 singletonsCurrentlyInCreation 集合中, * 用于表明 beanName 对应的 bean 正在创建中 */ beforeSingletonCreation(beanName); boolean newSingleton = false; boolean recordSuppressedExceptions = (this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = new LinkedHashSet<Exception>(); } try { // 通过 getObject 方法调用 createBean 方法创建 bean 实例 singletonObject = singletonFactory.getObject(); newSingleton = true; } catch (IllegalStateException ex) { singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { throw ex; } } catch (BeanCreationException ex) { if (recordSuppressedExceptions) { for (Exception suppressedException : this.suppressedExceptions) { ex.addRelatedCause(suppressedException); } } throw ex; } finally { if (recordSuppressedExceptions) { this.suppressedExceptions = null; } // 将 beanName 从 singletonsCurrentlyInCreation 移除 afterSingletonCreation(beanName); } if (newSingleton) { /* * 将 <beanName, singletonObject> 键值对添加到 singletonObjects 集合中, * 并从其他集合(比如 earlySingletonObjects)中移除 singletonObject 记录 */ addSingleton(beanName, singletonObject); } } return (singletonObject != NULL_OBJECT ? singletonObject : null); } } 上面的方法逻辑不是很复杂,这里简单总结一下。如下: 先从 singletonObjects 集合获取 bean 实例,若不为空,则直接返回 若为空,进入创建 bean 实例阶段。先将 beanName 添加到 singletonsCurrentlyInCreation 通过 getObject 方法调用 createBean 方法创建 bean 实例 将 beanName 从 singletonsCurrentlyInCreation 集合中移除 将 从上面的分析中,我们知道了 createBean 方法在何处被调用的。那么接下来我们一起深入 createBean 方法的源码中,来看看这个方法具体都做了什么事情。 2.2 createBean 方法全貌 createBean 和 getBean 方法类似,基本上都是空壳方法,只不过 createBean 的逻辑稍微多点,多做了一些事情。下面我们一起看看这个方法的实现逻辑,如下: protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException { if (logger.isDebugEnabled()) { logger.debug("Creating instance of bean '" + beanName + "'"); } RootBeanDefinition mbdToUse = mbd; // 解析 bean 的类型 Class<?> resolvedClass = resolveBeanClass(mbd, beanName); if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { mbdToUse = new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); } try { // 处理 lookup-method 和 replace-method 配置,Spring 将这两个配置统称为 override method mbdToUse.prepareMethodOverrides(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), beanName, "Validation of method overrides failed", ex); } try { // 在 bean 初始化前应用后置处理,如果后置处理返回的 bean 不为空,则直接返回 Object bean = resolveBeforeInstantiation(beanName, mbdToUse); if (bean != null) { return bean; } } catch (Throwable ex) { throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, "BeanPostProcessor before instantiation of bean failed", ex); } // 调用 doCreateBean 创建 bean Object beanInstance = doCreateBean(beanName, mbdToUse, args); if (logger.isDebugEnabled()) { logger.debug("Finished creating instance of bean '" + beanName + "'"); } return beanInstance; } 上面的代码不长,代码的执行流程比较容易看出,这里罗列一下: 解析 bean 类型 处理 lookup-method 和 replace-method 配置 在 bean 初始化前应用后置处理,若后置处理返回的 bean 不为空,则直接返回 若上一步后置处理返回的 bean 为空,则调用 doCreateBean 创建 bean 实例 下面我会分节对第2、3和4步的流程进行分析,步骤1的详细实现大家有兴趣的话,就自己去看看吧。 2.2.1 验证和准备 override 方法 当用户配置了 lookup-method 和 replace-method 时,Spring 需要对目标 bean 进行增强。在增强之前,需要做一些准备工作,也就是 prepareMethodOverrides 中的逻辑。下面来看看这个方法的源码: public void prepareMethodOverrides() throws BeanDefinitionValidationException { MethodOverrides methodOverrides = getMethodOverrides(); if (!methodOverrides.isEmpty()) { Set<MethodOverride> overrides = methodOverrides.getOverrides(); synchronized (overrides) { // 循环处理每个 MethodOverride 对象 for (MethodOverride mo : overrides) { prepareMethodOverride(mo); } } } } protected void prepareMethodOverride(MethodOverride mo) throws BeanDefinitionValidationException { // 获取方法名为 mo.getMethodName() 的方法数量,当方法重载时,count 的值就会大于1 int count = ClassUtils.getMethodCountForName(getBeanClass(), mo.getMethodName()); // count = 0,表明根据方法名未找到相应的方法,此时抛出异常 if (count == 0) { throw new BeanDefinitionValidationException( "Invalid method override: no method with name '" + mo.getMethodName() + "' on class [" + getBeanClassName() + "]"); } // 若 count = 1,表明仅存在已方法名为 mo.getMethodName(),这意味着方法不存在重载 else if (count == 1) { // 方法不存在重载,则将 overloaded 成员变量设为 false mo.setOverloaded(false); } } 上面的源码中,prepareMethodOverrides方法循环调用了prepareMethodOverride方法,并没其他的太多逻辑。主要准备工作都是在 prepareMethodOverride 方法中进行的,所以我们重点关注一下这个方法。prepareMethodOverride 这个方法主要用于获取指定方法的方法数量 count,并根据 count 的值进行相应的处理。count = 0 时,表明方法不存在,此时抛出异常。count = 1 时,设置 MethodOverride 对象的overloaded成员变量为 false。这样做的目的在于,提前标注名称mo.getMethodName()的方法不存在重载,在使用 CGLIB 增强阶段就不需要进行校验,直接找到某个方法进行增强即可。 上面的方法没太多的逻辑,比较简单,就先分析到这里。 2.2.2 bean 实例化前的后置处理 后置处理是 Spring 的一个拓展点,用户通过实现 BeanPostProcessor 接口,并将实现类配置到 Spring 的配置文件中(或者使用注解),即可在 bean 初始化前后进行自定义操作。关于后置处理较为详细的说明,可以参考我的了一篇文章Spring IOC 容器源码分析系列文章导读,这里就不赘述了。下面我们来看看 createBean 方法中的后置处理逻辑,如下: protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { Object bean = null; // 检测是否解析过,mbd.beforeInstantiationResolved 的值在下面的代码中会被设置 if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) { if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { Class<?> targetType = determineTargetType(beanName, mbd); if (targetType != null) { // 应用前置处理 bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName); if (bean != null) { // 应用后置处理 bean = applyBeanPostProcessorsAfterInitialization(bean, beanName); } } } // 设置 mbd.beforeInstantiationResolved mbd.beforeInstantiationResolved = (bean != null); } return bean; } protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) { for (BeanPostProcessor bp : getBeanPostProcessors()) { // InstantiationAwareBeanPostProcessor 一般在 Spring 框架内部使用,不建议用户直接使用 if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; // bean 初始化前置处理 Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName); if (result != null) { return result; } } } return null; } public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException { Object result = existingBean; for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { // bean 初始化后置处理 result = beanProcessor.postProcessAfterInitialization(result, beanName); if (result == null) { return result; } } return result; } 在 resolveBeforeInstantiation 方法中,当前置处理方法返回的 bean 不为空时,后置处理才会被执行。前置处理器是 InstantiationAwareBeanPostProcessor 类型的,该种类型的处理器一般用在 Spring 框架内部,比如 AOP 模块中的AbstractAutoProxyCreator抽象类间接实现了这个接口中的 postProcessBeforeInstantiation方法,所以 AOP 可以在这个方法中生成为目标类的代理对象。不过我在调试的过程中,发现 AOP 在此处生成代理对象是有条件的。一般情况下条件都不成立,也就不会在此处生成代理对象。至于这个条件为什么不成立,因 AOP 这一块的源码我还没来得及看,所以暂时还无法解答。等我看过 AOP 模块的源码后,我再来尝试分析这个条件。 2.2.3 调用 doCreateBean 方法创建 bean 这一节,我们来分析一下doCreateBean方法的源码。在 Spring 中,做事情的方法基本上都是以do开头的,doCreateBean 也不例外。那下面我们就来看看这个方法都做了哪些事情。 protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException { /* * BeanWrapper 是一个基础接口,由接口名可看出这个接口的实现类用于包裹 bean 实例。 * 通过 BeanWrapper 的实现类可以方便的设置/获取 bean 实例的属性 */ BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { // 从缓存中获取 BeanWrapper,并清理相关记录 instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper == null) { /* * 创建 bean 实例,并将实例包裹在 BeanWrapper 实现类对象中返回。createBeanInstance * 中包含三种创建 bean 实例的方式: * 1. 通过工厂方法创建 bean 实例 * 2. 通过构造方法自动注入(autowire by constructor)的方式创建 bean 实例 * 3. 通过无参构造方法方法创建 bean 实例 * * 若 bean 的配置信息中配置了 lookup-method 和 replace-method,则会使用 CGLIB * 增强 bean 实例。关于这个方法,后面会专门写一篇文章介绍,这里先说这么多。 */ instanceWrapper = createBeanInstance(beanName, mbd, args); } // 此处的 bean 可以认为是一个原始的 bean 实例,暂未填充属性 final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null); Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null); mbd.resolvedTargetType = beanType; // 这里又遇到后置处理了,此处的后置处理是用于处理已“合并的 BeanDefinition”。关于这种后置处理器具体的实现细节就不深入理解了,大家有兴趣可以自己去看 synchronized (mbd.postProcessingLock) { if (!mbd.postProcessed) { try { applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Post-processing of merged bean definition failed", ex); } mbd.postProcessed = true; } } /* * earlySingletonExposure 是一个重要的变量,这里要说明一下。该变量用于表示是否提前暴露 * 单例 bean,用于解决循环依赖。earlySingletonExposure 由三个条件综合而成,如下: * 条件1:mbd.isSingleton() - 表示 bean 是否是单例类型 * 条件2:allowCircularReferences - 是否允许循环依赖 * 条件3:isSingletonCurrentlyInCreation(beanName) - 当前 bean 是否处于创建的状态中 * * earlySingletonExposure = 条件1 && 条件2 && 条件3 * = 单例 && 是否允许循环依赖 && 是否存于创建状态中。 */ boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { if (logger.isDebugEnabled()) { logger.debug("Eagerly caching bean '" + beanName + "' to allow for resolving potential circular references"); } // 添加工厂对象到 singletonFactories 缓存中 addSingletonFactory(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { // 获取早期 bean 的引用,如果 bean 中的方法被 AOP 切点所匹配到,此时 AOP 相关逻辑会介入 return getEarlyBeanReference(beanName, mbd, bean); } }); } Object exposedObject = bean; try { // 向 bean 实例中填充属性,populateBean 方法也是一个很重要的方法,后面会专门写文章分析 populateBean(beanName, mbd, instanceWrapper); if (exposedObject != null) { /* * 进行余下的初始化工作,详细如下: * 1. 判断 bean 是否实现了 BeanNameAware、BeanFactoryAware、 * BeanClassLoaderAware 等接口,并执行接口方法 * 2. 应用 bean 初始化前置操作 * 3. 如果 bean 实现了 InitializingBean 接口,则执行 afterPropertiesSet * 方法。如果用户配置了 init-method,则调用相关方法执行自定义初始化逻辑 * 4. 应用 bean 初始化后置操作 * * 另外,AOP 相关逻辑也会在该方法中织入切面逻辑,此时的 exposedObject 就变成了 * 一个代理对象了 */ exposedObject = initializeBean(beanName, exposedObject, mbd); } } catch (Throwable ex) { if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { throw (BeanCreationException) ex; } else { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); } } if (earlySingletonExposure) { Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { // 若 initializeBean 方法未改变 exposedObject 的引用,则此处的条件为 true。 if (exposedObject == bean) { exposedObject = earlySingletonReference; } // 下面的逻辑我也没完全搞懂,就不分析了。见谅。 else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length); for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } if (!actualDependentBeans.isEmpty()) { throw new BeanCurrentlyInCreationException(beanName, "Bean with name '" + beanName + "' has been injected into other beans [" + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + "] in its raw version as part of a circular reference, but has eventually been " + "wrapped. This means that said other beans do not use the final version of the " + "bean. This is often the result of over-eager type matching - consider using " + "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."); } } } } try { // 注册销毁逻辑 registerDisposableBeanIfNecessary(beanName, bean, mbd); } catch (BeanDefinitionValidationException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); } return exposedObject; } 上面的注释比较多,分析的应该比较详细的。不过有一部分代码我暂时没看懂,就不分析了,见谅。下面我们来总结一下 doCreateBean 方法的执行流程吧,如下: 从缓存中获取 BeanWrapper 实现类对象,并清理相关记录 若未命中缓存,则创建 bean 实例,并将实例包裹在 BeanWrapper 实现类对象中返回 应用 MergedBeanDefinitionPostProcessor 后置处理器相关逻辑 根据条件决定是否提前暴露 bean 的早期引用(early reference),用于处理循环依赖问题 调用 populateBean 方法向 bean 实例中填充属性 调用 initializeBean 方法完成余下的初始化工作 注册销毁逻辑 doCreateBean 方法的流程比较复杂,步骤略多。由此也可了解到创建一个 bean 还是很复杂的,这中间要做的事情繁多。比如填充属性、对 BeanPostProcessor 拓展点提供支持等。以上的步骤对应的方法具体是怎样实现的,本篇文章并不打算展开分析。在后续的文章中,我会单独写文章分析几个逻辑比较复杂的步骤。有兴趣的阅读的朋友可以稍微等待一下,相关文章本周会陆续进行更新。 3. 总结 到这里,createBean 方法及其所调用的方法的源码就分析完了。总的来说,createBean 方法还是比较复杂的,需要多看几遍才能理清一些头绪。由于 createBean 方法比较复杂,对于以上的源码分析,我并不能保证不出错。如果有写错的地方,还请大家指点迷津。毕竟当局者迷,作为作者,我很难意识到哪里写的有问题。 好了,本篇文章到此结束。谢谢阅读。 参考 《Spring 源码深度解析》- 郝佳 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 为了写 Spring IOC 容器源码分析系列的文章,我特地写了一篇 Spring IOC 容器的导读文章。在导读一文中,我介绍了 Spring 的一些特性以及阅读 Spring 源码的一些建议。在做完必要的准备工作后,从本文开始,正式开始进入源码分析的阶段。 在本篇文章中,我将会详细分析BeanFactory的getBean(String)方法实现细节,getBean(String) 及所调用的方法总体来说实现上较为复杂,代码长度比较长。作为源码分析文章,本文的文章长度也会比较长,希望大家耐心读下去。 好了,其他的不多说了,进入主题环节吧。 2. 源码分析 简单说一下本章的内容安排吧,在本章的开始,也就是2.1节,我将会分析getBean(String)方法整体的实现逻辑。但不会分析它所调用的方法,这些方法将会在后续几节中依次进行分析。那接下来,我们就先来看看 getBean(String) 方法是如何实现的吧。 2.1 俯瞰 getBean(String) 源码 在本小节,我们先从战略上俯瞰 getBean(String) 方法的实现源码。代码如下: public Object getBean(String name) throws BeansException { // getBean 是一个空壳方法,所有的逻辑都封装在 doGetBean 方法中 return doGetBean(name, null, null, false); } protected <T> T doGetBean( final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException { /* * 通过 name 获取 beanName。这里不使用 name 直接作为 beanName 有两点原因: * 1. name 可能会以 & 字符开头,表明调用者想获取 FactoryBean 本身,而非 FactoryBean * 实现类所创建的 bean。在 BeanFactory 中,FactoryBean 的实现类和其他的 bean 存储 * 方式是一致的,即 <beanName, bean>,beanName 中是没有 & 这个字符的。所以我们需要 * 将 name 的首字符 & 移除,这样才能从缓存里取到 FactoryBean 实例。 * 2. 若 name 是一个别名,则应将别名转换为具体的实例名,也就是 beanName。 */ final String beanName = transformedBeanName(name); Object bean; /* * 从缓存中获取单例 bean。Spring 是使用 Map 作为 beanName 和 bean 实例的缓存的,所以这 * 里暂时可以把 getSingleton(beanName) 等价于 beanMap.get(beanName)。当然,实际的 * 逻辑并非如此简单,后面再细说。 */ Object sharedInstance = getSingleton(beanName); /* * 如果 sharedInstance = null,则说明缓存里没有对应的实例,表明这个实例还没创建。 * BeanFactory 并不会在一开始就将所有的单例 bean 实例化好,而是在调用 getBean 获取 * bean 时再实例化,也就是懒加载。 * getBean 方法有很多重载,比如 getBean(String name, Object... args),我们在首次获取 * 某个 bean 时,可以传入用于初始化 bean 的参数数组(args),BeanFactory 会根据这些参数 * 去匹配合适的构造方法构造 bean 实例。当然,如果单例 bean 早已创建好,这里的 args 就没有 * 用了,BeanFactory 不会多次实例化单例 bean。 */ if (sharedInstance != null && args == null) { if (logger.isDebugEnabled()) { if (isSingletonCurrentlyInCreation(beanName)) { logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference"); } else { logger.debug("Returning cached instance of singleton bean '" + beanName + "'"); } } /* * 如果 sharedInstance 是普通的单例 bean,下面的方法会直接返回。但如果 * sharedInstance 是 FactoryBean 类型的,则需调用 getObject 工厂方法获取真正的 * bean 实例。如果用户想获取 FactoryBean 本身,这里也不会做特别的处理,直接返回 * 即可。毕竟 FactoryBean 的实现类本身也是一种 bean,只不过具有一点特殊的功能而已。 */ bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); } /* * 如果上面的条件不满足,则表明 sharedInstance 可能为空,此时 beanName 对应的 bean * 实例可能还未创建。这里还存在另一种可能,如果当前容器有父容器,beanName 对应的 bean 实例 * 可能是在父容器中被创建了,所以在创建实例前,需要先去父容器里检查一下。 */ else { // BeanFactory 不缓存 Prototype 类型的 bean,无法处理该类型 bean 的循环依赖问题 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } // 如果 sharedInstance = null,则到父容器中查找 bean 实例 BeanFactory parentBeanFactory = getParentBeanFactory(); if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { // 获取 name 对应的 beanName,如果 name 是以 & 字符开头,则返回 & + beanName String nameToLookup = originalBeanName(name); // 根据 args 是否为空,以决定调用父容器哪个方法获取 bean if (args != null) { return (T) parentBeanFactory.getBean(nameToLookup, args); } else { return parentBeanFactory.getBean(nameToLookup, requiredType); } } if (!typeCheckOnly) { markBeanAsCreated(beanName); } try { // 合并父 BeanDefinition 与子 BeanDefinition,后面会单独分析这个方法 final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); checkMergedBeanDefinition(mbd, beanName, args); // 检查是否有 dependsOn 依赖,如果有则先初始化所依赖的 bean String[] dependsOn = mbd.getDependsOn(); if (dependsOn != null) { for (String dep : dependsOn) { /* * 检测是否存在 depends-on 循环依赖,若存在则抛异常。比如 A 依赖 B, * B 又依赖 A,他们的配置如下: * <bean id="beanA" class="BeanA" depends-on="beanB"> * <bean id="beanB" class="BeanB" depends-on="beanA"> * * beanA 要求 beanB 在其之前被创建,但 beanB 又要求 beanA 先于它 * 创建。这个时候形成了循环,对于 depends-on 循环,Spring 会直接 * 抛出异常 */ if (isDependent(beanName, dep)) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); } // 注册依赖记录 registerDependentBean(dep, beanName); try { // 加载 depends-on 依赖 getBean(dep); } catch (NoSuchBeanDefinitionException ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", ex); } } } // 创建 bean 实例 if (mbd.isSingleton()) { /* * 这里并没有直接调用 createBean 方法创建 bean 实例,而是通过 * getSingleton(String, ObjectFactory) 方法获取 bean 实例。 * getSingleton(String, ObjectFactory) 方法会在内部调用 * ObjectFactory 的 getObject() 方法创建 bean,并会在创建完成后, * 将 bean 放入缓存中。关于 getSingleton 方法的分析,本文先不展开,我会在 * 后面的文章中进行分析 */ sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { try { // 创建 bean 实例 return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } } }); // 如果 bean 是 FactoryBean 类型,则调用工厂方法获取真正的 bean 实例。否则直接返回 bean 实例 bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } // 创建 prototype 类型的 bean 实例 else if (mbd.isPrototype()) { Object prototypeInstance = null; try { beforePrototypeCreation(beanName); prototypeInstance = createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } // 创建其他类型的 bean 实例 else { String scopeName = mbd.getScope(); final Scope scope = this.scopes.get(scopeName); if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); } try { Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } } }); bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } catch (IllegalStateException ex) { throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider " + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex); } } } catch (BeansException ex) { cleanupAfterBeanCreationFailure(beanName); throw ex; } } // 如果需要进行类型转换,则在此处进行转换。类型转换这一块我没细看,就不多说了。 if (requiredType != null && bean != null && !requiredType.isInstance(bean)) { try { return getTypeConverter().convertIfNecessary(bean, requiredType); } catch (TypeMismatchException ex) { if (logger.isDebugEnabled()) { logger.debug("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", ex); } throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } } // 返回 bean return (T) bean; } 以上就是getBean(String)和doGetBean(String, Class, Object[], boolean)两个方法的分析。代码很长,需要一点耐心阅读。为了凸显方法的主逻辑,大家可以对代码进行一定的删减,删除一些日志和异常代码,也可以删除一些不是很重要的逻辑。另外由于 doGetBean 方法调用了其他的很多方法,在看代码时,经常会忘掉 doGetBean 所调用的方法是怎么实现的。比如 getSingleton 方法出现了两次,但两个方法并不同,在看第二个的 getSingleton 方法时,可能会忘掉第一个 getSingleton 是怎么实现的。另外,如果你想对比两个重载方法的异同,在 IDEA 里跳来跳去也是很不方便。为此,我使用了 sublime 进行分屏,左屏是删减后的 doGetBean 方法,右屏是 doGetBean 调用的一些方法,这样看起来会方便一点。忘了某个方法的实现逻辑后,可以到右屏查看,也可进行对比。分屏效果如下: 这里我为了演示,删除了不少东西。大家可以按需进行删减,并配上注释,辅助理解。 看完了源码,下面我来简单总结一下 doGetBean 的执行流程。如下: 转换 beanName 从缓存中获取实例 如果实例不为空,且 args = null。调用 getObjectForBeanInstance 方法,并按 name 规则返回相应的 bean 实例 若上面的条件不成立,则到父容器中查找 beanName 对有的 bean 实例,存在则直接返回 若父容器中不存在,则进行下一步操作 -- 合并 BeanDefinition 处理 depends-on 依赖 创建并缓存 bean 调用 getObjectForBeanInstance 方法,并按 name 规则返回相应的 bean 实例 按需转换 bean 类型,并返回转换后的 bean 实例。 以上步骤对应的流程图如下: 2.2 beanName 转换 在获取 bean 实例之前,Spring 第一件要做的事情是对参数 name 进行转换。转换的目的主要是为了解决两个问题,第一个是处理以字符 & 开头的 name,防止 BeanFactory 无法找到与 name 对应的 bean 实例。第二个是处理别名问题,Spring 不会存储 <别名, bean 实例> 这种映射,仅会存储 protected String transformedBeanName(String name) { // 这里调用了两个方法:BeanFactoryUtils.transformedBeanName(name) 和 canonicalName return canonicalName(BeanFactoryUtils.transformedBeanName(name)); } /** 该方法用于处理 & 字符 */ public static String transformedBeanName(String name) { Assert.notNull(name, "'name' must not be null"); String beanName = name; // 循环处理 & 字符。比如 name = "&&&&&helloService",最终会被转成 helloService while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); } return beanName; } /** 该方法用于转换别名 */ public String canonicalName(String name) { String canonicalName = name; String resolvedName; /* * 这里使用 while 循环进行处理,原因是:可能会存在多重别名的问题,即别名指向别名。比如下面 * 的配置: * <bean id="hello" class="service.Hello"/> * <alias name="hello" alias="aliasA"/> * <alias name="aliasA" alias="aliasB"/> * * 上面的别名指向关系为 aliasB -> aliasA -> hello,对于上面的别名配置,aliasMap 中数据 * 视图为:aliasMap = [<aliasB, aliasA>, <aliasA, hello>]。通过下面的循环解析别名 * aliasB 最终指向的 beanName */ do { resolvedName = this.aliasMap.get(canonicalName); if (resolvedName != null) { canonicalName = resolvedName; } } while (resolvedName != null); return canonicalName; } 2.3 从缓存中获取 bean 实例 对于单例 bean,Spring 容器只会实例化一次。后续再次获取时,只需直接从缓存里获取即可,无需且不能再次实例化(否则单例就没意义了)。从缓存中取 bean 实例的方法是getSingleton(String),下面我们就来看看这个方法实现方式吧。如下: public Object getSingleton(String beanName) { return getSingleton(beanName, true); } /** * 这里解释一下 allowEarlyReference 参数,allowEarlyReference 表示是否允许其他 bean 引用 * 正在创建中的 bean,用于处理循环引用的问题。关于循环引用,这里先简单介绍一下。先看下面的配置: * * <bean id="hello" class="xyz.coolblog.service.Hello"> * <property name="world" ref="world"/> * </bean> * <bean id="world" class="xyz.coolblog.service.World"> * <property name="hello" ref="hello"/> * </bean> * * 如上所示,hello 依赖 world,world 又依赖于 hello,他们之间形成了循环依赖。Spring 在构建 * hello 这个 bean 时,会检测到它依赖于 world,于是先去实例化 world。实例化 world 时,发现 * world 依赖 hello。这个时候容器又要去初始化 hello。由于 hello 已经在初始化进程中了,为了让 * world 能完成初始化,这里先让 world 引用正在初始化中的 hello。world 初始化完成后,hello * 就可引用到 world 实例,这样 hello 也就能完成初始了。关于循环依赖,我后面会专门写一篇文章讲 * 解,这里先说这么多。 */ protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 从 singletonObjects 获取实例,singletonObjects 中缓存的实例都是完全实例化好的 bean,可以直接使用 Object singletonObject = this.singletonObjects.get(beanName); /* * 如果 singletonObject = null,表明还没创建,或者还没完全创建好。 * 这里判断 beanName 对应的 bean 是否正在创建中 */ if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 从 earlySingletonObjects 中获取提前曝光的 bean,用于处理循环引用 singletonObject = this.earlySingletonObjects.get(beanName); // 如果如果 singletonObject = null,且允许提前曝光 bean 实例,则从相应的 ObjectFactory 获取一个原始的(raw)bean(尚未填充属性) if (singletonObject == null && allowEarlyReference) { // 获取相应的工厂类 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 提前曝光 bean 实例,用于解决循环依赖 singletonObject = singletonFactory.getObject(); // 放入缓存中,如果还有其他 bean 依赖当前 bean,其他 bean 可以直接从 earlySingletonObjects 取结果 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return (singletonObject != NULL_OBJECT ? singletonObject : null); } 上面的代码虽然不长,但是涉及到了好几个缓存集合。如果不知道这些缓存的用途是什么,上面源码可能就很难弄懂了。这几个缓存集合用的很频繁,在后面的代码中还会出现,所以这里介绍一下。如下: 缓存 用途 singletonObjects 用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用 earlySingletonObjects 用于存放还在初始化中的 bean,用于解决循环依赖 singletonFactories 用于存放 bean 工厂。bean 工厂所产生的 bean 是还未完成初始化的 bean。如代码所示,bean 工厂所生成的对象最终会被缓存到 earlySingletonObjects 中 关于 getSingleton 先说到这里,getSingleton 源码并不多。但涉及到了循环依赖的相关逻辑,如果对这一块不理解可能不知道代码所云。等后面分析循环依赖的时候,我会再次分析这个方法,所以暂时不理解也没关系。 2.4 合并父 BeanDefinition 与子 BeanDefinition Spring 支持配置继承,在 <bean id="hello" class="xyz.coolblog.innerbean.Hello"> <property name="content" value="hello"/> </bean> <bean id="hello-child" parent="hello"> <property name="content" value="I`m hello-child"/> </bean> 如上所示,hello-child 配置继承自 hello。hello-child 未配置 class 属性,这里我们让它继承父配置中的 class 属性。然后我们写点代码测试一下,如下: String configLocation = "application-parent-bean.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation); System.out.println("hello -> " + applicationContext.getBean("hello")); System.out.println("hello-child -> " + applicationContext.getBean("hello-child")); 测试结果如下: 由测试结果可以看出,hello-child 在未配置 class 的属性下也实例化成功了,表明它成功继承了父配置的 class 属性。 看完代码演示,接下来我们来看看源码吧。如下: protected RootBeanDefinition getMergedLocalBeanDefinition(String beanName) throws BeansException { // 检查缓存中是否存在“已合并的 BeanDefinition”,若有直接返回即可 RootBeanDefinition mbd = this.mergedBeanDefinitions.get(beanName); if (mbd != null) { return mbd; } // 调用重载方法 return getMergedBeanDefinition(beanName, getBeanDefinition(beanName)); } protected RootBeanDefinition getMergedBeanDefinition(String beanName, BeanDefinition bd) throws BeanDefinitionStoreException { // 继续调用重载方法 return getMergedBeanDefinition(beanName, bd, null); } protected RootBeanDefinition getMergedBeanDefinition( String beanName, BeanDefinition bd, BeanDefinition containingBd) throws BeanDefinitionStoreException { synchronized (this.mergedBeanDefinitions) { RootBeanDefinition mbd = null; // 我暂时还没去详细了解 containingBd 的用途,尽管从方法的注释上可以知道 containingBd 的大致用途,但没经过详细分析,就不多说了。见谅 if (containingBd == null) { mbd = this.mergedBeanDefinitions.get(beanName); } if (mbd == null) { // bd.getParentName() == null,表明无父配置,这时直接将当前的 BeanDefinition 升级为 RootBeanDefinition if (bd.getParentName() == null) { if (bd instanceof RootBeanDefinition) { mbd = ((RootBeanDefinition) bd).cloneBeanDefinition(); } else { mbd = new RootBeanDefinition(bd); } } else { BeanDefinition pbd; try { String parentBeanName = transformedBeanName(bd.getParentName()); /* * 判断父类 beanName 与子类 beanName 名称是否相同。若相同,则父类 bean 一定 * 在父容器中。原因也很简单,容器底层是用 Map 缓存 <beanName, bean> 键值对 * 的。同一个容器下,使用同一个 beanName 映射两个 bean 实例显然是不合适的。 * 有的朋友可能会觉得可以这样存储:<beanName, [bean1, bean2]> ,似乎解决了 * 一对多的问题。但是也有问题,调用 getName(beanName) 时,到底返回哪个 bean * 实例好呢? */ if (!beanName.equals(parentBeanName)) { /* * 这里再次调用 getMergedBeanDefinition,只不过参数值变为了 * parentBeanName,用于合并父 BeanDefinition 和爷爷辈的 * BeanDefinition。如果爷爷辈的 BeanDefinition 仍有父 * BeanDefinition,则继续合并 */ pbd = getMergedBeanDefinition(parentBeanName); } else { // 获取父容器,并判断,父容器的类型,若不是 ConfigurableBeanFactory 则判抛出异常 BeanFactory parent = getParentBeanFactory(); if (parent instanceof ConfigurableBeanFactory) { pbd = ((ConfigurableBeanFactory) parent).getMergedBeanDefinition(parentBeanName); } else { throw new NoSuchBeanDefinitionException(parentBeanName, "Parent name '" + parentBeanName + "' is equal to bean name '" + beanName + "': cannot be resolved without an AbstractBeanFactory parent"); } } } catch (NoSuchBeanDefinitionException ex) { throw new BeanDefinitionStoreException(bd.getResourceDescription(), beanName, "Could not resolve parent bean definition '" + bd.getParentName() + "'", ex); } // 以父 BeanDefinition 的配置信息为蓝本创建 RootBeanDefinition,也就是“已合并的 BeanDefinition” mbd = new RootBeanDefinition(pbd); // 用子 BeanDefinition 中的属性覆盖父 BeanDefinition 中的属性 mbd.overrideFrom(bd); } // 如果用户未配置 scope 属性,则默认将该属性配置为 singleton if (!StringUtils.hasLength(mbd.getScope())) { mbd.setScope(RootBeanDefinition.SCOPE_SINGLETON); } if (containingBd != null && !containingBd.isSingleton() && mbd.isSingleton()) { mbd.setScope(containingBd.getScope()); } if (containingBd == null && isCacheBeanMetadata()) { // 缓存合并后的 BeanDefinition this.mergedBeanDefinitions.put(beanName, mbd); } } return mbd; } } 上面的源码虽然有点长,但好在逻辑不是很复杂。加上我在源码里进行了比较详细的注解,我想耐心看一下还是可以看懂的,这里就不多说了。 2.5 从 FactoryBean 中获取 bean 实例 在经过前面这么多的步骤处理后,到这里差不多就接近 doGetBean 方法的尾声了。在本节中,我们来看看从 FactoryBean 实现类中获取 bean 实例的过程。关于 FactoryBean 的用法,我在导读那篇文章中已经演示过,这里就不再次说明了。那接下来,我们直入主题吧,相关的源码如下: protected Object getObjectForBeanInstance( Object beanInstance, String name, String beanName, RootBeanDefinition mbd) { // 如果 name 以 & 开头,但 beanInstance 却不是 FactoryBean,则认为有问题。 if (BeanFactoryUtils.isFactoryDereference(name) && !(beanInstance instanceof FactoryBean)) { throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass()); } /* * 如果上面的判断通过了,表明 beanInstance 可能是一个普通的 bean,也可能是一个 * FactoryBean。如果是一个普通的 bean,这里直接返回 beanInstance 即可。如果是 * FactoryBean,则要调用工厂方法生成一个 bean 实例。 */ if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) { return beanInstance; } Object object = null; if (mbd == null) { /* * 如果 mbd 为空,则从缓存中加载 bean。FactoryBean 生成的单例 bean 会被缓存 * 在 factoryBeanObjectCache 集合中,不用每次都创建 */ object = getCachedObjectForFactoryBean(beanName); } if (object == null) { // 经过前面的判断,到这里可以保证 beanInstance 是 FactoryBean 类型的,所以可以进行类型转换 FactoryBean<?> factory = (FactoryBean<?>) beanInstance; // 如果 mbd 为空,则判断是否存在名字为 beanName 的 BeanDefinition if (mbd == null && containsBeanDefinition(beanName)) { // 合并 BeanDefinition mbd = getMergedLocalBeanDefinition(beanName); } // synthetic 字面意思是"合成的"。通过全局查找,我发现在 AOP 相关的类中会将该属性设为 true。 // 所以我觉得该字段可能表示某个 bean 是不是被 AOP 增强过,也就是 AOP 基于原始类合成了一个新的代理类。 // 不过目前只是猜测,没有深究。如果有朋友知道这个字段的具体意义,还望不吝赐教 boolean synthetic = (mbd != null && mbd.isSynthetic()); // 调用 getObjectFromFactoryBean 方法继续获取实例 object = getObjectFromFactoryBean(factory, beanName, !synthetic); } return object; } protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) { /* * FactoryBean 也有单例和非单例之分,针对不同类型的 FactoryBean,这里有两种处理方式: * 1. 单例 FactoryBean 生成的 bean 实例也认为是单例类型。需放入缓存中,供后续重复使用 * 2. 非单例 FactoryBean 生成的 bean 实例则不会被放入缓存中,每次都会创建新的实例 */ if (factory.isSingleton() && containsSingleton(beanName)) { synchronized (getSingletonMutex()) { // 从缓存中取 bean 实例,避免多次创建 bean 实例 Object object = this.factoryBeanObjectCache.get(beanName); if (object == null) { // 使用工厂对象中创建实例 object = doGetObjectFromFactoryBean(factory, beanName); Object alreadyThere = this.factoryBeanObjectCache.get(beanName); if (alreadyThere != null) { object = alreadyThere; } else { // shouldPostProcess 等价于上一个方法中的 !synthetic,用于表示是否应用后置处理 if (object != null && shouldPostProcess) { if (isSingletonCurrentlyInCreation(beanName)) { return object; } beforeSingletonCreation(beanName); try { // 应用后置处理 object = postProcessObjectFromFactoryBean(object, beanName); } catch (Throwable ex) { throw new BeanCreationException(beanName, "Post-processing of FactoryBean's singleton object failed", ex); } finally { afterSingletonCreation(beanName); } } // 这里的 beanName 对应于 FactoryBean 的实现类, FactoryBean 的实现类也会被实例化,并被缓存在 singletonObjects 中 if (containsSingleton(beanName)) { // FactoryBean 所创建的实例会被缓存在 factoryBeanObjectCache 中,供后续调用使用 this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT)); } } } return (object != NULL_OBJECT ? object : null); } } // 获取非单例实例 else { // 从工厂类中获取实例 Object object = doGetObjectFromFactoryBean(factory, beanName); if (object != null && shouldPostProcess) { try { // 应用后置处理 object = postProcessObjectFromFactoryBean(object, beanName); } catch (Throwable ex) { throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex); } } return object; } } private Object doGetObjectFromFactoryBean(final FactoryBean<?> factory, final String beanName) throws BeanCreationException { Object object; try { // if 分支的逻辑是 Java 安全方面的代码,可以忽略,直接看 else 分支的代码 if (System.getSecurityManager() != null) { AccessControlContext acc = getAccessControlContext(); try { object = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() { @Override public Object run() throws Exception { return factory.getObject(); } }, acc); } catch (PrivilegedActionException pae) { throw pae.getException(); } } else { // 调用工厂方法生成 bean 实例 object = factory.getObject(); } } catch (FactoryBeanNotInitializedException ex) { throw new BeanCurrentlyInCreationException(beanName, ex.toString()); } catch (Throwable ex) { throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex); } if (object == null && isSingletonCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException( beanName, "FactoryBean which is currently in creation returned null from getObject"); } return object; } 上面的源码分析完了,代码虽长,但整体逻辑不是很复杂,这里简单总结一下。getObjectForBeanInstance 及它所调用的方法主要做了如下几件事情: 检测参数 beanInstance 的类型,如果是非 FactoryBean 类型的 bean,直接返回 检测 FactoryBean 实现类是否单例类型,针对单例和非单例类型进行不同处理 对于单例 FactoryBean,先从缓存里获取 FactoryBean 生成的实例 若缓存未命中,则调用 FactoryBean.getObject() 方法生成实例,并放入缓存中 对于非单例的 FactoryBean,每次直接创建新的实例即可,无需缓存 如果 shouldPostProcess = true,不管是单例还是非单例 FactoryBean 生成的实例,都要进行后置处理 本节涉及到了 FactoryBean 和后置处理两个特性,关于这两个特性,不熟悉的同学可以参考我在导读一文中的说明,这里就不过多解释了。 3. 总结 到这里,Spring IOC 容器获取 bean 实例这一块的内容就分析完了。如果大家是初次阅读 Spring 的源码,看不懂也没关系。多看几遍,认证思考一下,相信是能看得懂的。另外由于本人水平有限,以上的源码分析有误的地方,还望多指教,谢了。 好了,本文先到这里。又到周五了,祝大家在即将到来的周末玩的开心。over. 参考 《Spring 源码深度解析》- 郝佳 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本。经过十几年的迭代,现在的 Spring 框架已经非常成熟了。Spring 包含了众多模块,包括但不限于 Core、Bean、Context、AOP 和 Web 等。在今天,我们完全可以使用 Spring 所提供的一站式解决方案开发出我们所需要的应用。作为 Java 程序员,我们会经常和 Spring 框架打交道,所以还是很有必要弄懂 Spring 的原理。 本文是 Spring IOC 容器源码分析系列文章的第一篇文章,将会着重介绍 Spring 的一些使用方法和特性,为后续的源码分析文章做铺垫。另外需要特别说明一下,本系列的源码分析文章是基于Spring 4.3.17.RELEASE版本编写的,而非最新的5.0.6.RELEASE版本。好了,关于简介先说到这里,继续来说说下面的内容。 2. 文章编排 写 Spring IOC 这一块的文章,挺让我纠结的。我原本是打算在一篇文章中分析所有的源码,但是后来发现文章实在太长。主要是因为 Spring IOC 部分的源码实在太长,将这一部分的源码贴在一篇文章中还是很壮观的。当然估计大家也没兴趣读下去,所以决定对文章进行拆分。这里先贴一张文章切分前的目录结构: 如上图,由目录可以看出,假使在一篇文章中写完所有内容,文章的长度将会非常长。所以在经过思考后,我会将文章拆分成一系列的文章,如下: Spring IOC 容器源码分析 - 获取单例 bean - 已更新 Spring IOC 容器源码分析 - 创建单例 bean 的过程 - 已更新 Spring IOC 容器源码分析 - 创建原始 bean 对象 - 已更新 Spring IOC 容器源码分析 - 循环依赖的解决办法 - 已更新 Spring IOC 容器源码分析 - 填充属性到原始 bean 对象中 - 已更新 Spring IOC 容器源码分析 - 余下的初始化工作 - 已更新 上面文章对应的源码分析工作均已经完成,所有的文章将会在近期内进行更新。 3. Spring 模块结构 Spring 是分模块开发的,Spring 包含了很多模块,其中最为核心的是 bean 容器相关模块。像 AOP、MVC、Data 等模块都要依赖 bean 容器。这里先看一下 Spring 框架的结构图: 图片来源:Spring 官方文档 从上图中可以看出Core Container处于整个框架的最底层(忽略 Test 模块),在其之上有 AOP、Data、Web 等模块。既然 Spring 容器是最核心的部分,那么大家如果要读 Spring 的源码,容器部分必须先弄懂。本篇文章作为 Spring IOC 容器的开篇文章,就来简单介绍一下容器方面的知识。请继续往下看。 4. Spring IOC 部分特性介绍 本章将会介绍 IOC 中的部分特性,这些特性均会在后面的源码分析中悉数到场。如果大家不是很熟悉这些特性,这里可以看一下。 4.1 alias alias 的中文意思是“别名”,在 Spring 中,我们可以使用 alias 标签给 bean 起个别名。比如下面的配置: <bean id="hello" class="xyz.coolblog.service.Hello"> <property name="content" value="hello"/> </bean> <alias name="hello" alias="alias-hello"/> <alias name="alias-hello" alias="double-alias-hello"/> 这里我们给hello这个 beanName 起了一个别名alias-hello,然后又给别名alias-hello起了一个别名double-alias-hello。我们可以通过这两个别名获取到hello这个 bean 实例,比如下面的测试代码: public class ApplicationContextTest { @Test public void testAlias() { String configLocation = "application-alias.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation); System.out.println(" alias-hello -> " + applicationContext.getBean("alias-hello")); System.out.println("double-alias-hello -> " + applicationContext.getBean("double-alias-hello")); } } 测试结果如下: 4.2 autowire 本小节,我们来了解一下 autowire 这个特性。autowire 即自动注入的意思,通过使用 autowire 特性,我们就不用再显示的配置 bean 之间的依赖了。把依赖的发现和注入都交给 Spring 去处理,省时又省力。autowire 几个可选项,比如 byName、byType 和 constructor 等。autowire 是一个常用特性,相信大家都比较熟悉了,所以本节我们就 byName 为例,快速结束 autowire 特性的介绍。 当 bean 配置中的 autowire = byName 时,Spring 会首先通过反射获取该 bean 所依赖 bean 的名字(beanName),然后再通过调用 BeanFactory.getName(beanName) 方法即可获取对应的依赖实例。autowire = byName 原理大致就是这样,接下来我们来演示一下。 public class Service { private Dao mysqlDao; private Dao mongoDao; // 忽略 getter/setter @Override public String toString() { return super.toString() + "\n\t\t\t\t\t{" + "mysqlDao=" + mysqlDao + ", mongoDao=" + mongoDao + '}'; } } public interface Dao {} public class MySqlDao implements Dao {} public class MongoDao implements Dao {} 配置如下: <bean name="mongoDao" class="xyz.coolblog.autowire.MongoDao"/> <bean name="mysqlDao" class="xyz.coolblog.autowire.MySqlDao"/> <!-- 非自动注入,手动配置依赖 --> <bean name="service-without-autowire" class="xyz.coolblog.autowire.Service" autowire="no"> <property name="mysqlDao" ref="mysqlDao"/> <property name="mongoDao" ref="mongoDao"/> </bean> <!-- 通过设置 autowire 属性,我们就不需要像上面那样显式配置依赖了 --> <bean name="service-with-autowire" class="xyz.coolblog.autowire.Service" autowire="byName"/> 测试代码如下: String configLocation = "application-autowire.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation); System.out.println("service-without-autowire -> " + applicationContext.getBean("service-without-autowire")); System.out.println("service-with-autowire -> " + applicationContext.getBean("service-with-autowire")); 测试结果如下: 从测试结果可以看出,两种方式配置方式都能完成解决 bean 之间的依赖问题。只不过使用 autowire 会更加省力一些,配置文件也不会冗长。这里举的例子比较简单,假使一个 bean 依赖了十几二十个 bean,再手动去配置,恐怕就很难受了。 4.3 FactoryBean FactoryBean?看起来是不是很像 BeanFactory 孪生兄弟。不错,他们看起来很像,但是他们是不一样的。FactoryBean 是一种工厂 bean,与普通的 bean 不一样,FactoryBean 是一种可以产生 bean 的 bean,好吧说起来很绕嘴。FactoryBean 是一个接口,我们可以实现这个接口。下面演示一下: public class HelloFactoryBean implements FactoryBean<Hello> { @Override public Hello getObject() throws Exception { Hello hello = new Hello(); hello.setContent("hello"); return hello; } @Override public Class<?> getObjectType() { return Hello.class; } @Override public boolean isSingleton() { return true; } } 配置如下: <bean id="helloFactory" class="xyz.coolblog.service.HelloFactoryBean"/> 测试代码如下: public class ApplicationContextTest { @Test public void testFactoryBean() { String configLocation = "application-factory-bean.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation); System.out.println("helloFactory -> " + applicationContext.getBean("helloFactory")); System.out.println("&helloFactory -> " + applicationContext.getBean("&helloFactory")); } } 测试结果如下: 由测试结果可以看到,当我们调用 getBean("helloFactory") 时,ApplicationContext 会返回一个 Hello 对象,该对象是 HelloFactoryBean 的 getObject 方法所创建的。如果我们想获取 HelloFactoryBean 本身,则可以在 helloFactory 前加上一个前缀&,即&helloFactory。 4.4 factory-method 介绍完 FactoryBean,本节再来看看了一个和工厂相关的特性 -- factory-method。factory-method 可用于标识静态工厂的工厂方法(工厂方法是静态的),直接举例说明吧: public class StaticHelloFactory { public static Hello getHello() { Hello hello = new Hello(); hello.setContent("created by StaticHelloFactory"); return hello; } } 配置如下: <bean id="staticHelloFactory" class="xyz.coolblog.service.StaticHelloFactory" factory-method="getHello"/> 测试代码如下: public class ApplicationContextTest { @Test public void testFactoryMethod() { String configLocation = "application-factory-method.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation); System.out.println("staticHelloFactory -> " + applicationContext.getBean("staticHelloFactory")); } } 测试结果如下: 对于非静态工厂,需要使用 factory-bean 和 factory-method 两个属性配合。关于 factory-bean 这里就不继续说了,留给大家自己去探索吧。 4.5 lookup-method lookup-method 特性可能大家用的不多(我也没用过),不过它也是个有用的特性。在介绍这个特性前,先介绍一下背景。我们通过 BeanFactory getBean 方法获取 bean 实例时,对于 singleton 类型的 bean,BeanFactory 每次返回的都是同一个 bean。对于 prototype 类型的 bean,BeanFactory 则会返回一个新的 bean。现在考虑这样一种情况,一个 singleton 类型的 bean 中有一个 prototype 类型的成员变量。BeanFactory 在实例化 singleton 类型的 bean 时,会向其注入一个 prototype 类型的实例。但是 singleton 类型的 bean 只会实例化一次,那么它内部的 prototype 类型的成员变量也就不会再被改变。但如果我们每次从 singleton bean 中获取这个 prototype 成员变量时,都想获取一个新的对象。这个时候怎么办?举个例子(该例子源于《Spring 揭秘》一书),我们有一个新闻提供类(NewsProvider),这个类中有一个新闻类(News)成员变量。我们每次调用 getNews 方法都想获取一条新的新闻。这里我们有两种方式实现这个需求,一种方式是让 NewsProvider 类实现 ApplicationContextAware 接口(实现 BeanFactoryAware 接口也是可以的),每次调用 NewsProvider 的 getNews 方法时,都从 ApplicationContext 中获取一个新的 News 实例,返回给调用者。第二种方式就是这里的 lookup-method 了,Spring 会在运行时对 NewsProvider 进行增强,使其 getNews 可以每次都返回一个新的实例。说完了背景和解决方案,接下来就来写点测试代码验证一下。 在演示两种处理方式前,我们先来看看不使用任何处理方式,BeanFactory 所返回的 bean 实例情况。相关类定义如下: public class News { // 仅演示使用,News 类中无成员变量 } public class NewsProvider { private News news; public News getNews() { return news; } public void setNews(News news) { this.news = news; } } 配置信息如下: <bean id="news" class="xyz.coolblog.lookupmethod.News" scope="prototype"/> <bean id="newsProvider" class="xyz.coolblog.lookupmethod.NewsProvider"> <property name="news" ref="news"/> </bean> 测试代码如下: String configLocation = "application-lookup-method.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation); NewsProvider newsProvider = (NewsProvider) applicationContext.getBean("newsProvider"); System.out.println(newsProvider.getNews()); System.out.println(newsProvider.getNews()); 测试结果如下: 从测试结果中可以看出,newsProvider.getNews() 方法两次返回的结果都是一样的,这个是不满足要求的。 4.5.1 实现 ApplicationContextAware 接口 我们让 NewsProvider 实现 ApplicationContextAware 接口,实现代码如下: public class NewsProvider implements ApplicationContextAware { private ApplicationContext applicationContext; private News news; /** 每次都从 applicationContext 中获取一个新的 bean */ public News getNews() { return applicationContext.getBean("news", News.class); } public void setNews(News news) { this.news = news; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } } 配置和测试代码同上,测试结果如下: 这里两次获取的 news 并就不是同一个 bean 了,满足了我们的需求。 4.5.2 使用 lookup-method 特性 使用 lookup-method 特性,配置文件需要改一下。如下: <bean id="news" class="xyz.coolblog.lookupmethod.News" scope="prototype"/> <bean id="newsProvider" class="xyz.coolblog.lookupmethod.NewsProvider"> <lookup-method name="getNews" bean="news"/> </bean> NewsProvider 的代码沿用 4.5.1 小节之前贴的代码。测试代码稍微变一下,如下: String configLocation = "application-lookup-method.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation); NewsProvider newsProvider = (NewsProvider) applicationContext.getBean("newsProvider"); System.out.println("newsProvider -> " + newsProvider); System.out.println("news 1 -> " + newsProvider.getNews()); System.out.println("news 2 -> " + newsProvider.getNews()); 测试结果如下: 从上面的结果可以看出,new1 和 new2 指向了不同的对象。同时,大家注意看 newsProvider,似乎变的很复杂。由此可看出,NewsProvider 被 CGLIB 增强了。 4.6 depends-on 当一个 bean 直接依赖另一个 bean,可以使用 <ref/> 标签进行配置。不过如某个 bean 并不直接依赖于 其他 bean,但又需要其他 bean 先实例化好,这个时候就需要使用 depends-on 特性了。depends-on 特性比较简单,就不演示了。仅贴一下配置文件的内容,如下: 这里有两个简单的类,其中 Hello 需要 World 在其之前完成实例化。相关配置如下: <bean id="hello" class="xyz.coolblog.depnedson.Hello" depends-on="world"/> <bean id="world" class="xyz.coolblog.depnedson.World" /> 4.7 BeanPostProcessor BeanPostProcessor 是 bean 实例化时的后置处理器,包含两个方法,其源码如下: public interface BeanPostProcessor { // bean 初始化前的回调方法 Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; // bean 初始化后的回调方法 Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; } BeanPostProcessor 是 Spring 框架的一个扩展点,通过实现 BeanPostProcessor 接口,我们就可插手 bean 实例化的过程。比如大家熟悉的 AOP 就是在 bean 实例后期间将切面逻辑织入 bean 实例中的,AOP 也正是通过 BeanPostProcessor 和 IOC 容器建立起了联系。这里我来演示一下 BeanPostProcessor 的使用方式,如下: /** * 日志后置处理器,将会在 bean 创建前、后打印日志 */ public class LoggerBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("Before " + beanName + " Initialization"); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("After " + beanName + " Initialization"); return bean; } } 配置如下: <bean class="xyz.coolblog.beanpostprocessor.LoggerBeanPostProcessor"/> <bean id="hello" class="xyz.coolblog.service.Hello"/> <bean id="world" class="xyz.coolblog.service.World"/> 测试代码如下: public class ApplicationContextTest { @Test public void testBeanPostProcessor() { String configLocation = "application-bean-post-processor.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation); } } 测试结果如下: 与 BeanPostProcessor 类似的还有一个叫 BeanFactoryPostProcessor 拓展点,顾名思义,用户可以通过这个拓展点插手容器启动的过程。不过这个不属于本系列文章范畴,暂时先不细说了。 4.8 BeanFactoryAware Spring 中定义了一些列的 Aware 接口,比如这里的 BeanFactoryAware,以及 BeanNameAware 和 BeanClassLoaderAware 等等。通过实现这些 Aware 接口,我们可以在运行时获取一些配置信息或者其他一些信息。比如实现 BeanNameAware 接口,我们可以获取 bean 的配置名称(beanName)。通过实现 BeanFactoryAware 接口,我们可以在运行时获取 BeanFactory 实例。关于 Aware 类型接口的使用,可以参考4.5.1 实现 ApplicationContextAware 接口一节中的叙述,这里就不演示了。 5. 阅读源码的一些建议 我在去年八月份的时候,尝试过阅读 Spring 源码。不过 Spring 源码太多了,调用复杂,看着看着就迷失在了代码的海洋里。不过好在当时找到了一个经过简化后的类 Spring 框架,该框架黄亿华前辈在学习 Spring 源码时所写的 tiny-spring。如果大家觉得看 Spring 源码比较困难,可以先学习一下 tiny-spring 的源码,先对 Spring 代码结构有个大概的了解。 另外也建议大家自己动手实现一个简单的 IOC 容器,通过实践,才会有更多的感悟。我在去年八月份的时候,实现过一个简单的 IOC 和 AOP(可以参考我去年发的文章:仿照 Spring 实现简单的 IOC 和 AOP - 上篇),并在最后将两者整合到了一起。正是有了之前的实践,才使得我对 Spring 的原理有了更进一步的认识。当做完一些准备工作后,再次阅读 Spring 源码,就没以前那么痛苦了。当然,Spring 的代码经过十几年的迭代,代码量很大。我在分析的过程中也只是尽量保证搞懂重要的逻辑,无法做到面面俱到。不过,如果大家愿意去读 Spring 源码,我相信会比我理解的更透彻。 除了上面说的动手实践外,在阅读源码的过程中,如果实在看不懂,不妨调试一下。比如某个变量你不知道有什么用,但是它又比较关键,在多个地方都出现了,显然你必须要搞懂它。那么此时写点测试代码调试一下,立马就能知道它有什么用了。 以上是我在阅读源码时所使用过的一些方法,当然仅有上面那些可能还不够。本节的最后再推荐两本书,如下: 《Spring 揭秘》- 王福强著 《Spring源码深度解析》- 郝佳著 第二本书在豆瓣上的评分不太好,不过我觉得还好。这本书我仅看了容器部分的代码分析,总的来说还行。我在看循环依赖那一块的代码时,这本书还是给了我一些帮助的。好了,本节就到这里。 6. 写在最后 在本文的最后一章,我来说说我为什么阅读 Spring 的源码吧。对我个人而言,有两个原因。第一,作为 Java Web 开发人员,我们基本上绕不过 Spring 技术栈。当然除了 Spring,还有很多其他的选择(比如有些公司自己封装框架)。但不可否认,Spring 现在仍是主流。对于这样一个经常打交道的框架,弄懂实现原理,还有很有必要的。起码在它出错输出一大堆异常时,你不会很心慌,可以从容的 debug。第二,我去年的这个时候,工作差不多快满一年。我在写第一年的工作总结时,觉得很心慌。感觉第一年好像只学到了一点开发的皮毛,技术方面,没什么积累。加上后来想换工作,心里想着就自己现在的水平恐怕要失业了。所以为了平复自己焦虑的情绪,于是在去年的八月份的时候,我开始写博客了。到现在写了近30篇博客(比较少),其中有两篇文章被博客平台作为优秀文章推荐过。现在,又快到7月份了,工龄马上又要+1了。所以我现在在抓紧写 Spring 相关的文章,希望在六月份多写几篇。算是对自己工作满两年的一个阶段性技术总结,也是一个纪念吧。 当然,看懂源码并不是什么很了不起的事情,毕竟写这些源码的人才是真正的大神。我在大学的时候,自学 MFC(没错,就是微软早已淘汰的东西)。并读过侯捷老师著的《深入浅出MFC》一书,这本书中的一句话对我影响至深 - “勿在浮沙筑高台”。勿在浮沙筑高台,对于一个程序员来说,基础很重要。所以我现在也在不断的学习,希望能把基础打好,这样以后才能进入更高的层次。 好了,感谢大家耐心看完我的唠叨。本文先到这里,我要去写后续的文章了,后面再见。bye~ 附录:Spring 源码分析文章列表 Ⅰ. IOC 更新时间 标题 2018-05-30 Spring IOC 容器源码分析系列文章导读 2018-06-01 Spring IOC 容器源码分析 - 获取单例 bean 2018-06-04 Spring IOC 容器源码分析 - 创建单例 bean 的过程 2018-06-06 Spring IOC 容器源码分析 - 创建原始 bean 对象 2018-06-08 Spring IOC 容器源码分析 - 循环依赖的解决办法 2018-06-11 Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 2018-06-11 Spring IOC 容器源码分析 - 余下的初始化工作 Ⅱ. AOP 更新时间 标题 2018-06-17 Spring AOP 源码分析系列文章导读 2018-06-20 Spring AOP 源码分析 - 筛选合适的通知器 2018-06-20 Spring AOP 源码分析 - 创建代理对象 2018-06-22 Spring AOP 源码分析 - 拦截器链的执行过程 Ⅲ. MVC 更新时间 标题 2018-06-29 Spring MVC 原理探秘 - 一个请求的旅行过程 2018-06-30 Spring MVC 原理探秘 - 容器的创建过程 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数 -- 内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。 在 Java 中,Java 并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的。Java 代码需通过 JNI 才能调用。关于实现上的细节,我将会在第3章进行分析。 前面说了 CAS 操作的流程,并不是很难。但仅有上面的说明还不够,接下来我将会再介绍一点其他的背景知识。有这些背景知识,才能更好的理解后续的内容。 2.背景介绍 我们都知道,CPU 是通过总线和内存进行数据传输的。在多核心时代下,多个核心通过同一条总线和内存以及其他硬件进行通信。如下图: 图片出处:《深入理解计算机系统》 上图是一个较为简单的计算机结构图,虽然简单,但足以说明问题。在上图中,CPU 通过两个蓝色箭头标注的总线与内存进行通信。大家考虑一个问题,CPU 的多个核心同时对同一片内存进行操作,若不加以控制,会导致什么样的错误?这里简单说明一下,假设核心1经32位带宽的总线向内存写入64位的数据,核心1要进行两次写入才能完成整个操作。若在核心1第一次写入32位的数据后,核心2从核心1写入的内存位置读取了64位数据。由于核心1还未完全将64位的数据全部写入内存中,核心2就开始从该内存位置读取数据,那么读取出来的数据必定是混乱的。 不过对于这个问题,实际上不用担心。通过 Intel 开发人员手册,我们可以了解到自奔腾处理器开始,Intel 处理器会保证以原子的方式读写按64位边界对齐的四字(quadword)。 根据上面的说明,我们可总结出,Intel 处理器可以保证单次访问内存对齐的指令以原子的方式执行。但如果是两次访存的指令呢?答案是无法保证。比如递增指令inc dword ptr [...],等价于DEST = DEST + 1。该指令包含三个操作读->改->写,涉及两次访存。考虑这样一种情况,在内存指定位置处,存放了一个为1的数值。现在 CPU 两个核心同时执行该条指令。两个核心交替执行的流程如下: 核心1 从内存指定位置出读取数值1,并加载到寄存器中 核心2 从内存指定位置出读取数值1,并加载到寄存器中 核心1 将寄存器中值递减1 核心2 将寄存器中值递减1 核心1 将修改后的值写回内存 核心2 将修改后的值写回内存 经过执行上述流程,内存中的最终值时2,而我们期待的是3,这就出问题了。要处理这个问题,就要避免两个或多个核心同时操作同一片内存区域。那么怎样避免呢?这就要引入本文的主角 - lock 前缀。关于该指令的详细描述,可以参考 Intel 开发人员手册 Volume 2 Instruction Set Reference,Chapter 3 Instruction Set Reference A-L。我这里引用其中的一段,如下: LOCK—Assert LOCK# Signal Prefix Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal ensures that the processor has exclusive use of any shared memory while the signal is asserted. 上面描述的重点已经用黑体标出了,在多处理器环境下,LOCK# 信号可以确保处理器独占使用某些共享内存。lock 可以被添加在下面的指令前: ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG. 通过在 inc 指令前添加 lock 前缀,即可让该指令具备原子性。多个核心同时执行同一条 inc 指令时,会以串行的方式进行,也就避免了上面所说的那种情况。那么这里还有一个问题,lock 前缀是怎样保证核心独占某片内存区域的呢?答案如下: 在 Intel 处理器中,有两种方式保证处理器的某个核心独占某片内存区域。第一种方式是通过锁定总线,让某个核心独占使用总线,但这样代价太大。总线被锁定后,其他核心就不能访问内存了,可能会导致其他核心短时内停止工作。第二种方式是锁定缓存,若某处内存数据被缓存在处理器缓存中。处理器发出的 LOCK# 信号不会锁定总线,而是锁定缓存行对应的内存区域。其他处理器在这片内存区域锁定期间,无法对这片内存区域进行相关操作。相对于锁定总线,锁定缓存的代价明显比较小。关于总线锁和缓存锁,更详细的描述请参考 Intel 开发人员手册 Volume 3 Software Developer’s Manual,Chapter 8 Multiple-Processor Management。 3.源码分析 有了上面的背景知识,现在我们就可以从容不迫的阅读 CAS 的源码了。本章的内容将对 java.util.concurrent.atomic 包下的原子类 AtomicInteger 中的 compareAndSet 方法进行分析,相关分析如下: public class AtomicInteger extends Number implements java.io.Serializable { // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { // 计算变量 value 在类对象中的偏移 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; public final boolean compareAndSet(int expect, int update) { /* * compareAndSet 实际上只是一个壳子,主要的逻辑封装在 Unsafe 的 * compareAndSwapInt 方法中 */ return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } // ...... } public final class Unsafe { // compareAndSwapInt 是 native 类型的方法,继续往下看 public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); // ...... } // unsafe.cpp /* * 这个看起来好像不像一个函数,不过不用担心,不是重点。UNSAFE_ENTRY 和 UNSAFE_END 都是宏, * 在预编译期间会被替换成真正的代码。下面的 jboolean、jlong 和 jint 等是一些类型定义(typedef): * * jni.h * typedef unsigned char jboolean; * typedef unsigned short jchar; * typedef short jshort; * typedef float jfloat; * typedef double jdouble; * * jni_md.h * typedef int jint; * #ifdef _LP64 // 64-bit * typedef long jlong; * #else * typedef long long jlong; * #endif * typedef signed char jbyte; */ UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); // 根据偏移量,计算 value 的地址。这里的 offset 就是 AtomaicInteger 中的 valueOffset jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); // 调用 Atomic 中的函数 cmpxchg,该函数声明于 Atomic.hpp 中 return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END // atomic.cpp unsigned Atomic::cmpxchg(unsigned int exchange_value, volatile unsigned int* dest, unsigned int compare_value) { assert(sizeof(unsigned int) == sizeof(jint), "more work to do"); /* * 根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载 * 函数。相关的预编译逻辑如下: * * atomic.inline.hpp: * #include "runtime/atomic.hpp" * * // Linux * #ifdef TARGET_OS_ARCH_linux_x86 * # include "atomic_linux_x86.inline.hpp" * #endif * * // 省略部分代码 * * // Windows * #ifdef TARGET_OS_ARCH_windows_x86 * # include "atomic_windows_x86.inline.hpp" * #endif * * // BSD * #ifdef TARGET_OS_ARCH_bsd_x86 * # include "atomic_bsd_x86.inline.hpp" * #endif * * 接下来分析 atomic_windows_x86.inline.hpp 中的 cmpxchg 函数实现 */ return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value); } 上面的分析看起来比较多,不过主流程并不复杂。如果不纠结于代码细节,还是比较容易看懂的。接下来,我会分析 Windows 平台下的 Atomic::cmpxchg 函数。继续往下看吧。 // atomic_windows_x86.inline.hpp #define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } } 上面的代码由 LOCK_IF_MP 预编译标识符和 cmpxchg 函数组成。为了看到更清楚一些,我们将 cmpxchg 函数中的 LOCK_IF_MP 替换为实际内容。如下: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // 判断是否是多核 CPU int mp = os::is_MP(); __asm { // 将参数值放入寄存器中 mov edx, dest // 注意: dest 是指针类型,这里是把内存地址存入 edx 寄存器中 mov ecx, exchange_value mov eax, compare_value // LOCK_IF_MP cmp mp, 0 /* * 如果 mp = 0,表明是线程运行在单核 CPU 环境下。此时 je 会跳转到 L0 标记处, * 也就是越过 _emit 0xF0 指令,直接执行 cmpxchg 指令。也就是不在下面的 cmpxchg 指令 * 前加 lock 前缀。 */ je L0 /* * 0xF0 是 lock 前缀的机器码,这里没有使用 lock,而是直接使用了机器码的形式。至于这样做的 * 原因可以参考知乎的一个回答: * https://www.zhihu.com/question/50878124/answer/123099923 */ _emit 0xF0 L0: /* * 比较并交换。简单解释一下下面这条指令,熟悉汇编的朋友可以略过下面的解释: * cmpxchg: 即“比较并交换”指令 * dword: 全称是 double word,在 x86/x64 体系中,一个 * word = 2 byte,dword = 4 byte = 32 bit * ptr: 全称是 pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元 * [edx]: [...] 表示一个内存单元,edx 是寄存器,dest 指针值存放在 edx 中。 * 那么 [edx] 表示内存地址为 dest 的内存单元 * * 这一条指令的意思就是,将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值 * 进行对比,如果相同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中。 */ cmpxchg dword ptr [edx], ecx } } 到这里 CAS 的实现过程就讲完了,CAS 的实现离不开处理器的支持。以上这么多代码,其实核心代码就是一条带lock 前缀的 cmpxchg 指令,即lock cmpxchg dword ptr [edx], ecx。 4.ABA 问题 谈到 CAS,基本上都要谈一下 CAS 的 ABA 问题。CAS 由三个步骤组成,分别是“读取->比较->写回”。考虑这样一种情况,线程1和线程2同时执行 CAS 逻辑,两个线程的执行顺序如下: 时刻1:线程1执行读取操作,获取原值 A,然后线程被切换走 时刻2:线程2执行完成 CAS 操作将原值由 A 修改为 B 时刻3:线程2再次执行 CAS 操作,并将原值由 B 修改为 A 时刻4:线程1恢复运行,将比较值(compareValue)与原值(oldValue)进行比较,发现两个值相等。 然后用新值(newValue)写入内存中,完成 CAS 操作 如上流程,线程1并不知道原值已经被修改过了,在它看来并没什么变化,所以它会继续往下执行流程。对于 ABA 问题,通常的处理措施是对每一次 CAS 操作设置版本号。java.util.concurrent.atomic 包下提供了一个可处理 ABA 问题的原子类 AtomicStampedReference,具体的实现这里就不分析了,有兴趣的朋友可以自己去看看。 5.总结 写到这里,这篇文章总算接近尾声了。虽然 CAS 本身的原理,包括实现都不是很难,但是写起来真的不太好写。这里面涉及到了一些底层的知识,虽然能看懂,但想说明白,还是有点难度的。由于我底层的知识比较欠缺,上面的一些分析难免会出错。所以如有错误,请轻喷,当然最好能说明怎么错的,感谢。 好了,本篇文章就到这里。感谢阅读,再见。 参考 Compare-and-swap - wikipedia 多核环境下的内存屏障指令 - 云风 Intel® 64 and IA-32 Architectures Software Developer’s Manual 一条C语言语句不一定是原子操作,但是一个汇编指令是原子操作吗?- 知乎 下面这个宏中的emit指令是干什么的?- 知乎 消失的北桥 - txwm8905 附录 在前面源码分析一节中用到的几个文件,这里把路径贴出来。有助于大家进行索引,如下: 文件名 路径 Unsafe.java openjdk/jdk/src/share/classes/sun/misc/Unsafe.java unsafe.cpp openjdk/hotspot/src/share/vm/prims/unsafe.cpp atomic.cpp openjdk/hotspot/src/share/vm/runtime/atomic.cpp atomic_windows_x86.inline.hpp openjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 在分析完AbstractQueuedSynchronizer(以下简称 AQS)和ReentrantLock的原理后,本文将分析 java.util.concurrent 包下的两个线程同步组件CountDownLatch和CyclicBarrier。这两个同步组件比较常用,也经常被放在一起对比。通过分析这两个同步组件,可使我们对 Java 线程间协同有更深入的了解。同时通过分析其原理,也可使我们做到知其然,并知其所以然。 这里首先来介绍一下 CountDownLatch 的用途,CountDownLatch 允许一个或一组线程等待其他线程完成后再恢复运行。线程可通过调用await方法进入等待状态,在其他线程调用countDown方法将计数器减为0后,处于等待状态的线程即可恢复运行。CyclicBarrier (可循环使用的屏障)则与此不同,CyclicBarrier 允许一组线程到达屏障后阻塞住,直到最后一个线程进入到达屏障,所有线程才恢复运行。它们之间主要的区别在于唤醒等待线程的时机。CountDownLatch 是在计数器减为0后,唤醒等待线程。CyclicBarrier 是在计数器(等待线程数)增长到指定数量后,再唤醒等待线程。除此之外,两种之间还有一些其他的差异,这个将会在后面进行说明。 在下一章中,我将会介绍一下两者的实现原理,继续往下看吧。 2.原理 2.1 CountDownLatch 的实现原理 CountDownLatch 的同步功能是基于 AQS 实现的,CountDownLatch 使用 AQS 中的 state 成员变量作为计数器。在 state 不为0的情况下,凡是调用 await 方法的线程将会被阻塞,并被放入 AQS 所维护的同步队列中进行等待。大致示意图如下: 每个阻塞的线程都会被封装成节点对象,节点之间通过 prev 和 next 指针形成同步队列。初始情况下,队列的头结点是一个虚拟节点。该节点仅是一个占位符,没什么特别的意义。每当有一个线程调用 countDown 方法,就将计数器 state--。当 state 被减至0时,队列中的节点就会按照 FIFO 顺序被唤醒,被阻塞的线程即可恢复运行。 CountDownLatch 本身的原理并不难理解,不过如果大家想深入理解 CountDownLatch 的实现细节,那么需要先去学习一下 AQS 的相关原理。CountDownLatch 是基于 AQS 实现的,所以理解 AQS 是学习 CountDownLatch 的前置条件。我在之前写过一篇关于 AQS 的文章 Java 重入锁 ReentrantLock 原理分析,有兴趣的朋友可以去读一读。 2.2 CyclicBarrier 的实现原理 与 CountDownLatch 的实现方式不同,CyclicBarrier 并没有直接通过 AQS 实现同步功能,而是在重入锁 ReentrantLock 的基础上实现的。在 CyclicBarrier 中,线程访问 await 方法需先获取锁才能访问。在最后一个线程访问 await 方法前,其他线程进入 await 方法中后,会调用 Condition 的 await 方法进入等待状态。在最后一个线程进入 CyclicBarrier await 方法后,该线程将会调用 Condition 的 signalAll 方法唤醒所有处于等待状态中的线程。同时,最后一个进入 await 的线程还会重置 CyclicBarrier 的状态,使其可以重复使用。 在创建 CyclicBarrier 对象时,需要转入一个值,用于初始化 CyclicBarrier 的成员变量 parties,该成员变量表示屏障拦截的线程数。当到达屏障的线程数小于 parties 时,这些线程都会被阻塞住。当最后一个线程到达屏障后,此前被阻塞的线程才会被唤醒。 3.源码分析 通过前面简单的分析,相信大家对 CountDownLatch 和 CyclicBarrier 的原理有一定的了解了。那么接下来趁热打铁,我们一起探索一下这两个同步组件的具体实现吧。 3.1 CountDownLatch 源码分析 CountDownLatch 的原理不是很复杂,所以在具体的实现上,也不是很复杂。当然,前面说过 CountDownLatch 是基于 AQS 实现的,AQS 的实现则要复杂的多。不过这里仅要求大家掌握 AQS 的基本原理,知道它内部维护了一个同步队列,同步队列中的线程会按照 FIFO 依次获取同步状态就行了。好了,下面我们一起去看一下 CountDownLatch 的源码吧。 3.1.1 源码结构 CountDownLatch 的代码量不大,加上注释也不过300多行,所以它的代码结构也会比较简单。如下: 如上图,CountDownLatch 源码包含一个构造方法和一个私有成员变量,以及数个普通方法和一个重要的静态内部类 Sync。CountDownLatch 的主要逻辑都是封装在 Sync 和其父类 AQS 里的。所以分析 CountDownLatch 的源码,本质上是分析 Sync 和 AQS 的原理。相关的分析,将会在下一节中展开,本节先说到这。 3.1.2 构造方法及成员变量 本节来分析一下 CountDownLatch 的构造方法和其 Sync 类型的成员变量实现,如下: public class CountDownLatch { private final Sync sync; /** CountDownLatch 的构造方法,该方法要求传入大于0的整型数值作为计数器 */ public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); // 初始化 Sync this.sync = new Sync(count); } /** CountDownLatch 的同步控制器,继承自 AQS */ private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { // 设置 AQS state setState(count); } int getCount() { return getState(); } /** 尝试在共享状态下获取同步状态,该方法在 AQS 中是抽象方法,这里进行了覆写 */ protected int tryAcquireShared(int acquires) { /* * 如果 state = 0,则返回1,表明可获取同步状态, * 此时线程调用 await 方法时就不会被阻塞。 */ return (getState() == 0) ? 1 : -1; } /** 尝试在共享状态下释放同步状态,该方法在 AQS 中也是抽象方法 */ protected boolean tryReleaseShared(int releases) { /* * 下面的逻辑是将 state--,state 减至0时,调用 await 等待的线程会被唤醒。 * 这里使用循环 + CAS,表明会存在竞争的情况,也就是多个线程可能会同时调用 * countDown 方法。在 state 不为0的情况下,线程调用 countDown 是必须要完 * 成 state-- 这个操作。所以这里使用了循环 + CAS,确保 countDown 方法可正 * 常运行。 */ for (;;) { // 获取 state int c = getState(); if (c == 0) return false; int nextc = c-1; // 使用 CAS 设置新的 state 值 if (compareAndSetState(c, nextc)) return nextc == 0; } } } } 需要说明的是,Sync 中的 tryAcquireShared 和 tryReleaseShared 方法并不是直接给 await 和 countDown 方法调用了的,这两个方法以“try”开头的方法最终会在 AQS 中被调用。 3.1.3 await CountDownLatch中有两个版本的 await 方法,一个响应中断,另一个在此基础上增加了超时功能。本节将分析无超时功能的 await,如下: /** * 该方法会使线程进入等待状态,直到计数器减至0,或者线程被中断。当计数器为0时,调用 * 此方法将会立即返回,不会被阻塞住。 */ public void await() throws InterruptedException { // 调用 AQS 中的 acquireSharedInterruptibly 方法 sync.acquireSharedInterruptibly(1); } /** 带有超时功能的 await */ public boolean await(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); } +--- AbstractQueuedSynchronizer public final void acquireSharedInterruptibly(int arg) throws InterruptedException { // 若线程被中断,则直接抛出中断异常 if (Thread.interrupted()) throw new InterruptedException(); // 调用 Sync 中覆写的 tryAcquireShared 方法,尝试获取同步状态 if (tryAcquireShared(arg) < 0) /* * 若 tryAcquireShared 小于0,则表示获取同步状态失败, * 此时将线程放入 AQS 的同步队列中进行等待。 */ doAcquireSharedInterruptibly(arg); } 从上面的代码中可以看出,CountDownLatch await 方法实际上调用的是 AQS 的 acquireSharedInterruptibly 方法。该方法会在内部调用 Sync 所覆写的 tryAcquireShared 方法。在 state != 0时,tryAcquireShared 返回值 -1。此时线程将进入 doAcquireSharedInterruptibly 方法中,在此方法中,线程会被放入同步队列中进行等待。若 state = 0,此时 tryAcquireShared 返回1,acquireSharedInterruptibly 会直接返回。此时调用 await 的线程也不会被阻塞住。 3.1.4 countDown 与 await 方法一样,countDown 实际上也是对 AQS 方法的一层封装。具体的实现如下: /** 该方法的作用是将计数器进行自减操作,当计数器为0时,唤醒正在同步队列中等待的线程 */ public void countDown() { // 调用 AQS 中的 releaseShared 方法 sync.releaseShared(1); } +--- AbstractQueuedSynchronizer public final boolean releaseShared(int arg) { // 调用 Sync 中的 tryReleaseShared 尝试释放同步状态 if (tryReleaseShared(arg)) { /* * tryReleaseShared 返回 true 时,表明 state = 0,即计数器为0。此时调用 * doReleaseShared 方法唤醒正在同步队列中等待的线程 */ doReleaseShared(); return true; } return false; } 以上就是 countDown 的源码分析,不是很难懂,这里就不啰嗦了。 3.2 CyclicBarrier 源码分析 3.2.1 源码结构 如前面所说,CyclicBarrier 是基于重入锁 ReentrantLock 实现相关逻辑的。所以要弄懂 CyclicBarrier 的源码,仅需有 ReentrantLock 相关的背景知识即可。关于重入锁 ReentrantLock 方面的知识,有兴趣的朋友可以参考我之前写的文章 Java 重入锁 ReentrantLock 原理分析。下面看一下 CyclicBarrier 的代码结构吧,如下: 从上图可以看出,CyclicBarrier 包含了一个静态内部类Generation、数个方法和一些成员变量。结构上比 CountDownLatch 略为复杂一些,但总体仍比较简单。好了,接下来进入源码分析部分吧。 3.2.2 构造方法及成员变量 CyclicBarrier 包含两个有参构造方法,分别如下: /** 创建一个允许 parties 个线程通行的屏障 */ public CyclicBarrier(int parties) { this(parties, null); } /** * 创建一个允许 parties 个线程通行的屏障,若 barrierAction 回调对象不为 null, * 则在最后一个线程到达屏障后,执行相应的回调逻辑 */ public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; } 上面的第二个构造方法初始化了一些成员变量,下面我们就来说明一下这些成员变量的作用。 成员变量 作用 parties 线程数,即当 parties 个线程到达屏障后,屏障才会放行 count 计数器,当 count > 0 时,到达屏障的线程会进入等待状态。当最后一个线程到达屏障后,count 自减至0。最后一个到达的线程会执行回调方法,并唤醒其他处于等待状态中的线程。 barrierCommand 回调对象,如果不为 null,会在第 parties 个线程到达屏障后被执行 除了上面几个成员变量,还有一个成员变量需要说明一下,如下: /** * CyclicBarrier 是可循环使用的屏障,这里使用 Generation 记录当前轮次 CyclicBarrier * 的运行状态。当所有线程到达屏障后,generation 将会被更新,表示 CyclicBarrier 进入新一 * 轮的运行轮次中。 */ private Generation generation = new Generation(); private static class Generation { // 用于记录屏障有没有被破坏 boolean broken = false; } 3.2.3 await 上一节所提到的几个成员变量,在 await 方法中将会悉数登场。下面就来分析一下 await 方法的试下,如下: public int await() throws InterruptedException, BrokenBarrierException { try { // await 的逻辑封装在 dowait 中 return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } } private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { final ReentrantLock lock = this.lock; // 加锁 lock.lock(); try { final Generation g = generation; // 如果 g.broken = true,表明屏障被破坏了,这里直接抛出异常 if (g.broken) throw new BrokenBarrierException(); // 如果线程中断,则调用 breakBarrier 破坏屏障 if (Thread.interrupted()) { breakBarrier(); throw new InterruptedException(); } /* * index 表示线程到达屏障的顺序,index = parties - 1 表明当前线程是第一个 * 到达屏障的。index = 0,表明当前线程是最有一个到达屏障的。 */ int index = --count; // 当 index = 0 时,唤醒所有处于等待状态的线程 if (index == 0) { // tripped boolean ranAction = false; try { final Runnable command = barrierCommand; // 如果回调对象不为 null,则执行回调 if (command != null) command.run(); ranAction = true; // 重置屏障状态,使其进入新一轮的运行过程中 nextGeneration(); return 0; } finally { // 若执行回调的过程中发生异常,此时调用 breakBarrier 破坏屏障 if (!ranAction) breakBarrier(); } } // 线程运行到此处的线程都会被屏障挡住,并进入等待状态。 for (;;) { try { if (!timed) trip.await(); else if (nanos > 0L) nanos = trip.awaitNanos(nanos); } catch (InterruptedException ie) { /* * 若下面的条件成立,则表明本轮运行还未结束。此时调用 breakBarrier * 破坏屏障,唤醒其他线程,并抛出异常 */ if (g == generation && ! g.broken) { breakBarrier(); throw ie; } else { /* * 若上面的条件不成立,则有两种可能: * 1. g != generation * 此种情况下,表明循环屏障的第 g 轮次的运行已经结束,屏障已经 * 进入了新的一轮运行轮次中。当前线程在稍后返回 到达屏障 的顺序即可 * * 2. g = generation 但 g.broken = true * 此种情况下,表明已经有线程执行过 breakBarrier 方法了,当前 * 线程则会在稍后抛出 BrokenBarrierException */ Thread.currentThread().interrupt(); } } // 屏障被破坏,则抛出 BrokenBarrierException 异常 if (g.broken) throw new BrokenBarrierException(); // 屏障进入新的运行轮次,此时返回线程在上一轮次到达屏障的顺序 if (g != generation) return index; // 超时判断 if (timed && nanos <= 0L) { breakBarrier(); throw new TimeoutException(); } } } finally { lock.unlock(); } } /** 开启新的一轮运行过程 */ private void nextGeneration() { // 唤醒所有处于等待状态中的线程 trip.signalAll(); // 重置 count count = parties; // 重新创建 Generation,表明进入循环屏障进入新的一轮运行轮次中 generation = new Generation(); } /** 破坏屏障 */ private void breakBarrier() { // 设置屏障是否被破坏标志 generation.broken = true; // 重置 count count = parties; // 唤醒所有处于等待状态中的线程 trip.signalAll(); } 3.2.4 reset reset 方法用于强制重置屏障,使屏障进入新一轮的运行过程中。代码如下: public void reset() { final ReentrantLock lock = this.lock; lock.lock(); try { // 破坏屏障 breakBarrier(); // break the current generation // 开启新一轮的运行过程 nextGeneration(); // start a new generation } finally { lock.unlock(); } } reset 方法并不复杂,没什么好讲的。CyclicBarrier 中还有其他一些方法,均不复杂,这里就不一一分析了。 4.两者区别 看完上面的分析,相信大家对着两个同步组件有了更深入的认识。那么下面趁热打铁,简单对比一下两者之间的区别。这里用一个表格列举一下: 差异点 CountDownLatch CyclicBarrier 等待线程唤醒时机 计数器减至0时,唤醒等待线程 到达屏障的线程数达到 parties 时,唤醒等待线程 是否可循环使用 否 是 是否可设置回调 否 是 除了上面列举的差异点,还有一些其他方面的差异,这里就不一一列举了。 5.总结 分析完 CountDownLatch 和 CyclicBarrier,不知道大家有什么感觉。我个人的感觉是这两个类的源码并不复杂,比较好理解。当然,前提是建立在对 AQS 以及 ReentrantLock 有较深的理解之上。所以在学习这两个类的源码时,还是建议大家先看看前置知识。 好了,本文到这里就结束了。谢谢阅读,再见。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似。所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。ReentrantLock 的主要功能和 synchronized 关键字一致,均是用于多线程的同步。但除此之外,ReentrantLock 在功能上比 synchronized 更为丰富。比如 ReentrantLock 在加锁期间,可响应中断,可设置超时等。 ReentrantLock 是我们日常使用很频繁的一种锁,所以在使用之余,我们也应该去了解一下它的内部实现原理。ReentrantLock 内部是基于 AbstractQueuedSynchronizer(以下简称AQS)实现的。所以要想理解 ReentrantLock,应先去 AQS 相关原理。我在之前的文章 AbstractQueuedSynchronizer 原理分析 - 独占/共享模式 中,已经详细分析过 AQS 原理,有兴趣的朋友可以去看看。本文仅会在需要的时候对 AQS 相关原理进行简要说明,更详细的说明请参考我的其他文章。 2.原理 本章将会简单介绍重入锁 ReentrantLock 中的一些概念和相关原理,包括可重入、公平和非公平锁等原理。在介绍这些原理前,首先我会介绍 ReentrantLock 与 synchronized 关键字的相同和不同之处。在此之后才回去介绍重入、公平和非公平等原理。 2.1 与 synchronized 的异同 ReentrantLock 和 synchronized 都是用于线程的同步控制,但它们在功能上来说差别还是很大的。对比下来 ReentrantLock 功能明显要丰富的多。下面简单列举一下两者之间的差异,如下: 特性 synchronized ReentrantLock 相同 可重入 是 是 响应中断 否 是 超时等待 否 是 公平锁 否 是 非公平锁 是 是 是否可尝试加锁 否 是 是否是Java内置特性 是 否 自动获取/释放锁 是 否 对异常的处理 自动释放锁 需手动释放锁 除此之外,ReentrantLock 提供了丰富的接口用于获取锁的状态,比如可以通过isLocked()查询 ReentrantLock 对象是否处于锁定状态, 也可以通过getHoldCount()获取 ReentrantLock 的加锁次数,也就是重入次数等。而 synchronized 仅支持通过Thread.holdsLock查询当前线程是否持有锁。另外,synchronized 使用的是对象或类进行加锁,而 ReentrantLock 内部是通过 AQS 中的同步队列进行加锁,这一点和 synchronized 也是不一样的。 这里列举了不少两者的相同和不同之处,暂时这能想到这些。如果还有其他的区别,欢迎补充。 2.2 可重入 可重入这个概念并不难理解,本节通过一个例子简单说明一下。 现在有方法 m1 和 m2,两个方法均使用了同一把锁对方法进行同步控制,同时方法 m1 会调用 m2。线程 t 进入方法 m1 成功获得了锁,此时线程 t 要在没有释放锁的情况下,调用 m2 方法。由于 m1 和 m2 使用的是同一把可重入锁,所以线程 t 可以进入方法 m2,并再次获得锁,而不会被阻塞住。示例代码大致如下: void m1() { lock.lock(); try { // 调用 m2,因为可重入,所以并不会被阻塞 m2(); } finally { lock.unlock() } } void m2() { lock.lock(); try { // do something } finally { lock.unlock() } } 假如 lock 是不可重入锁,那么上面的示例代码必然会引起死锁情况的发生。这里请大家思考一个问题,ReentrantLock 的可重入特性是怎样实现的呢?简单说一下,ReentrantLock 内部是通过 AQS 实现同步控制的,AQS 有一个变量 state 用于记录同步状态。初始情况下,state = 0,表示 ReentrantLock 目前处于解锁状态。如果有线程调用 lock 方法进行加锁,state 就由0变为1,如果该线程再次调用 lock 方法加锁,就让其自增,即 state++。线程每调用一次 unlock 方法释放锁,会让 state--。通过查询 state 的数值,即可知道 ReentrantLock 被重入的次数了。这就是可重复特性的大致实现流程。 2.3 公平与非公平 公平与非公平指的是线程获取锁的方式。公平模式下,线程在同步队列中通过 FIFO 的方式获取锁,每个线程最终都能获取锁。在非公平模式下,线程会通过“插队”的方式去抢占锁,抢不到的则进入同步队列进行排队。默认情况下,ReentrantLock 使用的是非公平模式获取锁,而不是公平模式。不过我们也可通过 ReentrantLock 构造方法ReentrantLock(boolean fair)调整加锁的模式。 既然既然有两种不同的加锁模式,那么他们有什么优缺点呢?答案如下: 公平模式下,可保证每个线程最终都能获得锁,但效率相对比较较低。非公平模式下,效率比较高,但可能会导致线程出现饥饿的情况。即一些线程迟迟得不到锁,每次即将到手的锁都有可能被其他线程抢了。这里再提个问题,为啥非公平模式抢了其他线程获取锁的机会,而整个程序的运行效率会更高呢?说实话,开始我也不明白。不过好在《Java并发编程实战》在第13.3节 公平性(p232)说明了具体的原因,这里引用一下: 在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于这个线程已经被线程 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此会再次尝试获取锁。与此同时,如果 C 也请求这个锁,那么 C 很有可能会在 B 被完全唤醒前获得、使用以及释放这个锁。这样的情况时一种“双赢”的局面:B 获得锁的时刻并没有推迟,C 更早的获得了锁,并且吞吐量也获得了提高。 上面的原因大家看懂了吗?下面配个图辅助说明一下: 如上图,线程 C 在线程 B 苏醒阶段内获取和使用锁,并在线程 B 获取锁前释放了锁,所以线程 B 可以顺利获得锁。线程 C 在抢占锁的情况下,仍未影响线程 B 获取锁,因此是个“双赢”的局面。 除了上面的原因外,《Java并发编程的艺术》在其5.3.2 公平与非公平锁的区别(p137)分析了另一个可能的原因。即公平锁线程切换次数要比非公平锁线程切换次数多得多,因此效率上要低一些。更多的细节,可以参考作者的论述,这里不展开说明了。 本节最后说一下公平锁和非公平锁的使用场景。如果线程持锁时间短,则应使用非公平锁,可通过“插队”提升效率。如果线程持锁时间长,“插队”带来的效率提升可能会比较小,此时应使用公平锁。 3. 源码分析 3.1 代码结构 前面说到 ReentrantLock 是基于 AQS 实现的,AQS 很好的封装了同步队列的管理,线程的阻塞与唤醒等基础操作。基于 AQS 的同步组件,推荐的使用方式是通过内部非 public 静态类继承 AQS,并重写部分抽象方法。其代码结构大致如下: 上图中,Sync是一个静态抽象类,继承了 AbstractQueuedSynchronizer。公平和非公平锁的实现类NonfairSync和FairSync则继承自 Sync 。至于 ReentrantLock 中的其他一些方法,主要逻辑基本上都在几个内部类中实现的。 3.2 获取锁 在分析 ReentrantLock 加锁的代码前,下来简单介绍一下 AQS 同步队列的一些知识。AQS 维护了一个基于双向链表的同步队列,线程在获取同步状态失败的情况下,都会被封装成节点,然后加入队列中。同步队列大致示意图如下: 在同步队列中,头结点是获取同步状态的节点。其他节点在尝试获取同步状态失败后,会被阻塞住,暂停运行。当头结点释放同步状态后,会唤醒其后继节点。后继节点会将自己设为头节点,并将原头节点从队列中移除。大致示意图如下: 介绍完 AQS 同步队列,以及节点线程获取同步状态的过程。下面来分析一下 ReentrantLock 中获取锁方法的源码,如下: public void lock() { sync.lock(); } abstract static class Sync extends AbstractQueuedSynchronizer { // 这里的 lock 是抽象方法,具体的实现在两个子类中 abstract void lock(); // 省略其他无关代码 } lock 方法的实现很简单,不过这里的 lock 方法只是一个壳子而已。由于获取锁的方式有公平和非公平之分,所以具体的实现是在NonfairSync和FairSync两个类中。那么我们继续往下分析一下这两个类的实现。 3.2.1 公平锁 公平锁对应的逻辑是 ReentrantLock 内部静态类 FairSync,我们沿着上面的 lock 方法往下分析,如下: +--- ReentrantLock.FairSync.java final void lock() { // 调用 AQS acquire 获取锁 acquire(1); } +--- AbstractQueuedSynchronizer.java /** * 该方法主要做了三件事情: * 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回 * 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点, * 并将节点放入同步队列尾部 * 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁 */ public final void acquire(int arg) { // acquireQueued 和 addWaiter 属于 AQS 中的方法,这里不展开分析了 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } +--- ReentrantLock.FairSync.java protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 获取同步状态 int c = getState(); // 如果同步状态 c 为0,表示锁暂时没被其他线程获取 if (c == 0) { /* * 判断是否有其他线程等待的时间更长。如果有,应该先让等待时间更长的节点先获取锁。 * 如果没有,调用 compareAndSetState 尝试设置同步状态。 */ if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 将当前线程设置为持有锁的线程 setExclusiveOwnerThread(current); return true; } } // 如果当前线程为持有锁的线程,则执行重入逻辑 else if (current == getExclusiveOwnerThread()) { // 计算重入后的同步状态,acquires 一般为1 int nextc = c + acquires; // 如果重入次数超过限制,这里会抛出异常 if (nextc < 0) throw new Error("Maximum lock count exceeded"); // 设置重入后的同步状态 setState(nextc); return true; } return false; } +--- AbstractQueuedSynchronizer.java /** 该方法用于判断同步队列中有比当前线程等待时间更长的线程 */ public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; /* * 在同步队列中,头结点是已经获取了锁的节点,头结点的后继节点则是即将获取锁的节点。 * 如果有节点对应的线程等待的时间比当前线程长,则返回 true,否则返回 false */ return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } ReentrantLock 中获取锁的流程并不是很复杂,上面的代码执行流程如下: 调用 acquire 方法,将线程放入同步队列中进行等待 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑 3.2.2 非公平锁 分析完公平锁相关代码,下面再来看看非公平锁的源码分析,如下: +--- ReentrantLock.NonfairSync final void lock() { /* * 这里调用直接 CAS 设置 state 变量,如果设置成功,表明加锁成功。这里并没有像公平锁 * 那样调用 acquire 方法让线程进入同步队列进行排队,而是直接调用 CAS 抢占锁。抢占失败 * 再调用 acquire 方法将线程置于队列尾部排队。 */ if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } +--- AbstractQueuedSynchronizer /** 参考上一节的分析 */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } +--- ReentrantLock.NonfairSync protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } +--- ReentrantLock.Sync final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 获取同步状态 int c = getState(); // 如果同步状态 c = 0,表明锁当前没有线程获得,此时可加锁。 if (c == 0) { // 调用 CAS 加锁,如果失败,则说明有其他线程在竞争获取锁 if (compareAndSetState(0, acquires)) { // 设置当前线程为锁的持有线程 setExclusiveOwnerThread(current); return true; } } // 如果当前线程已经持有锁,此处条件为 true,表明线程需再次获取锁,也就是重入 else if (current == getExclusiveOwnerThread()) { // 计算重入后的同步状态值,acquires 一般为1 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); // 设置新的同步状态值 setState(nextc); return true; } return false; } 非公平锁的实现也不是很复杂,其加锁的步骤大致如下: 调用 compareAndSetState 方法抢占式加锁,加锁成功则将自己设为持锁线程,并返回 若加锁失败,则调用 acquire 方法,将线程置于同步队列尾部进行等待 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑 3.2.3 公平和非公平细节对比 如果大家之前阅读过公平锁和非公平锁的源码,会发现两者之间的差别不是很大。为了找出它们之间的差异,这里我将两者的对比代码放在一起,大家可以比较一下,如下: 从上面的源码对比图中,可以看出两种的差异并不大。那么现在请大家思考一个问题:在代码差异不大情况下,是什么差异导致了公平锁和非公平锁的产生呢?大家先思考一下,答案将会在下面展开说明。 在上面的源码对比图中,左边是非公平锁的实现,右边是公平锁的实现。从对比图中可看出,两者的 lock 方法有明显区别。非公平锁的 lock 方法会首先尝试去抢占设置同步状态,而不是直接调用 acquire 将线程放入同步队列中等待获取锁。除此之外,tryAcquire 方法实现上也有差异。由于非公平锁的 tryAcquire 逻辑主要封装在 Sync 中的 nonfairTryAcquire 方法里,所以我们直接对比这个方法即可。由上图可以看出,Sync 中的 nonfairTryAcquire 与公平锁中的 tryAcquire 实现上差异并不大,唯一的差异在第18行,这里我用一条红线标注了出来。公平锁的 tryAcquire 在第18行多出了一个条件,即!hasQueuedPredecessors()。这个方法的目的是判断是否有其他线程比当前线程在同步队列中等待的时间更长。有的话,返回 true,否则返回 false。比如下图: node1 对应的线程比 node2 对应的线程在队列中等待的时间更长,如果 node2 线程调用 hasQueuedPredecessors 方法,则会返回 true。如果 node1 调用此方法,则会返回 false。因为 node1 前面只有一个头结点,但头结点已经获取同步状态,不处于等待状态。所以在所有处于等待状态的节点中,没有节点比它等待的更长了。理解了 hasQueuedPredecessors 方法的用途后,那么现在请大家思考个问题,假如把条件去掉对公平锁会有什么影响呢?答案在 lock 所调用的 acquire 方法中,再来看一遍 acquire 方法源码: public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } acquire 方法先调用子类实现的 tryAcquire 方法,用于尝试获取同步状态,调用成功则直接返回。若调用失败,则应将线程插入到同步队列尾部,按照 FIFO 原则获取锁。如果我们把 tryAcquire 中的条件!hasQueuedPredecessors()去掉,公平锁将不再那么“谦让”,它将会像非公平锁那样抢占获取锁,抢占失败才会入队。若如此,公平锁将不再公平。 3.3 释放锁 分析完了获取锁的相关逻辑,接下来再来分析一下释放锁的逻辑。与获取锁相比,释放锁的逻辑会简单一些,因为释放锁的过程没有公平和非公平之分。好了,下面开始分析 unlock 的逻辑: +--- ReentrantLock public void unlock() { // 调用 AQS 中的 release 方法 sync.release(1); } +--- AbstractQueuedSynchronizer public final boolean release(int arg) { // 调用 ReentrantLock.Sync 中的 tryRelease 尝试释放锁 if (tryRelease(arg)) { Node h = head; /* * 如果头结点的等待状态不为0,则应该唤醒头结点的后继节点。 * 这里简单说个结论: * 头结点的等待状态为0,表示头节点的后继节点线程还是活跃的,无需唤醒 */ if (h != null && h.waitStatus != 0) // 唤醒头结点的后继节点,该方法的分析请参考我写的关于 AQS 的文章 unparkSuccessor(h); return true; } return false; } +--- ReentrantLock.Sync protected final boolean tryRelease(int releases) { /* * 用同步状态量 state 减去释放量 releases,得到本次释放锁后的同步状态量。 * 当将 state 为 0,锁才能被完全释放 */ int c = getState() - releases; // 检测当前线程是否已经持有锁,仅允许持有锁的线程执行锁释放逻辑 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 如果 c 为0,则表示完全释放锁了,此时将持锁线程设为 null if (c == 0) { free = true; setExclusiveOwnerThread(null); } // 设置新的同步状态 setState(c); return free; } 重入锁的释放逻辑并不复杂,这里就不多说了。 4.总结 本文分析了可重入锁 ReentrantLock 公平与非公平获取锁以及释放锁原理,并与 synchronized 关键字进行了类比。总体来说,ReentrantLock 的原理在熟悉 AQS 原理的情况下,理解并不是很复杂。ReentrantLock 是大家经常使用的一个同步组件,还是很有必要去弄懂它的原理的。 好了,本文到这里就结束了。谢谢大家的阅读,再见。 参考 《Java并发编程实战》- Brian Goetz / Tim Peierls / Joshua Bloch / Joseph Bowbeer / David Holmes / Doug Lea 《Java并发编程的艺术》- 方腾飞 / 魏鹏 / 程晓明 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 AbstractQueuedSynchronizer (抽象队列同步器,以下简称 AQS)出现在 JDK 1.5 中,由大师 Doug Lea 所创作。AQS 是很多同步器的基础框架,比如 ReentrantLock、CountDownLatch 和 Semaphore 等都是基于 AQS 实现的。除此之外,我们还可以基于 AQS,定制出我们所需要的同步器。 AQS 的使用方式通常都是通过内部类继承 AQS 实现同步功能,通过继承 AQS,可以简化同步器的实现。如前面所说,AQS 是很多同步器实现的基础框架。弄懂 AQS 对理解 Java 并发包里的组件大有裨益,这也是我学习 AQS 并写出这篇文章的缘由。另外,需要说明的是,AQS 本身并不是很好理解,细节很多。在看的过程中药有一定的耐心,做好看多遍的准备。好了,其他的就不多说了,开始进入正题吧。 2.原理概述 在 AQS 内部,通过维护一个FIFO 队列来管理多线程的排队工作。在公平竞争的情况下,无法获取同步状态的线程将会被封装成一个节点,置于队列尾部。入队的线程将会通过自旋的方式获取同步状态,若在有限次的尝试后,仍未获取成功,线程则会被阻塞住。大致示意图如下: 当头结点释放同步状态后,且后继节点对应的线程被阻塞,此时头结点 线程将会去唤醒后继节点线程。后继节点线程恢复运行并获取同步状态后,会将旧的头结点从队列中移除,并将自己设为头结点。大致示意图如下: 3.重要方法介绍 本节将介绍三组重要的方法,通过使用这三组方法即可实现一个同步组件。 第一组方法是用于访问/设置同步状态的,如下: 方法 说明 int getState() 获取同步状态 void setState() 设置同步状态 boolean compareAndSetState(int expect, int update) 通过 CAS 设置同步状态 第二组方需要由同步组件覆写。如下: 方法 说明 boolean tryAcquire(int arg) 独占式获取同步状态 boolean tryRelease(int arg) 独占式释放同步状态 int tryAcquireShared(int arg) 共享式获取同步状态 boolean tryReleaseShared(int arg) 共享式私房同步状态 boolean isHeldExclusively() 检测当前线程是否获取独占锁 第三组方法是一组模板方法,同步组件可直接调用。如下: 方法 说明 void acquire(int arg) 独占式获取同步状态,该方法将会调用 tryAcquire 尝试获取同步状态。获取成功则返回,获取失败,线程进入同步队列等待。 void acquireInterruptibly(int arg) 响应中断版的 acquire boolean tryAcquireNanos(int arg,long nanos) 超时+响应中断版的 acquire void acquireShared(int arg) 共享式获取同步状态,同一时刻可能会有多个线程获得同步状态。比如读写锁的读锁就是就是调用这个方法获取同步状态的。 void acquireSharedInterruptibly(int arg) 响应中断版的 acquireShared boolean tryAcquireSharedNanos(int arg,long nanos) 超时+响应中断版的 acquireShared boolean release(int arg) 独占式释放同步状态 boolean releaseShared(int arg) 共享式释放同步状态 上面列举了一堆方法,看似繁杂。但稍微理一下,就会发现上面诸多方法无非就两大类:一类是独占式获取和释放共享状态,另一类是共享式获取和释放同步状态。至于这两类方法的实现细节,我会在接下来的章节中讲到,继续往下看吧。 4.源码分析 4.1 节点结构 在并发的情况下,AQS 会将未获取同步状态的线程将会封装成节点,并将其放入同步队列尾部。同步队列中的节点除了要保存线程,还要保存等待状态。不管是独占式还是共享式,在获取状态失败时都会用到节点类。所以这里我们要先看一下节点类的实现,为后面的源码分析进行简单铺垫。源码如下: static final class Node { /** 共享类型节点,标记节点在共享模式下等待 */ static final Node SHARED = new Node(); /** 独占类型节点,标记节点在独占模式下等待 */ static final Node EXCLUSIVE = null; /** 等待状态 - 取消 */ static final int CANCELLED = 1; /** * 等待状态 - 通知。某个节点是处于该状态,当该节点释放同步状态后, * 会通知后继节点线程,使之可以恢复运行 */ static final int SIGNAL = -1; /** 等待状态 - 条件等待。表明节点等待在 Condition 上 */ static final int CONDITION = -2; /** * 等待状态 - 传播。表示无条件向后传播唤醒动作,详细分析请看第五章 */ static final int PROPAGATE = -3; /** * 等待状态,取值如下: * SIGNAL, * CANCELLED, * CONDITION, * PROPAGATE, * 0 * * 初始情况下,waitStatus = 0 */ volatile int waitStatus; /** * 前驱节点 */ volatile Node prev; /** * 后继节点 */ volatile Node next; /** * 对应的线程 */ volatile Thread thread; /** * 下一个等待节点,用在 ConditionObject 中 */ Node nextWaiter; /** * 判断节点是否是共享节点 */ final boolean isShared() { return nextWaiter == SHARED; } /** * 获取前驱节点 */ final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } /** addWaiter 方法会调用该构造方法 */ Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } /** Condition 中会用到此构造方法 */ Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } } 4.2 独占模式分析 4.2.1 获取同步状态 独占式获取同步状态时通过 acquire 进行的,下面来分析一下该方法的源码。如下: /** * 该方法将会调用子类复写的 tryAcquire 方法获取同步状态, * - 获取成功:直接返回 * - 获取失败:将线程封装在节点中,并将节点置于同步队列尾部, * 通过自旋尝试获取同步状态。如果在有限次内仍无法获取同步状态, * 该线程将会被 LockSupport.park 方法阻塞住,直到被前驱节点唤醒 */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } /** 向同步队列尾部添加一个节点 */ private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // 尝试以快速方式将节点添加到队列尾部 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 快速插入节点失败,调用 enq 方法,不停的尝试插入节点 enq(node); return node; } /** * 通过 CAS + 自旋的方式插入节点到队尾 */ private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize // 设置头结点,初始情况下,头结点是一个空节点 if (compareAndSetHead(new Node())) tail = head; } else { /* * 将节点插入队列尾部。这里是先将新节点的前驱设为尾节点,之后在尝试将新节点设为尾节 * 点,最后再将原尾节点的后继节点指向新的尾节点。除了这种方式,我们还先设置尾节点, * 之后再设置前驱和后继,即: * * if (compareAndSetTail(t, node)) { * node.prev = t; * t.next = node; * } * * 但但如果是这样做,会导致一个问题,即短时内,队列结构会遭到破坏。考虑这种情况, * 某个线程在调用 compareAndSetTail(t, node)成功后,该线程被 CPU 切换了。此时 * 设置前驱和后继的代码还没带的及执行,但尾节点指针却设置成功,导致队列结构短时内会 * 出现如下情况: * * +------+ prev +-----+ +-----+ * head | | <---- | | | | tail * | | ----> | | | | * +------+ next +-----+ +-----+ * * tail 节点完全脱离了队列,这样导致一些队列遍历代码出错。如果先设置 * 前驱,在设置尾节点。及时线程被切换,队列结构短时可能如下: * * +------+ prev +-----+ prev +-----+ * head | | <---- | | <---- | | tail * | | ----> | | | | * +------+ next +-----+ +-----+ * * 这样并不会影响从后向前遍历,不会导致遍历逻辑出错。 * * 参考: * https://www.cnblogs.com/micrari/p/6937995.html */ node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } /** * 同步队列中的线程在此方法中以循环尝试获取同步状态,在有限次的尝试后, * 若仍未获取锁,线程将会被阻塞,直至被前驱节点的线程唤醒。 */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // 循环获取同步状态 for (;;) { final Node p = node.predecessor(); /* * 前驱节点如果是头结点,表明前驱节点已经获取了同步状态。前驱节点释放同步状态后, * 在不出异常的情况下, tryAcquire(arg) 应返回 true。此时节点就成功获取了同 * 步状态,并将自己设为头节点,原头节点出队。 */ if (p == head && tryAcquire(arg)) { // 成功获取同步状态,设置自己为头节点 setHead(node); p.next = null; // help GC failed = false; return interrupted; } /* * 如果获取同步状态失败,则根据条件判断是否应该阻塞自己。 * 如果不阻塞,CPU 就会处于忙等状态,这样会浪费 CPU 资源 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { /* * 如果在获取同步状态中出现异常,failed = true,cancelAcquire 方法会被执行。 * tryAcquire 需同步组件开发者覆写,难免不了会出现异常。 */ if (failed) cancelAcquire(node); } } /** 设置头节点 */ private void setHead(Node node) { // 仅有一个线程可以成功获取同步状态,所以这里不需要进行同步控制 head = node; node.thread = null; node.prev = null; } /** * 该方法主要用途是,当线程在获取同步状态失败时,根据前驱节点的等待状态,决定后续的动作。比如前驱 * 节点等待状态为 SIGNAL,表明当前节点线程应该被阻塞住了。不能老是尝试,避免 CPU 忙等。 * ————————————————————————————————————————————————————————————————— * | 前驱节点等待状态 | 相应动作 | * ————————————————————————————————————————————————————————————————— * | SIGNAL | 阻塞 | * | CANCELLED | 向前遍历, 移除前面所有为该状态的节点 | * | waitStatus < 0 | 将前驱节点状态设为 SIGNAL, 并再次尝试获取同步状态 | * ————————————————————————————————————————————————————————————————— */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; /* * 前驱节点等待状态为 SIGNAL,表示当前线程应该被阻塞。 * 线程阻塞后,会在前驱节点释放同步状态后被前驱节点线程唤醒 */ if (ws == Node.SIGNAL) return true; /* * 前驱节点等待状态为 CANCELLED,则以前驱节点为起点向前遍历, * 移除其他等待状态为 CANCELLED 的节点。 */ if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * 等待状态为 0 或 PROPAGATE,设置前驱节点等待状态为 SIGNAL, * 并再次尝试获取同步状态。 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } private final boolean parkAndCheckInterrupt() { // 调用 LockSupport.park 阻塞自己 LockSupport.park(this); return Thread.interrupted(); } /** * 取消获取同步状态 */ private void cancelAcquire(Node node) { if (node == null) return; node.thread = null; // 前驱节点等待状态为 CANCELLED,则向前遍历并移除其他为该状态的节点 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; // 记录 pred 的后继节点,后面会用到 Node predNext = pred.next; // 将当前节点等待状态设为 CANCELLED node.waitStatus = Node.CANCELLED; /* * 如果当前节点是尾节点,则通过 CAS 设置前驱节点 prev 为尾节点。设置成功后,再利用 CAS 将 * prev 的 next 引用置空,断开与后继节点的联系,完成清理工作。 */ if (node == tail && compareAndSetTail(node, pred)) { /* * 执行到这里,表明 pred 节点被成功设为了尾节点,这里通过 CAS 将 pred 节点的后继节点 * 设为 null。注意这里的 CAS 即使失败了,也没关系。失败了,表明 pred 的后继节点更新 * 了。pred 此时已经是尾节点了,若后继节点被更新,则是有新节点入队了。这种情况下,CAS * 会失败,但失败不会影响同步队列的结构。 */ compareAndSetNext(pred, predNext, null); } else { int ws; // 根据条件判断是唤醒后继节点,还是将前驱节点和后继节点连接到一起 if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) /* * 这里使用 CAS 设置 pred 的 next,表明多个线程同时在取消,这里存在竞争。 * 不过此处没针对 compareAndSetNext 方法失败后做一些处理,表明即使失败了也 * 没关系。实际上,多个线程同时设置 pred 的 next 引用时,只要有一个能设置成 * 功即可。 */ compareAndSetNext(pred, predNext, next); } else { /* * 唤醒后继节点对应的线程。这里简单讲一下为什么要唤醒后继线程,考虑下面一种情况: * head node1 node2 tail * ws=0 ws=1 ws=-1 ws=0 * +------+ prev +-----+ prev +-----+ prev +-----+ * | | <---- | | <---- | | <---- | | * | | ----> | | ----> | | ----> | | * +------+ next +-----+ next +-----+ next +-----+ * * 头结点初始状态为 0,node1、node2 和 tail 节点依次入队。node1 自旋过程中调用 * tryAcquire 出现异常,进入 cancelAcquire。head 节点此时等待状态仍然是 0,它 * 会认为后继节点还在运行中,所它在释放同步状态后,不会去唤醒后继等待状态为非取消的 * 节点 node2。如果 node1 再不唤醒 node2 的线程,该线程面临无法被唤醒的情况。此 * 时,整个同步队列就回全部阻塞住。 */ unparkSuccessor(node); } node.next = node; // help GC } } private void unparkSuccessor(Node node) { int ws = node.waitStatus; /* * 通过 CAS 将等待状态设为 0,让后继节点线程多一次 * 尝试获取同步状态的机会 */ if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; /* * 这里如果 s == null 处理,是不是表明 node 是尾节点?答案是不一定。原因之前在分析 * enq 方法时说过。这里再啰嗦一遍,新节点入队时,队列瞬时结构可能如下: * node1 node2 * +------+ prev +-----+ prev +-----+ * head | | <---- | | <---- | | tail * | | ----> | | | | * +------+ next +-----+ +-----+ * * node2 节点为新入队节点,此时 tail 已经指向了它,但 node1 后继引用还未设置。 * 这里 node1 就是 node 参数,s = node1.next = null,但此时 node1 并不是尾 * 节点。所以这里不能从前向后遍历同步队列,应该从后向前。 */ for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) // 唤醒 node 的后继节点线程 LockSupport.unpark(s.thread); } 到这里,独占式获取同步状态的分析就讲完了。如果仅分析获取同步状态的大致流程,那么这个流程并不难。但若深入到细节之中,还是需要思考思考。这里对独占式获取同步状态的大致流程做个总结,如下: 调用 tryAcquire 方法尝试获取同步状态 获取成功,直接返回 获取失败,将线程封装到节点中,并将节点入队 入队节点在 acquireQueued 方法中自旋获取同步状态 若节点的前驱节点是头节点,则再次调用 tryAcquire 尝试获取同步状态 获取成功,当前节点将自己设为头节点并返回 获取失败,可能再次尝试,也可能会被阻塞。这里简单认为会被阻塞。 上面的步骤对应下面的流程图: 上面流程图参考自《Java并发编程》第128页图 5-5,这里进行了重新绘制,并做了一定的修改。 4.2.2 释放同步状态 相对于获取同步状态,释放同步状态的过程则要简单的多,这里简单罗列一下步骤: 调用 tryRelease(arg) 尝试释放同步状态 根据条件判断是否应该唤醒后继线程 就两个步骤,下面看一下源码分析。 public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; /* * 这里简单列举条件分支的可能性,如下: * 1. head = null * head 还未初始化。初始情况下,head = null,当第一个节点入队后,head 会被初始 * 为一个虚拟(dummy)节点。这里,如果还没节点入队就调用 release 释放同步状态, * 就会出现 h = null 的情况。 * * 2. head != null && waitStatus = 0 * 表明后继节点对应的线程仍在运行中,不需要唤醒 * * 3. head != null && waitStatus < 0 * 后继节点对应的线程可能被阻塞了,需要唤醒 */ if (h != null && h.waitStatus != 0) // 唤醒后继节点,上面分析过了,这里不再赘述 unparkSuccessor(h); return true; } return false; } 4.3 共享模式分析 与独占模式不同,共享模式下,同一时刻会有多个线程获取共享同步状态。共享模式是实现读写锁中的读锁、CountDownLatch 和 Semaphore 等同步组件的基础,搞懂了,再去理解一些共享同步组件就不难了。 4.3.1 获取同步状态 共享类型的节点获取共享同步状态后,如果后继节点也是共享类型节点,当前节点则会唤醒后继节点。这样,多个节点线程即可同时获取共享同步状态。 public final void acquireShared(int arg) { // 尝试获取共享同步状态,tryAcquireShared 返回的是整型 if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; // 这里和前面一样,也是通过有限次自旋的方式获取同步状态 for (;;) { final Node p = node.predecessor(); /* * 前驱是头结点,其类型可能是 EXCLUSIVE,也可能是 SHARED. * 如果是 EXCLUSIVE,线程无法获取共享同步状态。 * 如果是 SHARED,线程则可获取共享同步状态。 * 能不能获取共享同步状态要看 tryAcquireShared 具体的实现。比如多个线程竞争读写 * 锁的中的读锁时,均能成功获取读锁。但多个线程同时竞争信号量时,可能就会有一部分线 * 程因无法竞争到信号量资源而阻塞。 */ if (p == head) { // 尝试获取共享同步状态 int r = tryAcquireShared(arg); if (r >= 0) { // 设置头结点,如果后继节点是共享类型,唤醒后继节点 setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } /** * 这个方法做了两件事情: * 1. 设置自身为头结点 * 2. 根据条件判断是否要唤醒后继节点 */ private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // 设置头结点 setHead(node); /* * 这个条件分支由 propagate > 0 和 h.waitStatus < 0 两部分组成。 * h.waitStatus < 0 时,waitStatus = SIGNAL 或 PROPAGATE。这里仅依赖 * 条件 propagate > 0 判断是否唤醒后继节点是不充分的,至于原因请参考第五章 */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; /* * 节点 s 如果是共享类型节点,则应该唤醒该节点 * 至于 s == null 的情况前面分析过,这里不在赘述。 */ if (s == null || s.isShared()) doReleaseShared(); } } /** * 该方法用于在 acquires/releases 存在竞争的情况下,确保唤醒动作向后传播。 */ private void doReleaseShared() { /* * 下面的循环在 head 节点存在后继节点的情况下,做了两件事情: * 1. 如果 head 节点等待状态为 SIGNAL,则将 head 节点状态设为 0,并唤醒后继节点 * 2. 如果 head 节点等待状态为 0,则将 head 节点状态设为 PROPAGATE,保证唤醒能够正 * 常传播下去。关于 PROPAGATE 状态的细节分析,后面会讲到。 */ for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } /* * ws = 0 的情况下,这里要尝试将状态从 0 设为 PROPAGATE,保证唤醒向后 * 传播。setHeadAndPropagate 在读到 h.waitStatus < 0 时,可以继续唤醒 * 后面的节点。 */ else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } } 到这里,共享模式下获取同步状态的逻辑就分析完了,不过我这里只做了简单分析。相对于独占式获取同步状态,共享式的情况更为复杂。独占模式下,只有一个节点线程可以成功获取同步状态,也只有获取已同步状态节点线程才可以释放同步状态。但在共享模式下,多个共享节点线程可以同时获得同步状态,在一些线程获取同步状态的同时,可能还会有另外一些线程正在释放同步状态。所以,共享模式更为复杂。这里我的脑力跟不上了,没法面面俱到的分析,见谅。 最后说一下共享模式下获取同步状态的大致流程,如下: 获取共享同步状态 若获取失败,则生成节点,并入队 如果前驱为头结点,再次尝试获取共享同步状态 获取成功则将自己设为头结点,如果后继节点是共享类型的,则唤醒 若失败,将节点状态设为 SIGNAL,再次尝试。若再次失败,线程进入等待状态 4.3.2 释放共享状态 释放共享状态主要逻辑在 doReleaseShared 中,doReleaseShared 上节已经分析过,这里就不赘述了。共享节点线程在获取同步状态和释放同步状态时都会调用 doReleaseShared,所以 doReleaseShared 是多线程竞争集中的地方。 public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } 5.PROPAGATE 状态存在的意义 AQS 的节点有几种不同的状态,这个在 4.1 节介绍过。在这几个状态中,PROPAGATE 的用途可能是最不好理解的。网上包括一些书籍关于该状态的叙述基本都是一句带过,也就是 PROPAGATE 字面意义,即向后传播唤醒动作。至于怎么传播,鲜有资料说明过。不过,好在最终我还是找到了一篇详细叙述了 PROPAGATE 状态的文章。在博客园上,博友 活在夢裡 在他的文章 AbstractQueuedSynchronizer源码解读 对 PROPAGATE,以及其他的一些细节进行了说明,很有深度。在钦佩之余,不由得感叹作者思考的很深入。在征得他的同意后,我将在本节中引用他文章中对 PROPAGATE 状态说明的部分,并进行一定的补充说明。这里感谢作者 活在夢裡 的精彩分享,若不参考他的文章,我的这篇文章内容会比较空洞。好了,其他的不多说了,继续往下分析。 在本节中,将会说明两个个问题,如下: PROPAGATE 状态用在哪里,以及怎样向后传播唤醒动作的? 引入 PROPAGATE 状态是为了解决什么问题? 这两个问题将会在下面两节中分别进行说明。 5.1 利用 PROPAGATE 传播唤醒动作 PROPAGATE 状态是用来传播唤醒动作的,那么它是在哪里进行传播的呢?答案是在setHeadAndPropagate方法中,这里再来看看 setHeadAndPropagate 方法的实现: private void setHeadAndPropagate(Node node, int propagate) { Node h = head; setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } } 大家注意看 setHeadAndPropagate 方法中那个长长的判断语句,其中有一个条件是h.waitStatus < 0,当 h.waitStatus = SIGNAL(-1) 或 PROPAGATE(-3) 是,这个条件就会成立。那么 PROPAGATE 状态是在何时被设置的呢?答案是在doReleaseShared方法中,如下: private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) {...} // 如果 ws = 0,则将 h 状态设为 PROPAGATE else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } ... } } 再回到 setHeadAndPropagate 的实现,该方法既然引入了h.waitStatus < 0这个条件,就意味着仅靠条件propagate > 0判断是否唤醒后继节点线程的机制是不充分的。至于为啥不充分,请继续往看下看。 5.2 引入 PROPAGATE 所解决的问题 PROPAGATE 的引入是为了解决一个 BUG -- JDK-6801020,复现这个 BUG 的代码如下: import java.util.concurrent.Semaphore; public class TestSemaphore { private static Semaphore sem = new Semaphore(0); private static class Thread1 extends Thread { @Override public void run() { sem.acquireUninterruptibly(); } } private static class Thread2 extends Thread { @Override public void run() { sem.release(); } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10000000; i++) { Thread t1 = new Thread1(); Thread t2 = new Thread1(); Thread t3 = new Thread2(); Thread t4 = new Thread2(); t1.start(); t2.start(); t3.start(); t4.start(); t1.join(); t2.join(); t3.join(); t4.join(); System.out.println(i); } } } 根据 BUG 的描述消息可知 JDK 6u11,6u17 两个版本受到影响。那么,接下来再来看看引起这个 BUG 的代码 -- JDK 6u17 中 setHeadAndPropagate 和 releaseShared 两个方法源码,如下: private void setHeadAndPropagate(Node node, int propagate) { setHead(node); if (propagate > 0 && node.waitStatus != 0) { /* * Don't bother fully figuring out successor. If it * looks null, call unparkSuccessor anyway to be safe. */ Node s = node.next; if (s == null || s.isShared()) unparkSuccessor(node); } } // 和 release 方法的源码基本一样 public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } 下面来简单说明 TestSemaphore 这个类的逻辑。这个类持有一个数值为 0 的信号量对象,并创建了4个线程,线程 t1 和 t2 用于获取信号量,t3 和 t4 则是调用 release() 方法释放信号量。在一般情况下,TestSemaphore 这个类的代码都可以正常执行。但当有极端情况出现时,可能会导致同步队列挂掉。这里演绎一下这个极端情况,考虑某次循环时,队列结构如下: 时刻1:线程 t3 调用 unparkSuccessor 方法,head 节点状态由 SIGNAL(-1) 变为0,并唤醒线程 t1。此时信号量数值为1。 时刻2:线程 t1 恢复运行,t1 调用 Semaphore.NonfairSync 的 tryAcquireShared,返回0。然后线程 t1 被切换,暂停运行。 时刻3:线程 t4 调用 releaseShared 方法,因 head 的状态为0,所以 t4 不会调用 unparkSuccessor 方法。 时刻4:线程 t1 恢复运行,t1 成功获取信号量,调用 setHeadAndPropagate。但因为 propagate = 0,线程 t1 无法调用 unparkSuccessor 唤醒线程 t2,t2 面临无线程唤醒的情况。因为 t2 无法退出等待状态,所以 t2.join 会阻塞主线程,导致程序挂住。 下面再来看一下修复 BUG 后的代码,根据 BUG 详情页显示,该 BUG 在 JDK 1.7 中被修复。这里找一个 JDK 7 较早版本(JDK 7u10)的代码看一下,如下: private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } } public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } } 在按照上面的代码演绎一下逻辑,如下: 时刻1:线程 t3 调用 unparkSuccessor 方法,head 节点状态由 SIGNAL(-1) 变为0,并唤醒线程t1。此时信号量数值为1。 时刻2:线程 t1 恢复运行,t1 调用 Semaphore.NonfairSync 的 tryAcquireShared,返回0。然后线程 t1 被切换,暂停运行。 时刻3:线程 t4 调用 releaseShared 方法,检测到h.waitStatus = 0,t4 将头节点等待状态由0设为PROPAGATE(-3)。 时刻4:线程 t1 恢复运行,t1 成功获取信号量,调用 setHeadAndPropagate。因 propagate = 0,propagate > 0 条件不满足。而 h.waitStatus = PROPAGATE(-3),所以条件h.waitStatus < 0成立。进而,线程 t1 可以唤醒线程 t2,完成唤醒动作的传播。 到这里关于状态 PROPAGATE 的内容就讲完了。最后,简单总结一下本章开头提的两个问题。 问题一:PROPAGATE 状态用在哪里,以及怎样向后传播唤醒动作的? 答:PROPAGATE 状态用在 setHeadAndPropagate。当头节点状态被设为 PROPAGATE 后,后继节点成为新的头结点后。若 propagate > 0 条件不成立,则根据条件h.waitStatus < 0成立与否,来决定是否唤醒后继节点,即向后传播唤醒动作。 问题二:引入 PROPAGATE 状态是为了解决什么问题? 答:引入 PROPAGATE 状态是为了解决并发释放信号量所导致部分请求信号量的线程无法被唤醒的问题。 声明: 本章内容是在博友 活在夢裡 的文章 AbstractQueuedSynchronizer源码解读 基础上,进行了一定的补充说明。本章所参考的观点已经过原作者同意,为避免抄袭嫌疑,特此声明。 6.总结 到这里,本文就差不多结束了。如果大家从头看到尾,到这里就可以放松一下了。写到这里,我也可以放松一下了。这篇文章总共花费了我十多天的空闲时间,确实不容易。本来我只打算讲一下基本原理,但知道后来看到本文多次推荐的那篇文章。那篇文章给我的第一感觉是,作者很厉害。第二感觉是,我也要写出一篇较为深入的 AQS 分析文章。虽然写出来也不能怎么样,水平也不会因此提高多少,也不会去造个类似的轮子。但是写完后,确实感觉很有成就感。本文的最后,来说一下如何学习 AQS 原理。AQS 的大致原理不是很难理解,所以一开始不建议纠结细节,应该先弄懂它的大致原理。在此基础上,再去分析一些细节,分析细节时,要从多线程的角度去考虑。比如,有点地方 CAS 失败后要重试,有的不用重试。总体来说 AQS 的大致原理容易理解,细节部分比较复杂。很多细节要在脑子里演绎一遍,好好思考才能想通,有点烧脑。另外因为文章篇幅的问题,关于 AQS ConditionObject 部分的分析将会放在下一篇文章中进行。 最后,再向 AQS 的作者 Doug Lea 致以崇高的敬意。仅尽力弄懂 AQS 的原理都很难了,可想而知,实现 AQS 的难度有多大。 限于本人的能力,加之深入分析 AQS 本身就比较有难度。所以文中难免会有错误出现,如果不慎翻车,请见谅。也欢迎在评论区指明这些错误,感谢。 参考 AbstractQueuedSynchronizer源码解读 - 活在夢裡 《Java并发编程的艺术》 - 方腾飞 / 魏鹏 / 程晓明 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog.xyz 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销。在应用上,线程池可应用在后端相关服务中。比如 Web 服务器,数据库服务器等。以 Web 服务器为例,假如 Web 服务器会收到大量短时的 HTTP 请求,如果此时我们简单的为每个 HTTP 请求创建一个处理线程,那么服务器的资源将会很快被耗尽。当然我们也可以自己去管理并复用已创建的线程,以限制资源的消耗量,但这样会使用程序的逻辑变复杂。好在,幸运的是,我们不必那样做。在 JDK 1.5 中,官方已经提供了强大的线程池工具类。通过使用这些工具类,我们可以用低廉的代价使用多线程技术。 线程池作为 Java 并发重要的工具类,在会用的基础上,我觉得很有必要去学习一下线程池的相关原理。毕竟线程池除了要管理线程,还要管理任务,同时还要具备统计功能。所以多了解一点,还是可以扩充眼界的,同时也可以更为熟悉线程池技术。 2.继承体系 线程池所涉及到的接口和类并不是很多,其继承体系也相对简单。相关继承关系如下: 如上图,最顶层的接口 Executor 仅声明了一个方法execute。ExecutorService 接口在其父类接口基础上,声明了包含但不限于shutdown、submit、invokeAll、invokeAny 等方法。至于 ScheduledExecutorService 接口,则是声明了一些和定时任务相关的方法,比如 schedule和scheduleAtFixedRate。线程池的核心实现是在 ThreadPoolExecutor 类中,我们使用 Executors 调用newFixedThreadPool、newSingleThreadExecutor和newCachedThreadPool等方法创建线程池均是 ThreadPoolExecutor 类型。 以上是对线程池继承体系的简单介绍,这里先让大家对线程池大致轮廓有一定的了解。接下来我会介绍一下线程池的实现原理,继续往下看吧。 3.原理分析 3.1 核心参数分析 3.1.1 核心参数简介 如上节所说,线程池的核心实现即 ThreadPoolExecutor 类。该类包含了几个核心属性,这些属性在可在构造方法进行初始化。在介绍核心属性前,我们先来看看 ThreadPoolExecutor 的构造方法,如下: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 如上所示,构造方法的参数即核心参数,这里我用一个表格来简要说明一下各个参数的意义。如下: 参数 说明 corePoolSize 核心线程数。当线程数小于该值时,线程池会优先创建新线程来执行新任务 maximumPoolSize 线程池所能维护的最大线程数 keepAliveTime 空闲线程的存活时间 workQueue 任务队列,用于缓存未执行的任务 threadFactory 线程工厂。可通过工厂为新建的线程设置更有意义的名字 handler 拒绝策略。当线程池和任务队列均处于饱和状态时,使用拒绝策略处理新任务。默认是 AbortPolicy,即直接抛出异常 以上是各个参数的简介,下面我将会针对部分参数进行详细说明,继续往下看。 3.1.2 线程创建规则 在 Java 线程池实现中,线程池所能创建的线程数量受限于 corePoolSize 和 maximumPoolSize 两个参数值。线程的创建时机则和 corePoolSize 以及 workQueue 两个参数有关。下面列举一下线程创建的4个规则(线程池中无空闲线程),如下: 线程数量小于 corePoolSize,直接创建新线程处理新的任务 线程数量大于等于 corePoolSize,workQueue 未满,则缓存新任务 线程数量大于等于 corePoolSize,但小于 maximumPoolSize,且 workQueue 已满。则创建新线程处理新任务 线程数量大于等于 maximumPoolSize,且 workQueue 已满,则使用拒绝策略处理新任务 简化一下上面的规则: 序号 条件 动作 1 线程数 < corePoolSize 创建新线程 2 线程数 ≥ corePoolSize,且 workQueue 未满 缓存新任务 3 corePoolSize ≤ 线程数 < maximumPoolSize,且 workQueue 已满 创建新线程 4 线程数 ≥ maximumPoolSize,且 workQueue 已满 使用拒绝策略处理 3.1.3 资源回收 考虑到系统资源是有限的,对于线程池超出 corePoolSize 数量的空闲线程应进行回收操作。进行此操作存在一个问题,即回收时机。目前的实现方式是当线程空闲时间超过 keepAliveTime 后,进行回收。除了核心线程数之外的线程可以进行回收,核心线程内的空闲线程也可以进行回收。回收的前提是allowCoreThreadTimeOut属性被设置为 true,通过public void allowCoreThreadTimeOut(boolean) 方法可以设置属性值。 3.1.4 排队策略 如3.1.2 线程创建规则一节中规则2所说,当线程数量大于等于 corePoolSize,workQueue 未满时,则缓存新任务。这里要考虑使用什么类型的容器缓存新任务,通过 JDK 文档介绍,我们可知道有3中类型的容器可供使用,分别是同步队列,有界队列和无界队列。对于有优先级的任务,这里还可以增加优先级队列。以上所介绍的4中类型的队列,对应的实现类如下: 实现类 类型 说明 SynchronousQueue 同步队列 该队列不存储元素,每个插入操作必须等待另一个线程调用移除操作,否则插入操作会一直阻塞 ArrayBlockingQueue 有界队列 基于数组的阻塞队列,按照 FIFO 原则对元素进行排序 LinkedBlockingQueue 无界队列 基于链表的阻塞队列,按照 FIFO 原则对元素进行排序 PriorityBlockingQueue 优先级队列 具有优先级的阻塞队列 3.1.5 拒绝策略 如3.1.2 线程创建规则一节中规则4所说,线程数量大于等于 maximumPoolSize,且 workQueue 已满,则使用拒绝策略处理新任务。Java 线程池提供了4中拒绝策略实现类,如下: 实现类 说明 AbortPolicy 丢弃新任务,并抛出 RejectedExecutionException DiscardPolicy 不做任何操作,直接丢弃新任务 DiscardOldestPolicy 丢弃队列队首的元素,并执行新任务 CallerRunsPolicy 由调用线程执行新任务 以上4个拒绝策略中,AbortPolicy 是线程池实现类所使用的策略。我们也可以通过方法public void setRejectedExecutionHandler(RejectedExecutionHandler)修改线程池决绝策略。 3.2 重要操作 3.2.1 线程的创建与复用 在线程池的实现上,线程的创建是通过线程工厂接口ThreadFactory的实现类来完成的。默认情况下,线程池使用Executors.defaultThreadFactory()方法返回的线程工厂实现类。当然,我们也可以通过public void setThreadFactory(ThreadFactory)方法进行动态修改。具体细节这里就不多说了,并不复杂,大家可以自己去看下源码。 在线程池中,线程的复用是线程池的关键所在。这就要求线程在执行完一个任务后,不能立即退出。对应到具体实现上,工作线程在执行完一个任务后,会再次到任务队列获取新的任务。如果任务队列中没有任务,且 keepAliveTime 也未被设置,工作线程则会被一致阻塞下去。通过这种方式即可实现线程复用。 说完原理,再来看看线程的创建和复用的相关代码(基于 JDK 1.8),如下: +----ThreadPoolExecutor.Worker.java Worker(Runnable firstTask) { setState(-1); this.firstTask = firstTask; // 调用线程工厂创建线程 this.thread = getThreadFactory().newThread(this); } // Worker 实现了 Runnable 接口 public void run() { runWorker(this); } +----ThreadPoolExecutor.java final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); boolean completedAbruptly = true; try { // 循环从任务队列中获取新任务 while (task != null || (task = getTask()) != null) { w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { // 执行新任务 task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { // 线程退出后,进行后续处理 processWorkerExit(w, completedAbruptly); } } 3.2.2 提交任务 通常情况下,我们可以通过线程池的submit方法提交任务。被提交的任务可能会立即执行,也可能会被缓存或者被拒绝。任务的处理流程如下图所示: 上面的流程图不是很复杂,下面再来看看流程图对应的代码,如下: +---- AbstractExecutorService.java public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); // 创建任务 RunnableFuture<Void> ftask = newTaskFor(task, null); // 提交任务 execute(ftask); return ftask; } +---- ThreadPoolExecutor.java public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); // 如果工作线程数量 < 核心线程数,则创建新线程 if (workerCountOf(c) < corePoolSize) { // 添加工作者对象 if (addWorker(command, true)) return; c = ctl.get(); } // 缓存任务,如果队列已满,则 offer 方法返回 false。否则,offer 返回 true if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } // 添加工作者对象,并在 addWorker 方法中检测线程数是否小于最大线程数 else if (!addWorker(command, false)) // 线程数 >= 最大线程数,使用拒绝策略处理任务 reject(command); } private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get(); int rs = runStateOf(c); // Check if queue empty only if necessary. if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; for (;;) { int wc = workerCountOf(c); // 检测工作线程数与核心线程数或最大线程数的关系 if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { // 创建工作者对象,细节参考上一节所贴代码 w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { int rs = runStateOf(ctl.get()); if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); // 将 worker 对象添加到 workers 集合中 workers.add(w); int s = workers.size(); // 更新 largestPoolSize 属性 if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } if (workerAdded) { // 开始执行任务 t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; } 上面的代码略多,不过结合上面的流程图,和我所写的注释,理解主逻辑应该不难。 3.2.3 关闭线程池 我们可以通过shutdown和shutdownNow两个方法关闭线程池。两个方法的区别在于,shutdown 会将线程池的状态设置为SHUTDOWN,同时该方法还会中断空闲线程。shutdownNow 则会将线程池状态设置为STOP,并尝试中断所有的线程。中断线程使用的是Thread.interrupt方法,未响应中断方法的任务是无法被中断的。最后,shutdownNow 方法会将未执行的任务全部返回。 调用 shutdown 和 shutdownNow 方法关闭线程池后,就不能再向线程池提交新任务了。对于处于关闭状态的线程池,会使用拒绝策略处理新提交的任务。 4.几种线程池 一般情况下,我们并不直接使用 ThreadPoolExecutor 类创建线程池,而是通过 Executors 工具类去构建线程池。通过 Executors 工具类,我们可以构造5中不同的线程池。下面通过一个表格简单介绍一下几种线程池,如下: 静态构造方法 说明 newFixedThreadPool(int nThreads) 构建包含固定线程数的线程池,默认情况下,空闲线程不会被回收 newCachedThreadPool() 构建线程数不定的线程池,线程数量随任务量变动,空闲线程存活时间超过60秒后会被回收 newSingleThreadExecutor() 构建线程数为1的线程池,等价于 newFixedThreadPool(1) 所构造出的线程池 newScheduledThreadPool(int corePoolSize) 构建核心线程数为 corePoolSize,可执行定时任务的线程池 newSingleThreadScheduledExecutor() 等价于 newScheduledThreadPool(1) 5. 总结 好了,到此,本文的主要内容就结束了。在本文中,我对线程池的主要原理做了简要分析。虽然只是简要分析,但通过分析并撰写此篇文章,也使我个人对 Java 线程池有了更深的认识。需要说明的是,限于时间原因,本文并未将线程池所有的知识都说一遍。关于其他方面的东西,大家可以自己阅读以下 JDK 文档,或者翻翻源码,我这里就不多说了。 好了,本文到此结束,文中如有不妥错误之处欢迎大家指出。最后感谢大家阅读,下篇文章再见。 参考 聊聊并发(三)Java线程池的分析和使用 - 方腾飞 深入理解Java之线程池 - 海子 Java多线程18:线程池 - 五月的仓颉 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 本文是上一篇文章实践篇,在上一篇文章中,我分析了选择器 Selector 的原理。本篇文章,我们来说说 Selector 的应用,如标题所示,这里我基于 Java NIO 实现了一个简单的 HTTP 服务器。在接下来的章节中,我会详细讲解 HTTP 服务器实现的过程。另外,本文所对应的代码已经上传到 GitHub 上了,需要的自取,仓库地址为 toyhttpd。好了,废话不多说,进入正题吧。 2. 实现 本节所介绍的 HTTP 服务器是一个很简单的实现,仅支持 HTTP 协议极少的特性。包括识别文件后缀,并返回相应的 Content-Type。支持200、400、403、404、500等错误码等。由于支持的特性比较少,所以代码逻辑也比较简单,这里罗列一下: 处理请求,解析请求头 响应请求,从请求头中获取资源路径, 检测请求的资源路径是否合法 根据文件后缀匹配 Content-Type 读取文件数据,并设置 Content-Length,如果文件不存在则返回404 设置响应头,并将响应头和数据返回给浏览器。 接下来我们按照处理请求和响应请求两步操作,来说说代码实现。先来看看核心的代码结构,如下: /** * TinyHttpd * * @author code4wt * @date 2018-03-26 22:28:44 */ public class TinyHttpd { private static final int DEFAULT_PORT = 8080; private static final int DEFAULT_BUFFER_SIZE = 4096; private static final String INDEX_PAGE = "index.html"; private static final String STATIC_RESOURCE_DIR = "static"; private static final String META_RESOURCE_DIR_PREFIX = "/meta/"; private static final String KEY_VALUE_SEPARATOR = ":"; private static final String CRLF = "\r\n"; private int port; public TinyHttpd() { this(DEFAULT_PORT); } public TinyHttpd(int port) { this.port = port; } public void start() throws IOException { // 初始化 ServerSocketChannel ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress("localhost", port)); ssc.configureBlocking(false); // 创建 Selector Selector selector = Selector.open(); // 注册事件 ssc.register(selector, SelectionKey.OP_ACCEPT); while(true) { int readyNum = selector.select(); if (readyNum == 0) { continue; } Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> it = selectedKeys.iterator(); while (it.hasNext()) { SelectionKey selectionKey = it.next(); it.remove(); if (selectionKey.isAcceptable()) { SocketChannel socketChannel = ssc.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (selectionKey.isReadable()) { // 处理请求 request(selectionKey); selectionKey.interestOps(SelectionKey.OP_WRITE); } else if (selectionKey.isWritable()) { // 响应请求 response(selectionKey); } } } } private void request(SelectionKey selectionKey) throws IOException {...} private Headers parseHeader(String headerStr) {...} private void response(SelectionKey selectionKey) throws IOException {...} private void handleOK(SocketChannel channel, String path) throws IOException {...} private void handleNotFound(SocketChannel channel) {...} private void handleBadRequest(SocketChannel channel) {...} private void handleForbidden(SocketChannel channel) {...} private void handleInternalServerError(SocketChannel channel) {...} private void handleError(SocketChannel channel, int statusCode) throws IOException {...} private ByteBuffer readFile(String path) throws IOException {...} private String getExtension(String path) {...} private void log(String ip, Headers headers, int code) {} } 上面的代码是 HTTP 服务器的核心类的代码结构。其中 request 负责处理请求,response 负责响应请求。handleOK 方法用于响应正常的请求,handleNotFound 等方法用于响应出错的请求。readFile 方法用于读取资源文件,getExtension 则是获取文件后缀。 2.1 处理请求 处理请求的逻辑比较简单,主要的工作是解析消息头。相关代码如下: private void request(SelectionKey selectionKey) throws IOException { // 从通道中读取请求头数据 SocketChannel channel = (SocketChannel) selectionKey.channel(); ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); channel.read(buffer); buffer.flip(); byte[] bytes = new byte[buffer.limit()]; buffer.get(bytes); String headerStr = new String(bytes); try { // 解析请求头 Headers headers = parseHeader(headerStr); // 将请求头对象放入 selectionKey 中 selectionKey.attach(Optional.of(headers)); } catch (InvalidHeaderException e) { selectionKey.attach(Optional.empty()); } } private Headers parseHeader(String headerStr) { if (Objects.isNull(headerStr) || headerStr.isEmpty()) { throw new InvalidHeaderException(); } // 解析请求头第一行 int index = headerStr.indexOf(CRLF); if (index == -1) { throw new InvalidHeaderException(); } Headers headers = new Headers(); String firstLine = headerStr.substring(0, index); String[] parts = firstLine.split(" "); /* * 请求头的第一行必须由三部分构成,分别为 METHOD PATH VERSION * 比如: * GET /index.html HTTP/1.1 */ if (parts.length < 3) { throw new InvalidHeaderException(); } headers.setMethod(parts[0]); headers.setPath(parts[1]); headers.setVersion(parts[2]); // 解析请求头属于部分 parts = headerStr.split(CRLF); for (String part : parts) { index = part.indexOf(KEY_VALUE_SEPARATOR); if (index == -1) { continue; } String key = part.substring(0, index); if (index == -1 || index + 1 >= part.length()) { headers.set(key, ""); continue; } String value = part.substring(index + 1); headers.set(key, value); } return headers; } 简单总结一下上面的代码逻辑,首先是从通道中读取请求头,然后解析读取到的请求头,最后将解析出的 Header 对象放入 selectionKey 中。处理请求的逻辑很简单,不多说了。 2.2 响应请求 看完处理请求的逻辑,接下来再来看看响应请求的逻辑。代码如下: private void response(SelectionKey selectionKey) throws IOException { SocketChannel channel = (SocketChannel) selectionKey.channel(); // 从 selectionKey 中取出请求头对象 Optional<Headers> op = (Optional<Headers>) selectionKey.attachment(); // 处理无效请求,返回 400 错误 if (!op.isPresent()) { handleBadRequest(channel); channel.close(); return; } String ip = channel.getRemoteAddress().toString().replace("/", ""); Headers headers = op.get(); // 如果请求 /meta/ 路径下的资源,则认为是非法请求,返回 403 错误 if (headers.getPath().startsWith(META_RESOURCE_DIR_PREFIX)) { handleForbidden(channel); channel.close(); log(ip, headers, FORBIDDEN.getCode()); return; } try { handleOK(channel, headers.getPath()); log(ip, headers, OK.getCode()); } catch (FileNotFoundException e) { // 文件未发现,返回 404 错误 handleNotFound(channel); log(ip, headers, NOT_FOUND.getCode()); } catch (Exception e) { // 其他异常,返回 500 错误 handleInternalServerError(channel); log(ip, headers, INTERNAL_SERVER_ERROR.getCode()); } finally { channel.close(); } } // 处理正常的请求 private void handleOK(SocketChannel channel, String path) throws IOException { ResponseHeaders headers = new ResponseHeaders(OK.getCode()); // 读取文件 ByteBuffer bodyBuffer = readFile(path); // 设置响应头 headers.setContentLength(bodyBuffer.capacity()); headers.setContentType(ContentTypeUtils.getContentType(getExtension(path))); ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes()); // 将响应头和资源数据一同返回 channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer}); } // 处理请求资源未发现的错误 private void handleNotFound(SocketChannel channel) { try { handleError(channel, NOT_FOUND.getCode()); } catch (Exception e) { handleInternalServerError(channel); } } private void handleError(SocketChannel channel, int statusCode) throws IOException { ResponseHeaders headers = new ResponseHeaders(statusCode); // 读取文件 ByteBuffer bodyBuffer = readFile(String.format("/%d.html", statusCode)); // 设置响应头 headers.setContentLength(bodyBuffer.capacity()); headers.setContentType(ContentTypeUtils.getContentType("html")); ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes()); // 将响应头和资源数据一同返回 channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer}); } 上面的代码略长,不过逻辑仍然比较简单。首先,要判断请求头存在,以及资源路径是否合法。如果都合法,再去读取资源文件,如果文件不存在,则返回 404 错误码。如果发生其他异常,则返回 500 错误。如果没有错误发生,则正常返回响应头和资源数据。这里只贴了核心代码,其他代码就不贴了,大家自己去看吧。 2.3 效果演示 分析完代码,接下来看点轻松的吧。下面贴一张代码的运行效果图,如下: 3.总结 本文所贴的代码是我在学习 Selector 过程中写的,核心代码不到 300 行。通过动手写代码,也使得我加深了对 Selector 的了解。在学习 JDK 的过程中,强烈建议大家多动手写代码。通过写代码,并踩一些坑,才能更加熟练运用相关技术。这个是我写 NIO 系列文章的一个感触。 好了,本文到这里结束。谢谢阅读! 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 前面的文章说了缓冲区,说了通道,本文就来说说 NIO 中另一个重要的实现,即选择器 Selector。在更早的文章中,我简述了几种 IO 模型。如果大家看过之前的文章,并动手写过代码的话。再看 Java 的选择器大概就会知道它是什么了,以及怎么用了。选择器是 Java 多路复用模型的一个实现,可以同时监控多个非阻塞套接字通道。示意图大致如下: 如果大家了解过多路复用模型,那应该也会知道几种复用模型的实现。比如 select,poll 以及 Linux 下的 epoll 和 BSD 下的 kqueue。Java 的选择器并非凭空创造,而是在底层操作系统提供的接口的基础上封装而来。相关的细节,我随后会进行分析。 关于 Java 选择器的简介这里先说到这,接下来进入正题。 2.基本操作及实现 本章我将对 Selector 的创建,通道的注册,Selector 的选择过程进行分析。内容篇幅较大,希望大家耐心看完。由于 Selector 相关类在不同操作系统下的实现是不同的,加之个人对 Linux epoll 更为熟悉,所以本文所分析的源码也是和 epoll 相关的。好了,进入正题吧。 2.1 创建选择器 选择器 Selector 是一个抽象类,所以不能直接创建。Selector 提供了一个 open 方法,通过 open 方法既可以创建选择器实例。示例代码如下: Selector selector = Selector.open(); 上面的代码比较简单,只有一行。不过不要被表象迷惑,这行代码仅是完整实现的冰山一角,更复杂的逻辑则隐藏在水面之下。 在简介一节,我已经说了 Java 选择器是对底层多路复用接口的一个包装,这里的 open 方法也不例外。假设我们的 Java 运行在 Linux 平台下,那么 open 最终所做的事情应该是调用操作系统的epoll_create函数,用于创建 epoll 实例。真实情况是不是如此呢?答案就在冰山深处,接下来就让我们一起去求索吧。下面我们将沿着 open 方法一路走下去,如下: public abstract class Selector implements Closeable { public static Selector open() throws IOException { // 创建 SelectorProvider,再通过其 openSelector 方法创建 Selector return SelectorProvider.provider().openSelector(); } // 省略无关代码 } public abstract class SelectorProvider { public static SelectorProvider provider() { synchronized (lock) { if (provider != null) return provider; return AccessController.doPrivileged( new PrivilegedAction<SelectorProvider>() { public SelectorProvider run() { if (loadProviderFromProperty()) return provider; if (loadProviderAsService()) return provider; // 创建默认的 SelectorProvider provider = sun.nio.ch.DefaultSelectorProvider.create(); return provider; } }); } } } public class DefaultSelectorProvider { private DefaultSelectorProvider() { } /** * 根据系统名称创建相应的 SelectorProvider */ public static SelectorProvider create() { String osname = AccessController .doPrivileged(new GetPropertyAction("os.name")); if (osname.equals("SunOS")) return createProvider("sun.nio.ch.DevPollSelectorProvider"); if (osname.equals("Linux")) return createProvider("sun.nio.ch.EPollSelectorProvider"); // return new sun.nio.ch.PollSelectorProvider(); } /** * 加载 SelectorProvider 类,并创建实例 */ @SuppressWarnings("unchecked") private static SelectorProvider createProvider(String cn) { Class<SelectorProvider> c; try { c = (Class<SelectorProvider>)Class.forName(cn); } catch (ClassNotFoundException x) { throw new AssertionError(x); } try { return c.newInstance(); } catch (IllegalAccessException | InstantiationException x) { throw new AssertionError(x); } } } /** * 创建完 SelectorProvider,接下来要调用 openSelector 方法 * 创建 Selector 的继承类了。 */ public class EPollSelectorProvider extends SelectorProviderImpl { public AbstractSelector openSelector() throws IOException { return new EPollSelectorImpl(this); } } class EPollSelectorImpl extends SelectorImpl { EPollSelectorImpl(SelectorProvider sp) throws IOException { // 调用父类构造方法 super(sp); long pipeFds = IOUtil.makePipe(false); fd0 = (int) (pipeFds >>> 32); fd1 = (int) pipeFds; // 创建 EPollArrayWrapper,EPollArrayWrapper 是一个重要的实现 pollWrapper = new EPollArrayWrapper(); pollWrapper.initInterrupt(fd0, fd1); fdToKey = new HashMap<>(); } } public abstract class SelectorImpl extends AbstractSelector { protected SelectorImpl(SelectorProvider sp) { super(sp); keys = new HashSet<SelectionKey>(); selectedKeys = new HashSet<SelectionKey>(); /* 初始化 publicKeys 和 publicSelectedKeys, * publicKeys 即 selector.keys() 方法所返回的集合, * publicSelectedKeys 则是 selector.selectedKeys() 方法返回的集合 */ if (Util.atBugLevel("1.4")) { publicKeys = keys; publicSelectedKeys = selectedKeys; } else { publicKeys = Collections.unmodifiableSet(keys); publicSelectedKeys = Util.ungrowableSet(selectedKeys); } } } /** * EPollArrayWrapper 一个重要的实现,这一层再往下就是 C 代码了 */ class EPollArrayWrapper { EPollArrayWrapper() throws IOException { // 调用 epollCreate 方法创建 epoll 文件描述符 epfd = epollCreate(); // the epoll_event array passed to epoll_wait // 初始化 pollArray,该对象用于存储就绪文件描述符和事件 int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT; pollArray = new AllocatedNativeObject(allocationSize, true); pollArrayAddress = pollArray.address(); // eventHigh needed when using file descriptors > 64k if (OPEN_MAX > MAX_UPDATE_ARRAY_SIZE) eventsHigh = new HashMap<>(); } // epollCreate 方法是 native 类型的 private native int epollCreate(); } 以上代码时 Java 层面的,Java 层调用栈最下面的类是 EPollArrayWrapper(源码路径可以在附录中查找)。EPollArrayWrapper 是一个重要的实现,起着承上启下的作用。上层是 Java 代码,下层是 C 代码。上层的代码看完了,接下来看看冰山深处的 C 代码: JNIEXPORT jint JNICALL Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this) { // 调用 epoll_create 函数创建 epoll 实例,并返回文件描述符 epfd int epfd = epoll_create(256); if (epfd < 0) { JNU_ThrowIOExceptionWithLastError(env, "epoll_create failed"); } return epfd; } 上面的代码很简单,仅做了创建 epoll 实例这一件事。看到这里,答案就明了了。最后在附一张时序图帮助大家理清代码调用顺序,如下: 2.2 选择键 2.2.1 几种事件 选择键 SelectionKey 包含4种事件,分别是: public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4; 事件之间可以通过或运算进行组合,比如: int interestOps = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 2.2.2 两种事件集合:interestOps 和 readyOps interestOps 即感兴趣的事件集合,通道调用 register 方法注册时会设置此值,interestOps 可通过 SelectionKey interestOps() 方法获取。readyOps 是就绪事件集合,可通过 SelectionKey readyOps() 获取。 interestOps 和 readyOps 被声明在 SelectionKey 子类 SelectionKeyImpl 中,代码如下: public class SelectionKeyImpl extends AbstractSelectionKey { private volatile int interestOps; private int readyOps; } 接下来再来看看与 readyOps 事件集合相关的几个方法,如下: selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable(); 以上方法从字面意思上就可以知道有什么用,这里就不解释了。接下来以 isReadable 方法为例,简单看一下这个方法是如何实现。 public final boolean isReadable() { return (readyOps() & OP_READ) != 0; } 上面说到可以通过或运算组合事件,这里则是通过与运算来测试某个事件是否在事件集合中。比如 readyOps = SelectionKey.OP_READ | SelectionKey.OP_WRITE = 0101, readyOps & OP_READ = 0101 & 0001 = 0001, readyOps & OP_CONNECT = 0101 & 1000 = 0 readyOps & OP_READ != 0,所以 OP_READ 在事件集合中。readyOps & OP_CONNECT == 0,所以 OP_CONNECT 不在事件集合中。 2.2.3 attach 方法 attach 是一个好用的方法,通过这个方法,可以将对象暂存在 SelectionKey 中,待需要的时候直接取出来即可。比如本文对应的练习代码实现了一个简单的 HTTP 服务器,在读取用户请求数据后(即 selectionKey.isReadable() 为 true),会去解析请求头,然后将请求头信息通过 attach 方法放入 selectionKey 中。待通道可写后,再从 selectionKey 中取出请求头,并根据请求头回复客户端不同的消息。当然,这只是一个应用场景,attach 可能还有其他的应用场景,比如标识通道。不过其他的场景我没使用过,就不说了。attach 使用方式如下: selectionKey.attach(obj); Object attachedObj = selectionKey.attachment(); 2.3 通道注册 通道注册即将感兴趣的事件告知 Selector,待事件发生时,Selector 即可返回就绪事件,我们就可以去做后续的事情了。比如 ServerSocketChannel 通道通常对 OP_ACCEPT 事件感兴趣,那么我们就可以把这个事件注册给 Selector。待事件发生,即服务端接受客户端连接后,我们即可获取这个就绪的事件并做相应的操作。通道注册的示例代码如下: channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ); 起初我以为通道注册操作会调用操作系统的 epoll_ctl 函数,但最终通过看源码,发现自己的理解是错的。既然通道注册阶段不调用 epoll_ctl 函数。那么,epoll_ctl 什么时候才会被调用呢?如果不调用 epoll_ctl,那么注册过程都干了什么事情呢?关于第一个问题,本节还无法解答,不过第二个问题则可以说说。接下来让我们深入通道类 register 方法的调用栈中去探寻答案吧。 public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel { public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException { return register(sel, ops, null); } public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException; } public abstract class AbstractSelectableChannel extends SelectableChannel { private SelectionKey[] keys = null; public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException { synchronized (regLock) { // 省去一些校验代码 // 从 keys 数组中查找,查找条件为 k.selector() == sel SelectionKey k = findKey(sel); // 如果 k 不为空,则修改 k 所感兴趣的事件 if (k != null) { k.interestOps(ops); k.attach(att); } // k 为空,则创建一个 SelectionKey,并存储到 keys 数组中 if (k == null) { // New registration synchronized (keyLock) { if (!isOpen()) throw new ClosedChannelException(); k = ((AbstractSelector)sel).register(this, ops, att); addKey(k); } } return k; } } } public abstract class AbstractSelector extends Selector { protected abstract SelectionKey register(AbstractSelectableChannel ch, int ops, Object att); } public abstract class SelectorImpl extends AbstractSelector { protected final SelectionKey register(AbstractSelectableChannel ch, int ops, Object attachment) { if (!(ch instanceof SelChImpl)) throw new IllegalSelectorException(); // 创建 SelectionKeyImpl 实例 SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this); k.attach(attachment); synchronized (publicKeys) { implRegister(k); } k.interestOps(ops); return k; } } class EPollSelectorImpl extends SelectorImpl { protected void implRegister(SelectionKeyImpl ski) { if (closed) throw new ClosedSelectorException(); SelChImpl ch = ski.channel; int fd = Integer.valueOf(ch.getFDVal()); // 存储 fd 和 SelectionKeyImpl 的映射关系 fdToKey.put(fd, ski); pollWrapper.add(fd); // 将 SelectionKeyImpl 实例存储到 keys 中(这里的 keys 声明在 SelectorImpl 类中),keys 集合可由 selector.keys() 方法获取 keys.add(ski); } } public class SelectionKeyImpl extends AbstractSelectionKey { public SelectionKey interestOps(int ops) { ensureValid(); return nioInterestOps(ops); } public SelectionKey nioInterestOps(int ops) { if ((ops & ~channel().validOps()) != 0) throw new IllegalArgumentException(); // 转换并设置感兴趣的事件 channel.translateAndSetInterestOps(ops, this); // 设置 interestOps 变量 interestOps = ops; return this; } } class SocketChannelImpl extends SocketChannel implements SelChImpl { public void translateAndSetInterestOps(int ops, SelectionKeyImpl sk) { int newOps = 0; // 转换事件 if ((ops & SelectionKey.OP_READ) != 0) newOps |= PollArrayWrapper.POLLIN; if ((ops & SelectionKey.OP_WRITE) != 0) newOps |= PollArrayWrapper.POLLOUT; if ((ops & SelectionKey.OP_CONNECT) != 0) newOps |= PollArrayWrapper.POLLCONN; // 设置事件 sk.selector.putEventOps(sk, newOps); } } class class EPollSelectorImpl extends SelectorImpl { public void putEventOps(SelectionKeyImpl ski, int ops) { if (closed) throw new ClosedSelectorException(); SelChImpl ch = ski.channel; // 设置感兴趣的事件 pollWrapper.setInterest(ch.getFDVal(), ops); } } class EPollArrayWrapper { void setInterest(int fd, int mask) { synchronized (updateLock) { // 扩容 updateDescriptors 数组,并存储文件描述符 fd int oldCapacity = updateDescriptors.length; if (updateCount == oldCapacity) { int newCapacity = oldCapacity + INITIAL_PENDING_UPDATE_SIZE; int[] newDescriptors = new int[newCapacity]; System.arraycopy(updateDescriptors, 0, newDescriptors, 0, oldCapacity); updateDescriptors = newDescriptors; } updateDescriptors[updateCount++] = fd; // events are stored as bytes for efficiency reasons byte b = (byte)mask; assert (b == mask) && (b != KILLED); // 存储事件 setUpdateEvents(fd, b, false); } } private void setUpdateEvents(int fd, byte events, boolean force) { if (fd < MAX_UPDATE_ARRAY_SIZE) { if ((eventsLow[fd] != KILLED) || force) { eventsLow[fd] = events; } } else { Integer key = Integer.valueOf(fd); if (!isEventsHighKilled(key) || force) { eventsHigh.put(key, Byte.valueOf(events)); } } } } 到 setUpdateEvents 这个方法,整个调用栈就结束了。但是我们并未在调用栈中看到调用 epoll_ctl 函数的地方,也就是说,通道注册时,并不会立即调用 epoll_ctl,而是先将事件集合 events 存放在 eventsLow。至于 epoll_ctl 函数何时调用的,需要大家继续往下看了。 2.4 选择过程 2.4.1 选择方法 Selector 包含3种不同功能的选择方法,分别如下: int select() int select(long timeout) int selectNow() select() 是一个阻塞方法,仅在至少一个通道处于就绪状态时才返回。 select(long timeout) 同样也是阻塞方法,不过可对该方法设置超时时间(timeout > 0),使得线程不会被一直阻塞。如果 timeout = 0,会一直阻塞线程。 selectNow() 为非阻塞方法,调用后立即返回。 以上3个方法均返回 int 类型值,表示每次调用 select 或 selectNow 方法后,新就绪通道的数量。如果某个通道在上一次调用 select 方法时就已经处于就绪状态,但并未将该通道对应的 SelectionKey 对象从 selectedKeys 集合中移除。假设另一个的通道在本次调用 select 期间处于就绪状态,此时,select 返回1,而不是2。 2.4.2 选择过程 选择方法用起来虽然简单,但方法之下隐藏的逻辑还是比较复杂的。大致分为下面几个步骤: 检查已取消键集合 cancelledKeys 是否为空,不为空则将 cancelledKeys 的键从 keys 和 selectedKeys 中移除,并将键和通道注销。 调用操作系统的 epoll_ctl 函数将通道感兴趣的事件注册到 epoll 实例中 调用操作系统的 epoll_wait 函数监听事件 再次执行步骤1 更新 selectedKeys 集合,并返回就绪通道数量 上面五个步骤对应于 EPollSelectorImpl 类中 doSelect 方法的逻辑,如下: protected int doSelect(long timeout) throws IOException { if (closed) throw new ClosedSelectorException(); // 处理已取消键集合,对应步骤1 processDeregisterQueue(); try { begin(); // select 方法的核心,对应步骤2和3 pollWrapper.poll(timeout); } finally { end(); } // 处理已取消键集合,对应步骤4 processDeregisterQueue(); // 更新 selectedKeys 集合,并返回就绪通道数量,对应步骤5 int numKeysUpdated = updateSelectedKeys(); if (pollWrapper.interrupted()) { // Clear the wakeup pipe pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0); synchronized (interruptLock) { pollWrapper.clearInterrupted(); IOUtil.drain(fd0); interruptTriggered = false; } } return numKeysUpdated; } 接下来,我们按照上面的步骤顺序去分析代码实现。先来看看步骤1对应的代码: +----SelectorImpl.java void processDeregisterQueue() throws IOException { // Precondition: Synchronized on this, keys, and selectedKeys Set<SelectionKey> cks = cancelledKeys(); synchronized (cks) { if (!cks.isEmpty()) { Iterator<SelectionKey> i = cks.iterator(); // 遍历 cancelledKeys,执行注销操作 while (i.hasNext()) { SelectionKeyImpl ski = (SelectionKeyImpl)i.next(); try { // 执行注销逻辑 implDereg(ski); } catch (SocketException se) { throw new IOException("Error deregistering key", se); } finally { i.remove(); } } } } } +----EPollSelectorImpl.java protected void implDereg(SelectionKeyImpl ski) throws IOException { assert (ski.getIndex() >= 0); SelChImpl ch = ski.channel; int fd = ch.getFDVal(); // 移除 fd 和选择键键的映射关系 fdToKey.remove(Integer.valueOf(fd)); // 从 epoll 实例中删除事件 pollWrapper.remove(fd); ski.setIndex(-1); // 从 keys 和 selectedKeys 中移除选择键 keys.remove(ski); selectedKeys.remove(ski); // 注销选择键 deregister((AbstractSelectionKey)ski); // 注销通道 SelectableChannel selch = ski.channel(); if (!selch.isOpen() && !selch.isRegistered()) ((SelChImpl)selch).kill(); } 上面的代码代码逻辑不是很复杂,首先是获取 cancelledKeys 集合,然后遍历集合,并对每个选择键及其对应的通道执行注销操作。接下来再来看看步骤2和3对应的代码,如下: +----EPollArrayWrapper.java int poll(long timeout) throws IOException { // 调用 epoll_ctl 函数注册事件,对应步骤3 updateRegistrations(); // 调用 epoll_wait 函数等待事件发生,对应步骤4 updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd); for (int i=0; i<updated; i++) { if (getDescriptor(i) == incomingInterruptFD) { interruptedIndex = i; interrupted = true; break; } } return updated; } /** * Update the pending registrations. */ private void updateRegistrations() { synchronized (updateLock) { int j = 0; while (j < updateCount) { // 获取 fd 和 events,这两个值在调用 register 方法时被存储到数组中 int fd = updateDescriptors[j]; short events = getUpdateEvents(fd); boolean isRegistered = registered.get(fd); int opcode = 0; if (events != KILLED) { // 确定 opcode 的值 if (isRegistered) { opcode = (events != 0) ? EPOLL_CTL_MOD : EPOLL_CTL_DEL; } else { opcode = (events != 0) ? EPOLL_CTL_ADD : 0; } if (opcode != 0) { // 注册事件 epollCtl(epfd, opcode, fd, events); // 设置 fd 的注册状态 if (opcode == EPOLL_CTL_ADD) { registered.set(fd); } else if (opcode == EPOLL_CTL_DEL) { registered.clear(fd); } } } j++; } updateCount = 0; } // 下面两个均是 native 方法 private native void epollCtl(int epfd, int opcode, int fd, int events); private native int epollWait(long pollAddress, int numfds, long timeout, int epfd) throws IOException; } 看到 updateRegistrations 方法的实现,大家现在知道 epoll_ctl 这个函数是在哪里调用的了。在 3.2 节通道注册的结尾给大家埋了一个疑问,这里就是答案了。注册通道实际上只是先将事件收集起来,等调用 select 方法时,在一起通过 epoll_ctl 函数将事件注册到 epoll 实例中。 上面 epollCtl 和 epollWait 方法是 native 类型的,接下来我们再来看看这两个方法是如何实现的。如下: +----EPollArrayWrapper.c JNIEXPORT void JNICALL Java_sun_nio_ch_EPollArrayWrapper_epollCtl(JNIEnv *env, jobject this, jint epfd, jint opcode, jint fd, jint events) { struct epoll_event event; int res; event.events = events; event.data.fd = fd; // 调用 epoll_ctl 注册事件 RESTARTABLE(epoll_ctl(epfd, (int)opcode, (int)fd, &event), res); if (res < 0 && errno != EBADF && errno != ENOENT && errno != EPERM) { JNU_ThrowIOExceptionWithLastError(env, "epoll_ctl failed"); } } JNIEXPORT jint JNICALL Java_sun_nio_ch_EPollArrayWrapper_epollWait(JNIEnv *env, jobject this, jlong address, jint numfds, jlong timeout, jint epfd) { struct epoll_event *events = jlong_to_ptr(address); int res; if (timeout <= 0) { /* Indefinite or no wait */ // 调用 epoll_wait 等待事件 RESTARTABLE(epoll_wait(epfd, events, numfds, timeout), res); } else { /* Bounded wait; bounded restarts */ res = iepoll(epfd, events, numfds, timeout); } if (res < 0) { JNU_ThrowIOExceptionWithLastError(env, "epoll_wait failed"); } return res; } 上面的C代码没什么复杂的逻辑,这里就不多说了。如果大家对 epoll_ctl 和 epoll_wait 函数不了解,可以参考 Linux man-page。关于 epoll 的示例,也可以参考我的另一篇文章“基于epoll实现简单的web服务器”。 说完步骤2和3对应的代码,接下来再来说说步骤4和5。由于步骤4和步骤1是一样的,这里不再赘述。最后再来说说步骤5的逻辑。代码如下: +----EPollSelectorImpl.java private int updateSelectedKeys() { int entries = pollWrapper.updated; int numKeysUpdated = 0; for (int i=0; i<entries; i++) { /* 从 pollWrapper 成员变量的 pollArray 中获取文件描述符, * pollArray 中的数据由 epoll_wait 设置 */ int nextFD = pollWrapper.getDescriptor(i); SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD)); // ski is null in the case of an interrupt if (ski != null) { // 从 pollArray 中获取就绪事件集合 int rOps = pollWrapper.getEventOps(i); /* 如果 selectedKeys 已包含选择键,则选择键必须由新的事件发生时, * 才会将 numKeysUpdated + 1 */ if (selectedKeys.contains(ski)) { if (ski.channel.translateAndSetReadyOps(rOps, ski)) { numKeysUpdated++; } } else { // 转换并设置就绪事件集合 ski.channel.translateAndSetReadyOps(rOps, ski); if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) { // 更新 selectedKeys 集合,并将 numKeysUpdated + 1 selectedKeys.add(ski); numKeysUpdated++; } } } } // 返回 numKeysUpdated return numKeysUpdated; } +----SocketChannelImpl.java public boolean translateReadyOps(int ops, int initialOps, SelectionKeyImpl sk) { int intOps = sk.nioInterestOps(); // Do this just once, it synchronizes int oldOps = sk.nioReadyOps(); int newOps = initialOps; if ((ops & PollArrayWrapper.POLLNVAL) != 0) { return false; } if ((ops & (PollArrayWrapper.POLLERR | PollArrayWrapper.POLLHUP)) != 0) { newOps = intOps; sk.nioReadyOps(newOps); // No need to poll again in checkConnect, // the error will be detected there readyToConnect = true; return (newOps & ~oldOps) != 0; } /* * 转换事件 */ if (((ops & PollArrayWrapper.POLLIN) != 0) && ((intOps & SelectionKey.OP_READ) != 0) && (state == ST_CONNECTED)) newOps |= SelectionKey.OP_READ; if (((ops & PollArrayWrapper.POLLCONN) != 0) && ((intOps & SelectionKey.OP_CONNECT) != 0) && ((state == ST_UNCONNECTED) || (state == ST_PENDING))) { newOps |= SelectionKey.OP_CONNECT; readyToConnect = true; } if (((ops & PollArrayWrapper.POLLOUT) != 0) && ((intOps & SelectionKey.OP_WRITE) != 0) && (state == ST_CONNECTED)) newOps |= SelectionKey.OP_WRITE; // 设置事件 sk.nioReadyOps(newOps); // 如果新的就绪事件和老的就绪事件不相同,则返回true,否则返回 false return (newOps & ~oldOps) != 0; } 上面就是步骤5的逻辑了,简单总结一下。首先是获取就绪通道数量,然后再获取这些就绪通道对应的文件描述符 fd,以及就绪事件集合 rOps。之后调用 translateAndSetReadyOps 转换并设置就绪事件集合。最后,将选择键添加到 selectedKeys 集合中,并累加 numKeysUpdated 值,之后返回该值。 以上就是选择过程的代码讲解,贴了不少代码,可能不太好理解。Java NIO 和操作系统接口关联比较大,所以在学习 NIO 相关原理时,也应该去了解诸如 epoll 等系统调用的知识。没有这些背景知识,很多东西看起来不太好懂。好了,本节到此结束。 2.5 模板代码 使用 NIO 选择器编程时,主干代码的结构一般比较固定。所以把主干代码写好后,就可以往里填业务代码了。下面贴一个服务端的模板代码,如下: ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress("localhost", 8080)); ssc.configureBlocking(false); Selector selector = Selector.open(); ssc.register(selector, SelectionKey.OP_ACCEPT); while(true) { int readyNum = selector.select(); if (readyNum == 0) { continue; } Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> it = selectedKeys.iterator(); while(it.hasNext()) { SelectionKey key = it.next(); if(key.isAcceptable()) { // 接受连接 } else if (key.isReadable()) { // 通道可读 } else if (key.isWritable()) { // 通道可写 } it.remove(); } } 2.6 实例演示 原本打算将示例演示的代码放在本节中展示,奈何文章篇幅已经很大了,所以决定把本节的内容独立成文。在下一篇文章中,我将会演示使用 Java NIO 完成一个简单的 HTTP 服务器。这里先贴张效果图,如下: 3.总结 到这里,本文差不多就要结束了。原本只是打算简单说说 Selector 的用法,然后再写一份实例代码。但是后来发现这样写显得比较空洞,没什么深度。所以后来翻了一下 Selector 的源码,大致理解了 Selector 的逻辑,然后就有了上面的分析。不过 Selector 的逻辑并不止我上面所说的那些,还有一些内容我现在还没看,所以就没有讲。对于已写出来的分析,由于我个人水平有限,难免会有错误。如果有错误,也欢迎大家指出来,共同进步! 好了,本文到此结束,感谢大家的阅读。 参考 Java NIO Selector - jenkov.com Java NIO(6): Selector - 知乎 附录 文中贴的一些代码是没有包含在 JDK src.zip 包里的,这里单独列举出来,方便大家查找。 文件名 路径 DefaultSelectorProvider.java jdk/src/solaris/classes/sun/nio/ch/DefaultSelectorProvider.java EPollSelectorProvider.java jdk/src/solaris/classes/sun/nio/ch/EPollSelectorProvider.java SelectorImpl.java jdk/src/share/classes/sun/nio/ch/SelectorImpl.java EPollSelectorImpl.java jdk/src/solaris/classes/sun/nio/ch/EPollSelectorImpl.java EPollArrayWrapper.java jdk/src/solaris/classes/sun/nio/ch/EPollArrayWrapper.java SelectionKeyImpl.java jdk/src/share/classes/sun/nio/ch/SelectionKeyImpl.java SocketChannelImpl.java jdk/src/share/classes/sun/nio/ch/SocketChannelImpl.java EPollArrayWrapper.c jdk/src/solaris/native/sun/nio/ch/EPollArrayWrapper.c 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 前面一篇文章讲了文件通道,本文继续来说说另一种类型的通道 -- 套接字通道。在展开说明之前,咱们先来聊聊套接字的由来。套接字即 socket,最早由伯克利大学的研究人员开发,所以经常被称为Berkeley sockets。UNIX 4.2BSD 内核版本中加入了 socket 的实现,此后,很多操作系统都提供了自己的 socket 接口实现。通过 socket 接口,我们就可以与不同地址的计算机实现通信。 如果大家使用过 Unix/Linux 系统下的 socket 接口,那么对 socket 编程的过程应该有一些了解。对于 TCP 服务端,接口调用的顺序为socket() -> bind() -> listen() -> accept() -> 其他操作 -> close(),客户端的顺序为socket() -> connect() -> 其他操作 -> close()。如下图所示: * 图片来源于《深入理解计算机系统》 如上所示,直接调用操作系统 socket 相关接口还是比较麻烦的。所以我们的 Java 语言对上面的步骤进行了封装,方便使用。比如我们今天要讲的套接字通道就比原生的接口好用的多。好了,关于 socket 的简介先说到这,接下进入正题吧。 2 通道类型 Java 套接字通道包含三种类型,分别是 类型 说明 DatagramChannel UDP 网络套接字通道 SocketChannel TCP 网络套接字通道 ServerSocketChannel TCP 服务端套接字通道 Java 套接字通道类型对应于两种通信协议 TCP 和 UDP,这个大家应该都知道。本文将介绍 TCP 网络套接字通道的使用,并在最后实现一个简单的聊天功能。至于 UDP 类型的通道,大家可以自己看看。 3.基本操作 3.1 打开通道 SocketChannel 和 ServerSocketChannel 都是抽象类,所以不能直接通过构造方法创建通道。这两个类均是使用 open 方法创建通道,如下: SocketChannel socketChannel = SocketChannel.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 3.2 关闭通道 SocketChannel 和 ServerSocketChannel 均提供了 close 方法,用于关闭通道。示例如下: SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("www.coolblog.xyz", 80)); // do something... socketChannel.close(); /*******************************************************************/ ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(8080)); SocketChannel socketChannel = serverSocketChannel.accept(); // do something... socketChannel.close(); serverSocketChannel.close(); 3.3 读写操作 读操作 通过使用 SocketChannel 的 read 方法,并配合 ByteBuffer 字节缓冲区,即可以从 SocketChannel 中读取数据。示例如下: ByteBuffer buffer = ByteBuffer.allocate(32); int num = socketChannel.read(buffer); 写操作 读取数据使用的是 read 方法,那么写入自然也就是 write 方法了。NIO 通道是面向缓冲的,所以向管道中写入数据也需要和缓冲区配合才行。示例如下 String data = "Test data..." ByteBuffer buffer = ByteBuffer.allocate(32); buffer.clear(); buffer.put(data.getBytes()); bbuffer.flip(); channel.write(buffer); 3.4 非阻塞模式 与文件通道不同,套接字通道可以运行在非阻塞模式下。在此模式下,调用 connect(),read() 和 write() 等方法时,进程/线程会立即返回。设置非阻塞模式的方法为configureBlocking,我们来看一下该方法的使用示例: SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("www.coolblog.xyz", 80)); // 这里要循环检测是否已经连接上 while(!socketChannel.finishConnect()){ // do something } // 连接建立起来后,才能进行读取或写入操作 由于在非阻塞模式下,调用 connect 方法会立即返回。如果在连接未建立起来的情况下,从管道中读取,或向管道写入数据,会触发 NotYetConnectedException 异常。所以要进行循环检测,以保证连接完成建立。如果代码按照上面那样去写,会引发另外一个问题。非阻塞模式虽然不会阻塞线程,但是在方法返回后,还要进行循环检测,线程实际上还是被阻塞。出现这个问题的原因是和 Java NIO 套接字通道的 IO 模型有关,套接字通道采用的是“同步非阻塞”式 IO 模型,用户发起一个 IO 操作后,即可去做其他事情,不用等待 IO 完成。但是 IO 是否已完成,则需要用户自己时不时的去检测,这样实际上还是会浪费 CPU 资源。 关于 IO 模型相关的知识,大家可以参考我之前的一篇文章I/O模型简述 ,这里不再赘述。另外,大家还需要去参考一下权威资料《UNIX网络编程卷 第1卷:套接口API》第6章关于 IO 模型的介绍,那一章除了对5种 IO 模型进行了介绍,还介绍了同步与异步的概念,值得一读。好了,本节就先说到这里。 3.5 实例演示 本节用一个简单的例子来演示套接字通道的使用,这个例子演示了一个客户端与服务端互相聊天的场景。首先服务端会监听某个端口,等待客户端来连接。客户端连接后,由客户端先向服务端发送消息,然后服务端再回复一条消息。这样,客户端和服务端就能你一句我一句的聊起来了。背景先介绍到这,我们来看看代码实现吧,首先看看服务端的代码: package wetalk; import static wetalk.WeTalkUtils.recvMsg; import static wetalk.WeTalkUtils.sendMsg; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Scanner; /** * WeTalk 服务端 * @author coolblog.xyz * @date 2018-03-22 12:43:26 */ public class WeTalkServer { private static final String EXIT_MARK = "exit"; private int port; WeTalkServer(int port) { this.port = port; } public void start() throws IOException { // 创建服务端套接字通道,监听端口,并等待客户端连接 ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress(port)); System.out.println("服务端已启动,正在监听 " + port + " 端口......"); SocketChannel channel = ssc.accept(); System.out.println("接受来自" + channel.getRemoteAddress().toString().replace("/", "") + " 请求"); Scanner sc = new Scanner(System.in); while (true) { // 等待并接收客户端发送的消息 String msg = recvMsg(channel); System.out.println("\n客户端:"); System.out.println(msg + "\n"); // 输入信息 System.out.println("请输入:"); msg = sc.nextLine(); if (EXIT_MARK.equals(msg)) { sendMsg(channel, "bye~"); break; } // 回复客户端消息 sendMsg(channel, msg); } // 关闭通道 channel.close(); ssc.close(); } public static void main(String[] args) throws IOException { new WeTalkServer(8080).start(); } } 上面的代码基本上进行了逐步注释,应该不难理解,这里就不啰嗦了。上面有两个方法没有贴代码,就是sendMsg和recvMsg,由于通用操作,在下面的客户端代码里也可以使用,所以这里做了封装。封装代码如下: package wetalk; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; /** * 工具类 * * @author coolblog.xyz * @date 2018-03-22 13:13:41 */ public class WeTalkUtils { private static final int BUFFER_SIZE = 128; public static void sendMsg(SocketChannel channel, String msg) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); buffer.put(msg.getBytes()); buffer.flip(); channel.write(buffer); } public static String recvMsg(SocketChannel channel) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); channel.read(buffer); buffer.flip(); byte[] bytes = new byte[buffer.limit()]; buffer.get(bytes); return new String(bytes); } } 工具类的代码比较简单,没什么好说的。接下来再来看看客户端的代码。 package wetalk; import static wetalk.WeTalkUtils.recvMsg; import static wetalk.WeTalkUtils.sendMsg; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; import java.util.Scanner; /** * WeTalk 客户端 * @author coolblog.xyz * @date 2018-03-22 12:38:21 */ public class WeTalkClient { private static final String EXIT_MARK = "exit"; private String hostname; private int port; WeTalkClient(String hostname, int port) { this.hostname = hostname; this.port = port; } public void start() throws IOException { // 打开一个套接字通道,并向服务端发起连接 SocketChannel channel = SocketChannel.open(); channel.connect(new InetSocketAddress(hostname, port)); Scanner sc = new Scanner(System.in); while (true) { // 输入信息 System.out.println("请输入:"); String msg = sc.nextLine(); if (EXIT_MARK.equals(msg)) { sendMsg(channel, "bye~"); break; } // 向服务端发送消息 sendMsg(channel, msg); // 接受服务端返回的消息 msg = recvMsg(channel); System.out.println("\n服务端:"); System.out.println(msg + "\n"); } // 关闭通道 channel.close(); } public static void main(String[] args) throws IOException { new WeTalkClient("localhost", 8080).start(); } } 客户端做的事情也比较简单,首先是打开通道,然后连接服务单。紧接着进入 while 循环,然后就可以和服务端愉快的聊天了。 上面的代码和叙述都没啥意思,最后我们还是来看看上面代码的运行效果,一图胜前言。 4.总结 到这里,关于套接字通道的相关内容就讲完了,不知道大家有没有看懂。本文仅从使用的角度分析了套接字通道的用法,至于套接字通道的实现,这并不是本文关注的重点。实际上,我在上一篇文章中就说过,Java 所提供的很多类实际上是对操作系统层面上一些系统调用做了一层包装。所以大家在学习 Java 的同时,还应该去了解底层的一些东西,这样才算是知其然,又知其所以然。 好了,本文到这里就结束了,有错误的地方欢迎大家指出来。最后谢谢大家的阅读,祝周末愉快。 参考 《深入理解计算机系统》 《UNIX网络编程卷 第1卷:套接口API》 https://www.zhihu.com/question/27991975/answer/69041973 https://zhuanlan.zhihu.com/p/27365009 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 通道是 Java NIO 的核心内容之一,在使用上,通道需和缓存类(ByteBuffer)配合完成读写等操作。与传统的流式 IO 中数据单向流动不同,通道中的数据可以双向流动。通道既可以读,也可以写。这里我们举个例子说明一下,我们可以把通道看做水管,把缓存看做水塔,把文件看做水库,把水看做数据。当从磁盘中将文件数据读取到缓存中时,就是从水库向水塔里抽水。当然,从磁盘里读取数据并不会将读取的部分从磁盘里删除,但从水库里抽水,则水库里的水量在无补充的情况下确实变少了。当然,这只是一个小问题,大家不要扣这个细节哈,继续往下说。当水塔中存储了水之后,我们可以用这些水烧饭,浇花等,这就相当于处理缓存的数据。过了一段时间后,水塔需要进行清洗。这个时候需要把水塔里的水放回水库中,这就相当于向磁盘中写入数据。通过这里例子,大家应该知道通道是什么了,以及有什么用。既然知道了,那么我们继续往下看。 Java NIO 出现在 JDK 1.4 中,由于 NIO 效率高于传统的 IO,所以 Sun 公司从底层对传统 IO 的实现进行了修改。修改的方式就是在保证兼容性的情况下,使用 NIO 重构 IO 的方法实现,无形中提高了传统 IO 的效率。 2.基本操作 通道类型分为两种,一种是面向文件的,另一种是面向网络的。具体的类声明如下: FileChannel DatagramChannel SocketChannel ServerSocketChannel 正如上列表,NIO 通道涵盖了文件 IO,TCP 和 UDP 网络 IO 等通道类型。本文我们先来说说文件通道。 2.1 创建通道 FileChannel 是一个用于连接文件的通道,通过该通道,既可以从文件中读取,也可以向文件中写入数据。与SocketChannel 不同,FileChannel 无法设置为非阻塞模式,这意味着它只能运行在阻塞模式下。在使用FileChannel 之前,需要先打开它。由于 FileChannel 是一个抽象类,所以不能通过直接创建而来。必须通过像 InputStream、OutputStream 或 RandomAccessFile 等实例获取一个 FileChannel 实例。 FileInputStream fis = new FileInputStream(FILE_PATH); FileChannel channel = fis.getChannel(); FileOutputStream fos = new FileOutputStream(FILE_PATH); FileChannel channel = fis.getChannel(); RandomAccessFile raf = new RandomAccessFile(FILE_PATH , "rw"); FileChannel channel = raf.getChannel(); 2.2 读写操作 读写操作比较简单,这里直接上代码了。下面的代码会先向文件中写入数据,然后再将写入的数据读出来并打印。代码如下: // 获取管道 RandomAccessFile raf = new RandomAccessFile(FILE_PATH, "rw"); FileChannel rafChannel = raf.getChannel(); // 准备数据 String data = "新数据,时间: " + System.currentTimeMillis(); System.out.println("原数据:\n" + " " + data); ByteBuffer buffer = ByteBuffer.allocate(128); buffer.clear(); buffer.put(data.getBytes()); buffer.flip(); // 写入数据 rafChannel.write(buffer); rafChannel.close(); raf.close(); // 重新打开管道 raf = new RandomAccessFile(FILE_PATH, "rw"); rafChannel = raf.getChannel(); // 读取刚刚写入的数据 buffer.clear(); rafChannel.read(buffer); // 打印读取出的数据 buffer.flip(); byte[] bytes = new byte[buffer.limit()]; buffer.get(bytes); System.out.println("读取到的数据:\n" + " " + new String(bytes)); rafChannel.close(); raf.close(); 上面的代码输出结果如下: 2.3 数据转移操作 我们有时需要将一个文件中的内容复制到另一个文件中去,最容易想到的做法是利用传统的 IO 将源文件中的内容读取到内存中,然后再往目标文件中写入。现在,有了 NIO,我们可以利用更方便快捷的方式去完成复制操作。FileChannel 提供了一对数据转移方法 - transferFrom/transferTo,通过使用这两个方法,即可简化文件复制操作。 public static void main(String[] args) throws IOException { RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); // 将 fromFile 文件找那个的数据转移到 toFile 中去 System.out.println("before transfer: " + readChannel(toChannel)); fromChannel.transferTo(position, count, toChannel); System.out.println("after transfer : " + readChannel(toChannel)); fromChannel.close(); fromFile.close(); toChannel.close(); toFile.close(); } private static String readChannel(FileChannel channel) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(32); buffer.clear(); // 将 channel 读取位置设为 0,也就是文件开始位置 channel.position(0); channel.read(buffer); // 再次将文件位置归零 channel.position(0); buffer.flip(); byte[] bytes = new byte[buffer.limit()]; buffer.get(bytes); return new String(bytes); } 通过上面的代码,我们可以明显感受到,利用 transferTo 减少了编码量。那么为什么利用 transferTo 可以减少编码量呢?在解答这个问题前,先来说说程序读取数据和写入文件的过程。 我们现在所使用的 PC 操作系统,将内存分为了内核空间和用户空间。操作系统的内核和一些硬件的驱动程序就是运行在内核空间内,而用户空间就是我们自己写的程序所能运行的内存区域。这里,当我们调用 read 从磁盘中读取数据时,内核会首先将数据读取到内核空间中,然后再将数据从内核空间复制到用户空间内。也就是说,我们需要通过内核进行数据中转。同样,写入数据也是如此。系统先从用户空间将数据拷贝到内核空间中,然后再由内核空间向磁盘写入。相关示意图如下: 与上面的数据流向不同,FileChannel 的 transferTo 方法底层基于 sendfile64(Linux 平台下)系统调用实现。sendfile64 会直接在内核空间内进行数据拷贝,免去了内核往用户空间拷贝,用户空间再往内核空间拷贝这两步操作,因此提高了效率。其示意图如下: 通过上面的讲解,大家应该知道了 transferTo 和 transferFrom 的效率会高于传统的 read 和 write 在效率上的区别。区别的原因在于免去了内核空间和用户空间的相互拷贝,虽然内存间拷贝的速度比较快,但涉及到大量的数据拷贝时,相互拷贝的带来的消耗是不应该被忽略的。 讲完了背景知识,咱们再来看看 FileChannel 是怎样调用 sendfile64 这个函数的。相关代码如下: public long transferTo(long position, long count, WritableByteChannel target) throws IOException { // 省略一些代码 int icount = (int)Math.min(count, Integer.MAX_VALUE); if ((sz - position) < icount) icount = (int)(sz - position); long n; // Attempt a direct transfer, if the kernel supports it if ((n = transferToDirectly(position, icount, target)) >= 0) return n; // Attempt a mapped transfer, but only to trusted channel types if ((n = transferToTrustedChannel(position, icount, target)) >= 0) return n; // Slow path for untrusted targets return transferToArbitraryChannel(position, icount, target); } private long transferToDirectly(long position, int icount, WritableByteChannel target) throws IOException { // 省略一些代码 long n = -1; int ti = -1; try { begin(); ti = threads.add(); if (!isOpen()) return -1; do { n = transferTo0(thisFDVal, position, icount, targetFDVal); } while ((n == IOStatus.INTERRUPTED) && isOpen()); // 省略一些代码 return IOStatus.normalize(n); } finally { threads.remove(ti); end (n > -1); } } 从上面代码(transferToDirectly 方法可以在 openjdk/jdk/src/share/classes/sun/nio/ch/FileChannelImpl.java 中找到)中可以看得出 transferTo 的调用路径,先是调用 transferToDirectly,然后 transferToDirectly 再调用 transferTo0。transferTo0 是 native 类型的方法,我们再去看看 transferTo0 是怎样实现的,其代码在openjdk/jdk/src/solaris/native/sun/nio/ch/FileChannelImpl.c中。 JNIEXPORT jlong JNICALL Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this, jint srcFD, jlong position, jlong count, jint dstFD) { #if defined(__linux__) off64_t offset = (off64_t)position; jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count); if (n < 0) { if (errno == EAGAIN) return IOS_UNAVAILABLE; if ((errno == EINVAL) && ((ssize_t)count >= 0)) return IOS_UNSUPPORTED_CASE; if (errno == EINTR) { return IOS_INTERRUPTED; } JNU_ThrowIOExceptionWithLastError(env, "Transfer failed"); return IOS_THROWN; } return n; // 其他平台的代码省略 #endif } 如上所示,transferTo0 最终调用了 sendfile64 函数,关于 sendfile64 这个系统调用的详细说明,请参考 man-page,这里就不展开说明了。 2.4 内存映射 内存映射这个概念源自操作系统,是指将一个文件映射到某一段虚拟内存(物理内存可能不连续)上去。我们通过对这段虚拟内存的读写即可达到对文件的读写的效果,从而可以简化对文件的操作。当然,这只是内存映射的一个优点。内存映射还有其他的一些优点,比如两个进程映射同一个文件,可以实现进程间通信。再比如,C 程序运行时需要 C 标准库支持,操作系统将 C 标准库放到了内存中,普通的 C 程序只需要将 C 标准库映射到自己的进程空间内就行了,从而可以降低内存占用。以上简单介绍了内存映射的概念及作用,关于这方面的知识,建议大家去看《深入理解计算机系统》关于内存映射的章节,讲的很好。 Unix/Linux 操作系统内存映射的系统调用mmap,Java 在这个系统调用的基础上,封装了 Java 的内存映射方法。这里我就不一步一步往下追踪了,大家有兴趣可以自己追踪一下 Java 封装的内存映射方法的调用栈。下面来简单的示例演示一下内存映射的用法: // 从标准输入获取数据 Scanner sc = new Scanner(System.in); System.out.println("请输入:"); String str = sc.nextLine(); byte[] bytes = str.getBytes(); RandomAccessFile raf = new RandomAccessFile("map.txt", "rw"); FileChannel channel = raf.getChannel(); // 获取内存映射缓冲区,并向缓冲区写入数据 MappedByteBuffer mappedBuffer = channel.map(MapMode.READ_WRITE, 0, bytes.length); mappedBuffer.put(bytes); raf.close(); raf.close(); // 再次打开刚刚的文件,读取其中的内容 raf = new RandomAccessFile("map.txt", "rw"); channel = raf.getChannel(); System.out.println("\n文件内容:") System.out.println(readChannel(channel)); raf.close(); raf.close(); 上面的代码从标准输入中获取数据,然后将数据通过内存映射缓存写入到文件中。代码运行结果如下: 接下来在用 C 代码演示上面代码的功能,如下: #include <stdio.h> #include <fcntl.h> #include <sys/mman.h> #include <memory.h> #include <unistd.h> int main() { int dstfd; void *dst; char buf[64], out[64]; int len; printf("Please input:\n"); scanf("%s", buf); len = strlen(buf); // 打开文件 dstfd = open("dst.txt", O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); lseek(dstfd, len - 1, SEEK_SET); write(dstfd, "", 1); // 将文件映射到内存中 dst = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, dstfd, 0); // 将输入的数据拷贝到映射内存中 memcpy(dst, buf, len); munmap(dst, len); close(dstfd); // 重新打开文件,并输出文件内容 dstfd = open("dst.txt", O_RDONLY); dst = mmap(NULL, len, PROT_READ, MAP_SHARED, dstfd, 0); bzero(out, 64); memcpy(out, dst, len); printf("\nfile content:\n%s\n", out); munmap(dst, len); close(dstfd); return 0; } 关于 mmap 函数的参数说明,这里就不细说了,大家可以参考 man-page。上面的代码运行结果如下: 关于内存映射就说到了,更深入的分析需要涉及到很多操作系统层面的东西。我对这些东西了解的也不多,所以就不继续分析了,惭愧惭愧。 2.5 其他操作 FileChannel 还有一些其他的方法,这里通过一个表格来列举这些方法,就不一一展开说明了。如下: 方法名 用途 position 返回或修改通道读写位置 size 获取通道所关联文件的大小 truncate 截断通道所关联的文件 force 强制将通道中的新数据刷新到文件中 close 关闭通道 lock 对通道文件进行加锁 以上所列举的方法用起来比较简单,大家自己写代码验证一下吧,这里就不贴代码了。 3.总结 以上章节对 NIO 文件通道的用法和部分方法的实现进行了简单分析。从上面的分析可以看出,NIO FileChannel 在实现上,实际上是对底层操作系统的一些 API 进行了再次封装,也就是一层皮。有了这层封装后,对上就屏蔽了底层 API 的细节,以降低使用难度。Java 为了提高开发效率,屏蔽了操作系统层面的细节。虽然 Java 可以屏蔽这些细节,但作为开发人员,我觉得我们不能也去屏蔽这些细节(虽然不了解这些细节也能写代码),有时间还是应该多了解了解这些底层的东西。毕竟要想往更高的层次发展,这些底层的知识必不可少。说到这里,感觉很惭愧,我的技术基础也很薄弱。大学期间没有意识到专业基础课的重要性,学了很多东西,但忽略了基础。好在工作不久后看了很多牛人的博客,也意识到了自己的不足。现在静下心来打基础,算是亡羊补牢吧。 好了,关于文件通道的内容这里就说到这,谢谢大家的阅读。 参考 《Java 编程思想》 《深入理解计算机系统》 Java NIO Channel 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.简介 Java NIO 相关类在 JDK 1.4 中被引入,用于提高 I/O 的效率。Java NIO 包含了很多东西,但核心的东西不外乎 Buffer、Channel 和 Selector。本文中,我们先来聊聊的 Buffer 的实现。Channel 和 Selector 将在随后的文章中讲到。 2.继承体系 Buffer 的继承类比较多,用于存储各种类型的数据。包括 ByteBuffer、CharBuffer、IntBuffer、FloatBuffer 等等。这其中,ByteBuffer 最为常用。所以接下来将会主要分析 ByteBuffer 的实现。Buffer 的继承体系图如下: 3.属性及相关操作 Buffer 本质就是一个数组,只不过在数组的基础上进行适当的封装,方便使用。 Buffer 中有几个重要的属性,通过这几个属性来显示数据存储的信息。这个属性分别是: 属性 说明 capacity 容量 Buffer 所能容纳数据元素的最大数量,也就是底层数组的容量值。在创建时被指定,不可更改。 position 位置 下一个被读或被写的位置 limit 上界 可供读写的最大位置,用于限制 position,position < limit mark 标记 位置标记,用于记录某一次的读写位置,可以通过 reset 重新回到这个位置 3.1 ByteBuffer 初始化 ByteBuffer 可通过 allocate、allocateDirect 和 wrap 等方法初始化,这里以 allocate 为例: public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity); } HeapByteBuffer(int cap, int lim) { super(-1, 0, lim, cap, new byte[cap], 0); } ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) { super(mark, pos, lim, cap); this.hb = hb; this.offset = offset; } 上面是 allocate 创建 ByteBuffer 的过程,ByteBuffer 是抽象类,所以实际上创建的是其子类 HeapByteBuffer。HeapByteBuffer 在构造方法里调用父类构造方法,将一些参数值传递给父类。最后父类再做一次中转,相关参数最终被传送到 Buffer 的构造方法中了。我们再来看一下 Buffer 的源码: public abstract class Buffer { // Invariants: mark <= position <= limit <= capacity private int mark = -1; private int position = 0; private int limit; private int capacity; Buffer(int mark, int pos, int lim, int cap) { // package-private if (cap < 0) throw new IllegalArgumentException("Negative capacity: " + cap); this.capacity = cap; limit(lim); position(pos); if (mark >= 0) { if (mark > pos) throw new IllegalArgumentException("mark > position: (" + mark + " > " + pos + ")"); this.mark = mark; } } } Buffer 创建完成后,底层数组的结构信息如下: 上面的几个属性作为公共属性,被放在了 Buffer 中,相关的操作方法也是封装在 Buffer 中。那么接下来,我们来看看这些方法吧。 3.2 ByteBuffer 读写操作 ByteBuffer 读写操作时通过 get 和 put 完成的,这两个方法都有重载,我们只看其中一个。 // 读操作 public byte get() { return hb[ix(nextGetIndex())]; } final int nextGetIndex() { if (position >= limit) throw new BufferUnderflowException(); return position++; } // 写操作 public ByteBuffer put(byte x) { hb[ix(nextPutIndex())] = x; return this; } final int nextPutIndex() { if (position >= limit) throw new BufferOverflowException(); return position++; } 读写操作都会修改 position 的值,每次读写的位置是当前 position 的下一个位置。通过修改 position,我们可以读取指定位置的数据。当然,前提是 position < limit。Buffer 中提供了position(int) 方法用于修改 position 的值。 public final Buffer position(int newPosition) { if ((newPosition > limit) || (newPosition < 0)) throw new IllegalArgumentException(); position = newPosition; if (mark > position) mark = -1; return this; } 当我们向一个刚初始化好的 Buffer 中写入一些数据时,数据存储示意图如下: 如果我们想读取刚刚写入的数据,就需要修改 position 的值。否则 position 将指向没有存储数据的空间上,读取空白空间是没意义的。如上图,我们可以将 position 设置为 0,这样就能从头读取刚刚写入的数据。 仅修改 position 的值是不够的,如果想正确读取刚刚写入的数据,还需修改 limit 的值,不然还是会读取到空白空间上的内容。我们将 limit 指向数据区域的尾部,即可避免这个问题。修改 limit 的值通过 limit(int) 方法进行。 public final Buffer limit(int newLimit) { if ((newLimit > capacity) || (newLimit < 0)) throw new IllegalArgumentException(); limit = newLimit; if (position > limit) position = limit; if (mark > limit) mark = -1; return this; } 修改后,数据存储示意图如下: 上面为了正确读取写入的数据,需要两步操作。Buffer 中提供了一个便利的方法,将这两步操作合二为一,即 flip 方法。 public final Buffer flip() { // 1. 设置 limit 为当前位置 limit = position; // 1. 设置 position 为0 position = 0; mark = -1; return this; } 3.3 ByteBuffer 标记 我们在读取或写入的过程中,可以在感兴趣的位置打上一个标记,这样我们可以通过这个标记再次回到这个位置。Buffer 中,打标记的方法是 mark,回到标记位置的方法时 reset。简单看下源码吧。 public final Buffer mark() { mark = position; return this; } public final Buffer reset() { int m = mark; if (m < 0) throw new InvalidMarkException(); position = m; return this; } 打标记及回到标记位置的流程如下: 4.DirectByteBuffer 在 ByteBuffer 初始化一节中,我介绍了 ByteBuffer 的 allocate 方法,该方法实际上创建的是 HeapByteBuffer 对象。除了 allocate 方法,ByteBuffer 还有一个方法 allocateDirect。这个方法创建的是 DirectByteBuffer 对象。两者有什么区别呢?简单的说,allocate 方法所请求的空间是在 JVM 堆内进行分配的,而 allocateDirect 请求的空间则是在 JVM 堆外的,这部分空间不被 JVM 所管理。那么堆内空间和堆空间在使用上有什么不同呢?用一个表格列举一下吧。 空间类型 优点 缺点 堆内空间 分配速度快 JVM 整理内存空间时,堆内空间的位置会被搬动,比较笨重 堆外空间 1. 空间位置固定,不用担心空间被 JVM 随意搬动 2. 降低堆内空间的使用率 1. 分配速度慢 2. 回收策略比较复杂 DirectByteBuffer 牵涉的底层技术点比较多,想要弄懂,还需要好好打基础才行。由于本人目前能力很有限,关于 DirectByteBuffer 只能简单讲讲。待后续能力提高时,我会再来重写这部分的内容。如果想了解这方面的内容,建议大家看看其他的文章。 5.总结 Buffer 是 Java NIO 中一个重要的辅助类,使用比较频繁。在不熟悉 Buffer 的情况下,有时候很容易因为忘记调用 flip 或其他方法导致程序出错。不过好在 Buffer 的源码不难理解,大家可以自己看看,这样可以避免出现一些奇怪的错误。 好了,本文到这里就结束了,谢谢阅读! 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 简介 epoll 是 Linux 平台下特有的一种 I/O 复用模型实现,于 2002 年在 Linux kernel 2.5.44 中被引入。在 epoll 之前,Unix/Linux 平台下的 I/O 复用模型包含 select 和 poll 两个系统调用。随着因特网的发展,因特网的用户量越来越大,C10K 问题出现。基于 select 和 poll 编写的网络服务已经不能满足不能满足用户的需求了,业界迫切希望更高效的系统调用出现。在此背景下,FreeBSD 的 kqueue 和 Linux 的 epoll 被研发了出来。kqueue 和 epoll 的出现,终结了 C10K 问题,C10K 问题就此作古。 因为 Linux 系统的广泛应用,所以大家在说 I/O 复用时,更多的是想到了 epoll,而不是 kqueue,本文也不例外。本篇文章不会涉及 kqueue,大家有兴趣可以自己看看。 2. 基于 epoll 实现 web 服务器 在 Linux 中,epoll 并不是一个系统调用,而是 epoll_create、epoll_ctl 和 epoll_wait 三个系统调用的统称。关于这三个系统调用的细节,这里就不说明了,大家可以自己去查 man-page。接下来,我们来直接看一个例子,这个例子基于 epoll 和 TinyHttpd 实现了一个 I/O 复用版的 HTTP Server。在上代码前,我们先来演示这个玩具版 HTTP Server 的效果。 上面就是玩具版 HTTP Server 的运行效果了,看起来还行。在我第一次把它成功跑起来的时候,感觉很奇妙。好了,看完效果,接下来看代码吧,如下: #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/sysinfo.h> #include <sys/epoll.h> #include <signal.h> #include <fcntl.h> #include <sys/wait.h> #include <sys/types.h> #include "httpd.h" #define DEFAULT_PORT 8080 #define MAX_EVENT_NUM 1024 #define INFTIM -1 void process(int); void handle_subprocess_exit(int); int main(int argc, char *argv[]) { struct sockaddr_in server_addr; int listen_fd; int cpu_core_num; int on = 1; listen_fd = socket(AF_INET, SOCK_STREAM, 0); fcntl(listen_fd, F_SETFL, O_NONBLOCK); // 设置 listen_fd 为非阻塞 setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(DEFAULT_PORT); if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("bind error, message: "); exit(1); } if (listen(listen_fd, 5) == -1) { perror("listen error, message: "); exit(1); } printf("listening 8080\n"); signal(SIGCHLD, handle_subprocess_exit); cpu_core_num = get_nprocs(); printf("cpu core num: %d\n", cpu_core_num); // 根据 CPU 数量创建子进程,为了演示“惊群现象”,这里多创建一些子进程 for (int i = 0; i < cpu_core_num * 2; i++) { pid_t pid = fork(); if (pid == 0) { // 子进程执行此条件分支 process(listen_fd); exit(0); } } while (1) { sleep(1); } return 0; } void process(int listen_fd) { int conn_fd; int ready_fd_num; struct sockaddr_in client_addr; int client_addr_size = sizeof(client_addr); char buf[128]; struct epoll_event ev, events[MAX_EVENT_NUM]; // 创建 epoll 实例,并返回 epoll 文件描述符 int epoll_fd = epoll_create(MAX_EVENT_NUM); ev.data.fd = listen_fd; ev.events = EPOLLIN; // 将 listen_fd 注册到刚刚创建的 epoll 中 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) { perror("epoll_ctl error, message: "); exit(1); } while(1) { // 等待事件发生 ready_fd_num = epoll_wait(epoll_fd, events, MAX_EVENT_NUM, INFTIM); printf("[pid %d] 震惊!我又被唤醒了...\n", getpid()); if (ready_fd_num == -1) { perror("epoll_wait error, message: "); continue; } for(int i = 0; i < ready_fd_num; i++) { if (events[i].data.fd == listen_fd) { // 有新的连接 conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_size); if (conn_fd == -1) { sprintf(buf, "[pid %d] accept 出错了: ", getpid()); perror(buf); continue; } // 设置 conn_fd 为非阻塞 if (fcntl(conn_fd, F_SETFL, fcntl(conn_fd, F_GETFD, 0) | O_NONBLOCK) == -1) { continue; } ev.data.fd = conn_fd; ev.events = EPOLLIN; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) { perror("epoll_ctl error, message: "); close(conn_fd); } printf("[pid %d] 收到来自 %s:%d 的请求\n", getpid(), inet_ntoa(client_addr.sin_addr), client_addr.sin_port); } else if (events[i].events & EPOLLIN) { // 某个 socket 数据已准备好,可以读取了 printf("[pid %d] 处理来自 %s:%d 的请求\n", getpid(), inet_ntoa(client_addr.sin_addr), client_addr.sin_port); conn_fd = events[i].data.fd; // 调用 TinyHttpd 的 accept_request 函数处理请求 accept_request(conn_fd, &client_addr); close(conn_fd); } else if (events[i].events & EPOLLERR) { fprintf(stderr, "epoll error\n"); close(conn_fd); } } } } void handle_subprocess_exit(int signo) { printf("clean subprocess.\n"); int status; while(waitpid(-1, &status, WNOHANG) > 0); } 上面的代码有点长,不过还好,基本上都是模板代码,没什么特别复杂的逻辑。希望大家耐心看一下。 上面的代码基于epoll + 多进程的方式实现,开始,主进程会通过系统调用获取 CPU 核心数,然后根据核心数创建子进程。为了演示“惊群现象”,这里多创建了一倍的子进程。关于惊群现象,下一章会讲到,大家先别急哈。创建好子进程后,主进程不需再做什么事了,核心逻辑都会在子线程中执行。首先,每个子进程都会调用 epoll_create 在内核创建 epoll 实例,然后再通过 epoll_ctl 将 listen_fd 注册到 epoll 实例中,由内核进行监控。最后,再调用 epoll_wait 等待感兴趣的事件发生。当 listen_fd 中有新的连接时,epoll_wait 会返回。此时子进程调用 accept 接受连接,并把客户端 socket 注册到 epoll 实例中,等待 EPOLLIN 事件发生。当该事件发生后,即可接受数据,并根据 HTTP 请求信息返回相应的页面了。 这里说明一下,上面代码中处理 HTTP 请求的逻辑是写在 TinyHttpd 项目中的,TinyHttpd 是一个只有 500 行左右的超轻量型Http Server,很适合学习使用。为了适应需求,我对其源码进行了一定的修改,并添加了一些注释。本章的测试代码已经放到了 github 上,需要的同学自取,传送门 -> epoll_multiprocess_server.c。 3. 惊群及演示 “惊群现象”是指并发环境下,多线程或多进程等待同一个 socket 事件,当这个事件发生时,多线程/多进程被同时唤醒,这就是“惊群现象”。对应上面的代码,多个子进程通过调用 epoll_wait 等待 listen_fd 上某个事件发生。当有新连接进来时,多个进程会被同时唤醒去处理这个事件。但最终只有一个进程可以去处理事件,其他进程重新进入等待状态。使用上面的代码可以演示惊群现象,如下: 从上图可以看出,当 listen_fd 上有新连接事件发生时,进程19571和19573被唤醒。但最终进程19573成功处理了新连接事件,进程19571则失败了。 惊群现象会影响服务器性能,因为多个进程被唤醒,但最终只有一个进程可以成功处理事件。而 CPU 需要为一个事件的发生调度数个进程,因此会浪费 CPU 资源。 对于惊群现象,处理的思路一般有两种。一种是像 Lighttpd 那样,无视惊群。另一种是像 Nginx 那样,使用全局锁避免惊群。简单起见,本文测试代码采用的是 Lighttpd 的处理方式,即无视惊群。对于这两种思路的细节,由于本人未读过两个开源软件的代码,这里就不多说了。如果大家有兴趣,可以参考网上的一些博文。 4. 总结 epoll 是 I/O 复用模型重要的一个实现,性能优异,应用广泛。像 Linux 平台下的 JVM,NIO 部分就是基于 epoll 实现的。再如大名鼎鼎 Nginx 也是使用了 epoll。由此可以看出 epoll 的重要性,因此我们有很有必要去了解 epoll。本文通过一个测试程序简单演示了一个基于 epoll 的 HTTP Server,总体上也达到了学习 epoll 的目的。大家如果有兴趣,可以下载源码看看。当然,纸上学来终觉浅,还是要自己动手写才行。本文的测试代码是本人现学现卖写的,仅测试使用,写的不好的地方望谅解。 好了,本文到此结束,谢谢阅读! 参考 关于多进程epoll与“惊群”问题 - CSDN “惊群”,看看nginx是怎么解决它的 - CSDN 高性能网络编程(二):上一个10年,著名的C10K并发连接问题 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 前言 最近在学习 Java NIO 方面的知识,为了加深理解。特地去看了 Unix/Linux I/O 方面的知识,并写了一些代码进行验证。在本文接下来的一章中,我将通过举例的方式向大家介绍五种 I/O 模型。如果大家是第一次了解 I/O 模型方面的知识,理解起来会有一定的难度。所以在看文章的同时,我更建议大家动手去实现这些 I/O 模型,感觉会不一样。好了,下面咱们一起进入正题吧。 2. I/O 模型 本章将向大家介绍五种 I/O 模型,包括阻塞 I/O、非阻塞 I/O、I/O 复用、信号驱动式 I/O 、异步 I/O 等。本文的内容参考了《UNIX网络编程》,文中所用部分图片也是来自于本书。关于《UNIX网络编程》这本书,我想就不用多说了。很多写网络编程方面的文章一般都会参考该书,本文也不例外。如果大家想进深入学习网络编程,建议去读读这本书。 2.1 阻塞 I/O 模型 阻塞 I/O 是最简单的 I/O 模型,一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作。相关示意图如下: 上图中,应用进程通过系统调用 recvfrom 接收数据,但由于内核还未准备好数据报,应用进程就阻塞住了。直到内核准备好数据报,recvfrom 完成数据报复制工作,应用进程才能结束阻塞状态。 这里简单解释一下应用进程和内核的关系。内核即操作系统内核,用于控制计算机硬件。同时将用户态的程序和底层硬件隔离开,以保障整个计算机系统的稳定运转(如果用户态的程序可以控制底层硬件,那么一些病毒就会针对硬件进行破坏,比如 CIH 病毒)。应用进程即用户态进程,运行于操作系统之上,通过系统调用与操作系统进行交互。上图中,内核指的是 TCP/IP 等协议及相关驱动程序。客户端发送的请求,并不是直接送达给应用程序,而是要先经过内核。内核将请求数据缓存在内核空间,应用进程通过 recvfrom 调用,将数据从内核空间拷贝到自己的进程空间内。大致示意图如下: 阻塞 I/O 理解起来并不难,不过这里还是举个例子类比一下。假设大家日常工作流程设这样的(其实就是我日常工作的流程),我们写好代码后,本地测试无误,通过邮件的方式,告知运维同学发布服务。运维同学通过发布脚本打包代码,重启服务(心疼我司的人肉运维)。一般项目比较大时,重启一次比较耗时。而运维同学又有点死脑筋,非要等这个服务重启好,再去做其他事。结果一天等待的时间比真正工作的时间还要长,然后就被开了。运维同学用这个例子告诉我们,阻塞式 I/O 效率不太好。 2.2 非阻塞 I/O 模型 与阻塞 I/O 模型相反,在非阻塞 I/O 模型下。应用进程与内核交互,目的未达到时,不再一味的等着,而是直接返回。然后通过轮询的方式,不停的去问内核数据准备好没。示意图如下: 上图中,应用进程通过 recvfrom 系统调用不停的去和内核交互,直到内核准备好数据报。从上面的流程中可以看出,应用进程进入轮询状态时等同于阻塞,所以非阻塞的 I/O 似乎并没有提高进程工作效率。 再用上面的例子进行类比。公司辞退了上一个怠工的运维同学后,又招了一个运维同学。这个运维同学每次重启服务,隔一分钟去看一下,然后进入发呆状态。虽然真正的工作时间增加了,但是没用啊,等待的时间还是太长了。被公司发现后,又被辞了。 2.3 I/O 复用模型 Unix/Linux 环境下的 I/O 复用模型包含三组系统调用,分别是 select、poll 和 epoll(FreeBSD 中则为 kqueue)。select 出现的时间最早,在 BSD 4.2中被引入。poll 则是在 AT&T System V UNIX 版本中被引入(详情请参考 UNIX man-page)。epoll 出现在 Linux kernel 2.5.44 版本中,与之对应的 kqueue 调用则出现在 FreeBSD 4.1,早于 epoll。select 和 poll 出现的时间比较早,在当时也是比较先进的 I/O 模型了,满足了当时的需求。不过随着因特网用户的增长,C10K 问题出现。select 和 poll 已经不能满足需求了,研发更加高效的 I/O 模型迫在眉睫。到了 2000 年,FreeBSD 率先发布了 select、poll 的改进版 kqueue。Linux 平台则在 2002 年 2.5.44 中发布了 epoll。好了,关于三者的一些历史就说到这里。本节接下来将以 select 函数为例,简述该函数的使用过程。 select 有三个文件描述符集(readfds),分别是可读文件描述符集(writefds)、可写文件描述符集和异常文件描述符集(exceptfds)。应用程序可将某个 socket (文件描述符)设置到感兴趣的文件描述符集中,并调用 select 等待所感兴趣的事件发生。比如某个 socket 处于可读状态了,此时应用进程就可调用 recvfrom 函数把数据从内核空间拷贝到进程空间内,无需再等待内核准备数据了。示意图如下: 一般情况下,应用进程会将多个 socket 设置到感兴趣的文件描述符集中,并调用 select 等待所关注的事件(比如可读、可写)处于就绪状态。当某些 socket 处于就绪状态后,select 返回处于就绪状态的 sockct 数量。注意这里返回的是 socket 的数量,并不是具体的 socket。应用程序需要自己去确定哪些 socket 处于就绪状态了,确定之后即可进行后续操作。 I/O 复用本身不是很好理解,所以这里还是举例说明吧。话说公司的运维部连续辞退两个运维同学后,运维部的 leader 觉得需要亲自监督一下大家工作。于是 leader 在周会上和大家说,从下周开始,所有的发布邮件都由他接收,并由他转发给相关运维同学,同时也由他重启服务。各位运维同学需要告诉 leader 各自所负责监控的项目,服务重启好后,leader 会通过内部沟通工具通知相关运维同学。至于服务重启的结果(成功或失败),leader 不关心,需要运维同学自己去看。运维同学看好后,需要把结果回复给开发同学。 上面的流程可能有点啰嗦,所以还是看图吧。 把上面的流程进行分步,如下: 开发同学将发布邮件发送给运维 leader,并指明这个邮件应该转发给谁 运维告诉 leader,如果有发给我的邮件,请发送给我 leader 把邮件转发给相关的运维同学,并着手重启服务 运维同学看完邮件,告诉 leader 某某服务重启好后,请告诉我 服务重启好,leader 通知运维同学xx服务启动好了 运维同学查看服务启动情况,并返回信息给开发同学 这种方式为什么可以提高工作效率呢?原因在于运维同学一股脑把他所负责的几十个项目都告诉了 leader,由 leader 重启服务,并通知运维同学。运维同学这个时候等待 leader 的通知,只要其中一个或几个服务重启好了,运维同学就回接到通知,然后就可去干活了。而不是像以前一样,非要等某个服务重启好再进行后面的工作。 说一下上面例子的角色扮演。开发同学是客户端,leader 是内核。开发同学发的邮件相当于网络请求,leader 接收邮件,并重启服务,相当于内核准备数据。运维同学是服务端应用进程,告诉 leader 自己感兴趣的事情,并在最后将事情的处理结果返回给开发同学。 不知道大家有没有理解上面的例子,I/O 复用本身可能就不太好理解,所以看不懂也不要气馁。另外,上面的例子只是为了说明情况,现实中并不会是这样干,不然 leader 要累死了。如果大家觉得上面的例子不太好,我建议大家去看看权威资料《UNIX网络编程》。同时,如果能用 select 写个简单的 tcp 服务器,有助于加深对 I/O 复用的理解。如果不会写,也可以参考我写的代码 select_server.c。 2.4 信号驱动式 I/O 模型 信号驱动式 I/O 模型是指,应用进程告诉内核,如果某个 socket 的某个事件发生时,请向我发一个信号。在收到信号后,信号对应的处理函数会进行后续处理。示意图如下: 再用之前的例子进行说明。某个运维同学比较聪明,他写了一个监控系统。重启服务的过程由监控系统来做,做好后,监控系统会给他发个通知。在此之前,运维同学可以去做其他的事情,不用一直发呆等着了。运维同学收到通知后,首先去检查服务重启情况,接着再给开发同学回复邮件就行了。 相比之前的工作方式,是不是感觉这种方式更合理。从流程上来说,这种方式确实更合理。进程在信号到来之前,可以去做其他事情,而不用忙等。但现实中,这种 I/O 模型用的并不多。 2.5 异步 I/O 模型 异步 I/O 是指应用进程把文件描述符传给内核后,啥都不管了,完全由内核去操作这个文件描述符。内核完成相关操作后,会发信号告诉应用进程,某某 I/O 操作我完成了,你现在可以进行后续操作了。示意图如下: 上图通过 aio_read 把文件描述符、数据缓存空间,以及信号告诉内核,当文件描述符处于可读状态时,内核会亲自将数据从内核空间拷贝到应用进程指定的缓存空间呢。拷贝完在告诉进程 I/O 操作结束,你可以直接使用数据了。 接着上一节的例子进行类比,运维小哥升级了他的监控系统。此时,监控系统不光可以监控服务重启状态,还能把重启结果整理好,发送给开发小哥。而运维小哥要做的事情就更简单了,收收邮件,点点监控系统上的发布按钮。然后就可以悠哉悠哉的继续睡觉了,一天一天的就这么过去了。 2.6 总结 上面介绍了5种 I/O 模型,也通过举例的形式对每种模型进行了补充说明,不知道大家看懂没。抛开上面的 I/O 模型不谈,如果某种 I/O 模型能让进程的工作的时间大于等待的时间,那么这种模型就是高效的模型。在服务端请求量变大时,通过 I/O 复用模型可以让进程进入繁忙的工作状态中,减少忙等,进而提高了效率。 I/O 复用模型结果数次改进,目前性能已经很好了,也得到了广泛应用。像 Nginx,lighttd 等服务器软件都选用该模型。好了,关于 I/O 模型就说到这里。 最后附一张几种 I/O 模型的对比图: 3. 写在最后 前面简述了几种 I/O 模型,并辅以例子进行说明。关于 I/O 模型的文章,网上有很多。大家也是各开脑洞,用了不同的例子进行类比说明,包括但不限于送外卖、送快递、飞机调度等等。在写这篇文章前,我也是绞尽脑汁,希望想一个不同的例子,不然如果和别人的太像,免不了有抄袭的嫌疑。除此之外,举的例子还要尽量是大家都知道的,同时又能说明问题。所以这篇文章想例子想的也是挺累的。另外,限于本人语言水平,文中有些地方可能未能描述清楚。如果给大家造成了困扰,在这里说声抱歉。最后声明一下,本文的例子拿运维同学举例,本人并无意黑运维同学。我们公司运维自动化程度不高,运维同事们还是很辛苦的,心疼5分钟。 好了,本文到这里就结束了,谢谢大家阅读! 参考 《UNIX网络编程》 Java NIO(3): IO模型 - 知乎 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.概述 LinkedList 是 Java 集合框架中一个重要的实现,其底层采用的双向链表结构。和 ArrayList 一样,LinkedList 也支持空值和重复值。由于 LinkedList 基于链表实现,存储元素过程中,无需像 ArrayList 那样进行扩容。但有得必有失,LinkedList 存储元素的节点需要额外的空间存储前驱和后继的引用。另一方面,LinkedList 在链表头部和尾部插入效率比较高,但在指定位置进行插入时,效率一般。原因是,在指定位置插入需要定位到该位置处的节点,此操作的时间复杂度为O(N)。最后,LinkedList 是非线程安全的集合类,并发环境下,多个线程同时操作 LinkedList,会引发不可预知的错误。 以上是对 LinkedList 的简单介绍,接下来,我将会对 LinkedList 常用操作展开分析,继续往下看吧。 2.继承体系 LinkedList 的继承体系较为复杂,继承自 AbstractSequentialList,同时又实现了 List 和 Deque 接口。继承体系图如下(删除了部分实现的接口): LinkedList 继承自 AbstractSequentialList,AbstractSequentialList 又是什么呢?从实现上,AbstractSequentialList 提供了一套基于顺序访问的接口。通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问某种序列表(比如链表)的接口。深入源码,AbstractSequentialList 提供的方法基本上都是通过 ListIterator 实现的,比如: public E get(int index) { try { return listIterator(index).next(); } catch (NoSuchElementException exc) { throw new IndexOutOfBoundsException("Index: "+index); } } public void add(int index, E element) { try { listIterator(index).add(element); } catch (NoSuchElementException exc) { throw new IndexOutOfBoundsException("Index: "+index); } } // 留给子类实现 public abstract ListIterator<E> listIterator(int index); 所以只要继承类实现了 listIterator 方法,它不需要再额外实现什么即可使用。对于随机访问集合类一般建议继承 AbstractList 而不是 AbstractSequentialList。LinkedList 和其父类一样,也是基于顺序访问。所以 LinkedList 继承了 AbstractSequentialList,但 LinkedList 并没有直接使用父类的方法,而是重新实现了一套的方法。 另外,LinkedList 还实现了 Deque (double ended queue),Deque 又继承自 Queue 接口。这样 LinkedList 就具备了队列的功能。比如,我们可以这样使用: Queue<T> queue = new LinkedList<>(); 除此之外,我们基于 LinkedList 还可以实现一些其他的数据结构,比如栈,以此来替换 Java 集合框架中的 Stack 类(该类实现的不好,《Java 编程思想》一书的作者也对此类进行了吐槽)。 关于 LinkedList 继承体系先说到这,下面进入源码分析部分。 2.源码分析 2.1 查找 LinkedList 底层基于链表结构,无法向 ArrayList 那样随机访问指定位置的元素。LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后查找,时间复杂度为 O(N)。相关源码如下: public E get(int index) { checkElementIndex(index); return node(index).item; } Node<E> node(int index) { /* * 则从头节点开始查找,否则从尾节点查找 * 查找位置 index 如果小于节点数量的一半, */ if (index < (size >> 1)) { Node<E> x = first; // 循环向后查找,直至 i == index for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } 上面的代码比较简单,主要是通过遍历的方式定位目标位置的节点。获取到节点后,取出节点存储的值返回即可。这里面有个小优化,即通过比较 index 与节点数量 size/2 的大小,决定从头结点还是尾节点进行查找。查找操作的代码没什么复杂的地方,这里先讲到这里。 2.2 遍历 链表的遍历过程也很简单,和上面查找过程类似,我们从头节点往后遍历就行了。但对于 LinkedList 的遍历还是需要注意一些,不然可能会导致代码效率低下。通常情况下,我们会使用 foreach 遍历 LinkedList,而 foreach 最终转换成迭代器形式。所以分析 LinkedList 的遍历的核心就是它的迭代器实现,相关代码如下: public ListIterator<E> listIterator(int index) { checkPositionIndex(index); return new ListItr(index); } private class ListItr implements ListIterator<E> { private Node<E> lastReturned; private Node<E> next; private int nextIndex; private int expectedModCount = modCount; /** 构造方法将 next 引用指向指定位置的节点 */ ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index); nextIndex = index; } public boolean hasNext() { return nextIndex < size; } public E next() { checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; // 调用 next 方法后,next 引用都会指向他的后继节点 nextIndex++; return lastReturned.item; } // 省略部分方法 } 上面的方法很简单,大家应该都能很快看懂,这里就不多说了。下面来说说遍历 LinkedList 需要注意的一个点。 我们都知道 LinkedList 不擅长随机位置访问,如果大家用随机访问的方式遍历 LinkedList,效率会很差。比如下面的代码: List<Integet> list = new LinkedList<>(); list.add(1) list.add(2) ...... for (int i = 0; i < list.size(); i++) { Integet item = list.get(i); // do something } 当链表中存储的元素很多时,上面的遍历方式对于效率来说就是灾难。原因在于,通过上面的方式每获取一个元素,LinkedList 都需要从头节点(或尾节点)进行遍历,效率不可谓不低。在我的电脑(MacBook Pro Early 2015, 2.7 GHz Intel Core i5)实测10万级的数据量,耗时约7秒钟。20万级的数据量耗时达到了约34秒的时间。50万级的数据量耗时约250秒。从测试结果上来看,上面的遍历方式在大数据量情况下,效率很差。大家在日常开发中应该尽量避免这种用法。 2.3 插入 LinkedList 除了实现了 List 接口相关方法,还实现了 Deque 接口的很多方法,所以我们有很多种方式插入元素。但这里,我只打算分析 List 接口中相关的插入方法,其他的方法大家自己看吧。LinkedList 插入元素的过程实际上就是链表链入节点的过程,学过数据结构的同学对此应该都很熟悉了。这里简单分析一下,先看源码吧: /** 在链表尾部插入元素 */ public boolean add(E e) { linkLast(e); return true; } /** 在链表指定位置插入元素 */ public void add(int index, E element) { checkPositionIndex(index); // 判断 index 是不是链表尾部位置,如果是,直接将元素节点插入链表尾部即可 if (index == size) linkLast(element); else linkBefore(element, node(index)); } /** 将元素节点插入到链表尾部 */ void linkLast(E e) { final Node<E> l = last; // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空 final Node<E> newNode = new Node<>(l, e, null); // 将 last 引用指向新节点 last = newNode; // 判断尾节点是否为空,为空表示当前链表还没有节点 if (l == null) first = newNode; else l.next = newNode; // 让原尾节点后继引用 next 指向新的尾节点 size++; modCount++; } /** 将元素节点插入到 succ 之前的位置 */ void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; // 1. 初始化节点,并指明前驱和后继节点 final Node<E> newNode = new Node<>(pred, e, succ); // 2. 将 succ 节点前驱引用 prev 指向新节点 succ.prev = newNode; // 判断尾节点是否为空,为空表示当前链表还没有节点 if (pred == null) first = newNode; else pred.next = newNode; // 3. succ 节点前驱的后继引用指向新节点 size++; modCount++; } 上面是插入过程的源码,我对源码进行了比较详细的注释,应该不难看懂。上面两个 add 方法只是对操作链表的方法做了一层包装,核心逻辑在 linkBefore 和 linkLast 中。这里以 linkBefore 为例,它的逻辑流程如下: 创建新节点,并指明新节点的前驱和后继 将 succ 的前驱引用指向新节点 如果 succ 的前驱不为空,则将 succ 前驱的后继引用指向新节点 对应于下图: 以上就是插入相关的源码分析,并不复杂,就不多说了。继续往下分析。 2.4 删除 如果大家看懂了上面的插入源码分析,那么再看删除操作实际上也很简单了。删除操作通过解除待删除节点与前后节点的链接,即可完成任务。过程比较简单,看源码吧: public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { // 遍历链表,找到要删除的节点 for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); // 将节点从链表中移除 return true; } } } return false; } public E remove(int index) { checkElementIndex(index); // 通过 node 方法定位节点,并调用 unlink 将节点从链表中移除 return unlink(node(index)); } /** 将某个节点从链表中移除 */ E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; // prev 为空,表明删除的是头节点 if (prev == null) { first = next; } else { // 将 x 的前驱的后继指向 x 的后继 prev.next = next; // 将 x 的前驱引用置空,断开与前驱的链接 x.prev = null; } // next 为空,表明删除的是尾节点 if (next == null) { last = prev; } else { // 将 x 的后继的前驱指向 x 的前驱 next.prev = prev; // 将 x 的后继引用置空,断开与后继的链接 x.next = null; } // 将 item 置空,方便 GC 回收 x.item = null; size--; modCount++; return element; } 和插入操作一样,删除操作方法也是对底层方法的一层保证,核心逻辑在底层 unlink 方法中。所以长驱直入,直接分析 unlink 方法吧。unlink 方法的逻辑如下(假设删除的节点既不是头节点,也不是尾节点): 将待删除节点 x 的前驱的后继指向 x 的后继 将待删除节点 x 的前驱引用置空,断开与前驱的链接 将待删除节点 x 的后继的前驱指向 x 的前驱 将待删除节点 x 的后继引用置空,断开与后继的链接 对应下图: 结合上图,理解 LInkedList 删除操作应该不难。好了,LinkedList 的删除源码分析就讲到这。 总结 通过上面的分析,大家对 LinkedList 的底层实现应该很清楚了。总体来看 LinkedList 的源码并不复杂,大家耐心看一下,一般都能看懂。同时,通过本文,向大家展现了使用 LinkedList 的一个坑,希望大家在开发中尽量避免。好了,本文到这里就结束了,感谢阅读! 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1.概述 ArrayList 是一种变长的集合类,基于定长数组实现。ArrayList 允许空值和重复元素,当往 ArrayList 中添加的元素数量大于其底层数组容量时,其会通过扩容机制重新生成一个更大的数组。另外,由于 ArrayList 底层基于数组实现,所以其可以保证在 O(1) 复杂度下完成随机查找操作。其他方面,ArrayList 是非线程安全类,并发环境下,多个线程同时操作 ArrayList,会引发不可预知的错误。 ArrayList 是大家最为常用的集合类,作为一个变长集合类,其核心是扩容机制。所以只要知道它是怎么扩容的,以及基本的操作是怎样实现就够了。本文后续内容也将围绕这些点展开叙述。 2.源码分析 2.1 构造方法 ArrayList 有两个构造方法,一个是无参,另一个需传入初始容量值。大家平时最常用的是无参构造方法,相关代码如下: private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } 上面的代码比较简单,两个构造方法做的事情并不复杂,目的都是初始化底层数组 elementData。区别在于无参构造方法会将 elementData 初始化一个空数组,插入元素时,扩容将会按默认值重新初始化数组。而有参的构造方法则会将 elementData 初始化为参数值大小(>= 0)的数组。一般情况下,我们用默认的构造方法即可。倘若在可知道将会向 ArrayList 插入多少元素的情况下,应该使用有参构造方法。按需分配,避免浪费。 2.2 插入 对于数组(线性表)结构,插入操作分为两种情况。一种是在元素序列尾部插入,另一种是在元素序列其他位置插入。ArrayList 的源码里也体现了这两种插入情况,如下: /** 在元素序列尾部插入 */ public boolean add(E e) { // 1. 检测是否需要扩容 ensureCapacityInternal(size + 1); // Increments modCount!! // 2. 将新元素插入序列尾部 elementData[size++] = e; return true; } /** 在元素序列 index 位置处插入 */ public void add(int index, E element) { rangeCheckForAdd(index); // 1. 检测是否需要扩容 ensureCapacityInternal(size + 1); // Increments modCount!! // 2. 将 index 及其之后的所有元素都向后移一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 3. 将新元素插入至 index 处 elementData[index] = element; size++; } 对于在元素序列尾部插入,这种情况比较简单,只需两个步骤即可: 检测数组是否有足够的空间插入 将新元素插入至序列尾部 如下图: 如果是在元素序列指定位置(假设该位置合理)插入,则情况稍微复杂一点,需要三个步骤: 检测数组是否有足够的空间 将 index 及其之后的所有元素向后移一位 将新元素插入至 index 处 如下图: 从上图可以看出,将新元素插入至序列指定位置,需要先将该位置及其之后的元素都向后移动一位,为新元素腾出位置。这个操作的时间复杂度为O(N),频繁移动元素可能会导致效率问题,特别是集合中元素数量较多时。在日常开发中,若非所需,我们应当尽量避免在大集合中调用第二个插入方法。 以上是 ArrayList 插入相关的分析,上面的分析以及配图均未体现扩容机制。那么下面就来简单分析一下 ArrayList 的扩容机制。对于变长数据结构,当结构中没有空余空间可供使用时,就需要进行扩容。在 ArrayList 中,当空间用完,其会按照原数组空间的1.5倍进行扩容。相关源码如下: /** 计算最小容量 */ private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } /** 扩容的入口方法 */ private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } /** 扩容的核心方法 */ private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // newCapacity = oldCapacity + oldCapacity / 2 = oldCapacity * 1.5 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 扩容 elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); // 如果最小容量超过 MAX_ARRAY_SIZE,则将数组容量扩容至 Integer.MAX_VALUE return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } 上面就是扩容的逻辑,代码虽多,但很多都是边界检查,这里就不详细分析了。 2.3 删除 不同于插入操作,ArrayList 没有无参删除方法。所以其只能删除指定位置的元素或删除指定元素,这样就无法避免移动元素(除非从元素序列的尾部删除)。相关代码如下: /** 删除指定位置的元素 */ public E remove(int index) { rangeCheck(index); modCount++; // 返回被删除的元素值 E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) // 将 index + 1 及之后的元素向前移动一位,覆盖被删除值 System.arraycopy(elementData, index+1, elementData, index, numMoved); // 将最后一个元素置空,并将 size 值减1 elementData[--size] = null; // clear to let GC do its work return oldValue; } E elementData(int index) { return (E) elementData[index]; } /** 删除指定元素,若元素重复,则只删除下标最小的元素 */ public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { // 遍历数组,查找要删除元素的位置 for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } /** 快速删除,不做边界检查,也不返回删除的元素值 */ private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work } 上面的删除方法并不复杂,这里以第一个删除方法为例,删除一个元素步骤如下: 获取指定位置 index 处的元素值 将 index + 1 及之后的元素向前移动一位 将最后一个元素置空,并将 size 值减 1 返回被删除值,完成删除操作 如下图: 上面就是删除指定位置元素的分析,并不是很复杂。 现在,考虑这样一种情况。我们往 ArrayList 插入大量元素后,又删除很多元素,此时底层数组会空闲处大量的空间。因为 ArrayList 没有自动缩容机制,导致底层数组大量的空闲空间不能被释放,造成浪费。对于这种情况,ArrayList 也提供了相应的处理方法,如下: /** 将数组容量缩小至元素数量 */ public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } } 通过上面的方法,我们可以手动触发 ArrayList 的缩容机制。这样就可以释放多余的空间,提高空间利用率。 2.4 遍历 ArrayList 实现了 RandomAccess 接口(该接口是个标志性接口),表明它具有随机访问的能力。ArrayList 底层基于数组实现,所以它可在常数阶的时间内完成随机访问,效率很高。对 ArrayList 进行遍历时,一般情况下,我们喜欢使用 foreach 循环遍历,但这并不是推荐的遍历方式。ArrayList 具有随机访问的能力,如果在一些效率要求比较高的场景下,更推荐下面这种方式: for (int i = 0; i < list.size(); i++) { list.get(i); } 至于原因也不难理解,foreach 最终会被转换成迭代器遍历的形式,效率不如上面的遍历方式。 3.其他细节 3.1 快速失败机制 在 Java 集合框架中,很多类都实现了快速失败机制。该机制被触发时,会抛出并发修改异常ConcurrentModificationException,这个异常大家在平时开发中多多少少应该都碰到过。关于快速失败机制,ArrayList 的注释里对此做了解释,这里引用一下: The iterators returned by this class's iterator() and listIterator(int) methods are fail-fast if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own ListIterator remove() or ListIterator add(Object) methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. 上面注释大致意思是,ArrayList 迭代器中的方法都是均具有快速失败的特性,当遇到并发修改的情况时,迭代器会快速失败,以避免程序在将来不确定的时间里出现不确定的行为。 以上就是 Java 集合框架中引入快速失败机制的原因,并不难理解,这里不多说了。 3.2 关于遍历时删除 遍历时删除是一个不正确的操作,即使有时候代码不出现异常,但执行逻辑也会出现问题。关于这个问题,阿里巴巴 Java 开发手册里也有所提及。这里引用一下: 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。 相关代码(稍作修改)如下: List<String> a = new ArrayList<String>(); a.add("1"); a.add("2"); for (String temp : a) { System.out.println(temp); if("1".equals(temp)){ a.remove(temp); } } } 相信有些朋友应该看过这个,并且也执行过上面的程序。上面的程序执行起来不会虽不会出现异常,但代码执行逻辑上却有问题,只不过这个问题隐藏的比较深。我们把 temp 变量打印出来,会发现只打印了数字1,2没打印出来。初看这个执行结果确实很让人诧异,不明原因。如果死抠上面的代码,我们很难找出原因,此时需要稍微转换一下思路。我们都知道 Java 中的 foreach 是个语法糖,编译成字节码后会被转成用迭代器遍历的方式。所以我们可以把上面的代码转换一下,等价于下面形式: List<String> a = new ArrayList<>(); a.add("1"); a.add("2"); Iterator<String> it = a.iterator(); while (it.hasNext()) { String temp = it.next(); System.out.println("temp: " + temp); if("1".equals(temp)){ a.remove(temp); } } 这个时候,我们再去分析一下 ArrayList 的迭代器源码就能找出原因。 private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { // 并发修改检测,检测不通过则抛出异常 checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } // 省略不相关的代码 } 我们一步一步执行一下上面的代码,第一次进入 while 循环时,一切正常,元素 1 也被删除了。但删除元素 1 后,就无法再进入 while 循环,此时 it.hasNext() 为 false。原因是删除元素 1 后,元素计数器 size = 1,而迭代器中的 cursor 也等于 1,从而导致 it.hasNext() 返回false。归根结底,上面的代码段没抛异常的原因是,循环提前结束,导致 next 方法没有机会抛异常。不信的话,大家可以把代码稍微修改一下,即可发现问题: List<String> a = new ArrayList<>(); a.add("1"); a.add("2"); a.add("3"); Iterator<String> it = a.iterator(); while (it.hasNext()) { String temp = it.next(); System.out.println("temp: " + temp); if("1".equals(temp)){ a.remove(temp); } } 以上是关于遍历时删除的分析,在日常开发中,我们要避免上面的做法。正确的做法使用迭代器提供的删除方法,而不是直接删除。 4.总结 看到这里,大家对 ArrayList 应该又有了些新的认识。ArrayList 是一个比较基础的集合类,用的很多。它的结构简单(本质上就是一个变长的数组),实现上也不复杂。尽管如此,本文还是啰里啰嗦讲了很多,大家见谅。好了,本文到这里就结束了,感谢阅读。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。相对于另一种数据交换格式 XML,JSON 有着诸多优点。比如易读性更好,占用空间更少等。在 web 应用开发领域内,得益于 JavaScript 对 JSON 提供的良好支持,JSON 要比 XML 更受开发人员青睐。所以作为开发人员,如果有兴趣的话,还是应该深入了解一下 JSON 相关的知识。本着探究 JSON 原理的目的,我将会在这篇文章中详细向大家介绍一个简单的JSON解析器的解析流程和实现细节。由于 JSON 本身比较简单,解析起来也并不复杂。所以如果大家感兴趣的话,在看完本文后,不妨自己动手实现一个 JSON 解析器。好了,其他的话就不多说了,接下来让我们移步到重点章节吧。 2. JSON 解析器实现原理 JSON 解析器从本质上来说就是根据 JSON 文法规则创建的状态机,输入是一个 JSON 字符串,输出是一个 JSON 对象。一般来说,解析过程包括词法分析和语法分析两个阶段。词法分析阶段的目标是按照构词规则将 JSON 字符串解析成 Token 流,比如有如下的 JSON 字符串: { "name" : "小明", "age": 18 } 结果词法分析后,得到一组 Token,如下:{、 name、 :、 小明、 ,、 age、 :、 18、 } 图1 词法分析器输入输出 词法分析解析出 Token 序列后,接下来要进行语法分析。语法分析的目的是根据 JSON 文法检查上面 Token 序列所构成的 JSON 结构是否合法。比如 JSON 文法要求非空 JSON 对象以键值对的形式出现,形如 object = {string : value}。如果传入了一个格式错误的字符串,比如 { "name", "小明" } 那么在语法分析阶段,语法分析器分析完 Token name后,认为它是一个符合规则的 Token,并且认为它是一个键。接下来,语法分析器读取下一个 Token,期望这个 Token 是 :。但当它读取了这个 Token,发现这个 Token 是 ,,并非其期望的:,于是文法分析器就会报错误。 图2 语法分析器输入输出 这里简单总结一下上面两个流程,词法分析是将字符串解析成一组 Token 序列,而语法分析则是检查输入的 Token 序列所构成的 JSON 格式是否合法。这里大家对 JSON 的解析流程有个印象就好,接下来我会详细分析每个流程。 2.1 词法分析 在本章开始,我说了词法解析的目的,即按照“构词规则”将 JSON 字符串解析成 Token 流。请注意双引号引起来词--构词规则,所谓构词规则是指词法分析模块在将字符串解析成 Token 时所参考的规则。在 JSON 中,构词规则对应于几种数据类型,当词法解析器读入某个词,且这个词类型符合 JSON 所规定的数据类型时,词法分析器认为这个词符合构词规则,就会生成相应的 Token。这里我们可以参考http://www.json.org/对 JSON 的定义,罗列一下 JSON 所规定的数据类型: BEGIN_OBJECT({) END_OBJECT(}) BEGIN_ARRAY([) END_ARRAY(]) NULL(null) NUMBER(数字) STRING(字符串) BOOLEAN(true/false) SEP_COLON(:) SEP_COMMA(,) 当词法分析器读取的词是上面类型中的一种时,即可将其解析成一个 Token。我们可以定义一个枚举类来表示上面的数据类型,如下: public enum TokenType { BEGIN_OBJECT(1), END_OBJECT(2), BEGIN_ARRAY(4), END_ARRAY(8), NULL(16), NUMBER(32), STRING(64), BOOLEAN(128), SEP_COLON(256), SEP_COMMA(512), END_DOCUMENT(1024); TokenType(int code) { this.code = code; } private int code; public int getTokenCode() { return code; } } 在解析过程中,仅有 TokenType 类型还不行。我们除了要将某个词的类型保存起来,还需要保存这个词的字面量。所以,所以这里还需要定义一个 Token 类。用于封装词类型和字面量,如下: public class Token { private TokenType tokenType; private String value; // 省略不重要的代码 } 定义好了 Token 类,接下来再来定义一个读取字符串的类。如下: public CharReader(Reader reader) { this.reader = reader; buffer = new char[BUFFER_SIZE]; } /** * 返回 pos 下标处的字符,并返回 * @return * @throws IOException */ public char peek() throws IOException { if (pos - 1 >= size) { return (char) -1; } return buffer[Math.max(0, pos - 1)]; } /** * 返回 pos 下标处的字符,并将 pos + 1,最后返回字符 * @return * @throws IOException */ public char next() throws IOException { if (!hasMore()) { return (char) -1; } return buffer[pos++]; } public void back() { pos = Math.max(0, --pos); } public boolean hasMore() throws IOException { if (pos < size) { return true; } fillBuffer(); return pos < size; } void fillBuffer() throws IOException { int n = reader.read(buffer); if (n == -1) { return; } pos = 0; size = n; } } 有了 TokenType、Token 和 CharReader 这三个辅助类,接下来我们就可以实现词法解析器了。 public class Tokenizer { private CharReader charReader; private TokenList tokens; public TokenList tokenize(CharReader charReader) throws IOException { this.charReader = charReader; tokens = new TokenList(); tokenize(); return tokens; } private void tokenize() throws IOException { // 使用do-while处理空文件 Token token; do { token = start(); tokens.add(token); } while (token.getTokenType() != TokenType.END_DOCUMENT); } private Token start() throws IOException { char ch; for(;;) { if (!charReader.hasMore()) { return new Token(TokenType.END_DOCUMENT, null); } ch = charReader.next(); if (!isWhiteSpace(ch)) { break; } } switch (ch) { case '{': return new Token(TokenType.BEGIN_OBJECT, String.valueOf(ch)); case '}': return new Token(TokenType.END_OBJECT, String.valueOf(ch)); case '[': return new Token(TokenType.BEGIN_ARRAY, String.valueOf(ch)); case ']': return new Token(TokenType.END_ARRAY, String.valueOf(ch)); case ',': return new Token(TokenType.SEP_COMMA, String.valueOf(ch)); case ':': return new Token(TokenType.SEP_COLON, String.valueOf(ch)); case 'n': return readNull(); case 't': case 'f': return readBoolean(); case '"': return readString(); case '-': return readNumber(); } if (isDigit(ch)) { return readNumber(); } throw new JsonParseException("Illegal character"); } private Token readNull() {...} private Token readBoolean() {...} private Token readString() {...} private Token readNumber() {...} } 上面的代码是词法分析器的实现,部分代码这里没有贴出来,后面具体分析的时候再贴。先来看看词法分析器的核心方法 start,这个方法代码量不多,并不复杂。其通过一个死循环不停的读取字符,然后再根据字符的类型,执行不同的解析逻辑。上面说过,JSON 的解析过程比较简单。原因在于,在解析时,只需通过每个词第一个字符即可判断出这个词的 Token Type。比如: 第一个字符是{、}、[、]、,、:,直接封装成相应的 Token 返回即可 第一个字符是n,期望这个词是null,Token 类型是NULL 第一个字符是t或f,期望这个词是true或者false,Token 类型是 BOOLEAN 第一个字符是",期望这个词是字符串,Token 类型为String 第一个字符是0~9或-,期望这个词是数字,类型为NUMBER 正如上面所说,词法分析器只需要根据每个词的第一个字符,即可知道接下来它所期望读取的到的内容是什么样的。如果满足期望了,则返回 Token,否则返回错误。下面就来看看词法解析器在碰到第一个字符是n和"时的处理过程。先看碰到字符n的处理过程: private Token readNull() throws IOException { if (!(charReader.next() == 'u' && charReader.next() == 'l' && charReader.next() == 'l')) { throw new JsonParseException("Invalid json string"); } return new Token(TokenType.NULL, "null"); } 上面的代码很简单,词法分析器在读取字符n后,期望后面的三个字符分别是u,l,l,与 n 组成词 null。如果满足期望,则返回类型为 NULL 的 Token,否则报异常。readNull 方法逻辑很简单,不多说了。接下来看看 string 类型的数据处理过程: private Token readString() throws IOException { StringBuilder sb = new StringBuilder(); for (;;) { char ch = charReader.next(); // 处理转义字符 if (ch == '\\') { if (!isEscape()) { throw new JsonParseException("Invalid escape character"); } sb.append('\\'); ch = charReader.peek(); sb.append(ch); // 处理 Unicode 编码,形如 \u4e2d。且只支持 \u0000 ~ \uFFFF 范围内的编码 if (ch == 'u') { for (int i = 0; i < 4; i++) { ch = charReader.next(); if (isHex(ch)) { sb.append(ch); } else { throw new JsonParseException("Invalid character"); } } } } else if (ch == '"') { // 碰到另一个双引号,则认为字符串解析结束,返回 Token return new Token(TokenType.STRING, sb.toString()); } else if (ch == '\r' || ch == '\n') { // 传入的 JSON 字符串不允许换行 throw new JsonParseException("Invalid character"); } else { sb.append(ch); } } } private boolean isEscape() throws IOException { char ch = charReader.next(); return (ch == '"' || ch == '\\' || ch == 'u' || ch == 'r' || ch == 'n' || ch == 'b' || ch == 't' || ch == 'f'); } private boolean isHex(char ch) { return ((ch >= '0' && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')); } string 类型的数据解析起来要稍微复杂一些,主要是需要处理一些特殊类型的字符。JSON 所允许的特殊类型的字符如下: \"\\\b\f\n\r\t\u four-hex-digits\/ 最后一种特殊字符\/代码中未做处理,其他字符均做了判断,判断逻辑在 isEscape 方法中。在传入 JSON 字符串中,仅允许字符串包含上面所列的转义字符。如果乱传转义字符,解析时会报错。对于 STRING 类型的词,解析过程始于字符",也终于"。所以在解析的过程中,当再次遇到字符",readString 方法会认为本次的字符串解析过程结束,并返回相应类型的 Token。 上面说了 null 类型和 string 类型的数据解析过程,过程并不复杂,理解起来应该不难。至于 boolean 和 number 类型的数据解析过程,大家有兴趣的话可以自己看源码,这里就不在说了。 2.2 语法分析 当词法分析结束后,且分析过程中没有抛出错误,那么接下来就可以进行语法分析了。语法分析过程以词法分析阶段解析出的 Token 序列作为输入,输出 JSON Object 或 JSON Array。语法分析器的实现的文法如下: object = {} | { members } members = pair | pair , members pair = string : value array = [] | [ elements ] elements = value | value , elements value = string | number | object | array | true | false | null 语法分析器的实现需要借助两个辅助类,也就是语法分析器的输出类,分别是 JsonObject 和 JsonArray。代码如下: public class JsonObject { private Map<String, Object> map = new HashMap<String, Object>(); public void put(String key, Object value) { map.put(key, value); } public Object get(String key) { return map.get(key); } public List<Map.Entry<String, Object>> getAllKeyValue() { return new ArrayList<>(map.entrySet()); } public JsonObject getJsonObject(String key) { if (!map.containsKey(key)) { throw new IllegalArgumentException("Invalid key"); } Object obj = map.get(key); if (!(obj instanceof JsonObject)) { throw new JsonTypeException("Type of value is not JsonObject"); } return (JsonObject) obj; } public JsonArray getJsonArray(String key) { if (!map.containsKey(key)) { throw new IllegalArgumentException("Invalid key"); } Object obj = map.get(key); if (!(obj instanceof JsonArray)) { throw new JsonTypeException("Type of value is not JsonArray"); } return (JsonArray) obj; } @Override public String toString() { return BeautifyJsonUtils.beautify(this); } } public class JsonArray implements Iterable { private List list = new ArrayList(); public void add(Object obj) { list.add(obj); } public Object get(int index) { return list.get(index); } public int size() { return list.size(); } public JsonObject getJsonObject(int index) { Object obj = list.get(index); if (!(obj instanceof JsonObject)) { throw new JsonTypeException("Type of value is not JsonObject"); } return (JsonObject) obj; } public JsonArray getJsonArray(int index) { Object obj = list.get(index); if (!(obj instanceof JsonArray)) { throw new JsonTypeException("Type of value is not JsonArray"); } return (JsonArray) obj; } @Override public String toString() { return BeautifyJsonUtils.beautify(this); } public Iterator iterator() { return list.iterator(); } } 语法解析器的核心逻辑封装在了 parseJsonObject 和 parseJsonArray 两个方法中,接下来我会详细分析 parseJsonObject 方法,parseJsonArray 方法大家自己分析吧。parseJsonObject 方法实现如下: private JsonObject parseJsonObject() { JsonObject jsonObject = new JsonObject(); int expectToken = STRING_TOKEN | END_OBJECT_TOKEN; String key = null; Object value = null; while (tokens.hasMore()) { Token token = tokens.next(); TokenType tokenType = token.getTokenType(); String tokenValue = token.getValue(); switch (tokenType) { case BEGIN_OBJECT: checkExpectToken(tokenType, expectToken); jsonObject.put(key, parseJsonObject()); // 递归解析 json object expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case END_OBJECT: checkExpectToken(tokenType, expectToken); return jsonObject; case BEGIN_ARRAY: // 解析 json array checkExpectToken(tokenType, expectToken); jsonObject.put(key, parseJsonArray()); expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case NULL: checkExpectToken(tokenType, expectToken); jsonObject.put(key, null); expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case NUMBER: checkExpectToken(tokenType, expectToken); if (tokenValue.contains(".") || tokenValue.contains("e") || tokenValue.contains("E")) { jsonObject.put(key, Double.valueOf(tokenValue)); } else { Long num = Long.valueOf(tokenValue); if (num > Integer.MAX_VALUE || num < Integer.MIN_VALUE) { jsonObject.put(key, num); } else { jsonObject.put(key, num.intValue()); } } expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case BOOLEAN: checkExpectToken(tokenType, expectToken); jsonObject.put(key, Boolean.valueOf(token.getValue())); expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; break; case STRING: checkExpectToken(tokenType, expectToken); Token preToken = tokens.peekPrevious(); /* * 在 JSON 中,字符串既可以作为键,也可作为值。 * 作为键时,只期待下一个 Token 类型为 SEP_COLON。 * 作为值时,期待下一个 Token 类型为 SEP_COMMA 或 END_OBJECT */ if (preToken.getTokenType() == TokenType.SEP_COLON) { value = token.getValue(); jsonObject.put(key, value); expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN; } else { key = token.getValue(); expectToken = SEP_COLON_TOKEN; } break; case SEP_COLON: checkExpectToken(tokenType, expectToken); expectToken = NULL_TOKEN | NUMBER_TOKEN | BOOLEAN_TOKEN | STRING_TOKEN | BEGIN_OBJECT_TOKEN | BEGIN_ARRAY_TOKEN; break; case SEP_COMMA: checkExpectToken(tokenType, expectToken); expectToken = STRING_TOKEN; break; case END_DOCUMENT: checkExpectToken(tokenType, expectToken); return jsonObject; default: throw new JsonParseException("Unexpected Token."); } } throw new JsonParseException("Parse error, invalid Token."); } private void checkExpectToken(TokenType tokenType, int expectToken) { if ((tokenType.getTokenCode() & expectToken) == 0) { throw new JsonParseException("Parse error, invalid Token."); } } parseJsonObject 方法解析流程大致如下: 读取一个 Token,检查这个 Token 是否是其所期望的类型 如果是,更新期望的 Token 类型。否则,抛出异常,并退出 重复步骤1和2,直至所有的 Token 都解析完,或出现异常 上面的步骤并不复杂,但有可能不好理解。这里举个例子说明一下,有如下的 Token 序列: {、 id、 :、 1、 } parseJsonObject 解析完 { Token 后,接下来它将期待 STRING 类型的 Token 或者 END_OBJECT 类型的 Token 出现。于是 parseJsonObject 读取了一个新的 Token,发现这个 Token 的类型是 STRING 类型,满足期望。于是 parseJsonObject 更新期望Token 类型为 SEL_COLON,即:。如此循环下去,直至 Token 序列解析结束或者抛出异常退出。 上面的解析流程虽然不是很复杂,但在具体实现的过程中,还是需要注意一些细节问题。比如: 在 JSON 中,字符串既可以作为键,也可以作为值。作为键时,语法分析器期待下一个 Token 类型为 SEP_COLON。而作为值时,则期待下一个 Token 类型为 SEP_COMMA 或 END_OBJECT。所以这里要判断该字符串是作为键还是作为值,判断方法也比较简单,即判断上一个 Token 的类型即可。如果上一个 Token 是 SEP_COLON,即:,那么此处的字符串只能作为值了。否则,则只能做为键。 对于整数类型的 Token 进行解析时,简单点处理,可以直接将该整数解析成 Long 类型。但考虑到空间占用问题,对于 [Integer.MIN_VALUE, Integer.MAX_VALUE] 范围内的整数来说,解析成 Integer 更为合适,所以解析的过程中也需要注意一下。 3. 测试及效果展示 为了验证代码的正确性,这里对代码进行了简单的测试。测试数据来自网易音乐,大约有4.5W个字符。为了避免每次下载数据,因数据发生变化而导致测试不通过的问题。我将某一次下载的数据保存在了 music.json 文件中,后面每次测试都会从文件中读取数据。关于测试部分,这里就不贴代码和截图了。大家有兴趣的话,可以自己下载源码测试玩玩。 测试就不多说了,接下来看看 JSON 美化效果展示。这里随便模拟点数据,就模拟王者荣耀里的狄仁杰英雄信息吧(对,这个英雄我经常用)。如下图: 图3 JSON 美化结果 关于 JSON 美化的代码这里也不讲解了,并非重点,只算一个彩蛋吧。 4. 写作最后 到此,本文差不多要结束了。本文对应的代码已经放到了 github 上,需要的话,大家可自行下载。传送门 -> JSONParser。这里需要声明一下,本文对应的代码实现了一个比较简陋的 JSON 解析器,实现的目的是探究 JSON 的解析原理。JSONParser 只算是一个练习性质的项目,代码实现的并不优美,而且缺乏充足的测试。同时,限于本人的能力(编译原理基础基本可以忽略),我并无法保证本文以及对应的代码中不出现错误。如果大家在阅读代码的过程中,发现了一些错误,或者写的不好的地方,可以提出来,我来修改。如果这些错误对你造成了困扰,这里先说一声很抱歉。最后,本文及实现主要参考了一起写一个JSON解析器和如何编写一个JSON解析器两篇文章及两篇文章对应的实现代码,在这里向着两篇博文的作者表示感谢。好了,本文到此结束,祝大家生生活愉快!再见。 参考 一起写一个JSON解析器 如何编写一个JSON解析器 介绍JSON 写一个 JSON、XML 或 YAML 的 Parser 的思路是什么?-- 知乎 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 背景 某天,我在写代码的时候,无意中点开了 String hashCode 方法。然后大致看了一下 hashCode 的实现,发现并不是很复杂。但是我从源码中发现了一个奇怪的数字,也就是本文的主角31。这个数字居然不是用常量声明的,所以没法从字面意思上推断这个数字的用途。后来带着疑问和好奇心,到网上去找资料查询一下。在看完资料后,默默的感叹了一句,原来是这样啊。那么到底是哪样呢?在接下来章节里,请大家带着好奇心和我揭开数字31的用途之谜。 2. 选择数字31的原因 在详细说明 String hashCode 方法选择数字31的作为乘子的原因之前,我们先来看看 String hashCode 方法是怎样实现的,如下: public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } 上面的代码就是 String hashCode 方法的实现,是不是很简单。实际上 hashCode 方法核心的计算逻辑只有三行,也就是代码中的 for 循环。我们可以由上面的 for 循环推导出一个计算公式,hashCode 方法注释中已经给出。如下: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 这里说明一下,上面的 s 数组即源码中的 val 数组,是 String 内部维护的一个 char 类型数组。这里我来简单推导一下这个公式: 假设 n=3 i=0 -> h = 31 * 0 + val[0] i=1 -> h = 31 * (31 * 0 + val[0]) + val[1] i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] h = 31*31*31*0 + 31*31*val[0] + 31*val[1] + val[2] h = 31^(n-1)*val[0] + 31^(n-2)*val[1] + val[2] 上面的公式包括公式的推导并不是本文的重点,大家了解了解即可。接下来来说说本文的重点,即选择31的理由。从网上的资料来看,一般有如下两个原因: 第一,31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。另外一些相近的质数,比如37、41、43等等,也都是不错的选择。那么为啥偏偏选中了31呢?请看第二个原因。 第二、31可以被 JVM 优化,31 * i = (i << 5) - i。 上面两个原因中,第一个需要解释一下,第二个比较简单,就不说了。下面我来解释第一个理由。一般在设计哈希算法时,会选择一个特殊的质数。至于为啥选择质数,我想应该是可以降低哈希算法的冲突率。至于原因,这个就要问数学家了,我几乎可以忽略的数学水平解释不了这个原因。上面说到,31是一个不大不小的质数,是优选乘子。那为啥同是质数的2和101(或者更大的质数)就不是优选乘子呢,分析如下。 这里先分析质数2。首先,假设 n = 6,然后把质数2和 n 带入上面的计算公式。并仅计算公式中次数最高的那一项,结果是2^5 = 32,是不是很小。所以这里可以断定,当字符串长度不是很长时,用质数2做为乘子算出的哈希值,数值不会很大。也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会导致冲突率上升。 上面说了,质数2做为乘子会导致哈希值分布在一个较小区间内,那么如果用一个较大的大质数101会产生什么样的结果呢?根据上面的分析,我想大家应该可以猜出结果了。就是不用再担心哈希值会分布在一个小的区间内了,因为101^5 = 10,510,100,501。但是要注意的是,这个计算结果太大了。如果用 int 类型表示哈希值,结果会溢出,最终导致数值信息丢失。尽管数值信息丢失并不一定会导致冲突率上升,但是我们暂且先认为质数101(或者更大的质数)也不是很好的选择。最后,我们再来看看质数31的计算结果:31^5 = 28629151,结果值相对于32和10,510,100,501来说。是不是很nice,不大不小。 上面用了比较简陋的数学手段证明了数字31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。接下来我会用详细的实验来验证上面的结论,不过在验证前,我们先看看 Stack Overflow 上关于这个问题的讨论,Why does Java's hashCode() in String use 31 as a multiplier?。其中排名第一的答案引用了《Effective Java》中的一段话,这里也引用一下: The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: `31 * i == (i << 5) - i``. Modern VMs do this sort of optimization automatically. 简单翻译一下: 选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。 排名第二的答案设这样说的: As Goodrich and Tamassia point out, If you take over 50,000 English words (formed as the union of the word lists provided in two variants of Unix), using the constants 31, 33, 37, 39, and 41 will produce less than 7 collisions in each case. Knowing this, it should come as no surprise that many Java implementations choose one of these constants. 这段话也翻译一下: 正如 Goodrich 和 Tamassia 指出的那样,如果你对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个,所以在上面几个常数中,常数 31 被 Java 实现所选用也就不足为奇了。 上面的两个答案完美的解释了 Java 源码中选用数字 31 的原因。接下来,我将针对第二个答案就行验证,请大家继续往下看。 3. 实验及数据可视化 本节,我将使用不同的数字作为乘子,对超过23万个英文单词进行哈希运算,并计算哈希算法的冲突率。同时,我也将针对不同乘子算出的哈希值分布情况进行可视化处理,让大家可以直观的看到数据分布情况。本次实验所使用的数据是 Unix/Linux 平台中的英文字典文件,文件路径为 /usr/share/dict/words。 3.1 哈希值冲突率计算 计算哈希算法冲突率并不难,比如可以一次性将所有单词的 hash code 算出,并放入 Set 中去除重复值。之后拿单词数减去 set.size() 即可得出冲突数,有了冲突数,冲突率就可以算出来了。当然,如果使用 JDK8 提供的流式计算 API,则可更方便算出,代码片段如下: public static Integer hashCode(String str, Integer multiplier) { int hash = 0; for (int i = 0; i < str.length(); i++) { hash = multiplier * hash + str.charAt(i); } return hash; } /** * 计算 hash code 冲突率,顺便分析一下 hash code 最大值和最小值,并输出 * @param multiplier * @param hashs */ public static void calculateConflictRate(Integer multiplier, List<Integer> hashs) { Comparator<Integer> cp = (x, y) -> x > y ? 1 : (x < y ? -1 : 0); int maxHash = hashs.stream().max(cp).get(); int minHash = hashs.stream().min(cp).get(); // 计算冲突数及冲突率 int uniqueHashNum = (int) hashs.stream().distinct().count(); int conflictNum = hashs.size() - uniqueHashNum; double conflictRate = (conflictNum * 1.0) / hashs.size(); System.out.println(String.format("multiplier=%4d, minHash=%11d, maxHash=%10d, conflictNum=%6d, conflictRate=%.4f%%", multiplier, minHash, maxHash, conflictNum, conflictRate * 100)); } 结果如下: 从上图可以看出,使用较小的质数做为乘子时,冲突率会很高。尤其是质数2,冲突率达到了 55.14%。同时我们注意观察质数2作为乘子时,哈希值的分布情况。可以看得出来,哈希值分布并不是很广,仅仅分布在了整个哈希空间的正半轴部分,即 0 ~ 231-1。而负半轴 -231 ~ -1,则无分布。这也证明了我们上面断言,即质数2作为乘子时,对于短字符串,生成的哈希值分布性不佳。然后再来看看我们之前所说的 31、37、41 这三个不大不小的质数,表现都不错,冲突数都低于7个。而质数 101 和 199 表现的也很不错,冲突率很低,这也说明哈希值溢出并不一定会导致冲突率上升。但是这两个家伙一言不合就溢出,我们认为他们不是哈希算法的优选乘子。最后我们再来看看 32 和 36 这两个偶数的表现,结果并不好,尤其是 32,冲突率超过了了50%。尽管 36 表现的要好一点,不过和 31,37相比,冲突率还是比较高的。当然并非所有的偶数作为乘子时,冲突率都会比较高,大家有兴趣可以自己验证。 3.2 哈希值分布可视化 上一节分析了不同数字作为乘子时的冲突率情况,这一节来分析一下不同数字作为乘子时,哈希值的分布情况。在详细分析之前,我先说说哈希值可视化的过程。我原本是打算将所有的哈希值用一维散点图进行可视化,但是后来找了一圈,也没找到合适的画图工具。加之后来想了想,一维散点图可能不合适做哈希值可视化,因为这里有超过23万个哈希值。也就意味着会在图上显示超过23万个散点,如果不出意外的话,这23万个散点会聚集的很密,有可能会变成一个大黑块,就失去了可视化的意义了。所以这里选择了另一种可视化效果更好的图表,也就是 excel 中的平滑曲线的二维散点图(下面简称散点曲线图)。当然这里同样没有把23万散点都显示在图表上,太多了。所以在实际绘图过程中,我将哈希空间等分成了64个子区间,并统计每个区间内的哈希值数量。最后将分区编号做为X轴,哈希值数量为Y轴,就绘制出了我想要的二维散点曲线图了。这里举个例子说明一下吧,以第0分区为例。第0分区数值区间是[-2147483648, -2080374784),我们统计落在该数值区间内哈希值的数量,得到 <分区编号, 哈希值数量> 数值对,这样就可以绘图了。分区代码如下: /** * 将整个哈希空间等分成64份,统计每个空间内的哈希值数量 * @param hashs */ public static Map<Integer, Integer> partition(List<Integer> hashs) { // step = 2^32 / 64 = 2^26 final int step = 67108864; List<Integer> nums = new ArrayList<>(); Map<Integer, Integer> statistics = new LinkedHashMap<>(); int start = 0; for (long i = Integer.MIN_VALUE; i <= Integer.MAX_VALUE; i += step) { final long min = i; final long max = min + step; int num = (int) hashs.parallelStream() .filter(x -> x >= min && x < max).count(); statistics.put(start++, num); nums.add(num); } // 为了防止计算出错,这里验证一下 int hashNum = nums.stream().reduce((x, y) -> x + y).get(); assert hashNum == hashs.size(); return statistics; } 本文中的哈希值是用整形表示的,整形的数值区间是 [-2147483648, 2147483647],区间大小为 2^32。所以这里可以将区间等分成64个子区间,每个自子区间大小为 2^26。详细的分区对照表如下: 分区编号 分区下限 分区上限 分区编号 分区下限 分区上限 0 -2147483648 -2080374784 32 0 67108864 1 -2080374784 -2013265920 33 67108864 134217728 2 -2013265920 -1946157056 34 134217728 201326592 3 -1946157056 -1879048192 35 201326592 268435456 4 -1879048192 -1811939328 36 268435456 335544320 5 -1811939328 -1744830464 37 335544320 402653184 6 -1744830464 -1677721600 38 402653184 469762048 7 -1677721600 -1610612736 39 469762048 536870912 8 -1610612736 -1543503872 40 536870912 603979776 9 -1543503872 -1476395008 41 603979776 671088640 10 -1476395008 -1409286144 42 671088640 738197504 11 -1409286144 -1342177280 43 738197504 805306368 12 -1342177280 -1275068416 44 805306368 872415232 13 -1275068416 -1207959552 45 872415232 939524096 14 -1207959552 -1140850688 46 939524096 1006632960 15 -1140850688 -1073741824 47 1006632960 1073741824 16 -1073741824 -1006632960 48 1073741824 1140850688 17 -1006632960 -939524096 49 1140850688 1207959552 18 -939524096 -872415232 50 1207959552 1275068416 19 -872415232 -805306368 51 1275068416 1342177280 20 -805306368 -738197504 52 1342177280 1409286144 21 -738197504 -671088640 53 1409286144 1476395008 22 -671088640 -603979776 54 1476395008 1543503872 23 -603979776 -536870912 55 1543503872 1610612736 24 -536870912 -469762048 56 1610612736 1677721600 25 -469762048 -402653184 57 1677721600 1744830464 26 -402653184 -335544320 58 1744830464 1811939328 27 -335544320 -268435456 59 1811939328 1879048192 28 -268435456 -201326592 60 1879048192 1946157056 29 -201326592 -134217728 61 1946157056 2013265920 30 -134217728 -67108864 62 2013265920 2080374784 31 -67108864 0 63 2080374784 2147483648 接下来,让我们对照上面的分区表,对数字2、3、17、31、101的散点曲线图进行简单的分析。先从数字2开始,数字2对于的散点曲线图如下: 上面的图还是很一幕了然的,乘子2算出的哈希值几乎全部落在第32分区,也就是 [0, 67108864)数值区间内,落在其他区间内的哈希值数量几乎可以忽略不计。这也就不难解释为什么数字2作为乘子时,算出哈希值的冲突率如此之高的原因了。所以这样的哈希算法要它有何用啊,拖出去斩了吧。接下来看看数字3作为乘子时的表现: 3作为乘子时,算出的哈希值分布情况和2很像,只不过稍微好了那么一点点。从图中可以看出绝大部分的哈希值最终都落在了第32分区里,哈希值的分布性很差。这个也没啥用,拖出去枪毙5分钟吧。在看看数字17的情况怎么样: 数字17作为乘子时的表现,明显比上面两个数字好点了。虽然哈希值在第32分区和第34分区有一定的聚集,但是相比较上面2和3,情况明显好好了很多。除此之外,17作为乘子算出的哈希值在其他区也均有分布,且较为均匀,还算是一个不错的乘子吧。 接下来来看看我们本文的主角31了,31作为乘子算出的哈希值在第33分区有一定的小聚集。不过相比于数字17,主角31的表现又好了一些。首先是哈希值的聚集程度没有17那么严重,其次哈希值在其他区分布的情况也要好于17。总之,选31,准没错啊。 最后再来看看大质数101的表现,不难看出,质数101作为乘子时,算出的哈希值分布情况要好于主角31,有点喧宾夺主的意思。不过不可否认的是,质数101的作为乘子时,哈希值的分布性确实更加均匀。所以如果不在意质数101容易导致数据信息丢失问题,或许其是一个更好的选择。 4.写在最后 经过上面的分析与实践,我想大家应该明白了 String hashCode 方法中选择使用数字31作为乘子的原因了。本文本质是一篇简单的科普文而已,并没有银弹。如果大家读完后觉得又涨知识了,那这篇文章的目的就达到了。最后,本篇文章的配图画的还是很辛苦的,所以如果大家觉得文章不错,不妨就给个赞吧,就当是对我的鼓励了。另外,如果文章中有不妥或者错误的地方,也欢迎指出来。如果能不吝赐教,那就更好了。最后祝大家生活愉快,再见。 本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
1. 概述 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。在一些场景下,该特性很有用,比如缓存。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。所以,要看懂 LinkedHashMap 的源码,需要先看懂 HashMap 的源码。关于 HashMap 的源码分析,本文并不打算展开讲了。大家可以参考我之前的一篇文章“HashMap 源码详细分析(JDK1.8)”。在那篇文章中,我配了十多张图帮助大家学习 HashMap 源码。 本篇文章的结构与我之前两篇关于 Java 集合类(集合框架)的源码分析文章不同,本文将不再分析集合类的基本操作(查找、遍历、插入、删除),而是把重点放在双向链表的维护上。包括链表的建立过程,删除节点的过程,以及访问顺序维护的过程等。好了,接下里开始分析吧。 2. 原理 上一章说了 LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构。该结构由数组和链表或红黑树组成,结构示意图大致如下: LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。其结构可能如下图: 上图中,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了。 上面的结构并不是很难理解,虽然引入了红黑树,导致结构看起来略为复杂了一些。但大家完全可以忽略红黑树,而只关注链表结构本身。好了,接下来进入细节分析吧。 3. 源码分析 3.1 Entry 的继承体系 在对核心内容展开分析之前,这里先插队分析一下键值对节点的继承体系。先来看看继承体系结构图: 上面的继承体系乍一看还是有点复杂的,同时也有点让人迷惑。HashMap 的内部类 TreeNode 不继承它的了一个内部类 Node,却继承自 Node 的子类 LinkedHashMap 内部类 Entry。这里这样做是有一定原因的,这里先不说。先来简单说明一下上面的继承体系。LinkedHashMap 内部类 Entry 继承自 HashMap 内部类 Node,并新增了两个引用,分别是 before 和 after。这两个引用的用途不难理解,也就是用于维护双向链表。同时,TreeNode 继承 LinkedHashMap 的内部类 Entry 后,就具备了和其他 Entry 一起组成链表的能力。但是这里需要大家考虑一个问题。当我们使用 HashMap 时,TreeNode 并不需要具备组成链表能力。如果继承 LinkedHashMap 内部类 Entry ,TreeNode 就多了两个用不到的引用,这样做不是会浪费空间吗?简单说明一下这个问题(水平有限,不保证完全正确),这里这么做确实会浪费空间,但与 TreeNode 通过继承获取的组成链表的能力相比,这点浪费是值得的。在 HashMap 的设计思路注释中,有这样一段话: Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. 大致的意思是 TreeNode 对象的大小约是普通 Node 对象的2倍,我们仅在桶(bin)中包含足够多的节点时再使用。当桶中的节点数量变少时(取决于删除和扩容),TreeNode 会被转成 Node。当用户实现的 hashCode 方法具有良好分布性时,树类型的桶将会很少被使用。 通过上面的注释,我们可以了解到。一般情况下,只要 hashCode 的实现不糟糕,Node 组成的链表很少会被转成由 TreeNode 组成的红黑树。也就是说 TreeNode 使用的并不多,浪费那点空间是可接受的。假如 TreeNode 机制继承自 Node 类,那么它要想具备组成链表的能力,就需要 Node 去继承 LinkedHashMap 的内部类 Entry。这个时候就得不偿失了,浪费很多空间去获取不一定用得到的能力。 说到这里,大家应该能明白节点类型的继承体系了。这里单独拿出来说一下,为下面的分析做铺垫。叙述略为啰嗦,见谅。 3.1 链表的建立过程 链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新。 Map 类型的集合类是通过 put(K,V) 方法插入键值对,LinkedHashMap 本身并没有覆写父类的 put 方法,而是直接使用了父类的实现。但在 HashMap 中,put 方法插入的是 HashMap 内部类 Node 类型的节点,该类型的节点并不具备与 LinkedHashMap 内部类 Entry 及其子类型节点组成链表的能力。那么,LinkedHashMap 是怎样建立链表的呢?在展开说明之前,我们先看一下 LinkedHashMap 插入操作相关的代码: // HashMap 中实现 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } // HashMap 中实现 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) {...} // 通过节点 hash 定位节点所在的桶位置,并检测桶中是否包含节点引用 if ((p = tab[i = (n - 1) & hash]) == null) {...} else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) {...} else { // 遍历链表,并统计链表长度 for (int binCount = 0; ; ++binCount) { // 未在单链表中找到要插入的节点,将新节点接在单链表的后面 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) {...} break; } // 插入的节点已经存在于单链表中 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) {...} afterNodeAccess(e); // 回调方法,后续说明 return oldValue; } } ++modCount; if (++size > threshold) {...} afterNodeInsertion(evict); // 回调方法,后续说明 return null; } // HashMap 中实现 Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } // LinkedHashMap 中覆写 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); // 将 Entry 接在双向链表的尾部 linkNodeLast(p); return p; } // LinkedHashMap 中实现 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; // last 为 null,表明链表还未建立 if (last == null) head = p; else { // 将新节点 p 接在链表尾部 p.before = last; last.after = p; } } 上面就是 LinkedHashMap 插入相关的源码,这里省略了部分非关键的代码。我根据上面的代码,可以知道 LinkedHashMap 插入操作的调用过程。如下: 我把 newNode 方法红色背景标注了出来,这一步比较关键。LinkedHashMap 覆写了该方法。在这个方法中,LinkedHashMap 创建了 Entry,并通过 linkNodeLast 方法将 Entry 接在双向链表的尾部,实现了双向链表的建立。双向链表建立之后,我们就可以按照插入顺序去遍历 LinkedHashMap,大家可以自己写点测试代码验证一下插入顺序。 以上就是 LinkedHashMap 维护插入顺序的相关分析。本节的最后,再额外补充一些东西。大家如果仔细看上面的代码的话,会发现有两个以after开头方法,在上文中没有被提及。在 JDK 1.8 HashMap 的源码中,相关的方法有3个: // Callbacks to allow LinkedHashMap post-actions void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { } void afterNodeRemoval(Node<K,V> p) { } 根据这三个方法的注释可以看出,这些方法的用途是在增删查等操作后,通过回调的方式,让 LinkedHashMap 有机会做一些后置操作。上述三个方法的具体实现在 LinkedHashMap 中,本节先不分析这些实现,相关分析会在后续章节中进行。 3.2 链表节点的删除过程 与插入操作一样,LinkedHashMap 删除操作相关的代码也是直接用父类的实现。在删除节点时,父类的删除逻辑并不会修复 LinkedHashMap 所维护的双向链表,这不是它的职责。那么删除及节点后,被删除的节点该如何从双链表中移除呢?当然,办法还算是有的。上一节最后提到 HashMap 中三个回调方法运行 LinkedHashMap 对一些操作做出响应。所以,在删除及节点后,回调方法 afterNodeRemoval 会被调用。LinkedHashMap 覆写该方法,并在该方法中完成了移除被删除节点的操作。相关源码如下: // HashMap 中实现 public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } // HashMap 中实现 final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) {...} else { // 遍历单链表,寻找要删除的节点,并赋值给 node 变量 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) {...} // 将要删除的节点从单链表中移除 else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); // 调用删除回调方法进行后续操作 return node; } } return null; } // LinkedHashMap 中覆写 void afterNodeRemoval(Node<K,V> e) { // unlink LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 将 p 节点的前驱后后继引用置空 p.before = p.after = null; // b 为 null,表明 p 是头节点 if (b == null) head = a; else b.after = a; // a 为 null,表明 p 是尾节点 if (a == null) tail = b; else a.before = b; } 删除的过程并不复杂,上面这么多代码其实就做了三件事: 根据 hash 定位到桶位置 遍历链表或调用红黑树相关的删除方法 从 LinkedHashMap 维护的双链表中移除要删除的节点 举个例子说明一下,假如我们要删除下图键值为 3 的节点。 根据 hash 定位到该节点属于3号桶,然后在对3号桶保存的单链表进行遍历。找到要删除的节点后,先从单链表中移除该节点。如下: 然后再双向链表中移除该节点: 删除及相关修复过程并不复杂,结合上面的图片,大家应该很容易就能理解,这里就不多说了。 3.3 访问顺序的维护过程 前面说了插入顺序的实现,本节来讲讲访问顺序。默认情况下,LinkedHashMap 是按插入顺序维护链表。不过我们可以在初始化 LinkedHashMap,指定 accessOrder 参数为 true,即可让它按访问顺序维护链表。访问顺序的原理上并不复杂,当我们调用get/getOrDefault/replace等方法时,只需要将这些方法访问的节点移动到链表的尾部即可。相应的源码如下: // LinkedHashMap 中覆写 public V get(Object key) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null) return null; // 如果 accessOrder 为 true,则调用 afterNodeAccess 将被访问节点移动到链表最后 if (accessOrder) afterNodeAccess(e); return e.value; } // LinkedHashMap 中覆写 void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; // 如果 b 为 null,表明 p 为头节点 if (b == null) head = a; else b.after = a; if (a != null) a.before = b; /* * 这里存疑,父条件分支已经确保节点 e 不会是尾节点, * 那么 e.after 必然不会为 null,不知道 else 分支有什么作用 */ else last = b; if (last == null) head = p; else { // 将 p 接在链表的最后 p.before = last; last.after = p; } tail = p; ++modCount; } } 上面就是访问顺序的实现代码,并不复杂。下面举例演示一下,帮助大家理解。假设我们访问下图键值为3的节点,访问前结构为: 访问后,键值为3的节点将会被移动到双向链表的最后位置,其前驱和后继也会跟着更新。访问后的结构如下: 3.4 基于 LinkedHashMap 实现缓存 前面介绍了 LinkedHashMap 是如何维护插入和访问顺序的,大家对 LinkedHashMap 的原理应该有了一定的认识。本节我们来写一些代码实践一下,这里通过继承 LinkedHashMap 实现了一个简单的 LRU 策略的缓存。在写代码之前,先介绍一下前置知识。 在3.1节分析链表建立过程时,我故意忽略了部分源码分析。本节就把忽略的部分补上,先看源码吧: void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; // 根据条件判断是否移除最近最少被访问的节点 if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } } // 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; } 上面的源码的核心逻辑在一般情况下都不会被执行,所以之前并没有进行分析。上面的代码做的事情比较简单,就是通过一些条件,判断是否移除最近最少被访问的节点。看到这里,大家应该知道上面两个方法的用途了。当我们基于 LinkedHashMap 实现缓存时,通过覆写removeEldestEntry方法可以实现自定义策略的 LRU 缓存。比如我们可以根据节点数量判断是否移除最近最少被访问的节点,或者根据节点的存活时间判断是否移除该节点等。本节所实现的缓存是基于判断节点数量是否超限的策略。在构造缓存对象时,传入最大节点数。当插入的节点数超过最大节点数时,移除最近最少被访问的节点。实现代码如下: public class SimpleCache<K, V> extends LinkedHashMap<K, V> { private static final int MAX_NODE_NUM = 100; private int limit; public SimpleCache() { this(MAX_NODE_NUM); } public SimpleCache(int limit) { super(limit, 0.75f, true); this.limit = limit; } public V save(K key, V val) { return put(key, val); } public V getOne(K key) { return get(key); } public boolean exists(K key) { return containsKey(key); } /** * 判断节点数是否超限 * @param eldest * @return 超限返回 true,否则返回 false */ @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > limit; } } 测试代码如下: public class SimpleCacheTest { @Test public void test() throws Exception { SimpleCache<Integer, Integer> cache = new SimpleCache<>(3); for (int i = 0; i < 10; i++) { cache.save(i, i * i); } System.out.println("插入10个键值对后,缓存内容:"); System.out.println(cache + "\n"); System.out.println("访问键值为7的节点后,缓存内容:"); cache.getOne(7); System.out.println(cache + "\n"); System.out.println("插入键值为1的键值对后,缓存内容:"); cache.save(1, 1); System.out.println(cache); } } 测试结果如下: 在测试代码中,设定缓存大小为3。在向缓存中插入10个键值对后,只有最后3个被保存下来了,其他的都被移除了。然后通过访问键值为7的节点,使得该节点被移到双向链表的最后位置。当我们再次插入一个键值对时,键值为7的节点就不会被移除。 本节作为对前面内的补充,简单介绍了 LinkedHashMap 在其他方面的应用。本节内容及相关代码并不难理解,这里就不在赘述了。 4. 总结 本文从 LinkedHashMap 维护双向链表的角度对 LinkedHashMap 的源码进行了分析,并在文章的结尾基于 LinkedHashMap 实现了一个简单的 Cache。在日常开发中,LinkedHashMap 的使用频率虽不及 HashMap,但它也个重要的实现。在 Java 集合框架中,HashMap、LinkedHashMap 和 TreeMap 三个映射类基于不同的数据结构,并实现了不同的功能。HashMap 底层基于拉链式的散列结构,并在 JDK 1.8 中引入红黑树优化过长链表的问题。基于这样结构,HashMap 可提供高效的增删改查操作。LinkedHashMap 在其之上,通过维护一条双向链表,实现了散列数据结构的有序遍历。TreeMap 底层基于红黑树实现,利用红黑树的性质,实现了键值对排序功能。我在前面几篇文章中,对 HashMap 和 TreeMap 以及他们均使用到的红黑树进行了详细的分析,有兴趣的朋友可以去看看。 到此,本篇文章就写完了,感谢大家的阅读! 附录:映射类文章列表 红黑树详细分析 TreeMap源码分析 HashMap 源码详细分析(JDK1.8) 本文在知识共享许可协议 4.0 下发布,转载请注明出处 作者:code4fun 本文同步发布在我的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
一、概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap。HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现。HashMap 允许 null 键和 null 值,在计算哈键的哈希值时,null 键哈希值为 0。HashMap 并不保证键值对的顺序,这意味着在进行某些操作后,键值对的顺序可能会发生变化。另外,需要注意的是,HashMap 是非线程安全类,在多线程环境下可能会存在问题。 在本篇文章中,我将会对 HashMap 中常用方法、重要属性及相关方法进行分析。需要说明的是,HashMap 源码中可分析的点很多,本文很难一一覆盖,请见谅。 二、原理 上一节说到 HashMap 底层是基于散列算法实现,散列算法分为散列再探测和拉链式。HashMap 则使用了拉链式的散列算法,并在 JDK 1.8 中引入了红黑树优化过长的链表。数据结构示意图如下: 对于拉链式的散列算法,其数据结构是由数组和链表(或树形结构)组成。在进行增删查等操作时,首先要定位到元素的所在桶的位置,之后再从链表中定位该元素。比如我们要查询上图结构中是否包含元素35,步骤如下: 定位元素35所处桶的位置,index = 35 % 16 = 3 在3号桶所指向的链表中继续查找,发现35在链表中。 上面就是 HashMap 底层数据结构的原理,HashMap 基本操作就是对拉链式散列算法基本操作的一层包装。不同的地方在于 JDK 1.8 中引入了红黑树,底层数据结构由数组+链表变为了数组+链表+红黑树,不过本质并未变。好了,原理部分先讲到这,接下来说说源码实现。 三、源码分析 本篇文章所分析的源码版本为 JDK 1.8。与 JDK 1.7 相比,JDK 1.8 对 HashMap 进行了一些优化。比如引入红黑树解决过长链表效率低的问题。重写 resize 方法,移除了 alternative hashing 相关方法,避免重新计算键的 hash 等。不过本篇文章并不打算对这些优化进行分析,本文仅会分析 HashMap 常用的方法及一些重要属性和相关方法。如果大家对红黑树感兴趣,可以阅读我的另一篇文章 - 红黑树详细分析。 3.1 构造方法 3.1.1 构造方法分析 HashMap 的构造方法不多,只有四个。HashMap 构造方法做的事情比较简单,一般都是初始化一些重要变量,比如 loadFactor 和 threshold。而底层的数据结构则是延迟到插入键值对时再进行初始化。HashMap 相关构造方法如下: /** 构造方法 1 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** 构造方法 2 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** 构造方法 3 */ public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } /** 构造方法 4 */ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } 上面4个构造方法中,大家平时用的最多的应该是第一个了。第一个构造方法很简单,仅将 loadFactor 变量设为默认值。构造方法2调用了构造方法3,而构造方法3仍然只是设置了一些变量。构造方法4则是将另一个 Map 中的映射拷贝一份到自己的存储结构中来,这个方法不是很常用。 上面就是对构造方法简单的介绍,构造方法本身并没什么太多东西,所以就不说了。接下来说说构造方法所初始化的几个的变量。 3.1.2 初始容量、负载因子、阈值 我们在一般情况下,都会使用无参构造方法创建 HashMap。但当我们对时间和空间复杂度有要求的时候,使用默认值有时可能达不到我们的要求,这个时候我们就需要手动调参。在 HashMap 构造方法中,可供我们调整的参数有两个,一个是初始容量 initialCapacity,另一个负载因子 loadFactor。通过这两个设定这两个参数,可以进一步影响阈值大小。但初始阈值 threshold 仅由 initialCapacity 经过移位操作计算得出。他们的作用分别如下: 名称 用途 initialCapacity HashMap 初始容量 loadFactor 负载因子 threshold 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容 相关代码如下: /** The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /** The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f; final float loadFactor; /** The next size value at which to resize (capacity * load factor). */ int threshold; 如果大家去看源码,会发现 HashMap 中没有定义 initialCapacity 这个变量。这个也并不难理解,从参数名上可看出,这个变量表示一个初始容量,只是构造方法中用一次,没必要定义一个变量保存。但如果大家仔细看上面 HashMap 的构造方法,会发现存储键值对的数据结构并不是在构造方法里初始化的。这就有个疑问了,既然叫初始容量,但最终并没有用与初始化数据结构,那传这个参数还有什么用呢?这个问题我先不解释,给大家留个悬念,后面会说明。 默认情况下,HashMap 初始容量是16,负载因子为 0.75。这里并没有默认阈值,原因是阈值可由容量乘上负载因子计算而来(注释中有说明),即threshold = capacity * loadFactor。但当你仔细看构造方法3时,会发现阈值并不是由上面公式计算而来,而是通过一个方法算出来的。这是不是可以说明 threshold 变量的注释有误呢?还是仅这里进行了特殊处理,其他地方遵循计算公式呢?关于这个疑问,这里也先不说明,后面在分析扩容方法时,再来解释这个问题。接下来,我们来看看初始化 threshold 的方法长什么样的的,源码如下: /** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } 上面的代码长的有点不太好看,反正我第一次看的时候不明白它想干啥。不过后来在纸上画画,知道了它的用途。总结起来就一句话:找到大于或等于 cap 的最小2的幂。至于为啥要这样,后面再解释。我们先来看看 tableSizeFor 方法的图解: 上面是 tableSizeFor 方法的计算过程图,这里cap = 536,870,913 = 2<sup>29</sup> + 1,多次计算后,算出n + 1 = 1,073,741,824 = 2<sup>30</sup>。通过图解应该可以比较容易理解这个方法的用途,这里就不多说了。 说完了初始阈值的计算过程,再来说说负载因子(loadFactor)。对于 HashMap 来说,负载因子是一个很重要的参数,该参数反应了 HashMap 桶数组的使用情况(假设键值对节点均匀分布在桶数组中)。通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。至于负载因子怎么调节,这个看使用场景了。一般情况下,我们用默认值就可以了。 3.2 查找 HashMap 的查找操作比较简单,查找步骤与原理篇介绍一致,即先定位键值对所在的桶的位置,然后再对链表或红黑树进行查找。通过这两步即可完成查找,该操作相关代码如下: public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 1. 定位键值对所在桶的位置 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { // 2. 如果 first 是 TreeNode 类型,则调用黑红树查找方法 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 2. 对链表进行查找 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } 查找的核心逻辑是封装在 getNode 方法中的,getNode 方法源码我已经写了一些注释,应该不难看懂。我们先来看看查找过程的第一步 - 确定桶位置,其实现代码如下: // index = (n - 1) & hash first = tab[(n - 1) & hash] 这里通过(n - 1)& hash即可算出桶的在桶数组中的位置,可能有的朋友不太明白这里为什么这么做,这里简单解释一下。HashMap 中桶数组的大小 length 总是2的幂,此时,(n - 1) & hash 等价于对 length 取余。但取余的计算效率没有位运算高,所以(n - 1) & hash也是一个小的优化。举个例子说明一下吧,假设 hash = 185,n = 16。计算过程示意图如下: 上面的计算并不复杂,这里就不多说了。 在上面源码中,除了查找相关逻辑,还有一个计算 hash 的方法。这个方法源码如下: /** * 计算键的 hash 值 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 看这个方法的逻辑好像是通过位运算重新计算 hash,那么这里为什么要这样做呢?为什么不直接用键的 hashCode 方法产生的 hash 呢?大家先可以思考一下,我把答案写在下面。 这样做有两个好处,我来简单解释一下。我们再看一下上面求余的计算图,图中的 hash 是由键的 hashCode 产生。计算余数时,由于 n 比较小,hash 只有低4位参与了计算,高位的计算可以认为是无效的。这样导致了计算结果只与低位信息有关,高位数据没发挥作用。为了处理这个缺陷,我们可以上图中的 hash 高4位数据与低4位数据进行异或运算,即 hash ^ (hash >>> 4)。通过这种方式,让高位数据与低位数据进行异或,以此加大低位信息的随机性,变相的让高位数据参与到计算中。此时的计算过程如下: 在 Java 中,hashCode 方法产生的 hash 是 int 类型,32 位宽。前16位为高位,后16位为低位,所以要左移16位。 上面所说的是重新计算 hash 的一个好处,除此之外,重新计算 hash 的另一个好处是可以增加 hash 的复杂度。当我们覆写 hashCode 方法时,可能会写出分布性不佳的 hashCode 方法,进而导致 hash 的冲突率比较高。通过移位和异或运算,可以让 hash 变得更复杂,进而影响 hash 的分布性。这也就是为什么 HashMap 不直接使用键对象原始 hash 的原因了。 3.3 遍历 和查找查找一样,遍历操作也是大家使用频率比较高的一个操作。对于 遍历 HashMap,我们一般都会用下面的方式: for(Object key : map.keySet()) { // do something } 或 for(HashMap.Entry entry : map.entrySet()) { // do something } 从上面代码片段中可以看出,大家一般都是对 HashMap 的 key 集合或 Entry 集合进行遍历。上面代码片段中用 foreach 遍历 keySet 方法产生的集合,在编译时会转换成用迭代器遍历,等价于: Set keys = map.keySet(); Iterator ite = keys.iterator(); while (ite.hasNext()) { Object key = ite.next(); // do something } 大家在遍历 HashMap 的过程中会发现,多次对 HashMap 进行遍历时,遍历结果顺序都是一致的。但这个顺序和插入的顺序一般都是不一致的。产生上述行为的原因是怎样的呢?大家想一下原因。我先把遍历相关的代码贴出来,如下: public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new KeySet(); keySet = ks; } return ks; } /** * 键集合 */ final class KeySet extends AbstractSet<K> { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator<K> iterator() { return new KeyIterator(); } public final boolean contains(Object o) { return containsKey(o); } public final boolean remove(Object key) { return removeNode(hash(key), key, null, false, true) != null; } // 省略部分代码 } /** * 键迭代器 */ final class KeyIterator extends HashIterator implements Iterator<K> { public final K next() { return nextNode().key; } } abstract class HashIterator { Node<K,V> next; // next entry to return Node<K,V> current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry // 寻找第一个包含链表节点引用的桶 do {} while (index < t.length && (next = t[index++]) == null); } } public final boolean hasNext() { return next != null; } final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { // 寻找下一个包含链表节点引用的桶 do {} while (index < t.length && (next = t[index++]) == null); } return e; } //省略部分代码 } 如上面的源码,遍历所有的键时,首先要获取键集合KeySet对象,然后再通过 KeySet 的迭代器KeyIterator进行遍历。KeyIterator 类继承自HashIterator类,核心逻辑也封装在 HashIterator 类中。HashIterator 的逻辑并不复杂,在初始化时,HashIterator 先从桶数组中找到包含链表节点引用的桶。然后对这个桶指向的链表进行遍历。遍历完成后,再继续寻找下一个包含链表节点引用的桶,找到继续遍历。找不到,则结束遍历。举个例子,假设我们遍历下图的结构: HashIterator 在初始化时,会先遍历桶数组,找到包含链表节点引用的桶,对应图中就是3号桶。随后由 nextNode 方法遍历该桶所指向的链表。遍历完3号桶后,nextNode 方法继续寻找下一个不为空的桶,对应图中的7号桶。之后流程和上面类似,直至遍历完最后一个桶。以上就是 HashIterator 的核心逻辑的流程,对应下图: 遍历上图的最终结果是 19 -> 3 -> 35 -> 7 -> 11 -> 43 -> 59,为了验证正确性,简单写点测试代码跑一下看看。测试代码如下: /** * 应在 JDK 1.8 下测试,其他环境下不保证结果和上面一致 */ public class HashMapTest { @Test public void testTraversal() { HashMap<Integer, String> map = new HashMap(16); map.put(7, ""); map.put(11, ""); map.put(43, ""); map.put(59, ""); map.put(19, ""); map.put(3, ""); map.put(35, ""); System.out.println("遍历结果:"); for (Integer key : map.keySet()) { System.out.print(key + " -> "); } } } 遍历结果如下: 在本小节的最后,抛两个问题给大家。在 JDK 1.8 版本中,为了避免过长的链表对 HashMap 性能的影响,特地引入了红黑树优化性能。但在上面的源码中并没有发现红黑树遍历的相关逻辑,这是为什么呢?对于被转换成红黑树的链表该如何遍历呢?大家可以先想想,然后可以去源码或本文后续章节中找答案。 3.4 插入 3.4.1 插入逻辑分析 通过前两节的分析,大家对 HashMap 低层的数据结构应该了然于心了。即使我不说,大家也应该能知道 HashMap 的插入流程是什么样的了。首先肯定是先定位要插入的键值对属于哪个桶,定位到桶后,再判断桶是否为空。如果为空,则将键值对存入即可。如果不为空,则需将键值对接在链表最后一个位置,或者更新键值对。这就是 HashMap 的插入流程,是不是觉得很简单。当然,大家先别高兴。这只是一个简化版的插入流程,真正的插入流程要复杂不少。首先 HashMap 是变长集合,所以需要考虑扩容的问题。其次,在 JDK 1.8 中,HashMap 引入了红黑树优化过长链表,这里还要考虑多长的链表需要进行优化,优化过程又是怎样的问题。引入这里两个问题后,大家会发现原本简单的操作,现在略显复杂了。在本节中,我将先分析插入操作的源码,扩容、树化(链表转为红黑树,下同)以及其他和树结构相关的操作,随后将在独立的两小结中进行分析。接下来,先来看一下插入操作的源码: public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 初始化桶数组 table,table 被延迟到插入新数据时再进行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 如果桶中不包含键值对节点引用,则将新键值对节点的引用存入桶中即可 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 对链表进行遍历,并统计链表长度 for (int binCount = 0; ; ++binCount) { // 链表中不包含要插入的键值对节点时,则将该节点接在链表的最后 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 如果链表长度大于或等于树化阈值,则进行树化操作 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 条件为 true,表示当前链表包含要插入的键值对,终止遍历 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 判断要插入的键值对是否存在 HashMap 中 if (e != null) { // existing mapping for key V oldValue = e.value; // onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 键值对数量超过阈值时,则进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 插入操作的入口方法是 put(K,V),但核心逻辑在V putVal(int, K, V, boolean, boolean) 方法中。putVal 方法主要做了这么几件事情: 当桶数组 table 为空时,通过扩容的方式初始化 table 查找要插入的键值对是否已经存在,存在的话根据条件判断是否用新值替换旧值 如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树 判断键值对数量是否大于阈值,大于的话则进行扩容操作 以上就是 HashMap 插入的逻辑,并不是很复杂,这里就不多说了。接下来来分析一下扩容机制。 3.4.2 扩容机制 在 Java 中,数组的长度是固定的,这意味着数组只能存储固定量的数据。但在开发的过程中,很多时候我们无法知道该建多大的数组合适。建小了不够用,建大了用不完,造成浪费。如果我们能实现一种变长的数组,并按需分配空间就好了。好在,我们不用自己实现变长数组,Java 集合框架已经实现了变长的数据结构。比如 ArrayList 和 HashMap。对于这类基于数组的变长数据结构,扩容是一个非常重要的操作。下面就来聊聊 HashMap 的扩容机制。 在详细分析之前,先来说一下扩容相关的背景知识: 在 HashMap 中,桶数组的长度均是2的幂,阈值大小为桶数组长度与负载因子的乘积。当 HashMap 中的键值对数量超过阈值时,进行扩容。 HashMap 的扩容机制与其他变长集合的套路不太一样,HashMap 按当前桶数组长度的2倍进行扩容,阈值也变为原来的2倍(如果计算过程中,阈值溢出归零,则按阈值公式重新计算)。扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去。以上就是 HashMap 的扩容大致过程,接下来我们来看看具体的实现: final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 如果 table 不为空,表明已经初始化过了 if (oldCap > 0) { // 当 table 容量超过容量最大值,则不再扩容 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 按旧容量和阈值的2倍计算新容量和阈值的大小 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold /* * 初始化时,将 threshold 的值赋值给 newCap, * HashMap 使用 threshold 变量暂时保存 initialCapacity 参数的值 */ newCap = oldThr; else { // zero initial threshold signifies using defaults /* * 调用无参构造方法时,桶数组容量为默认容量, * 阈值为默认容量与默认负载因子乘积 */ newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // newThr 为 0 时,按阈值计算公式进行计算 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 创建新的桶数组,桶数组的初始化也是在这里完成的 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 如果旧的桶数组不为空,则遍历桶数组,并将键值对映射到新的桶数组中 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 重新映射时,需要对红黑树进行拆分 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 遍历链表,并将链表节点按原顺序进行分组 do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 将分组后的链表映射到新桶中 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } 上面的源码有点长,希望大家耐心看懂它的逻辑。上面的源码总共做了3件事,分别是: 计算新桶数组的容量 newCap 和新阈值 newThr 根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的 将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组。 上面列的三点中,创建新的桶数组就一行代码,不用说了。接下来,来说说第一点和第三点,先说说 newCap 和 newThr 计算过程。该计算过程对应 resize 源码的第一和第二个条件分支,如下: // 第一个条件分支 if ( oldCap > 0) { // 嵌套条件分支 if (oldCap >= MAXIMUM_CAPACITY) {...} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {...} } else if (oldThr > 0) {...} else {...} // 第二个条件分支 if (newThr == 0) {...} 通过这两个条件分支对不同情况进行判断,进而算出不同的容量值和阈值。它们所覆盖的情况如下: 分支一: 条件 覆盖情况 备注 oldCap > 0 桶数组 table 已经被初始化 oldThr > 0 threshold > 0,且桶数组未被初始化 调用 HashMap(int) 和 HashMap(int, float) 构造方法时会产生这种情况,此种情况下 newCap = oldThr,newThr 在第二个条件分支中算出 oldCap == 0 && oldThr == 0 桶数组未被初始化,且 threshold 为 0 调用 HashMap() 构造方法会产生这种情况。 这里把oldThr > 0情况单独拿出来说一下。在这种情况下,会将 oldThr 赋值给 newCap,等价于newCap = threshold = tableSizeFor(initialCapacity)。我们在初始化时传入的 initialCapacity 参数经过 threshold 中转最终赋值给了 newCap。这也就解答了前面提的一个疑问:initialCapacity 参数没有被保存下来,那么它怎么参与桶数组的初始化过程的呢? 嵌套分支: 条件 覆盖情况 备注 oldCap >= 230 桶数组容量大于或等于最大桶容量 230 这种情况下不再扩容 newCap < 230 && oldCap > 16 新桶数组容量小于最大值,且旧桶数组容量大于 16 该种情况下新阈值 newThr = oldThr << 1,移位可能会导致溢出 这里简单说明一下移位导致的溢出情况,当 loadFactor小数位为 0,整数位可被2整除且大于等于8时,在某次计算中就可能会导致 newThr 溢出归零。见下图: 分支二: 条件 覆盖情况 备注 newThr == 0 第一个条件分支未计算 newThr 或嵌套分支在计算过程中导致 newThr 溢出归零 说完 newCap 和 newThr 的计算过程,接下来再来分析一下键值对节点重新映射的过程。 在 JDK 1.8 中,重新映射节点需要考虑节点类型。对于树形节点,需先拆分红黑树再映射。对于链表类型节点,则需先对链表进行分组,然后再映射。需要的注意的是,分组后,组内节点相对位置保持不变。关于红黑树拆分的逻辑将会放在下一小节说明,先来看看链表是怎样进行分组映射的。 我们都知道往底层数据结构中插入节点时,一般都是先通过模运算计算桶位置,接着把节点放入桶中即可。事实上,我们可以把重新映射看做插入操作。在 JDK 1.7 中,也确实是这样做的。但在 JDK 1.8 中,则对这个过程进行了一定的优化,逻辑上要稍微复杂一些。在详细分析前,我们先来回顾一下 hash 求余的过程: 上图中,桶数组大小 n = 16,hash1 与 hash2 不相等。但因为只有后4位参与求余,所以结果相等。当桶数组扩容后,n 由16变成了32,对上面的 hash 值重新进行映射: 扩容后,参与模运算的位数由4位变为了5位。由于两个 hash 第5位的值是不一样,所以两个 hash 算出的结果也不一样。上面的计算过程并不难理解,继续往下分析。 假设我们上图的桶数组进行扩容,扩容后容量 n = 16,重新映射过程如下: 依次遍历链表,并计算节点 hash & oldCap 的值。如下图所示 如果值为0,将 loHead 和 loTail 指向这个节点。如果后面还有节点 hash & oldCap 为0的话,则将节点链入 loHead 指向的链表中,并将 loTail 指向该节点。如果值为1的话,则让 hiHead 和 hiTail 指向该节点。完成遍历后,可能会得到两条链表,此时就完成了链表分组: 最后再将这两条链接存放到相应的桶中,完成扩容。如下图: 从上图可以发现,重新映射后,两条链表中的节点顺序并未发生变化,还是保持了扩容前的顺序。以上就是 JDK 1.8 中 HashMap 扩容的代码讲解。另外再补充一下,JDK 1.8 版本下 HashMap 扩容效率要高于之前版本。如果大家看过 JDK 1.7 的源码会发现,JDK 1.7 为了防止因 hash 碰撞引发的拒绝服务攻击,在计算 hash 过程中引入随机种子。以增强 hash 的随机性,使得键值对均匀分布在桶数组中。在扩容过程中,相关方法会根据容量判断是否需要生成新的随机种子,并重新计算所有节点的 hash。而在 JDK 1.8 中,则通过引入红黑树替代了该种方式。从而避免了多次计算 hash 的操作,提高了扩容效率。 本小节的内容讲就先讲到这,接下来,来讲讲链表与红黑树相互转换的过程。 3.4.3 链表树化、红黑树链化与拆分 JDK 1.8 对 HashMap 实现进行了改进。最大的改进莫过于在引入了红黑树处理频繁的碰撞,代码复杂度也随之上升。比如,以前只需实现一套针对链表操作的方法即可。而引入红黑树后,需要另外实现红黑树相关的操作。红黑树是一种自平衡的二叉查找树,本身就比较复杂。本篇文章中并不打算对红黑树展开介绍,本文仅会介绍链表树化需要注意的地方。至于红黑树详细的介绍,如果大家有兴趣,可以参考我的另一篇文章 - 红黑树详细分析。 在展开说明之前,先把树化的相关代码贴出来,如下: static final int TREEIFY_THRESHOLD = 8; /** * 当桶数组容量小于该值时,优先进行扩容,而不是树化 */ static final int MIN_TREEIFY_CAPACITY = 64; static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } } /** * 将普通节点链表转换成树形节点链表 */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 桶数组容量小于 MIN_TREEIFY_CAPACITY,优先进行扩容而不是树化 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { // hd 为头节点(head),tl 为尾节点(tail) TreeNode<K,V> hd = null, tl = null; do { // 将普通节点替换成树形节点 TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); // 将普通链表转成由树形节点链表 if ((tab[index] = hd) != null) // 将树形链表转换成红黑树 hd.treeify(tab); } } TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); } 在扩容过程中,树化要满足两个条件: 链表长度大于等于 TREEIFY_THRESHOLD 桶数组容量大于等于 MIN_TREEIFY_CAPACITY 第一个条件比较好理解,这里就不说了。这里来说说加入第二个条件的原因,个人觉得原因如下: 当桶数组容量比较小时,键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。毕竟高碰撞率是因为桶数组容量较小引起的,这个是主因。容量小时,优先扩容可以避免一些列的不必要的树化过程。同时,桶容量较小时,扩容会比较频繁,扩容时需要拆分红黑树并重新映射。所以在桶容量比较小的情况下,将长链表转成红黑树是一件吃力不讨好的事。 回到上面的源码中,我们继续看一下 treeifyBin 方法。该方法主要的作用是将普通链表转成为由 TreeNode 型节点组成的链表,并在最后调用 treeify 是将该链表转为红黑树。TreeNode 继承自 Node 类,所以 TreeNode 仍然包含 next 引用,原链表的节点顺序最终通过 next 引用被保存下来。我们假设树化前,链表结构如下: HashMap 在设计之初,并没有考虑到以后会引入红黑树进行优化。所以并没有像 TreeMap 那样,要求键类实现 comparable 接口或提供相应的比较器。但由于树化过程需要比较两个键对象的大小,在键类没有实现 comparable 接口的情况下,怎么比较键与键之间的大小了就成了一个棘手的问题。为了解决这个问题,HashMap 是做了三步处理,确保可以比较出两个键的大小,如下: 比较键与键之间 hash 的大小,如果 hash 相同,继续往下比较 检测键类是否实现了 Comparable 接口,如果实现调用 compareTo 方法进行比较 如果仍未比较出大小,就需要进行仲裁了,仲裁方法为 tieBreakOrder(大家自己看源码吧) tie break 是网球术语,可以理解为加时赛的意思,起这个名字还是挺有意思的。 通过上面三次比较,最终就可以比较出孰大孰小。比较出大小后就可以构造红黑树了,最终构造出的红黑树如下: 橙色的箭头表示 TreeNode 的 next 引用。由于空间有限,prev 引用未画出。可以看出,链表转成红黑树后,原链表的顺序仍然会被引用仍被保留了(红黑树的根节点会被移动到链表的第一位),我们仍然可以按遍历链表的方式去遍历上面的红黑树。这样的结构为后面红黑树的切分以及红黑树转成链表做好了铺垫,我们继续往下分析。 红黑树拆分 扩容后,普通节点需要重新映射,红黑树节点也不例外。按照一般的思路,我们可以先把红黑树转成链表,之后再重新映射链表即可。这种处理方式是大家比较容易想到的,但这样做会损失一定的效率。不同于上面的处理方式,HashMap 实现的思路则是上好佳(上好佳请把广告费打给我)。如上节所说,在将普通链表转成红黑树时,HashMap 通过两个额外的引用 next 和 prev 保留了原链表的节点顺序。这样再对红黑树进行重新映射时,完全可以按照映射链表的方式进行。这样就避免了将红黑树转成链表后再进行映射,无形中提高了效率。 以上就是红黑树拆分的逻辑,下面看一下具体实现吧: // 红黑树转链表阈值 static final int UNTREEIFY_THRESHOLD = 6; final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0; /* * 红黑树节点仍然保留了 next 引用,故仍可以按链表方式遍历红黑树。 * 下面的循环是对红黑树节点进行分组,与上面类似 */ for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } if (loHead != null) { // 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表 if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { tab[index] = loHead; /* * hiHead == null 时,表明扩容后, * 所有节点仍在原位置,树结构不变,无需重新树化 */ if (hiHead != null) loHead.treeify(tab); } } // 与上面类似 if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } } 从源码上可以看得出,重新映射红黑树的逻辑和重新映射链表的逻辑基本一致。不同的地方在于,重新映射后,会将红黑树拆分成两条由 TreeNode 组成的链表。如果链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。否则根据条件重新将 TreeNode 链表树化。举个例子说明一下,假设扩容后,重新映射上图的红黑树,映射结果如下: 红黑树链化 前面说过,红黑树中仍然保留了原链表节点顺序。有了这个前提,再将红黑树转成链表就简单多了,仅需将 TreeNode 链表转成 Node 类型的链表即可。相关代码如下: final Node<K,V> untreeify(HashMap<K,V> map) { Node<K,V> hd = null, tl = null; // 遍历 TreeNode 链表,并用 Node 替换 for (Node<K,V> q = this; q != null; q = q.next) { // 替换节点类型 Node<K,V> p = map.replacementNode(q, null); if (tl == null) hd = p; else tl.next = p; tl = p; } return hd; } Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) { return new Node<>(p.hash, p.key, p.value, next); } 上面的代码并不复杂,不难理解,这里就不多说了。到此扩容相关内容就说完了,不知道大家理解没。 3.5 删除 如果大家坚持看完了前面的内容,到本节就可以轻松一下。当然,前提是不去看红黑树的删除操作。不过红黑树并非本文讲解重点,本节中也不会介绍红黑树相关内容,所以大家不用担心。 HashMap 的删除操作并不复杂,仅需三个步骤即可完成。第一步是定位桶位置,第二步遍历链表并找到键值相等的节点,第三步删除节点。相关源码如下: public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && // 1. 定位桶位置 (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; // 如果键的值与链表第一个节点相等,则将 node 指向该节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { // 如果是 TreeNode 类型,调用红黑树的查找逻辑定位待删除节点 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { // 2. 遍历链表,找到待删除节点 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } // 3. 删除节点,并修复链表或红黑树 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; } 删除操作本身并不复杂,有了前面的基础,理解起来也就不难了,这里就不多说了。 3.6 其他细节 前面的内容分析了 HashMap 的常用操作及相关的源码,本节内容再补充一点其他方面的东西。 被 transient 所修饰 table 变量 如果大家细心阅读 HashMap 的源码,会发现桶数组 table 被申明为 transient。transient 表示易变的意思,在 Java 中,被该关键字修饰的变量不会被默认的序列化机制序列化。我们再回到源码中,考虑一个问题:桶数组 table 是 HashMap 底层重要的数据结构,不序列化的话,别人还怎么还原呢? 这里简单说明一下吧,HashMap 并没有使用默认的序列化机制,而是通过实现readObject/writeObject两个方法自定义了序列化的内容。这样做是有原因的,试问一句,HashMap 中存储的内容是什么?不用说,大家也知道是键值对。所以只要我们把键值对序列化了,我们就可以根据键值对数据重建 HashMap。有的朋友可能会想,序列化 table 不是可以一步到位,后面直接还原不就行了吗?这样一想,倒也是合理。但序列化 talbe 存在着两个问题: table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间 同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。 以上两个问题中,第一个问题比较好理解,第二个问题解释一下。HashMap 的get/put/remove等方法第一步就是根据 hash 找到键所在的桶位置,但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。 综上所述,大家应该能明白 HashMap 不序列化 table 的原因了。 3.7 总结 本章对 HashMap 常见操作相关代码进行了详细分析,并在最后补充了一些其他细节。在本章中,插入操作一节的内容说的最多,主要是因为插入操作涉及的点特别多,一环扣一环。包含但不限于“table 初始化、扩容、树化”等,总体来说,插入操作分析起来难度还是很大的。好在,最后分析完了。 本章篇幅虽比较大,但仍未把 HashMap 所有的点都分析到。比如,红黑树的增删查等操作。当然,我个人看来,以上的分析已经够了。毕竟大家是类库的使用者而不是设计者,没必要去弄懂每个细节。所以如果某些细节实在看不懂的话就跳过吧,对我们开发来说,知道 HashMap 大致原理即可。 好了,本章到此结束。 四、写在最后 写到这里终于可以松一口气了,这篇文章前前后后花了我一周多的时间。在我写这篇文章之前,对 HashMap 认识仅限于原理层面,并未深入了解。一开始,我觉得关于 HashMap 没什么好写的,毕竟大家对 HashMap 多少都有一定的了解。但等我深入阅读 HashMap 源码后,发现之前的认知是错的。不是没什么可写的,而是可写的点太多了,不知道怎么写了。JDK 1.8 版本的 HashMap 实现上比之前版本要复杂的多,想弄懂众多的细节难度还是不小的。仅自己弄懂还不够,还要写出来,难度就更大了,本篇文章基本上是在边读源码边写的状态下完成的。由于时间和能力有限,加之文章篇幅比较大,很难保证不出错分析过程及配图不出错。如果有错误,希望大家指出来,我会及时修改,这里先谢谢大家。 好了,本文就到这里了,谢谢大家的阅读! 参考 JDK 源码中 HashMap 的 hash 方法原理是什么?- 知乎 Java 8系列之重新认识HashMap - 美团技术博客 python内置的hash函数对于字符串来说,每次得到的值不一样?- 知乎 Java中HashMap关键字transient的疑惑 - segmentFault 本文在知识共享许可协议 4.0 下发布,转载请注明出处 作者:code4fun 为了获得更好的分类阅读体验, 请移步至本人的个人博客:http://www.coolblog.xyz 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
一、简介 TreeMap最早出现在JDK 1.2中,是 Java 集合框架中比较重要一个的实现。TreeMap 底层基于红黑树实现,可保证在log(n)时间复杂度内完成 containsKey、get、put 和 remove 操作,效率很高。另一方面,由于 TreeMap 基于红黑树实现,这为 TreeMap 保持键的有序性打下了基础。总的来说,TreeMap 的核心是红黑树,其很多方法也是对红黑树增删查基础操作的一个包装。所以只要弄懂了红黑树,TreeMap 就没什么秘密了。 二、概览 TreeMap继承自AbstractMap,并实现了 NavigableMap接口。NavigableMap 接口继承了SortedMap接口,SortedMap 最终继承自Map接口,同时 AbstractMap 类也实现了 Map 接口。以上就是 TreeMap 的继承体系,描述起来有点乱,不如看图了: 上图就是 TreeMap 的继承体系图,比较直观。这里来简单说一下继承体系中不常见的接口NavigableMap和SortedMap,这两个接口见名知意。先说 NavigableMap 接口,NavigableMap 接口声明了一些列具有导航功能的方法,比如: /** * 返回红黑树中最小键所对应的 Entry */ Map.Entry<K,V> firstEntry(); /** * 返回最大的键 maxKey,且 maxKey 仅小于参数 key */ K lowerKey(K key); /** * 返回最小的键 minKey,且 minKey 仅大于参数 key */ K higherKey(K key); // 其他略 通过这些导航方法,我们可以快速定位到目标的 key 或 Entry。至于 SortedMap 接口,这个接口提供了一些基于有序键的操作,比如 /** * 返回包含键值在 [minKey, toKey) 范围内的 Map */ SortedMap<K,V> headMap(K toKey);(); /** * 返回包含键值在 [fromKey, toKey) 范围内的 Map */ SortedMap<K,V> subMap(K fromKey, K toKey); // 其他略 以上就是两个接口的介绍,很简单。至于 AbstractMap 和 Map 这里就不说了,大家有兴趣自己去看看 Javadoc 吧。关于 TreeMap 的继承体系就这里就说到这,接下来我们进入细节部分分析。 三、源码分析 JDK 1.8中的TreeMap源码有两千多行,还是比较多的。本文并不打算逐句分析所有的源码,而是挑选几个常用的方法进行分析。这些方法实现的功能分别是查找、遍历、插入、删除等,其他的方法小伙伴们有兴趣可以自己分析。TreeMap实现的核心部分是关于红黑树的实现,其绝大部分的方法基本都是对底层红黑树增、删、查操作的一个封装。如简介一节所说,只要弄懂了红黑树原理,TreeMap 就没什么秘密了。关于红黑树的原理,请参考本人的另一篇文章-红黑树详细分析,本篇文章不会对此展开讨论。 3.1 查找 TreeMap基于红黑树实现,而红黑树是一种自平衡二叉查找树,所以 TreeMap 的查找操作流程和二叉查找树一致。二叉树的查找流程是这样的,先将目标值和根节点的值进行比较,如果目标值小于根节点的值,则再和根节点的左孩子进行比较。如果目标值大于根节点的值,则继续和根节点的右孩子比较。在查找过程中,如果目标值和二叉树中的某个节点值相等,则返回 true,否则返回 false。TreeMap 查找和此类似,只不过在 TreeMap 中,节点(Entry)存储的是键值对<k,v>。在查找过程中,比较的是键的大小,返回的是值,如果没找到,则返回null。TreeMap 中的查找方法是get,具体实现在getEntry方法中,相关源码如下: public V get(Object key) { Entry<K,V> p = getEntry(key); return (p==null ? null : p.value); } final Entry<K,V> getEntry(Object key) { // Offload comparator-based version for sake of performance if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root; // 查找操作的核心逻辑就在这个 while 循环里 while (p != null) { int cmp = k.compareTo(p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; } 查找操作的核心逻辑就是getEntry方法中的while循环,大家对照上面的说的流程,自己看一下吧,比较简单,就不多说了。 3.2 遍历 遍历操作也是大家使用频率较高的一个操作,对于TreeMap,使用方式一般如下: for(Object key : map.keySet()) { // do something } 或 for(Map.Entry entry : map.entrySet()) { // do something } 从上面代码片段中可以看出,大家一般都是对 TreeMap 的 key 集合或 Entry 集合进行遍历。上面代码片段中用 foreach 遍历 keySet 方法产生的集合,在编译时会转换成用迭代器遍历,等价于: Set keys = map.keySet(); Iterator ite = keys.iterator(); while (ite.hasNext()) { Object key = ite.next(); // do something } 另一方面,TreeMap 有一个特性,即可以保证键的有序性,默认是正序。所以在遍历过程中,大家会发现 TreeMap 会从小到大输出键的值。那么,接下来就来分析一下keySet方法,以及在遍历 keySet 方法产生的集合时,TreeMap 是如何保证键的有序性的。相关代码如下: public Set<K> keySet() { return navigableKeySet(); } public NavigableSet<K> navigableKeySet() { KeySet<K> nks = navigableKeySet; return (nks != null) ? nks : (navigableKeySet = new KeySet<>(this)); } static final class KeySet<E> extends AbstractSet<E> implements NavigableSet<E> { private final NavigableMap<E, ?> m; KeySet(NavigableMap<E,?> map) { m = map; } public Iterator<E> iterator() { if (m instanceof TreeMap) return ((TreeMap<E,?>)m).keyIterator(); else return ((TreeMap.NavigableSubMap<E,?>)m).keyIterator(); } // 省略非关键代码 } Iterator<K> keyIterator() { return new KeyIterator(getFirstEntry()); } final class KeyIterator extends PrivateEntryIterator<K> { KeyIterator(Entry<K,V> first) { super(first); } public K next() { return nextEntry().key; } } abstract class PrivateEntryIterator<T> implements Iterator<T> { Entry<K,V> next; Entry<K,V> lastReturned; int expectedModCount; PrivateEntryIterator(Entry<K,V> first) { expectedModCount = modCount; lastReturned = null; next = first; } public final boolean hasNext() { return next != null; } final Entry<K,V> nextEntry() { Entry<K,V> e = next; if (e == null) throw new NoSuchElementException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); // 寻找节点 e 的后继节点 next = successor(e); lastReturned = e; return e; } // 其他方法省略 } 上面的代码比较多,keySet 涉及的代码还是比较多的,大家可以从上往下看。从上面源码可以看出 keySet 方法返回的是KeySet类的对象。这个类实现了Iterable接口,可以返回一个迭代器。该迭代器的具体实现是KeyIterator,而 KeyIterator 类的核心逻辑是在PrivateEntryIterator中实现的。上面的代码虽多,但核心代码还是 KeySet 类和 PrivateEntryIterator 类的 nextEntry方法。KeySet 类就是一个集合,这里不分析了。而 nextEntry 方法比较重要,下面简单分析一下。 在初始化 KeyIterator 时,会将 TreeMap 中包含最小键的 Entry 传给 PrivateEntryIterator。当调用 nextEntry 方法时,通过调用 successor 方法找到当前 entry 的后继,并让 next 指向后继,最后返回当前的 entry。通过这种方式即可实现按正序返回键值的的逻辑。 好了,TreeMap 的遍历操作就讲到这。遍历操作本身不难,但讲的有点多,略显啰嗦,大家见怪。 3.3 插入 相对于前两个操作,插入操作明显要复杂一些。当往 TreeMap 中放入新的键值对后,可能会破坏红黑树的性质。这里为了描述方便,把 Entry 称为节点。并把新插入的节点称为N,N 的父节点为P。P 的父节点为G,且 P 是 G 的左孩子。P 的兄弟节点为U。在往红黑树中插入新的节点 N 后(新节点为红色),会产生下面5种情况: N 是根节点 N 的父节点是黑色 N 的父节点是红色,叔叔节点也是红色 N 的父节点是红色,叔叔节点是黑色,且 N 是 P 的右孩子 N 的父节点是红色,叔叔节点是黑色,且 N 是 P 的左孩子 上面5中情况中,情况2不会破坏红黑树性质,所以无需处理。情况1 会破坏红黑树性质2(根是黑色),情况3、4、和5会破坏红黑树性质4(每个红色节点必须有两个黑色的子节点)。这个时候就需要进行调整,以使红黑树重新恢复平衡。至于怎么调整,可以参考我另一篇关于红黑树的文章(红黑树详细分析),这里不再重复说明。接下来分析一下插入操作相关源码: public V put(K key, V value) { Entry<K,V> t = root; // 1.如果根节点为 null,将新节点设为根节点 if (t == null) { compare(key, key); root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; if (cpr != null) { // 2.为 key 在红黑树找到合适的位置 do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } else { // 与上面代码逻辑类似,省略 } Entry<K,V> e = new Entry<>(key, value, parent); // 3.将新节点链入红黑树中 if (cmp < 0) parent.left = e; else parent.right = e; // 4.插入新节点可能会破坏红黑树性质,这里修正一下 fixAfterInsertion(e); size++; modCount++; return null; } put 方法代码如上,逻辑和二叉查找树插入节点逻辑一致。重要的步骤我已经写了注释,并不难理解。插入逻辑的复杂之处在于插入后的修复操作,对应的方法fixAfterInsertion,该方法的源码和说明如下: 到这里,插入操作就讲完了。接下来,来说说 TreeMap 中最复杂的部分,也就是删除操作了。 3.4 删除 删除操作是红黑树最复杂的部分,原因是该操作可能会破坏红黑树性质5(从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点),修复性质5要比修复其他性质(性质2和4需修复,性质1和3不用修复)复杂的多。当删除操作导致性质5被破坏时,会出现8种情况。为了方便表述,这里还是先做一些假设。我们把最终被删除的节点称为 X,X 的替换节点称为 N。N 的父节点为P,且 N 是 P 的左孩子。N 的兄弟节点为S,S 的左孩子为 SL,右孩子为 SR。这里特地强调 X 是 最终被删除 的节点,是原因二叉查找树会把要删除有两个孩子的节点的情况转化为删除只有一个孩子的节点的情况,该节点是欲被删除节点的前驱和后继。 接下来,简单列举一下删除节点时可能会出现的情况,先列举较为简单的情况: 最终被删除的节点 X 是红色节点 X 是黑色节点,但该节点的孩子节点是红色 比较复杂的情况: 替换节点 N 是新的根 N 为黑色,N 的兄弟节点 S 为红色,其他节点为黑色。 N 为黑色,N 的父节点 P,兄弟节点 S 和 S 的孩子节点均为黑色。 N 为黑色,P 是红色,S 和 S 孩子均为黑色。 N 为黑色,P 可红可黑,S 为黑色,S 的左孩子 SL 为红色,右孩子 SR 为黑色 N 为黑色,P 可红可黑,S 为黑色,SR 为红色,SL 可红可黑 上面列举的8种情况中,前两种处理起来比较简单,后6种情况中情况2~6较为复杂。接下来我将会对情况2~6展开分析,删除相关的源码如下: public V remove(Object key) { Entry<K,V> p = getEntry(key); if (p == null) return null; V oldValue = p.value; deleteEntry(p); return oldValue; } private void deleteEntry(Entry<K,V> p) { modCount++; size--; /* * 1. 如果 p 有两个孩子节点,则找到后继节点, * 并把后继节点的值复制到节点 P 中,并让 p 指向其后继节点 */ if (p.left != null && p.right != null) { Entry<K,V> s = successor(p); p.key = s.key; p.value = s.value; p = s; } // p has 2 children // Start fixup at replacement node, if it exists. Entry<K,V> replacement = (p.left != null ? p.left : p.right); if (replacement != null) { /* * 2. 将 replacement parent 引用指向新的父节点, * 同时让新的父节点指向 replacement。 */ replacement.parent = p.parent; if (p.parent == null) root = replacement; else if (p == p.parent.left) p.parent.left = replacement; else p.parent.right = replacement; // Null out links so they are OK to use by fixAfterDeletion. p.left = p.right = p.parent = null; // 3. 如果删除的节点 p 是黑色节点,则需要进行调整 if (p.color == BLACK) fixAfterDeletion(replacement); } else if (p.parent == null) { // 删除的是根节点,且树中当前只有一个节点 root = null; } else { // 删除的节点没有孩子节点 // p 是黑色,则需要进行调整 if (p.color == BLACK) fixAfterDeletion(p); // 将 P 从树中移除 if (p.parent != null) { if (p == p.parent.left) p.parent.left = null; else if (p == p.parent.right) p.parent.right = null; p.parent = null; } } } 从源码中可以看出,remove方法只是一个简单的保证,核心实现在deleteEntry方法中。deleteEntry 主要做了这么几件事: 如果待删除节点 P 有两个孩子,则先找到 P 的后继 S,然后将 S 中的值拷贝到 P 中,并让 P 指向 S 如果最终被删除节点 P(P 现在指向最终被删除节点)的孩子不为空,则用其孩子节点替换掉 如果最终被删除的节点是黑色的话,调用 fixAfterDeletion 方法进行修复 上面说了 replacement 不为空时,deleteEntry 的执行逻辑。上面说的略微啰嗦,如果简单说的话,7个字即可总结:找后继 -> 替换 -> 修复。这三步中,最复杂的是修复操作。修复操作要重新使红黑树恢复平衡,修复操作的源码分析如下: fixAfterDeletion 方法分析如下: 上面对 fixAfterDeletion 部分代码逻辑就行了分析,通过配图的形式解析了每段代码逻辑所处理的情况。通过图解,应该还是比较好理解的。好了,TreeMap 源码先分析到这里。 四、总结 本文可以看做是本人”红黑树详细分析”一文的延续。前一篇文章从理论层面上详细分析了红黑树插入和删除操作可能会导致的问题,以及如何修复。本文则从实践层面是分析了插入和删除操作在具体的实现中时怎样做的。另外,本文选择了从集合框架常用方法这一角度进行分析,详细分析了查找、遍历、插入和删除等方法。总体来说,分析的还是比较详细的。当然限于本人的水平,文中可能会存在一些错误的论述。如果大家发现了,欢迎指出来。如果这些错误的论述对你造成了困扰,我这里先说声抱歉。如果你也在学习 TreeMap 源码,希望这篇文章能够帮到你。 最后感谢大家花时间的阅读我的文章,顺祝大家写代码无BUG,下篇文章见。 本文在知识共享许可协议 4.0 下发布,转载请注明出处 作者:coolblog.xyz 为了获得更好的阅读体验, 请移步至本人的个人博客:http://www.coolblog.xyz 我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。 本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。