【RocketMQ】消息的拉取

news2024/11/25 11:24:49

在上一讲中,介绍了消息的存储,生产者向Broker发送消息之后,数据会写入到CommitLog中,这一讲,就来看一下消费者是如何从Broker拉取消息的。

RocketMQ消息的消费以组为单位,有两种消费模式:

广播模式:同一个消息队列可以分配给组内的每个消费者,每条消息可以被组内的消费者进行消费。

集群模式:同一个消费组下,一个消息队列同一时间只能分配给组内的一个消费者,也就是一条消息只能被组内的一个消费者进行消费。

通常使用集群模式的情况比较多,接下来以集群模式(Push模式)为例看一下消息的拉取过程。

消费者启动时处理

消费者在启动的时候主要做了以下几件事情:

  1. Topic订阅处理;
  2. MQClientInstance实例创建;
  3. 加载消费进度存储对象,里面存储了每个消息队列的消费进度;
  4. 从NameServer更新Topic路由信息;
  5. 向Broker进行注册;
  6. 触发负载均衡;

主题订阅处理

RocketMQ消费者以组为单位,启用消费者时,需要设置消费者组名称以及要订阅的Topic信息(需要知道要消费哪个Topic上面的消息):

@RunWith(MockitoJUnitRunner.class)
public class DefaultMQPushConsumerTest {

    @Mock
    private MQClientAPIImpl mQClientAPIImpl;

    private static DefaultMQPushConsumer pushConsumer;

    @Before
    public void init() throws Exception {
        // ...
        // 消费者组名称
        String consumerGroup = "FooBarGroup";
        // 实例化DefaultMQPushConsumer
        pushConsumer = new DefaultMQPushConsumer(consumerGroup);
        pushConsumer.setNamesrvAddr("127.0.0.1:9876");
        // ...
        // 设置订阅的主题
        pushConsumer.subscribe("FooBar", "*");
        // 启动消费者
        pushConsumer.start();
    }
}

所以消费者启动的时候,首先会获取订阅的Topic信息,由于一个消费者可以订阅多个Topic,所以消费者使用一个Map存储订阅的Topic信息,KEY为Topic名称,VALUE为对应的表达式,之后会遍历每一个订阅的Topic,然后将其封装为SubscriptionData对象,并加入到负载均衡对象RebalanceImpl中,等待进行负载均衡。

MQClientInstance实例创建

MQClientInstance中有以下几个关键信息:

  • 消息拉取服务:对应实现类为PullMessageService,是用来从Broker拉取消息的服务;
  • 负载均衡服务:对应的实现类为RebalanceService,是用来进行负载均衡,为每个消费者分配对应的消费队列;
  • 消费者列表(consumerTable):记录该实例上的所有消费者信息,key为消费者组名称,value为消费者对应的MQConsumerInner对象,每一个消费者启动的时候会向这里注册,将自己加入到consumerTable中;

需要注意MQClientInstance实例是以clientId为单位创建的,相同的clientId共用一个MQClientInstance实例,clientId由以下信息进行拼装:
(1)服务器的IP;
(2)实例名称(instanceName);
(3)单元名称(unitName)(不为空的时候才拼接);
最终拼接的clientId字符串为:服务器IP + @ + 实例名称 + @ + 单元名称
所以在同一个服务器上,如果实例名称和单元名称也相同的话,所有的消费者会共同使用一个MQClientInstance实例。

MQClientInstance启动的时候会把消息拉取服务和负载均衡服务也启动(启动对应的线程)。

获取Topic路由信息

前面已经得知了当前消费者订阅的Topic信息,接下来需要知道这些Topic的分布情况,也就是分布在哪些Broker上,Topic的分布信息可以从NameServer中获取到,因为Broker会向NameServer进行注册,上报自己负责的Topic信息,所以这一步消费者向NameServer发送请求,从NameServer中拉取最新的Topic的路由信息缓存在本地。

加载消费进度

消费者在进行消费的时候,需要知道应该从哪个位置开始拉取消息,OffsetStore类中记录这些数据,不同的模式对应的实现类不同:

  • 集群模式:消息的消费进度保存在Broker中,由Broker记录每个消费队列的消费进度,对应实现类为RemoteBrokerOffsetStore
  • 广播模式:消息的消费进度保存在消费者端,对应实现类为LocalFileOffsetStore

