文章目录
一、dubbo架构
高性能的Java RPC框架,dubbo被阿里捐献给了apache。dubbo架构:
节点角色说明
- Provider 暴露服务的服务提供方
- Consumer 调用远程服务的服务消费方
- Registry 服务注册与发现的注册中心
- Monitor 统计服务的调用次数和调用时间的监控中心
- Container 服务运行容器
调用关系说明:
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
Dubbo中负载均衡策略:
Random(随机)
: 随机选择一个提供者实例进行调用,每个提供者的概率是相等的。适用于服务提供者之间性能相近的情况。RoundRobin (轮询): 默认
。循环地选择提供者实例,依次进行调用。适用于提供者性能相似、资源均衡的情况LeaStActive最小活跃数)
:选择当前活跃数(正在处理请求的数量)最少的提供者实例进行调用。适用于提供者性能不均衡的情况。可以避免过度调用性能较美
的提供者。ConsistentHash (一致性哈希)
:将请求根据哈希值分配给提供者实例,可以保证同一客户端的请求始终被分配到同一个提供者,适用于需要保持会话状态的场景。WeightedRandom(加权随机)
: 基于权重随机选择提供者实例,权重越高的提供者被选中的概率越大。适用于资源不均衡的情况WeightedRoundRobin(加权轮询)
: 基于权重循环选择提供者实例,权重越高的提供者被轮询得越多。适用于资源不均衡的情况WeightedLeastActive (加权最小活跃数)
: 基于权重选择当前活跃数最少的提供者实例进行调用。适用于资源不均衡的情况。
Dubbo允许在
消费者端通过配置来选择使用哪种负载均衡策略
,默认使用的是RoundRobin轮询策略
。根据不同的应用场景和性能需求,可以选择合适的负载均衡策略来优化系统的性能和资源利用。
Dubbo中6种容错机制:
容错机制是为了应对服务调用过程中的异常和错误情况,确保系统的高可用性和稳定性。Dubbo提供了多种容错策略,每种策略都有特定的应用场景和使用方法。以下是Dubbo的主要容错机制:
1)失败重试 (Failover):
策略描述: 默认的容错机制
。当调用失败时,自动切换到其他提供者进行重试。默认情况下,Dubbo会进行2次重试(共3次尝试)。
适用场景: 适用于读操作或幂等的写操作。
配置方式:
<dubbo:service retries="2" />
<dubbo:reference retries="2" />
示例:
<dubbo:reference interface="com.example.Service" cluster="failover" retries="2" />
2)快速失败 (Failfast):
策略描述: 快速失败,只进行一次调用,失败后立即抛出异常。
适用场景: 适用于实时性要求高的操作,如重要的更新操作。
配置方式:
<dubbo:reference interface="com.example.Service" cluster="failfast" />
3)失败安全 (Failsafe):
策略描述: 失败时忽略异常,仅记录日志,不抛出异常。
适用场景: 适用于不重要的通知操作,如日志记录。
配置方式:
<dubbo:reference interface="com.example.Service" cluster="failsafe" />
4)失败自动恢复 (Failback):
策略描述: 失败后,后台记录失败请求,定时重发。通常用于消息通知操作。
适用场景: 适用于需要保证消息最终送达但对实时性要求不高的场景。
配置方式:
<dubbo:reference interface="com.example.Service" cluster="failback" />
5)广播调用 (Broadcast):
策略描述: 广播调用所有提供者,任意一个失败会抛出异常。通常用于更新服务。
适用场景: 适用于需要所有提供者都成功执行的操作,如批量更新配置。
配置方式:
<dubbo:reference interface="com.example.Service" cluster="broadcast" />
6)并行调用 (Forking):
策略描述: 并行调用多个提供者,只要有一个成功即返回。适用于实时性要求高的读操作。
适用场景: 适用于对实时性要求高且容忍部分冗余调用的场景。
配置方式:
<dubbo:reference interface="com.example.Service" cluster="forking" forks="2" />
容错配置示例:
在Dubbo中,通过配置cluster属性来指定容错策略。例如,针对某个服务引用配置失败重试策略:<dubbo:reference interface="com.example.Service" cluster="failover" retries="3" />
具体配置策略应该根据应用场景和业务需求来选择,确保服务的高可用性和稳定性。合理的容错配置可以有效提高系统的健壮性,减少由于服务调用失败导致的系统异常。
二、Zookeeper注册中心
dubbo官方文档可知,dubbo依赖注册中心,所以使用dubbo,需要提前搭建好注册中心。
dubbo官网-》文档-》用户文档-》参考手册-》注册中心参考手册-》推荐使用Zookeeper注册中心。
1、Zookeeper下载
Zookeeper官网下载:https://zookeeper.apache.org/,我下载的是zookeeper-3.5.9版本:
下载完,解压后,新建个文件夹data,用来存放后期临时数据(随便起名字和路径,只要与后面配置文件中dataDir指向的地址一致就行)
在conf文件夹中新建zoo.cfg文件(将zoo_sample.cfg内容复制到zoo.cfg里进行修改),调整dataDir即可:
2、zoo.cfg参数配置说明
(1)tickTime=2000:通信心跳数,Zookeeper服务器心跳时间,单位毫秒
Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。
它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2tickTime)
(2)initLimit=10:Leader和Follower初始通信时限
集群中的follower跟随者服务器与leader领导者服务器之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器连接到Leader的时限。
投票选举新leader的初始化时间
Follower在启动过程中,会从Leader同步所有最新数据,然后确定自己能够对外服务的起始状态。
Leader允许Follower在initLimit时间内完成这个工作。
当已经超过 10个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 102000=20 秒
(3)syncLimit=5:Leader和Follower同步通信时限
集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除Follwer。总的时间长度就是 5*2000=10秒
在运行过程中,Leader负责与ZK集群中所有机器进行通信,例如通过一些心跳检测机制,来检测机器的存活状态。
如果L发出心跳包在syncLimit之后,还没有从F那收到响应,那么就认为这个F已经不在线了。
(4)dataDir:数据文件目录+数据持久化路径
保存内存数据库快照信息的位置,如果没有其他说明,更新的事务日志也保存到数据库。
(5)clientPort=2181:客户端连接端口
这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
server.A=B:C:D:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。
3、启动Zookeeper
进到bin目录下,cmd,执行zkServer.cmd启动Zookeeper会报如下错误:
经查找原因是,这个版本下载下来目录中没有lib文件夹,处理办法,去官网下载相应版本的bin的包,将apache-zookeeper-3.5.9-bin.tar.gz解压后文件夹中的lib包复制到apache-zookeeper-3.5.9.tar.gz解压后的文件夹中,重新cmd里启动,可成功启动,显示绑定了2181端口:
4、测试Zookeeper
zookeeper启动好了,接下来测试一下,还是bin下cmd,执行zkCli.cmd,打开zookeeper客户端:ls /
查看一下根目录节点。至此,注册中心就搭建好了。
如果启动客户端报错:Will not attempt to authenticate using SASL (unknown error),可能是你上一步不小心把启动的zk关掉了又,也可能是其他原因。
三、监控中心
dubbo2.6版本以后都是springboot方式启动,可以直接运行main方法,或者使用 java -jar 很方便;
2.6版本之前都是war包-》web项目-》放到tomcat里。
1、dubbo-admin新版本安装
从github下载dubbo admin项目:https://github.com/apache/dubbo-admin,解压;
新版本的dubbo-admin已经修改了方式,使用了前后端分离的方式,结构中的dubbo-admin-server是我们的服务后端,dubbo-admin-ui是我们的前端:
2、安装node.js环境
前端又是使用的node.js,所以想要启动dubbo-admin首先需要安装node.js环境,进入nodejs官网https://nodejs.org/en/下载,一路next安装即可(没必要下载最新的,使用最多人使用的版本即可)
安装完,命令行测试一下:
我们的nodejs环境已经安装完成。
3、启动dubbo-admin-server服务端
1)修改配置
修改【dubbo-admin-develop\dubbo-admin-server\server\src\main\resource】下的application.properties 配置文件,注意配置zk的进程路径,必须配置你的zk运行的ip和端口。(改成自己服务器或虚拟机的地址,如果是在本地搭建的则不需要修改)
默认端口为8080,如果需要更改,加入配置 server.port=<端口号>,此处修改为8081
注:此处可以看出登录的用户名密码默认root/root
2)启动应用
配置完成后,第一种方式-》idea中启动【dubbo-admin-develop\dubbo-admin-server\src\main\java\org\apache\dubbo\admin\DubboAdminApplication.java】运行即可。
此处用第二种方式启动方式-》命令行操作:
进入dubbo-admin-server 目录,打开cmd窗口,在当前目录下执行以下两条命令:
mvn install -Dmaven.test.skip=true //跳过测试环节
进入dubbo-admin-server\target 目录,执行jar包 :
java -jar dubbo-admin-server-0.2.0-SNAPSHOT.jar
服务端就此启动完成,在8081端口。
4、构建dubbo-admin-ui
1)配置国内taobao镜像: npm config set registry http://registry.npm.taobao.org/
查看镜像:npm get registry
2)进入dubbo-admin-ui文件夹下执行命令:
npm install //安装依赖,进行mode_modules依赖构建
【构建成功后会生成node_modules文件夹】
npm run dev //启动Vue项目
注:如果失败或者停留在某个地方很久可以试一下换镜像:
npm config set registry https://registry.npm.taobao.org
npm config set registry http://registry.cnpmjs.org/
npm config set registry https://registry.npmjs.org/
npm cache clean --force 换完源之后执行这个命令清除一下缓存
注:修改前端工程端口及配置信息:
如果修改了server的启动端口那么要相应修改ui模块里面的连接server的端口设置修改要让他们统一。打开文件【dubbo-admin-develop\dubbo-admin-ui\config\index.js】,找到【proxyTable】,将里面的target改成我们后端服务的ip加端口,然后保存。 但是我这个版本并没有这样操作修改也能访问!!!!不知道是内置了还是什么情况待研究!
5、访问dubbo-admin
http://localhost:8081/ root/root 登录管理控制台:
四、如何在Dubbo中配置服务消费者的引用
1.XML配置文件方式
在Dubbo的XML配置文件中,你可以使用 元素来配置服务消费者的引用,同时指定各种属性,如版本、超时、重试次数等。以下是一个示例配置:
<!-- 消费者端应用名 -->
<dubbo:application name="consumer-app' />
!-- 注册中心配置 -->
<dubbo:registry address="zookeeper://localhost:2181" />
-- 配置消费者引用 -->
<dubbo: reference id="demoService" interface="com.example.DemoService"version="1.0.0" timeout="3000' retries="2' />
提供者端服务 -->
<dubbo:service interface="com.example.DemoService" ref="demoServiceImpl" />
解释:
id :引用的唯一标识,可在其他地方引用该服务
interface: 指定服务接口,
version : 指定服务版本号,用于区分不同版本的提供者
timeout :指定调用超时时间,单位为毫秒
retries: 指定重试次数,当调用失败时会自动重试
2.注解方式
除了XML配置文件方式,你还可以使用注解来配置服务消费者的引用。以下是一个示例:
@Service
public class ConsumerService f
AReference(interfaceClass = DemoService, class, version = "1,0.g", timeout = 3000, retries = 2)
private DemoService demoService;
//...
}
解释:
@Reference:使用该注解标注在需要引用服务的字段上。
interfaceCLass:指定服务接口。
version : 指定服务版本号
timeout : 指定调用超时时间,单位为毫秒
retries:指定重试次数。
无论是XML配置文件方式还是注解方式,你都可以根据实际需求配置服务消费者的引用,指定版本、超时、重试次数等信息,以便更好地控制调用行为和保障系统稳定性。
五、如何处理Dubbo中的版本兼容性问题?
Dubbo提供了版本兼容性的支持,允许在服务的不同版本之间进行调用。版本兼容性问题主要涉及到服务接口的变更,包括新增方法、删除方法、修改方法参数等情况。以
下是处理Dubbo中版本兼容性问题的一些方法:
1.接口设计遵循规范
:在设计服务接口时,应该蓬循良好的接口设计原则,考虑到可能的变更情况。尽量保持接口稳定,避免频繁地修改接口,以减少版本兼容性的问题。
2.使用版本号
:在Dubbo服务接口上可以添加version属性、用于区分不同版本的接口。消费者和提供者都可以声明一使用的版本,从而在服务治理过程中进行匹配和调用。
3.使用泛化调用
:如果在消费者端不知道具体的接口定义,或者服务接口发生了较大的变化,可以使用Dubbo的泛化调用(Generic Service)来调用服务。泛化调用不依赖于具体的接口定义,适用于版本兼容性问题较大的情况。
4.适配器模式
:在服务接口发生变化时,可以引入适配器模式来实现新旧版本的兼容性。日版本的接口可以通过适配器转换成新版本的接口调用。
5.服务版本注册
:在Dubbo的注册中心中。可以为不同版本的服务提供者注册不同的路径。使得消费者可以根据版本洗择调用相应的提供者。
示例场景:
假设有一个名为 UserService 的服务接口,初始版本是1.0.0,包含了-一个查询用户信息的方法 getUserInfo 。后续需要新增一个方法 updatelserInfo,于是发布了新版本2.0.0。
1.版本号配置: 在服务提供者的Dubbo配置中,设置版本号
<!-- 版本为1.0.0的服务提供者 -->
<dubbo:service interface="com.example.UserService" ref="userService" version="1.0.0" />
<!-- 版本为2.0.0的服务提供者 -->
<dubbo:service interface="com.example.UserService" ref="userService" version="2.0.0" />
2.消费者调用: 在消费者端,可以指定调用的版本
// 调用版本为1.0.0的接口
userService.getUserInfo(userId);
// 调用版本为2.0.0的接口
userService.updateUserInfo(userId,userInfo);
通过使用版本号进行区分,消费者可以在不同版本之间选择调用合适的服务接口,从而实现版本兼容性的问题处理。
六、Dubbo的泛化调用 (Generic Service) 是什么?
Dubbo是一个开源的分布式服务框架,用于构建高性能、可扩展的分布式应用。Dubbo的泛化调用(Genearic Serice)是一种特殊的调用方式,允许消费者在不事先了解提供者接口定义的情况下,通过泛化的方式调用远程服务。这种调用方式适用于以下情景:
- 1.
接口不可知或动态变化的情况
:泛化调用可以解决消费者在不知道具体接口定义的情况下仍然能够调用远程服务。这在一些场景中很有用,例如消费者和提供者之间的接口定义可能经常变化,或者消费者需要动态调用多个不同接口的服务。 - 2.
跨语言调用
:如果消费者和提供者使用不同的编程语言,而且没有共享的接口定义、泛化调用可以作为一种通用的调用方式,使得跨语言调用变得可能。 - 3.
泛化调试
: 在开发和调试阶段,泛化调用可以帮助开发人员快速验证提供者的功能。而无需依赖具体的接口定义和实现。 - 4.
服务治理和版本兼容
:在服务治理场景中,消费者可能需要通过泛化调用来动态获取提供者的接口和方法信息,从而适应不同版本的服务。
泛化调用的实现机制是,消费者通过构建一个泛化的参数对象,包含了远程调用所需的服务名、方法名以及参数列表。然后,消费者将泛化的参数对象发送给提供者,提供者根据参数对象的信息执行相应的方法调用,并将结果返回给消费者。由于泛化调用不依赖于具体的接口定义,因此在使用时需要注意参数和返回值的类型转换以及异常处理等问题。
七、Dubbo的线程模型是什么?
Dubbo采用了一种基于线程池
和异步调用
的线程模型。
Dubbo的线程模型主要包括以下几个关键组件:
- 1.Acceptor线程池: 用于处理消费者的请求连接,建立TCP连接
- 2.I/O线程池 (Netty): 用于处理请求和响应的读写操作,包括解码、编码等。默认使用 Netty 作为底层 NIO 框架,Netty 的 I/O 线程池主要负责处理连接的建立、关闭和数据读写操作。
- 3.业务线程池: 负责具体的业务逻辑处理。请求被 I/O 线程接收后,通常会被放入业务线程池中进行处理,包括调用远程服务、执行回调函数等。
- 4.Callback线程池: 用于处理异步调用的响应回调函数。
工作流程:
- 接收请求:消费者通过Acceptor线程池与提供者建立TCP连接。当客户端发起请求时,Netty 的 I/O 线程会接收请求并进行基本的协议解码。
- 任务分发:解码后的请求被分发到业务线程池,业务线程池的线程从任务队列中获取任务进行处理。
- 业务处理:业务线程处理具体的服务逻辑,处理完成后将结果返回给客户端。如果是异步调用,会将回调函数提交给Callback线程池
默认配置:
默认情况下,Dubbo 的业务线程池采用
固定大小的线程池
,大小为200
。如果未指定队列长度,使用 LinkedBlockingQueue,表示任务队列是无界
的。
自定义线程池:
Dubbo 支持自定义线程池,用户可以实现 ExecutorRepository 接口来自定义线程池的创建和管理。
八、如何实现Dubbo跨集群的服务调用?
实现Dubbo跨集群的服务调用可以通过配置不同的注册中心
和分组
来实现。Dubbo支持在同一个应用中通过不同的注册中心和分组来调用不同集群的服务,从而实现跨集群。
1)配置不同的注册中心:在Dubbo的配置文件中,可以配置多个不同的注册中心,每个注册中心对应一个集群。例如,你可以配置两个Zookeeper注册中心,分别对应不同的集群:
<!-- 集群A的注册中心配置 -->
<dubbo:registry address="zookeeper://clusterA-zookeeper1:2181,clusterA-zookeeper2:2181"/>
<!-- 集群B的注册中心配置 -->
<dubbo:registry address="zookeeper://clusterB-zookeeper1:2181,clusterB-zookeeper2:2181" />
2)配置服务分组:在Dubbo的服务提供者和消费者端,可以通过设置 grouD 属性来指定服务分组。这样可以将不同的服务分组对应到不同的集群上:
<!-- 集群A的服务提供者配置 -->
<dubbo:service interface="com.example,UserService" ref="userService" group="groupA"/>
<!-- 集群B的服务提供者配置 -->
<dubbo:service interface="com.example,UserService" ref="userService" group="groupB" />
<!-- 集群A的服务消费者配置 -->
<dubbo: reference id="userServiceA" interface="com. example.UserService" group="groupA" />
<!-- 集群B的服务消费者配置 -->
<dubbo: reference id="userServiceB" interface="com. example, UserService"group="groupB" />
3)跨集群调用:在消费者端,可以根据不同的服务分组来调用不同集群的服务
// 调用集群A的服务
User userA = userServiceA.getUserInfo(userId);
// 调用集群B的服务
User userB = userServiceB.getUserInfo(userId);
通过配置不同的注册中心和设置服务分组,你可以在Dubbo中实现跨集群的服务调用。这样可以灵活地管理和调用不同集群的服务,满足多集群环境下的需求。
九、Dubbo中的服务治理是指什么?它解决了哪些问题?
Dubbo中的服务治理是一种管理和维护分布式服务的方法和工具,旨在解决分布式系统中服务的注册、发现、路由、负载均衡、容错等问题,以保障系统的可用性、性能和稳定性
。服务治理通过一系列的手段和策略
,帮助开发者更好地管理和监控分布式系统中的服务,确保系统能够按预期运行。
服务治理解决的问题包括:
- 1.
服务注册与发现
: 如何管理和维护服务提供者和消费者之间的关系,确保消费者能够发现并调用合适的服务提供者 - 2.
负载均衡
: 如何将客户端的请求均匀分布到多个服务提供者上,避免某个提供者过载,同时提高系统的性能和资源利用率。 - 3.
容错机制
:如何处理服务提供者的故障和异常情况,避免由干单点故障导致整个系统崩溃。 - 4.
动态路由
:如何根据服务提供者的状态和性能选择合话的调用路径,优化服务调用的效率和响应时间 - 5.
服务降级
:如何在服务不可用或性能下降时,提供备用方案,保障核心业务的正常运行。 - 6.
配置管理
: 如何管理服务的配置信息,实现配置的动态更新和版本控制。
Dubbo中的服务治理手段包括:
- 1.
注册中心
: 通过注册中心,管理和维护服务的注册、发现和订阅关系,Dubbo支持多种注册中心如ZooKeeper、Consul、Nacos等 - 2.
路由规则
:根据路由规则,将消费者的请求分发给合适的提供者,Dubbo支持多种负载均衡策略和路由规则。 - 3.
容错策略
: 针对不同的故障情况,Dubbo提供了多种容错机制,如Failover、Failfast、Failsafe等. - 4.
动态配置
: Dubbo支持动态配置管理,可以根据需要对服务的参数、权重、路由规则等进行动态调整 - 5.
监控和治理控制台
: Dubbo提供了监控和治理控制台,用于实时监控服务的运行状态、性能指标等,以及对服务进行动态管理和配署
十、Dubbo的SPI(Service Provider Interface) 机制
在Apache Dubbo中,SPI(Service Provider Interface)是一种用于扩展机制的接口
,允许用户和开发者根据需要自定义和扩展框架的功能。SPI机制是一种设计模式,用于实现模块化和插件化,通过接口和配置实现动态加载和扩展
。
在Dubbo中,SPI机制的核心思想是将接口与实现解精,通过在META-INF目录下的 dubbo 文件夹中提供扩展配置文件
,实现接口的自动发现和加载。这使得框架的用户可以根据需要自由地添加、替换或定制实现,而无需修改框架的核心代码。
使用SPI机制的步骤如下:
1.定义接口: 首先需要定义一个接口,该接口将作为扩展点的定义,供扩展实现。
- 使用@SPI注解定义接口,并指定默认实现(可选)。
- 例如:
@SPI("default")
public interface MyService {
void execute();
}
2.实现接口: 在Dubbo中,扩展实现类需要遵循接口的命名规则,即接口名+“默认扩展名”。例如,接口 com,aLibaba,dubbo,rpc,Protocol 的默认扩展名是 dubbo,则默认扩展实现类的全限定名是 com,alibaba.dubbo,rpc,protocol.dubbo,DubboProtocol
- 创建接口的实现类,并在配置文件中进行配置。
- 例如:
public class DefaultMyService implements MyService {
@Override
public void execute() {
System.out.println("Default implementation");
}
}
public class CustomMyService implements MyService {
@Override
public void execute() {
System.out.println("Custom implementation");
}
}
3.创建扩展配置文件:在META-INF目录下的 dubbo 文件夹中,创建以接口全限定名为文件名的配置文件。配置文件中指定扩展实现类的全限定名。
- 在META-INF/dubbo目录下创建一个与接口同名的文件,并在文件中配置实现类。
- 例如,META-INF/dubbo/com.example.MyService 文件内容:
default=com.example.impl.DefaultMyService
custom=com.example.impl.CustomMyService
4.使用ExtensionLoader加载扩展:
- 通过Dubbo提供的ExtensionLoader来加载和使用扩展。
- 例如:
MyService myService = ExtensionLoader.getExtensionLoader(MyService.class).getExtension("custom");
myService.execute(); // 输出 "Custom implementation"
这种方式使得框架的扩展能力变得非常强大,可以根据实际需求进行定制和替换,而无需修改原有的代码。