大型分布式综合项目实战(JAVA架构),高并发,分布式,web安全,缓存架构实战
大型分布式综合项目实战(JAVA架构),高并发,分布式,web安全,缓存架构实战
你好,我是逸章,从事软件开发和系统架构设计工作十余年,长期负责分布式系统的构建和优化工作。工作这十余年来,见证了新型互联网和物联网系统的兴起和发展,也见证了 Dubbo、Spring Cloud 等分布式系统开发框架的不断演进。
在当下的软件开发过程中,分布式系统的设计和实现已经非常普遍。面对微服务架构的遍地开花以及中台思想的日渐兴起,如何高效采用主流的分布式开发工具和机制来满足不断变化的业务需求,已经成为很多架构师和开发人员所需要规划和落实的一大课题。
为什么要学习分布式系统开发的核心机制?
想要开发一个分布式系统实际上并没有想象的那么容易,抛开业务层面的规划和设计,开发人员在技术实现层面上也面临着一系列的问题,比如:
- 如何针对分布式应用场景选择合适的开源框架?
- 如何正确、高效地使用这些开源框架?
- 如何快速分析和解决框架使用过程中的问题?
- 如何基于这些框架做定制化的开发以满足差异化需求?
导致这些问题产生的一个核心原因是开发人员并没有深入了解分布式开发所涉及到的核心开发机制,无法真正理解这些框架背后的实现原理,也就无法应对上述问题。我认为,这门专栏能够帮助大家很好的解决这些问题。
这是一门关于源码解析类的专栏,整个专栏基于框架源码解读来剖析互联网分布式系统开发核心机制。包括如下所示的五大主题:
我们将从这五大类主题出发解析 Dubbo、Spring Cloud 和 Mybatis 这三个在互联网行业中开发分布式系统时所使用的最广泛的开源框架,以及从这些框架提炼出来的分布式系统开发的核心机制。
整个专栏分为上下两个专栏,其中上半部分将讲解剖析框架代码的结构、剖析架构模式与设计模式以及剖析通用机制与组件这三大类主题,而下半部分将包括如何剖析分布式服务组件和微服务架构组件的系统方法和技术体系。
同时,这也是一门关于面试技巧类的专栏。我们还将从应对技术原理分析类面试角度出发系统讲解开发人员所应该具备的面试技巧,以及围绕以上五大类主题所展开的面试题分析和讲解。最终,我们也将从构建属于自己的技术知识体系角度出发探讨技术原理的相通性以及技术成长的方法论。
专栏内容有多适合你?
工具和框架发展日新月异,例如在我们的专栏中会经常提到的 Dubbo 框架在 2013 年开始已经几乎不再更新,而这两年又重新启动维护工作,目前已经成为 Apache 的顶级项目。
不难想象在不久的将来,业界还会出现第二个、第三个类似 Dubbo 这样的框架。我们知道 Dubbo 是阿里巴巴开源的一个分布式服务框架,其背后的核心原理就是实现了 RPC 和服务治理。
对于任何一个分布式服务框架而言,RPC 和服务治理都是不可缺少的组件,而关于RPC和服务治理的相关设计方法和实现原理在很长一段时间内实际上都没有什么变化。我们只要能够对 RPC 和服务治理有深刻的理解,那么关于 Dubbo 同类型的任何新框架,如果出现类似“RPC 架构核心组件、Dubbo 框架原理”这样的面试题,我们都应该有明确的回答思路和方法。为了达到这样的境界,这就要深入分析 Dubbo 等框架所具备的技术知识体系,以及这些技术知识体系之间存在的的相通性。
在分布式系统开发技术中,这样的案例还有很多,比如:
- 系统扩展性设计在框架中有哪些通用的表现形式?
- 如何基于SPI机制实现微内核架构?
- 管道-过滤器模式如何帮助框架实现非功能性需求?
- 如何基于代理机制实现远程调用和数据访问?
- 缓存的通用实现方式是怎么样的?
- …
这些主题我们都会在整个专栏中进行交叉讲解,并提供相应的学习方法和技巧。站在专栏学习的角度上,我们也希望读者能够以理解技术原理相通性作为目标进行持续的学习和提升。这是本专栏的设计初衷。
如果你是以下用户,那么本专栏非常适合你:
- 需要引入 Dubbo、Spring Cloud 等开源框架的开发人员
本专栏适合想要在日常开发过程中引入 Dubbo 和 Spring Cloud 等框架来实现分布式系统的开发人员。我们将介绍这些框架在远程调用、负载均衡、注册中心、配置中心、服务熔断、服务降级、消息通信等方面的核心实现机制,这些功能都是构建分布式系统的基础。
- 需要进一步理解主流分布式框架内部实现原理的开发人员
本专栏也适合于想要深入理解分布式框架内部实现原理的开发人员。我们将从如何剖析框架源码的方法论开始讲起,并给出各个核心功能背后赌赢的各个核心技术点的源码解析,同时也会对微内核架构等架构模式和设计模式等通用的实现机制做详细剖析。
- 需要应对技术原理型面试的开发人员
如何你想要通过大厂的面试找到一份满意的工作,那么这个专栏非常适合你。我们针对框架的特性以及背后的技术原理,梳理了来自阿里、京东、网易等大厂的技术原理型面试题数百题,系统讲解如何回答这些面试题所需的知识体系,以及帮助你如何应对这些面试题的面试技巧。
- 需要将主流分布式框架更好的应用到日常研发过程的开发人员
如果你已经对 Dubbo、Spring Cloud、Mybatis 等框架有过一定的使用经验,那么本专栏同样适合你。我们将基于这些框架系统阐述其在业务系统中使用的各种方式和开发技巧,以及如何扩展这些框架现有功能的系统方法。
- 需要通过学习优秀开源框架来实现自我提升的开发人员
作为业界主流的优秀开源框架,Dubbo、Spring Cloud 等框架在其架构设计和功能实现上具有很多值得深入分析和学习的知识点和技术体系。如果你对开源框架有强烈的学习欲望,希望通过学习优秀的开源框架来实现自我提升,那么本专栏同样适合你。我们会在专栏中穿插如何分析框架的架构体系,如何剖析源码的组成结构等主题来帮助你更好的掌握自我学习和提升的系统方法。
你能获得什么?
学习分布式系统开发框架的应用方式和实现原理
显然,这是这门专栏的核心价值,我们将学习 Dubbo、Spring Cloud 和 Mybatis 这三款主流开源框架的核心功能特性来满足日常开发分布式系统的需要,设计的核心主题包括但不限于远程调用、负载均衡、注册中心、配置中心、服务熔断、服务降级、消息通信等等。
学习技术原理的相通性
当我们面对像 Dubbo 这样优秀的开源框架时,学习的实际上并不只是框架本身。因为框架中所包含的技术体系并不是只能应用于这个框架,而是可以应用与其他框架的类似场景,也就是说技术体系具有相通性。
我们通过一个示例来阐述这个观点。 随着本专栏内容的展开,我们会知道 Dubbo 中实现了注册中心,我们可以基于注册中心实现服务注册和发现机制。而在注册中心的实现方式上,Dubbo 采用了 Zookeeper 作为其分布式协调的实现框架。我们可以基于 Zookeeper 所提供的动态监听机制来判断某个服务实例是否可用等操作。
另一方面,如果你熟悉 ShardingSphere 等分布式数据库框架的实现原理,你就会发现 ShardingSphere 中同样采用了这种实现方式来基于 Zookeeper 构建强大的注册中心。甚至于在大数据开发的主流技术 Hadoop 生态中,也大量采用了 Zookeeper 来完成类似的场景。
通过对这些框架的深入学习,你会发现类似的例子还有很多,包括:
- 基于 SPI 机制的微内核架构
- 基于 Zookeeper 的配置中心
- 基于 Nacos 的注册中心
- 整合 Spring Intergation 的消息传递机制
- …
通过对 Dubbo、Spring Cloud 等主流框架中所包含的这些技术体系的学习,一方面,能够让你具备对这些知识体系的系统化理解;另一方面,也可以直接通过这些框架本身掌握这些技术体系的具体应用场景和实现方式。
学习实战型面试题的应对方法
在本专栏中,基于源码解读的学习过程的目标是帮助大家能够找到一份理想而能够胜任的工作。因此,在我们讨论关于分布式系统核心实现机制时,会结合各个主题给出面试题的分析以及解答的技巧。这些面试题来自笔者在日常开发以及面试过程中的持续总结,也来自于阿里、京东、网易等国内一线互联网公司的真实案例。实战型面试题的深入解析会贯穿我们整个专栏。
学习从源码分析到日常开发的技巧
通过专栏帮忙大家完成技术原理类面试是本专栏的一大目标,但也不是唯一目标。所谓理论指导实践,作为扩展,我们希望通过对 Dubbo、Spring Cloud 等优秀开源框架的学习,掌握系统架构设计和实现过程中的方法和技巧,并指导日常的开发工作。
这是一个从源码分析到日常开发的过程,而且是一个不断演进的过程。所谓理论指导实践,我们需要从纷繁复杂的技术知识体系和各种层出不穷的工具框架中抓住其背后的原理,从而才能更好的应用到现实的开发过程中。一般而言,从开源框架中进行提炼并能够直接应用到日常开发过程中的技术体系通常包括:
- 设计模式的应用,如工厂模式、策略模式、模板方法等
- 架构模式的应用,如微内核架构、管道-过滤器架构等
- 常见缓存的应用以及自定义缓存机制的实现
- 网络通信组件的应用以及RPC架构的实现
- Spring 家族框架的集成和整合
- …
事实上,现在很多大型互联网公司的面试风格上就是偏向与考察面试者的原理分析能力和以及应用能力。从源码解析到日常应用是本专栏的一个核心目标,同样会贯穿我们整个专栏体系。
专栏目录
- 开篇词:论技术原理的相通性
第一部分:剖析框架代码的结构
- 剖析框架代码的结构的系统方法
- 基于组件设计原则剖析代码结构:框架代码结构与组件设计原则
- 基于组件设计原则剖析代码结构:基于组件设计原则分析Dubbo和Mybatis的代码结构
- 基于组件设计原则剖析代码结构:循环依赖及其消除方法实例
- 基于架构演进过程剖析代码结构:Dubbo框架的架构演进过程分析(上)
- 基于架构演进过程剖析代码结构:Dubbo框架的架构演进过程分析(下)
- 基于核心执行流程剖析代码结构:Mybatis框架主流程分析(上)
- 基于核心执行流程剖析代码结构:Mybatis框架主流程分析(下)
- 基于基础架构组成剖析代码结构:RPC基础架构与实现示例
- 基于基础架构组成剖析代码结构:Dubbo中的网络通信
- 基于基础架构组成剖析代码结构:Dubbo中的序列化
- 基于基础架构组成剖析代码结构:Dubbo中的传输协议
- 基于基础架构组成剖析代码结构:Dubbo中的远程调用
- 基于可扩展性设计剖析代码结构:常见系统可扩展性实现方法
- 基于可扩展性设计剖析代码结构:Mybatis TypeHandler机制
第二部分:剖析框架中的架构模式与设计模式
- 剖析模式应用的系统方法
- 架构模式之微内核模式:微内核模式及Java SPI机制
- 架构模式之微内核模式:微内核模式在Dubbo中的应用
- 架构模式之微内核模式:Dubbo中的扩展点
- 架构模式之管道-过滤器模式:管道-过滤器模式与实现示例
- 架构模式之管道-过滤器模式:管道-过滤器模式在Dubbo中的应用
- 架构模式之管道-过滤器模式:管道-过滤器模式在Mybatis中的应用
- 经典设计模式:创建型模式及其在Mybatis中的应用
- 经典设计模式:结构型模式及其在Mybatis中的应用(上)
- 经典设计模式:结构型模式及其在Mybatis中的应用(下)
- 经典设计模式:行为型模式及其在Mybatis中的应用
第三部分:剖析通用机制与组件
- 系统启动和初始化方法:Spring中的启动扩展点(上)
- 系统启动和初始化方法:Spring中的启动扩展点(下)
- 系统启动和初始化方法:Dubbo启动过程解析
- 系统启动和初始化方法:Mybatis-Spring启动过程解析
- 系统启动和初始化方法:Spring Boot应用程序的自动配置机制
- 系统启动和初始化方法:Mybatis Spring Boot Starter启动过程解析
- 自定义配置标签:基于Spring的自定义配置标签实现方案
- 自定义配置标签:Dubbo和Mybatis-Spring中的自定义标签体系
- 处处是代理:代理机制的应用场景和实现方式
- 处处是代理:Dubbo远程访问中的代理机制
- 处处是代理:Mybatis数据访问中的代理机制
- 缓存:Mybatis一级缓存剖析
- 缓存:Mybatis二级缓存剖析
- 资源池:资源池模式与数据库连接池
- 资源池:Mybatis中的数据库连接池
结语一:构建属于自己的技术知识体系
结语二:应对技术原理分析类面试的系统方法
适读人群
- 需要引入 Dubbo、Spring Cloud 等开源框架的开发人员
- 需要进一步理解主流分布式框架内部实现原理的开发人员
- 需要应对技术原理型面试的开发人员
- 需要将主流分布式框架更好的应用到日常研发过程的开发人员
- 需要通过学习优秀开源框架来实现自我提升的开发人员
作者介绍
逸章,资深技术专家。10 余年软件行业从业经验,前后就职于多家大型上市公司和互联网独角兽公司,担任架构师、技术总监等职位。主持过十余个架构设计和技术管理类培训课程,对分布式系统架构设计和实现有丰富的经验和深入的理解。
购买须知
- 本专栏为图文内容,共计 41 篇,每周更新 2 篇。
- 付费用户可享受文章永久阅读权限。
- 本专栏为虚拟产品,一经付费概不退款,敬请谅解。
- 本专栏可在 GitChat 服务号、App 及网页端 gitbook.cn 上购买,一端购买,多端阅读。
订阅福利
- 订购本专栏可获得专属海报(在 GitChat 服务号领取),分享专属海报每成功邀请一位好友购买,即可获得 25% 的返现奖励,多邀多得,上不封顶,立即提现。
- 提现流程:在 GitChat 服务号中点击「我-我的邀请-提现」。
- ①点击这里跳转至》第 4 篇《翻阅至文末获得入群口令。
- ②购买本专栏后,服务号会自动弹出入群二维码和入群口令。如果你没有收到那就先关注微信服务号「GitChat」,或者加我们的小助手「GitChatty6」咨询。
课程内容
开篇词:论技术原理的相通性
从今天开始,我将和大家一起全面梳理互联网分布式系统开发的核心机制。我们会结合 Dubbo、Spring Cloud、Mybatis 等主流开源框架来介绍分布式开发的各个主题,并通过源码分析的方式帮忙大家理解和掌握这些开源框架背后的技术原理和应用。
在此之前,作为整个课程的开篇,我们将通过一些案例来阐述一个核心观点,即技术原理之前存在相通性。这个核心观点是促使这个课程诞生的动机,同时也是我们学习的方向和目标。
测试:从几道面试题看看你对技术原理的掌握程度
以下是笔者在阿里巴巴、网易、蘑菇街等一些国内大型互联网公司面试时碰到的几道面试题,供大家参考:
- 微内核架构模式的特点、实现方式及应用场景?
- 如何理解代理模式在远程方法调动中的作用?
- RPC 架构核心组件、Dubbo 框架原理?
- 微服务架构中注册中心、配置中心的设计和实现方法?
- 如何实现一个轻量级的客户端负载均衡机制?
- 如何在设计系统时预留可扩展点?
关于这些面试题的具体回答思路以及所涉及的知识体系我们在本课程中都会具体展开,在这里列举这些面试题的目的在于引出一个话题,即面对这些看上去比较抽象而复杂的问题时,我们应该如何应对?
很多读者可能对以上问题有一定的了解,有些则可能连题目的意思都无法正确把握。但对于那些能够回答这些问题的人而言,知道的可能也仅仅是针对这些问题的答案,也就是说对于已经接触过或使用过的工具框架而言肯定多少能明白其中的原理,但是如果是那些没有用过的工具和框架呢?
工具和框架发展日新月异,例如在我们的课程中会经常提到的 Dubbo 框架在 2013 年开始已经几乎不再更新,而这两年又重新启动维护工作,目前已经成为 Apache 的顶级项目。不难想象在不久的将来,业界还会出现第二个、第三个类似 Dubbo 这样的框架。我们知道 Dubbo 是阿里巴巴开源的一个分布式服务框架,其背后的核心原理就是实现了 RPC 和服务治理。对于任何一个分布式服务框架而言,RPC 和服务治理都是不可缺少的组件,而关于 RPC 和服务治理的相关设计方法和实现原理在很长一段时间内实际上都没有什么变化。我们只要能够对 RPC 和服务治理有深刻的理解,那么关于 Dubbo 同类型的任何新框架,如果出现类似“RPC 架构核心组件、Dubbo 框架原理”这样的面试题,我们都应该有明确的回答思路和方法。为了达到这样的境界,这就要引出本篇的核心主题,即技术知识体系之间存在一定的相通性。我们将通过具体示例来阐述和论证这一观点。
论技术原理的相通性示例之一:RPC 架构
关于 RPC 架构的组成有一定的固定结构,一般认为包括网络通信、序列化 / 反序列化、传输协议和服务调用等四个组件,而我们也知道 RPC 架构是构成分布式服务架构和微服务架构的基本结构。对于服务的提供者和调用者而言,RPC 架构表现为一种对称结果(见下图)。
对于常见的分布式系统而言,RPC 架构的应用非常常见,因为后面的课程中会有详细讨论,所以这里不对上图做具体展开。从技术原理的相通性上讲,我们这里要讲的是它在另一个领域,即大数据体系中的应用。以 Hadoop 为代表的大数据生态系统中的大多数工具本质上也表现为一种分布式系统,也需要 RPC 架构这样的远程通信工具实现各个节点组件之间的协作和管理。
我们来分析一下 Hadoop 中的 RPC 实现机制。 Hadoop 采用了 RpcEngine 接口封装了 RPC 的整个调用过程,RpcEngine 接口提供了多个实现类,包括 WritableRpcEngine(见下图中的左半部分)和 ProtobufRpcEngine(见下图中的右半部分)。WritableRpcEngine 使用 Hadoop 自带的 Writable 序列化机制实现远程调用过程中的序列化,包括 Invoker 类、Invocation 类和 Server 类等三个核心类。Invoker 是 Java 动态代理核心接口 InvocationHandler 的实现类,用于序列化并生成 Invocation 对象并将 Invocation 对象发送到远程服务器,同时获取响应并反序列化。而 Invocation 类代表 RPC 请求对象,包括方法、参数等调用信息。最后的 Server 类启动 Socket 监听 RPC 请求,调用 WritableRpcInvoker 响应请求。这里的 WritableRpcInvoker 负责响应远程客户端请求,发送序列化请求,并通过反射调用服务端程序并包装结果对象。另一方面,在 ProtobufRpcEngine 中,Invoker 的序列化 / 反序列化工具使用 Protobuf,并使用 RPCRequest/ResponseWrapper 包装 RPC 请求。而 ProtobufRpcInvoker 与 WritableRpcInvoker 的主要区别就是序列化 / 反序列化方式的不同。
Hadoop RPC 客户端请求流程如下图所示,从下图中我们不难看到很多熟悉的名称,包括 Proxy、Invoker、Protocol 等,这些名词在 RPC 对称架构图中都有所体现。可以看到整个请求过程也是一个比较标准的动态代理实现方式,作为一种通用性实现机制,代理模式及其应用场景也在本课程中会有详细展开。
在这个案例中,我们看到了动态代理、序列化、服务发布和引用等 RPC 架构中通用的技术体系,如果你已经掌握了这些知识点,那么就算没有具体分析过 Hadoop 中实现远程通信的过程,你也应该能够通过这些通用技术体系构建对 Hadoop RPC 的理解模型并在具体的面试过程中表现出对这块内容的理解程度。
论技术原理的相通性示例之二:分布式协调
在本课程中,我们会基于 Dubbo 介绍注册中心这一服务化架构中的基础组件。而讲到注册中心就不得不提及分布式协调。对于分布式协调,我们需要了解到的是它不仅仅可以实现注册中心,还可以适用于很多场景。对于这些不同的场景,我们只需要把握针对分布式协调的基本知识体系,分析原理和解决问题的思路都是高度一致的。
分布式协调实现的主流工具是 Zookeeper,其核心是一个精简的文件系统,提供基于目录节点树方式的数据存储。Zookeeper 的基本组成结构是 ZNode,表示路径的同时也能存储数据,所有的 ZNode 通过路径被引用。一方面 ZNode 能够做到原子性访问,即所有请求的处理结果在整个 Zookeeper 集群中的所有机器是一致的。同时也能确保顺序(Sequential)性访问,从同一客户端发起的事务请求,会按照其发起顺序严格的应用到 Zookeeper 中去。
同时,Zookeeper 有两个特性与分布式协调直接相关。首先是它的会话(Session)机制。会话是客户端和服务器端的 TCP 连接,能够发送请求并接收 Watcher 事件。从类型而言,会话又可以分为短暂(Ephemeral)性会话和持久(Persistent)会话两种,前者在会话断开的同时会自动删除会话对应的 ZNode,而后者则不会。然后,对于每个 ZNode,Zookeeper 都提供了 Watcher 机制。Watcher 机制本质上就是分布式的回调,ZNode 的客户端关注 ZNode 发生的变化,一旦发生变化则回传消息到客户端,然后客户端的消息处理函数得到调用。在 Zookeeper 中,任何读操作都能够设置 Watcher。
基于 Zookeeper 的分布式协调机制,我们也可以实现与 Dubbo 中注册中心类似的共享存储方案。在注册中心中,所有服务在指定路径服务根目录下创建临时节点,所有访问这些服务的客户端监听该目录。当有新节点加入时,Zookeeper 实时通知到所有客户端,客户端做相应路由信息维护;而当某个节点宕机时,客户端通过 Watcher 同样会收到通知。整个流程的示意图见下图。关于 Dubbo 中如何集成 Zookeeper 以构建强大的注册中心,我们在课程的后续篇章中会有详细介绍。
有了会话和 Watcher 机制,我们就可以实现如下图所示的分布式环境下配置信息共享管理。在图中,通过监控“/Configuration”节点,我们就可以实时获取该节点下的配置数据并更新位于客户端的本地数据。
最后,我们再来看一下分布式锁的实现。在 Zookeeper 中,整个分布式锁的实现流程参考下图,可以看到核心的原理还是依赖于临时节点和 Watcher 机制。
通过以上讨论可以得出的结论是:不同场景表现出不同的需求,但这些需求的背后却可以通过同一种技术体系进行分析并解决。以分布式协调为例,Zookeeper 的临时节点和 Watcher 机制提供了一套可用于多种不同场景的统一解决方案。通过掌握其中的基本原理,我们面对自己并不熟悉但又类似的场景时就能得出正确的解决方案。
小结与预告
以上就是技术原理相通性的两个典型案例,在分布式系统开发技术中,这样的案例还有很多,我们会在整个课程中进行交叉讲解,并提供相应的学习方法和技巧。站在课程学习的角度上,我们也希望读者能够以理解技术原理相通性作为目标进行持续的学习和提升。
从下一篇开始,我们将进入整个课程的第一大部分,即剖析框架代码的结构的系统方法。当我们拿到 Dubbo、Spring Cloud 等开源框架之后,如何来分析这些框架的代码结构,并从中吸收框架级别的设计方法和技巧呢?让我们来一起看一下。
剖析框架代码的结构的系统方法
在阅读开源框架时,我们碰到的一大问题在于常常会不由自主的陷入代码的细节而无法把握框架代码的整体结构。市面上主流的、被大家所熟知而广泛应用的代码框架肯定考虑的非常周全,其代码结构不可避免存在一定的复杂性。如何快速把握开源框架的代码结构是我们课程的一大目标,为此我们需要梳理剖析框架代码结构的系统方法。
剖析框架代码结构的思路和方法
剖析框架代码结构的五大主题
本课程将从如下图所示的五个主题对如何剖析框架代码结构的系统方法展开详细讨论。
基于组件设计原则剖析代码结构
在笔者与很多同学进行沟通和交流时,发现大家在学习开源框架的源码时普遍存在一个问题,即一不小心就扎进细节,没办法找到代码的整体结构。目前市面上能被大家所熟知而广泛应用的代码框架功能强大而完善,代码结构也相对复杂。如果我们没有很好的方法来把握代码的整体结构,在阅读源码时很容易产生一种挫败感。当我们拿到一个框架的源代码时,首先应该问如下一个问题,即:
为什么这个框架的代码结构要这么设计
任何框架之所以能称为框架,能被广大开发人员所认同,在其架构设计上势必遵循了一定的原则和策略。其中,组件设计原则很大程度上是一切软件系统设计的基本原则,需要通过实践加深对其的理解和应用。通过对组件设计原则的理解,也可以将其应用到日常的系统设计过程中。
基于架构演进过程剖析代码结构
有过一定分布式系统开发经历的同学都知道 RPC 架构,RPC 架构是一切分布式系统设计和实现的基础。基于基础的 RPC 架构,业界也衍生除了很多著名的开发框架,例如本课程将详细分析的 Dubbo 框架。Dubbo 是一个典型的 RPC 框架,包含了 RPC 的实现方式。另一方面,我们看到的是一个成熟、稳定且符合设计原则的 Dubbo 框架,但我相信 Dubbo 的开发人员也不是一开始就把 Dubbo 设计成现在这个代码结构。换个角度,如果现在我们自己来设计这样一个框架,我们通常会采用一定的策略来一步一步从简单到复杂、从核心功能到辅助机制逐步实现和完善一个框架,这也是软件开发的一个基本规律。针对这个角度,当我们想要解读 Dubbo 这样的框架而又觉得无从下手时,可以考虑如下所示的一个核心问题,即:
如何从易到难对框架进行逐步拆解?
在设计和实现上,Dubbo 是一个非常典型的从易到难、从底层到高层、从技术内核到业务实现逐步完整的开源框架,具体良好的框架演进性。同时,Dubbo 框架在代码实现上也具备相当的复杂度,需要我们通过这种逐步演进的剖析方法掌握其实现原理。对于其他框架而言,这点同样使用。
基于核心执行流程剖析代码结构
如果想要跳出源码阅读的困境,不想浪费大量时间阅读代码细节,不要始终只能捕捉到对系统的片段化认识,我们就必须转换到另一种观点来看待系统。就“框架和结构”这个主题而言,我们进行源码解读的主要目标是明确代码的结构,前面的课程中我们已经学习了从设计原则、架构演进等不同角度出发实现这个目标。事实上,还有一个比较容易理解和把握的方法可以帮忙我们梳理代码结构,这就是代码的执行流程。任何系统的行为都可以认为是流程的组合,看似复杂的代码结构通过分析一般都能梳理出一条贯穿全局的主流程。当我们拿到一个框架的源代码时,不妨先来问自己一个问题:
如何抓住主流程对框架进行分层剖析?
提出一个问题,我们就要解决一个问题,而解决一个问题的方法通常是继续抛出其他问题。那么,对于一个框架而言,什么才是它的主流程呢?这个问题看上去很难回答,但实时上并非如此。当我们已经着手去分析一个框架的代码结构时,意味着至少我们已经使用过这个框架。基于这个这点,采自上而下的方式,通常都能明确所谓的主流程。举例来说,对于前面提到的 Dubbo 框架,一次 RPC 请求就是它的主流程。
基于基础架构组成剖析代码结构
诚然,我们处在一个行业快速变化、技术发展日新月异,新的工具框架也层出不穷。但是,今天笔者想分享的一个观点是:就算新的工具框架不断出现,但这些工具框架背后的很多基础性的架构实际上已经存在了十几年、甚至几十年。我们只要掌握了这些基础性架构,不管面对新框架还是老框架,都应该能够快速掌握其基础的代码结构,从而在学习效率上达到事半功倍的效果。围绕“框架和结构”主题,今天我们要讨论的一个问题是:
如何掌握从基础架构扩展到具体实现框架的方法?
这里所谓的基础性架构有很多,比方说上文中提到的 RPC 架构已经足足存在和发展了 40 余年,其原型可追溯至 1974 年发布的 RFC 674 草案——“过程调用协议文档第 2 版”。RPC 架构也是构建一切分布式系统的基础性架构,而我们今天继续要讨论的 Dubbo 也只不过是 RPC 架构的一款典型实现框架而已,在阅读 Dubbo 等 RPC 实现框架的源码时,我们同样可以基于 RPC 架构来梳理代码结构。
基于可扩展性设计剖析代码结构
可扩展性(Extensibility)和可伸缩性(Scalability)构成了架构设计的核心话题。从笔者的经历而言,一个系统在面临可伸缩性问题之前,首先要面对的往往是可扩展性问题。在经历了一段时间的系统开发之后,最让人头痛的事情莫过于面对一堆新的需求,我们如何以最小的代价和最快的速度将其添加到目前的系统中去,这就需要我们在设计系统的过程中充分考虑预留可扩展点。那么问题就来了,即:
如何在框架中预留可扩展点?
在软件架构模式中,存在一条开闭原则(Open Closed Principle,OCP),即指软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的。扩展点是这一模式的一种有效的实现方式,在本课程将要介绍的 Spring Boot、Dubbo、Mybatis 等开发框架中,到处可以看到可扩展点的身影,这些扩展点的实现方法各有特色。掌握这些扩展点是理解这些框架原理的一条捷径。
每个主题的行文思路
对于上述的每个主题,我们的行文思路都将遵循下图所示的递进结构。
- 抛出源码阅读过程中碰到的问题
在阅读开源框架的源码过程中,我们势必会遇到很多问题。以下一节课中将要介绍的“基于组件设计原则剖析代码结构”为例,当我们拿到 Dubbo 或 Mybatis 时,看到了纷繁复杂的代码结构时,可能首先要问的问题就是“为什么代码结构要这么设计?”。
- 给出问题的解决思路和方法
对于具体的某个问题,我们需要梳理应对的思路和方法。从这个角度讲,我们在面试或日常工作中碰到问题时同样需要经历这一步骤。显然,每个问题的解决方法都可能有所不同,对于前面抛出的“为什么代码结构要这么设计?”这个问题,我们应该明白这些框架代码结构背后实际上都是遵循了一定的组件设计原则,这些设计原则会引导我们解决问题。
- 阐述解决问题所需要的知识体系
有了思路和方法之后,我们就需要具备背后的知识体系。这是本课程能够给到大家的最核心的内容。对于“为什么代码结构要这么设计?”这个问题,“组件设计原则”是我们需要掌握的知识体系,因此我们需要掌握与之相关的各方面理论和原理。
- 基于框架源码分析所提出的方法
即使我们掌握了所需的知识体系,但如何将这些理论知识应用于实际又是一个很有挑战的事情。因此,对于“组件设计原则”这个主题,我们需要基于 Dubbo、Mybatis 等主流框架来展开讨论,详细阐述组件设计原则在其中的应用,从而能够更好的指导我们的日常开发工作。
剖析框架代码结构作为剖析分布式系统开发核心机制剖析的一大模块,关注的是对开发框架整体结构的把握,帮助大家梳理对框架代码结构的正确认识,这些认识对本课程后续内容也有指导作用。
技术原理类面试题分析
在不断学习技术的过程中,很多处于初级的开发人员都有自己的目标和方向,但是在纷繁复杂的技术知识体系和各种层出不穷的工具框架面前就显的无从下手。而有些同学已经跨越了初级阶段,并按照自己的方法正在系统的梳理各种技术知识体系,但很多时候会发现效果不是很好,自身提升的速度比较缓慢。本课程关注于“源码解读”这一切入点,其目标就在于为这两类人提供一个简单的方法促使其能够快速成长。
学习开源框架的代码是一种过程,而不是结果。在本课程中,基于源码解读的学习过程的目标是帮助大家能够找到一份理想而能够胜任的工作。因此,在我们讨论关于分布式系统核心实现机制时,会结合各个主题给出面试题的分析以及解答的技巧。这些面试题来自笔者在日常开发过程中的持续总结,也来自于阿里、京东、网易等国内一线互联网公司的真实案例。
对于每道技术原理类面试题而言,我们认为每个面试者都应该有自己的解题思路。同一个知识点、同一个工具或框架在不同公司的问法可能是不一样的,但把握其中的基本原理都是回答问题的切入点。当我们对问题的原理有了明确的认识,这是第一点。本课程的目的就是帮助大家梳理分布式系统开发过程中的各种技术原理,我们在《开篇词汇:论技术原理的相通性》中已经知道技术原理从很大程度上讲是相通的,可以做到触类旁通。
另一方面,光有思路还不够,我们还需要不断的练习。因此,我们在每个主题的结语部分同样会结合这个主题的内容给出相关练习题供大家进行分析和解答,从而达到练习的效果。
从源码分析到日常开发
通过课程帮忙大家完成技术原理类面试是本课程的一大目标,但也不是唯一目标。作为扩展,我们希望通过对 Dubbo、Spring Cloud 等优秀开源框架的学习,掌握系统架构设计和实现过程中的方法和技巧,并指导日常的开发工作。例如,在下一节介绍组件设计原则时,我们还将重点描述“循环依赖”这一特定的组件设计原则,并给出能够指导日常开发工作的案例分析。
这是一个从源码分析到日常开发的过程,而且是一个不断演进的过程。所谓理论指导实践,我们一定要从纷繁复杂的技术知识体系和各种层出不穷的工具框架中抓住其背后的原理,然后做到用自己的语言和方法对这些原理进行阐述,也就是能够构建属于你自己的技术知识体系。事实上,现在很多大型互联网公司的面试风格上就是偏向与考察面试者的原理分析能力和以及应用能力,这点会贯穿我们这个课程。
小结与预告
本文梳理了剖析框架代码的结构的系统方法,一共分成五大主题,并给出了每个主题的行文思路。同时,本文对课程的学习目标也做了进一步明确,一方面用于面试,一方面则用于指导日常开发。
从下一篇开始,我们将正式进入第一个主题的介绍,即基于基于组件设计原则剖析代码结构。
基于组件设计原则剖析代码结构:框架代码结构与组件设计原则
本节关注于分析代码结构的第一个核心主题,即基于组件设计原则来剖析代码结构。组件设计原则很大程度上是一切软件系统设计的基本原则,需要通过实践加深对其的理解和应用。
为什么代码结构要这么设计?
在笔者与很多同学进行沟通和交流时,发现大家在学习开源框架的源码时普遍存在一个问题,即一不小心就扎进细节,没办法找到代码的整体结构。目前市面上能被大家所熟知而广泛应用的代码框架功能强大而完善,代码结构也相对复杂。如果我们没有很好的方法来把握代码的整体结构,在阅读源码时很容易产生一种挫败感。当我们拿到一个框架的源代码时,首先应该问如下一个问题:
为什么这个框架的代码结构要这么设计?
本节内容先从这一问题入手梳理分析代码结构的系统方法。让我们引入本课程中将要介绍的第一个框架:Dubbo。Dubbo 是 Alibaba 开源的一个分布式服务框架,在互联网行业应用和扩展仍然十分广泛。Dubbo 的核心功能为我们进行分布式系统设计提供了两大方案,即高性能和透明化的 RPC 实现方案和服务治理方案。
Dubbo 源代码可以从 https://github.com/alibaba/dubbo 下载(本课程使用的还是应用最广泛的 2.6.x 版本),代码组织结构下图 2-x 所示。我们看到 Dubbo 在代码结构上一共包含 common、remoting、rpc、cluster、registry、monitor、config 和 container 等 8 大核心包。这些包的详细介绍在本文以及后续的文章内容中会有详细介绍,这里暂时不做展开。
让我们再引入另一个非常主流的开源框架:Mybatis。MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO 为数据库中的记录。
Mybatis 的源码可以从 https://github.com/mybatis/mybatis-3 下载,本课程中使用的是 3.5.x 版本,其代码组织结构下图所示。我们看到 Mybatis 的结构比较复杂,包含了 session、mapping、binding 等 10 余个核心包。
针对 Mybatis 中各个核心包的详细介绍同样不是本篇文章的重点,在这里我们关注的是从整理结构上把握这些框架的包组织结构。在了解了 Dubbo 和 Mybatis 这两个框架的包结构之后,我们再从前文中“为什么这个框架的代码结构要这么设计?”这个问题出发可以延伸出以下问题:
- 这些框架的开发人员是如何设计和规划这些代码结构的?
- 这些代码结构的背后是否遵循了一定的原则?
- 如何评价这些代码结构的优劣性?
- 如何从这些框架的代码结构中获取经验从而可以学以致用?
源码阅读需要有突破点,本课程会引导大家逐步挖掘这些突破点。而对以上问题的发散和总结就是我们进行源码解读的一个突破点。想要理解代码结构,我们还是需要从一些基本原理入手,这就是接下去将要介绍组件设计原则。
组件设计原则
组件(Component)设计原则有时候也称为分包(Package)原则,可以用来设计和规划上一小节中提到的 Dubbo、Mybatis 等框架的代码结构。任何一个软件系统都可以看做是一系列组件的集合,良好的组件设计能够把系统分解为一些大小恰到好处的组件,从而使每个开发团队都可以只关注单个的组件而无需关心整个系统。但在我们刚开始阅读某个框架的源码时,为了避免过多的扎进细节关注而只关注某一个具体组件,同样可以使用这些原则来管理我们的学习预期。
对于组件而言,最核心的设计要点就是内聚(Cohesion)和耦合(Coupling),所谓内聚是指一个组件内各个元素彼此结合的紧密程度,而耦合指的是一个软件结构内不同组件之间互连程度的度量。基于这两个设计要点,组件设计原则也包括组件内聚原则(Component Cohesion Principle)和组件耦合原则(Component Coupling Principle)两大类。组件内聚原则用于指导把类划分到包中,而组件耦合原则用来处理包与包之间的关系。
现在我们拿到了 Dubbo、Mybatis 这样的框架源码,也看到了这些框架内容有很多包结构。请注意,我们还没到要弄清楚为什么要将某些类放到同一个包中的时候(避免扎入细节)。从梳理代码结构的角度出发,我们首先应该关注的是组件之间的关系,即应用组件耦合原则来分析代码结构。组件耦合原则也包含以下三条设计原则:
- 无环依赖原则
无环依赖原则(Acyclic Dependencies Principle,ADP)认为在组件之间不应该存在循环依赖关系。通过将系统划分为不同的可发布组件,对某一个组件的修改所产生的影响不应该必须扩展到其他组件。
- 稳定抽象原则
稳定抽象原则(Stable Abstractions Principle,SAP)认为组件的抽象程度应该与其稳定程度保持一致。即一个稳定的组件应该也是抽象的,这样该组件的稳定性就不会无法扩展。另一方面,一个不稳定的组件应该是具体的,因为他的不稳定性使其内部代码更易于修改。
- 稳定依赖原则
稳定依赖原则(Stable Dependencies Principle,SDP)认为被依赖者应该比依赖者更稳定。一个好的设计中的 组件之间的依赖应该朝着稳定的方向进行。一个组件只应该依赖那些比自己更稳定的组件。
从原则的命名上我们也不难看出,组件耦合原则实际上关注的是系统的稳定性(Stablility)。那么什么是系统的稳定性?现实生活中,如果某一个事物不容易被移动,就认为它是稳定的。而在软件开发过程汇总,如果某一个包被许多其他的软件包所依赖,也就是具有很多输入依赖关系的包就是稳定的,因为它的变化可能需要其他依赖它的包做相应的修改,而这种修改显然需要非常大的工作量。
我们来看几个具体的例子。在下图中我们看到存在一个 X 组件被三个其他组件所依赖,我们认为组件 X 是稳定的,因为 X 被很多其他组件依赖。
而在下图中存在一个 Y 组件,但我们认为组件 Y 是不稳定的,因为 Y 没有被其他的组件所依赖,但 Y 自身依赖很多别的组件。
面试题分析
判断一个框架好坏有什么方法?
- 考点分析
这个一个比较大的话题,也是考验面试者综合能力的一个常见方法。一般而言,面试官对这种问题本身也不一定有标准答案,更多的是考察面试者的知识面已经对问题的抽象能力。
- 解题思路
针对该问题,我们可以从很多方面进行切入,如架构的扩展性和维护性等架构设计的方法、设计模式的合理应用等。通过今天内容的学习,我们明白组件设计原则也是判断一个框架好坏的一个重要方面。面试时通过介绍组织设计原则的基本概念,结合具体开源框架中的应用,相信是针对这一问题的一种不大常见但又非常重要的回答方案。
- 建议回答与本文内容
本文介绍的组件耦合原则部分内容是这个问题的建议答案,同时本文中关于 Dubbo 和 Mybatis 框架的对比也可以作为答案的一部分。
组件在设计耦合都上有什么通用的原则和方法?
- 考点分析
相较上一个问题,这个问题更加有针对性,直接指向了组件设计原则这一特定话题,需要面试人具备这部分内容有一定的了解。
- 解题思路
组件设计上核心就是高内聚和低耦合,在设计原则上也包括了组件耦合原则和组件内聚原则这两大组成部分。本文中对其中的组件耦合三大原则进行了介绍,分别是稳定抽象原则、稳定依赖原则和无环依赖原则。这些原则看起来比较抽象,但实际上能够指导我们日常的开发工作。回答这个问题时,先明确这些原则的概念和设计思想,然后最好能列举一两个日常开发中的真实案例。当然,本课程中介绍的关于开源框架的分析也是很好的面试内容。
- 建议回答与本文内容
本文给出了组件设计原则的总的描述,以及关于组件耦合三大原则的介绍,可以直接作为问题的答案。
小结与预告
本节基于框架代码结构的讨论引出了组件设计原则,并对其中的三条组件耦合原则进行了展开。现实开源框架代码中的包结构通常比较复杂,可能很难找到这些一眼就能判断其稳定性的组件,这时候我们就需要借助一些量化标准来对包结构的稳定性进行衡量。幸运的是,业界已经存在了这样的量化标准以及对应的衡量工作。让我们先来看看具体的量化标准,以及测量这些标准的工具,这就是下一篇内容。
基于组件设计原则剖析代码结构:循环依赖及其消除方法实例
我们前面介绍 Dubbo 和 Mybatis 的代码结构时主要关注的是稳定抽象和稳定依赖原则,而在组件耦合原则中还包括一条很重要的原则,即无环依赖原则。借助于 JDepend,我们也可以发现结构中存在的循环依赖关系。根据无环依赖原则,系统设计中不应该存在循环依赖。
依赖关系有三种基本的表现形式(见下图),其中类似 Package1 依赖于 Package2 这种直接依赖最容易识别和管理;间接依赖即直接依赖关系的衍生,当 Package1 依赖 Package2,而 Package2 又依赖 Package3 时,Package1 就与 Package3 发生了间接依赖关系;所谓循环依赖就是 Package1 和 Package2 之间相互依赖,循环依赖有时候并不像图中描述的那么容易识别,因为产生循环依赖的多个组件之间可能同时存在各种直接和间接依赖关系。
循环依赖示例
上面的描述比较抽象,我们看一下具体示例代码就比较容易理解循环依赖的产生过程。本文中的代码示例参考了《Java 应用架构设计:模块化模式与 OSGi》一书中的内容,并做了调整和优化。
首先,我们有一个代表用户账户的 Account 类,代码如下:
public class Account { private List<Order> orders; public BigDecimal getDiscountAmount() { //根据账号下的订单数来模拟折扣力度 if (orders.size() > 5) { return new BigDecimal(0.1); } else { return new BigDecimal(0.03); } } public List<Order> getOrders() { return this.orders; } public void createOrder(BigDecimal chargeAmount) { Order order = new Order(this, chargeAmount); if (orders == null) { orders = new ArrayList<Order>(); } orders.add(order); }}
然后,我们还有一个代表用户下单的订单类,代码如下:
public class Order { private BigDecimal chargeAmount; private Account account; public Order(Account account, BigDecimal chargeAmount) { this.account = account; this.chargeAmount = chargeAmount; } public BigDecimal getChargeAmount() { return this.chargeAmount; } public BigDecimal pay() { BigDecimal discount = new BigDecimal(1).subtract(this.account.getDiscountAmount()); BigDecimal paidAmount = this.chargeAmount.multiply(discount); //这里省略具体的支付业务代码 return paidAmount; }}
上述 Account 类和 Order 的代码都非常简单,但实际上已经构成了一种循环依赖,因为 Account 对象可以创建 Order 对象并保持 Order 对象列表,而 Order 对象同样需要使用 Account 对象,并根据 Account 对象中的打折(Discount)信息计算 Order 金额,这样对象 Account 和 Order 之间就存在循环依赖关系。JDepend 的分析结果如下,显然印证了我们的结论。
消除循环依赖的方法
如何消除这种循环依赖?软件行业有一句很经典的话,即当我们碰到问题无从下手时,不妨考虑一下是否可以通过“加一层”的方法进行解决。消除循环依赖的基本思路也是这样,就是通过在两个相互循环依赖的组件之间添加中间层,变循环依赖为间接依赖。有三种策略可以做到这一点,分别是上移、下移和回调。
上移
关系上移意味着把两个相互依赖组件中的交互部分抽象出来形成一个新的组件,而新组件同时包含着原有两个组件的引用,这样就把循环依赖关系剥离出来并上升到一个更高层次的组件中。下图就是使用上移策略对 Account 和 Order 原始关系进行重构的结果,我们引入了一个新的组件 Mediator,并通过其提供的 pay 方法对循环依赖进行了剥离,该方法同时使用 Order 和 Account 作为参数并实现了 Order 中根据 Account 的打折信息进行金额计算的业务逻辑。Mediator 组件消除了 Order 中原有对 Account 的依赖关系并在依赖关系上处于 Account 和 Order 的上层。使用上移之后的包关系调整如下。
相应的 Mediator 类示例代码也比较简单,如下所示。
public class PaymentMediator { private Account account; public PaymentMediator(Account account) { this.account = account; } public BigDecimal pay(Order order) { BigDecimal discount = new BigDecimal(1).subtract(this.account.getDiscountAmount()); BigDecimal paidAmount = order.getChargeAmount().multiply(discount); //这里省略具体的支付业务代码 return paidAmount; } }
下移
关系下移策略与上移策略切入点刚好相反。我们同样针对 Account 和 Order 的循环依赖关系进行重构,重构的方法是抽象出一个 Calculator 组件专门包含打折信息的金额计算方法,该 Calculator 由 Account 创建,并注入到 Order 的 pay 方法中去(见下图)。通过这种方式,原有的 Order 对 Account 的依赖关系就转变为 Order 对 Calculator 的依赖关系,而 Account 因为是 Calculator 的创建者同样依赖于 Calculator,这种生成一个位于 Account 和 Order 之下但能同样消除 Order 中原有对 Account 的依赖关系的组件的策略,就称之为下移。
相应的 Calculator 类示例代码也如下所示。
public class DiscountCalculator { private Integer orderNums; public DiscountCalculator(Integer orderNums) { this.orderNums = orderNums; } public BigDecimal getDiscountAmount() { if (orderNums.intValue() > 5) { return new BigDecimal(0.1); } else { return new BigDecimal(0.03); } }}
回调
回调(Callback)本质上就是一种双向调用模式,也就是说,被调用方在被调用的同时也会调用对方。在面向对象的语言中,回调通常是通过接口或抽象类的方式来实现。下图就是通过回调机制进行依赖关系重构后的结果。我们抽象出一个 Calculator 接口用于封装金额计算逻辑,该接口与 Order 处于同一层次,而 Account 则实现了该接口,这样 Order 对 Account 的依赖就转变成 Order 对 Calculator 接口的依赖,也就是把对 Account 的直接依赖转变成了间接依赖。通过依赖注入机制,我们可以很容易的实现 Order 和 Account 之间的有效交互。
基于回调的代码结构相对复杂一点,我们首先需要定一个回调接口 DiscountCalculator,代码如下所示。
public interface DiscountCalculator { public BigDecimal getDiscountAmount();}
调整后的 Account 类需要实现该接口,代码如下。
public class Account implements DiscountCalculator { private List<Order> orders; public BigDecimal getDiscountAmount() { if (orders.size() > 5) { return new BigDecimal(0.1); } else { return new BigDecimal(0.03); } } public List<Order> getOrders() { return this.orders; } public void createOrder(BigDecimal chargeAmount) { Order order = new Order(this, chargeAmount); if (orders == null) { orders = new ArrayList<Order>(); } orders.add(order); }}
最后,Order 类中直接注入 DiscountCalculator,当执行支付操作时,则回调 Account 类中的业务逻辑完成计算。
private BigDecimal chargeAmount; private DiscountCalculator discounter; public Order(DiscountCalculator discounter, BigDecimal chargeAmount) { this.discounter = discounter; this.chargeAmount = chargeAmount; } public BigDecimal getChargeAmount() { return this.chargeAmount; } public BigDecimal pay() { BigDecimal discount = new BigDecimal(1).subtract(this.discounter.getDiscountAmount()); BigDecimal paidAmount = this.chargeAmount.multiply(discount); //这里省略具体的支付业务代码 return paidAmount; }}
至此,这个示例就讲完了。该示例非常简单,我们通过肉眼就能发现其中存在的循环依赖关系。但在日常开发过程中,我们的代码可能非常复杂,设计数十个甚至数百个类之间的交互,很难一下子就能发现其中的不合理依赖关系。这时候,我们就可以通过引入 JDepend 等类似的工具进行代码结构分析,并使用本篇中介绍的三种消除循环依赖的技术进行重构。
面试题分析
如何判断代码中是否存在循环依赖?
- 考点分析
这是一个关于代码设计质量的一个常见问题,因为所有人都认为代码中存在循环依赖是一件不好的事情。笔者就比较喜欢对面试者抛出这个问题来看对方的应对思路。
- 解题思路
以笔者的经历而言,这个问题回答好的同学不多,一方面主要原因在于大家不了解类似 JDepend 这样的工具,而另一方面,平时针对组件图等类似的设计手段用的也比较少,没有对循环依赖的表现形式有充分的认识。所以从解题思路上,我们可以采用知识点和表现形式并行的方法进展展开。
- 本文内容与建议回答
判断循环依赖的主要方法还是通过 JDepend 等工具,如果不采用这种自动化工具,我们也可以自己通过绘制组件图的方式进行人工的梳理和判断。
如何消除代码中的循环依赖?
- 考点分析
这个问题比较常见,也是考查我们理解组件设计原则以及处理复杂代码关系的一个很好的切入点。有些面试官会引申到 Spring 框架中的依赖注入循环依赖的处理机制。
- 解题思路
上移、下移和回调是消除代码中循环依赖的三种常见方式,需要面试者牢记。同时,针对这种问题,最好有日常开发过程中所经历过的真实案例进行补充,回答起来思路就会很清晰,效果也最好。
- 本文内容与建议回答
本文中给出的组件图以及代码示例,可以直接作为问题的答案。
日常开发技巧
包括 Dubbo、Mybatis 等优秀开源框架在内的现实中的很多代码实际上都存在循环依赖情况,换句话说,一旦代码结构变得复杂,没有循环依赖的代码实际上是不大可能存在的。因此,关键点在于如何有效识别循环依赖,并在成本可控的范围内对其进行重构。今天我们介绍的内容基本上可以直接应用与日常开发过程中。通过上移、下移和回调这三大重构手段可以满足日常开发过程中大部分消除循环依赖的场景。大家可以先用 JDepend 工具得到量化数据,然后根据需要分别进行重构。
小结与预告
本文讨论了循环依赖以及消除方法。这样,我们花了三篇文章对”基于组件设计原则剖析代码结构”这个主题进行了详细分析。从下一篇开始,我们将通过两篇文章的内容讲解分析代码结构的另一种方法,即”基于架构演进过程剖析代码结构”。