这里关注集群模式,在集群模式下,加载消费进度时,会进入RemoteBrokerOffsetStore的load方法,load方法是从本地加载文件读取消费进度,因为集群模式下需要从Broker获取,所以load方法什么也没干,在负载均衡分配了消息队列,进行消息拉取的时候再向Broker发送请求获取消费进度。

向Broker进行注册

由于消费者增加或者减少会影响消息队列的分配,所以Broker需要感知消费者的上下线情况,消费者在启动时会向所有的Broker发送心跳包进行注册,通知Broker消费者上线。

Broker收到消费者发送的心跳包之后,会从请求中解析相关信息,将该消费者注册到Broker维护的消费者列表consumerTable中,其中KEY为消费者组名称,Value为该消费组的详细信息(ConsumerGroupInfo对象),里面记录了该消费组下所有消费者的Channel信息。

触发负载均衡

启动最后一步,会立即触发一次负载均衡,为消费者分配消息队列。

负载均衡

负载均衡是通过消费者启动时创建的MQClientInstance实例实现的(doRebalance方法),它的处理逻辑如下:

  1. MQClientInstance中有一个消费者列表consumerTable,存放了该实例上注册的所有消费者对象,Key为组名称,Value为消费者,所以会遍历所有的消费者,对该实例上注册的每一个消费者进行负载均衡;

  2. 对于每一个消费者,需要获取其订阅的所有Topic信息,然后再对每一个Topic进行负载均衡,前面可知消费者订阅的Topic信息被封装为了SubscriptionData对象,所以这里获取到所有的SubscriptionData对象进行遍历,开始为每一个消费者分配消息队列;

分配消息队列

这里我们关注集群模式下的分配,它的处理逻辑如下:

  1. 根据Topic获取该Topic下的所有消费队列(MessageQueue对象);

消费者在启动时向NameServer发送请求获取Topic的路由信息,从中解析中每个主题对应的消息队列,放入负载均衡对象的topicSubscribeInfoTable变量中,所以这一步直接从topicSubscribeInfoTable中获取主题对应的消息队列即可。

  1. 根据主题信息和消费者组名称,查找订阅了该主题的所有消费者的ID:
    (1)根据主题选取Broker:从NameServer中拉取的主题路信息中可以找到每个主题分布在哪些Broker上,从中随机选取一个Broker;
    (2)向Broker发送请求:根据上一步获取到的Broker,向其发送请求,查找订阅了该主题的所有消费者的ID(消费者会向Broker注册,所以可以通过Broker查找订阅了某个Topic的消费者);

  2. 如果主题对应的消息队列集合和获取到的消费者ID都不为空,对消息队列集合和消费ID集合进行排序;

  3. 获取分配策略,根据具体的分配策略,为当前的消费者分配对应的消费队列,RocketMQ默认提供了以下几种分配策略:

    • AllocateMessageQueueAveragely:平均分配策略,根据消息队列的数量和消费者的个数计算每个消费者分配的队列个数。

    • AllocateMessageQueueAveragelyByCircle:平均轮询分配策略,将消息队列逐个分发给每个消费者。

    • AllocateMessageQueueConsistentHash:根据一致性 hash进行分配。

    • AllocateMessageQueueByConfig:根据配置,为每一个消费者配置固定的消息队列 。

    • AllocateMessageQueueByMachineRoom:分配指定机房下的消息队列给消费者。

    • AllocateMachineRoomNearby:优先分配给同机房的消费者。

  4. 根据最新分配的消息队列,更新当前消费者负责的消息处理队列;

更新消息处理队列

每个消息队列(MessageQueue)对应一个处理队列(ProcessQueue),后续使用这个ProcessQueue记录的信息进行消息拉取:

分配给当前消费者的所有消息队列,由一个Map存储(processQueueTable),KEY为消息队列,value为对应的处理队列:

由于负载均衡之后,消费者负责的消息队列可能发生变化,所以这里需要更新当前消费者负责的消息队列,它主要是拿负载均衡后重新分配给当前消费的消息队列集合与上一次记录的分配信息做对比,有以下两种情况:

(1)某个消息队列之前分配给了当前消费者,但是这次没有,说明此队列不再由当前消费者消负责,需要进行删除,此时将该消息队列对应的处理队列中的dropped状态置为true即可;
(2)某个消费者之前未分配给当前消费者,但是本次负载均衡之后分配给了当前消费者,需要进行新增,会新建一个处理队列(ProcessQueue)加入到processQueueTable中;

对于情况2,由于是新增分配的消息队列,消费者还需要知道从哪个位置开始拉取消息,所以需要通过OffsetStore来获取存储的消费进度,也就是上次消费到哪条消息了,然后判断本次从哪条消息开始拉取。前面在消费者启动的提到集群模式下对应的实现类为RemoteBrokerOffsetStore,再进入到这一步的时候,才会向Broker发送请求,获取消息队列的消费进度,并更新到offsetTable中。

从Broker获取消费进度之后,有以下几种拉取策略:
(1)CONSUME_FROM_LAST_OFFSET(上次消费位置开始拉取):从OffsetStore获取消息队列对应的消费进度值lastOffset,判断是否大于等于0,如果大于0则返回lastOffset的值,从这个位置继续拉取;
(2)CONSUME_FROM_FIRST_OFFSET(第一个位置开始拉取):从OffsetStore获取消息队列对应的消费进度值lastOffset,如果大于等于0,依旧从这个位置继续拉取,否则才从第一条消息拉取,此时返回值为0;
(3)CONSUME_FROM_TIMESTAMP(根据时间戳拉取):从OffsetStore获取消息队列对应的消费进度值lastOffset,如果大于等于0,依旧从这个位置继续拉取,否则在不是重试TOPIC的情况下,根据消费者的启动时间查找应该从什么位置开始消费;

nextOffset拉取偏移量的值确定之后,会将ProcessQueue加入到processQueueTable中,并构建对应的消息拉取请求PullRequest,并设置以下信息:

  • consumerGroup:消费者组名称;
  • nextOffset:从哪条消息开始拉取,设置的是上面计算的消息拉取偏移量nextOffset的值;
  • MessageQueue:消息队列,从哪个消息队列上面消费消息;
  • ProcessQueue:处理队列,消息队列关联的处理队列;

PullRequest构建完毕之后会将其加入到消息拉取服务中的一个阻塞队列中,等待消息拉取服务进行处理。

消息拉取

消费者发送拉取请求

消息拉取服务中,使用了一个阻塞队列,阻塞队列中存放的是消息拉取请求PullRequest对象,如果有消息拉取请求到来,就会从阻塞队列中取出对应的请求进行处理,从Broker拉取消息,拉取消息的处理逻辑如下:

  1. 从拉取请求PullRequest中获取对应的处理队列ProcessQueue,先判断是否置为Dropped删除状态,如果处于删除状态不进行处理;
  2. 从处理队列中获取缓存的消息的数量及大小进行验证判断是否超过了设定的值,因为处理队列中之前可能已经拉取了消息还未处理完毕,为了不让消息堆积需要先处理之前的消息,所以会延迟50毫秒后重新加入到拉取请求队列中处理;
  3. 判断是否是顺序消费,这里先不讨论顺序消费,如果是非顺序消费,判断processQueue中队列最大偏移量和最小偏移量的间距是否超过ConsumeConcurrentlyMaxSpan的值,如果超过需要进行流量控制,延迟50毫秒后重新加入队列中进行处理;
  4. 向Broker发送拉取消息请求,从Broker拉取消息:
    (1)ProcessQueue关联了一个消息队列MessageQueue对象,消息队列对象中有其所在的Broker名称,根据名称再查找该Broker的详细信息;
    (2)根据第(1)步的查找结果,构建消息拉取请求,在请求中设置本次要拉取消息的Topic名称、消息队列ID等信息,然后向Broker发送请求;
  5. 消费者处理拉取请求返回结果,上一步向Broker发送请求的时候可以同步发送也可以异步发送请求,对于异步发送请求当请求返回成功之后,会有一个回调函数,在回调函数中处理消息拉取结果。

Broker处理消息拉取请求

ConsumeQueue
RocketMQ在消息存储的时候将消息顺序写入CommitLog文件,如果想根据Topic对消息进行查找,需要扫描所有CommitLog文件,这种方式性能低下,所以RocketMQ又设计了ConsumeQueue存储消息的逻辑索引,在RocketMQ的存储文件目录下,有一个consumequeue文件夹,里面又按Topic分组,每个Topic一个文件夹,Topic文件夹内是该Topic的所有消息队列,以消息队列ID命名文件夹,每个消息队列都有自己对应的ConsumeQueue文件:

ConsumeQueue中存储的每条数据大小是固定的,总共20个字节:

  • 消息在CommitLog文件的偏移量,占用8个字节;
  • 消息大小,占用4个字节;
  • 消息Tag的hashcode值,用于tag过滤,占用8个字节;

Broker在收到消费发送的拉取消息请求后,会根据拉取请求中的Topic名称和消息队列ID(queueId)查找对应的消费信息ConsumeQueue对象:
Broker中的consumeQueueTable中存储了每个Topic对应的消费队列信息,Key为Topic名称,Value为Topic对应的消费队列信息,它又是一个MAP,其中Key为消息队列ID(queueId),value为该消息队列的消费消费信息(ConsumeQueue对象)。

在获取到息ConsumeQueue之后,从中可以获取其中记录的最小偏移量minOffset和最大偏移量maxOffset,然后与拉取请求中携带的消息偏移量offset的值对比进行合法校验,校验通过才可以查找消息,对于消息查找结果大概有如下几种状态:

nextOffsetCorrection方法:用于校正消费者的拉取偏移量,不过需要注意,当前Broker是主节点或者开启了OffsetCheckInSlave校验时,才会对拉取偏移量进行纠正,所以以下几种状态中如果调用了此方法进行校正,前提是满足此条件。

  1. NO_MESSAGE_IN_QUEUE:如果CommitLog中的最大偏移量maxOffset值为0,说明当前消息队列中还没有消息,返回NO_MESSAGE_IN_QUEUE状态;
  2. OFFSET_TOO_SMALL:如果待拉取偏移量offset的值小于CommitLog文件的最小偏移量minOffset,说明拉取进度值过小,调用nextOffsetCorrection校正下一次的拉取偏移量为CommitLog文件的最小偏移量(需要满足校正的条件),并将这个偏移量放入nextBeginOffset变量;
  3. OFFSET_OVERFLOW_ONE:如果待拉取偏移量offset等于CommitLog文件的最大偏移量maxOffset,依旧调用nextOffsetCorrection方法进行校正(需要满足校正的条件),只不过校正的时候使用的还是offset的值,可以理解为这种情况什么也没干。
  4. OFFSET_OVERFLOW_BADLY:如果待拉取偏移量offset大于CommitLog文件最大偏移量maxOffset,说明拉取偏移量越界,此时有以下两种情况:
    • 如果最小偏移minOffset量为0,调用nextOffsetCorrection方法校正下一次拉取偏移量为minOffset的值(需要满足校正的条件),也就是告诉消费者,下次从偏移量为0的位置开始拉取消息;
    • 如果最小偏移量minOffset不为0,调用nextOffsetCorrection方法校正下一次拉取偏移量为maxOffset的值(需要满足校正的条件),将下一次拉取偏移量的值设置为最大偏移量;
  5. NO_MATCHED_LOGIC_QUEUE:如果根据主题未找到消息队列,返回此状态;
  6. FOUND:待拉取消息偏移量offset的值介于最大最小偏移量之间,此时可以正常查找消息;

需要注意以上是消息查找的结果状态,Broker并没有使用这个状态直接返回给消费者,而是又做了一次处理。

经过以上步骤后,除了查找到的消息内容,Broker还会在消息返回结果中设置以下信息:

  1. 查找结果状态;
  2. 下一次拉取的偏移量,也就是nextBeginOffset变量的值;
  3. CommitLog文件的最小偏移量minOffset和最大偏移量maxOffset

消费者对拉取结果的处理

消费者收到Broker返回的响应后,对响应结果进行处理:

  1. FOUND:消息拉取请求成功,此时从响应中获取Broker返回的下一次拉取偏移量的值,更新到拉取请求中,然后进行以下判断:
    • 如果拉取到的消息内容为空,将拉取请求放入到阻塞队列中再进行一次拉取;
    • 如果拉取到的消息内容不为空,将消息提交到ConsumeMessageService中进行消费(异步处理),然后判断拉取间隔的值是否大于0,如果大于0,会延迟一段时间进行下一次拉取,如果拉取间隔小于0表示需要立刻进行下一次拉取,此时将拉取请求加入阻塞队列中进行下一次拉取。
  2. NO_MATCHED_MSG:没有匹配的消息,使用Broker返回的下一次拉取偏移量的值作为新的拉取消息偏移量,然后将拉取请求加入阻塞队列中立刻进行下一次进行拉取。
  3. OFFSET_ILLEGAL:拉取偏移量不合法,此时使用Broker返回的下一次拉取偏移量的值,更新到消费者记录的消息拉取偏移量中(offsetStore),并持久化保存,然后将当前的拉取请求中的处理队列状态置为dorp并删除处理队列,等待下一次重新构建拉取请求进行处理。

RocketMQ消息拉取相关源码可参考:【RocketMQ】【源码】消息的拉取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1009543.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言——三子棋游戏

本文目录 三子棋游戏简介三子棋游戏功能说明游戏界面 C语言代码实现多个文件共同实现game.hgame.ctest.c 三子棋游戏实现逻辑分析编写test.c 文件实现menu()函数实现game()函数打印空棋盘选手落子判断输赢实现game()函数 源代码game.hgame.ctest.c 更多C语言实战项目&#xff0…

微信会员卡开发流程

功能需求: 通过微信第三方平台创建的模板小程序,想要实现用户在小程序支付一定金额后领取会员卡,领取会员卡后可给用户下发一定数量的优惠券,并且实现用户在小程序消费享受商品折扣。 开发流程: 一、了解微信的3个平…

AMS爆炸来袭,上线即巅峰

1.关于首发项目Antmons(AMS)空投结果 Gate.io Startup 首发项目Antmons代币AMS于Aug15th,AM 07:00开始下单,24小时内下单同等对待总共有15,950人下单,下单总价值超过1,000万美金分发系数约为0.001640495298341。根据上线规则AMS项目认购成功,…

67、数据源配置 及 配置多个数据源--C3P0 数据源 和 Hikari 数据源

★ Spring Boot如何选择DataSource数据源 优先级从高到低&#xff1a; HikariCP > Tomcat pooling DataSource > Commons DBCP2 如果要使用Tomcat pooling DataSource这种池化数据源&#xff0c; 那么可以用</exclusions>这个把HikariCP 排除掉&#xff0c;然后sp…

前端html原生页面兼容多端H5和移动端适配方案

目录 图片代码最后 图片 是一个注册页面 代码 自己查看效果 注意: 单位全部用rem这样才能保证兼容性适配多端&#xff0c;px转rem转换公式 1px 1/37.5rem 所以想要20px应该对应20/37.5 0.53rem <!DOCTYPE html> <html lang"en"><head><met…

生成随机数列向量并保存到不同的log文件中

随机数列要求 长度 10元素随机每个数列中有两个相同的元素生成到100个文本中每个文本数列数量为 1000 代码 import torch import pickle import numpy as np import os import shutil import loggingdef creat_logger(log_path,logging_name,suf_name):if not os.path.exists(…

免费知识管理系统,让企业管理文档数据更便捷

编者按&#xff1a;本文详细介绍了免费强大的低代码平台在构建知识管理系统方面的优势&#xff0c;并介绍了其知识管理系统独特的功能。只需轻松操作&#xff0c;即可体验到该平台带来的便捷与高效&#xff01;快来了解如何利用这一神奇的工具&#xff0c;让知识管理变得更加轻…

DataX实现Mysql与ElasticSearch(ES)数据同步

文章目录 一、Linux环境要求二、准备工作2.1 Linux安装jdk2.2 linux安装python2.3 下载DataX&#xff1a; 三、DataX压缩包导入&#xff0c;解压缩四、编写同步Job五、执行Job六、定时更新6.1 创建定时任务6.2 提交定时任务6.3 查看定时任务 七、增量更新思路 一、Linux环境要求…

定制化图标——Element UI 组件图标替换指南

本篇博客将介绍如何在使用 Element UI 组件时对原生图标进行定制化替换&#xff0c;提供了适用于满足个性化需求的方法和技巧。 引言 Element UI 是一款基于 Vue.js 的流行 UI 组件库&#xff0c;在前端开发中得到广泛应用。然而&#xff0c;在使用 Element UI 的组件时&#…

java内存泄漏和内存溢出oom排查思路

1、可能出现 2、如何去进行排查 3、visualvm分析 4、Jprofile分析 案例

使用C#开发163邮件发送功能

创建SMTP服务器&#xff08;发送邮件需要SMTP服务器代发&#xff09; 这里介绍创建网易SMTP&#xff08;SMTP是邮件通讯格式&#xff09;服务器&#xff1a; 1.先注册一个163网易邮箱 2.注册成功后登陆该邮箱 3.在该邮箱中找到设置>POP3/SMTP/IMAP点击进入&#xff0c;如下…

【LeetCode-简单题】541. 反转字符串 II

文章目录 题目方法一&#xff1a;双指针 题目 方法一&#xff1a;双指针 题目的意思&#xff1a; 通俗一点说&#xff0c;每隔k个反转k个&#xff0c;末尾不够k个时全部反转&#xff1b; 需要注意右边界的取值 int r Math.min(l k -1,n-1);//取右边界与n-1的最小值 确定边界…

抖店什么类目容易起店?新开通的抖店,最好是从这几个类目做起

我是王路飞。 做抖店之前&#xff0c;你需要先考虑好自己要做什么类目&#xff0c;适合做什么类目。 如果你什么都不了解&#xff0c;单凭自己的个人喜好去确定类目&#xff0c;大概率是做不起来的。 因为你作为一个没有电商经验的新手&#xff0c;并没有足够的能力判断抖音…

2020南京站ICPC F Firworks - 概率 + 三分

几何分布&#xff1a;每次时间发生的概率互不影响 每次成功的概率是1 - &#xff08;1 - p&#xff09;^ k,所以期望就为1 / &#xff08;1 - &#xff08;1 - p&#xff09;^ k&#xff09;。 期望再乘以该次花费的时间就是在第k轮放烟花的答案&#xff0c;然后让我们求最小…

celery app control inspect python后台控制

i tasks.app.control.inspect() # 创建inspect对象&#xff0c;可以操作tasks的队列管理 i.active(safeNone) # Return list of tasks currently executed by workers. i.reserved(safeNone) # Return list of currently reserved tasks, not including scheduled/active c…

2023年华为杯研究生数学建模竞赛辅导

2023年华为杯研究生数学建模竞赛辅导 各研究生培养单位&#xff1a; 中国研究生数学建模竞赛作为教育部学位管理与研究生教育司指导&#xff0c;中国学位与研究生教育学会、中国科协青少年科技中心主办的“中国研究生创新实践系列大赛”主题赛事之一&#xff0c;是一项面向在校…

【SpringBoot】生成二维码、在图片中嵌入二维码

背景 说明&#xff1a;本文章是介绍&#xff0c;在一张背景图片中嵌入生成的二维码和中文文字。 用处&#xff1a;比如活动分享二维码的时候&#xff0c;提供一张背景图&#xff0c;然后在背景图中嵌入二维码等。 注意&#xff1a;二维码和文字的位置需要你自行调整。 一、依赖…

el-dialog__body的border-radius属性失效解决思路

我的代码 .select-page :deep(.el-dialog__body) {padding: 0;width: 39.35vw;height: 60.03vh;background: #FFFFFF;border-radius: 3.78vh; }我查到的解决方案&#xff1a; 1、设置border&#xff1a;none; 去掉边框&#xff1b; 2、设置border-radius:40px; &#xff1b; 3…

电工-实验图解二极管伏安特性曲线和主要参数

实验图解二极管伏安特性曲线和主要参数 晶体二极管主要是由一个PN结构成&#xff0c;因此它应该与PN结具有相同的特性&#xff0c;即具有单向导电性。下面介绍加在二极管两端的电压和流过二极管的电流之间的关系。即二极管的伏安特性及二极管主要参数。 二极管伏安特性曲线 …

Java高级: 反射

目录 反射反射概述反射获取类的字节码反射获取类的构造器反射获取构造器的作用反射获取成员变量&使用反射获取成员方法反射获取成员方法的作用 反射的应用案例 接下来我们学习的反射、动态代理、注解等知识点&#xff0c;在以后开发中极少用到&#xff0c;这些技术都是以后…