【RocketMQ】消息拉模式分析

2023-10-27

RocketMQ有两种获取消息的方式,分别为推模式和拉模式。

推模式
推模式在【RocketMQ】消息的拉取一文中已经讲过,虽然从名字上看起来是消息到达Broker后推送给消费者,实际上还是需要消费向Broker发送拉取请求获取消息内容,推模式对应的消息消费实现类为DefaultMQPushConsumerImpl,回顾一下推模式下的消息消费过程:

  1. 消费者在启动的时候做一些初始化工作,它会创建MQClientInstance并进行启动;
  2. MQClientInstance中引用了消息拉取服务PullMessageService和负载均衡服务RebalanceService,它们都继承了ServiceThread,MQClientInstance在启动后也会对它们进行启动,所以消息拉取线程和负载均衡线程也就启动了;
  3. 负载均衡服务启动后,会对该消费者订阅的主题进行负载均衡,为消费者分配消息队列,并创建PullRequest拉取请求,用于拉取消息;
  4. PullMessageService中等待阻塞队列中PullRequest拉取请求的到来,接着会调用DefaultMQPushConsumerImplpullMessage方法进行消息拉取;
  5. 消费者向Broker发送拉取消息的请求,从Broker拉取消息;
  6. 消费者对Broker返回的响应数据进行处理,解析消息进行消费;

推模式下进行消息消费的例子:

@RunWith(MockitoJUnitRunner.class)
public class DefaultMQPushConsumerTest {
    private String consumerGroup;
    private String topic = "FooBar";
    private String brokerName = "BrokerA";
    private MQClientInstance mQClientFactory;

    @Mock
    private MQClientAPIImpl mQClientAPIImpl;
    private static DefaultMQPushConsumer pushConsumer;

    @Before
    public void init() throws Exception {
        // ...
        // 消费者组
        consumerGroup = "FooBarGroup" + System.currentTimeMillis();
        // 实例化DefaultMQPushConsumer
        pushConsumer = new DefaultMQPushConsumer(consumerGroup);
        pushConsumer.setNamesrvAddr("127.0.0.1:9876");
        // 设置拉取间隔
        pushConsumer.setPullInterval(60 * 1000);
        // 注册消息监听器
        pushConsumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                Optional.ofNullable(result).orElse(new ArrayList<MessageExt>()).stream().forEach(x-> {
                    // 处理消息
                    System.out.println(new String(x.getBody()));
                });
                return null;
            }
        });
        // ...
        // 设置订阅的主题
        pushConsumer.subscribe(topic, "*");
        // 启动消费者
        pushConsumer.start();
    }
}

消息推模式的详细过程可参考【RocketMQ】消息的拉取,接下来我们看一下拉模式。

拉模式
首先来看一下拉模式下进行消息消费的例子,拉模式下需要消费者不断调用poll方法获取消息,底层是一个阻塞队列,如果队列中没有数据,会进入等待直到队列中增加了数据:

 private void testPull() {
        // 创建DefaultLitePullConsumer
        DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("LitePullConsumerGroup");;
        try {
            litePullConsumer.setNamesrvAddr("127.0.0.1:9876");
            litePullConsumer.subscribe("LitePullConsumerTest", "*");
            litePullConsumer.start();
            litePullConsumer.setPollTimeoutMillis(20 * 1000);
            while(true) {
                // 获取消息
                List<MessageExt> result = litePullConsumer.poll();
                Optional.ofNullable(result).orElse(new ArrayList<MessageExt>()).stream().forEach(x-> {
                    // 处理消息
                    System.out.println(new String(x.getBody()));
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            litePullConsumer.shutdown();
        }
    }

推模式与拉模式的区别
对比上面推模式进行消费的例子,从使用方式上来讲,推模式不需要消费者主动去拉取消息,只需要注册消息监听器,当有消息到达时,触发consumeMessage方法进行消息消费,从表面上看就像是Broker主动推送给消费者一样,所以叫做推模式,尽管底层还是需要消费者发起拉取请求向Broker拉取消息

拉模式在使用方式上,需要消费者主动调用poll方法获取消息,从表面上看消费者需要不断主动进行消息拉取,所以叫做拉模式。

拉模式实现原理

拉模式下对应的消息拉取实现类为DefaultLitePullConsumerImpl,在DefaultLitePullConsumerDefaultMQPullConsumer被标注了@Deprecated,已不推荐使用)的构造函数中,可以看到对其进行了实例化,并在start方进行了启动:

public class DefaultLitePullConsumer extends ClientConfig implements LitePullConsumer {
    // 拉模式下默认的消息拉取实现类
    private final DefaultLitePullConsumerImpl defaultLitePullConsumerImpl;

    public DefaultLitePullConsumer(final String namespace, final String consumerGroup, RPCHook rpcHook) {
        this.namespace = namespace;
        this.consumerGroup = consumerGroup;
        // 创建DefaultLitePullConsumerImpl
        defaultLitePullConsumerImpl = new DefaultLitePullConsumerImpl(this, rpcHook);
    }

    @Override
    public void start() throws MQClientException {
        setTraceDispatcher();
        setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
        // 启动DefaultLitePullConsumerImpl
        this.defaultLitePullConsumerImpl.start();
        // ...
    }
}

与消息推模式类似,DefaultLitePullConsumerImpl的start的方法主要做一些初始化的工作:

  1. 初始化客户端实例对象mQClientFactory,对应实现类为MQClientInstance,拉取服务线程、负载均衡线程都是通过MQClientInstance启动的;
  2. 初始化负载均衡类,拉模式对应的负载均衡类为RebalanceLitePullImpl
  3. 创建消息拉取API对象PullAPIWrapper,用于向Broker发送拉取消息的请求;
  4. 初始化消息拉取偏移量;
  5. 启动一些定时任务;
public class DefaultLitePullConsumerImpl implements MQConsumerInner {
    public synchronized void start() throws MQClientException {
        switch (this.serviceState) {
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;
                this.checkConfig();
                if (this.defaultLitePullConsumer.getMessageModel() == MessageModel.CLUSTERING) {
                    this.defaultLitePullConsumer.changeInstanceNameToPID();
                }
                // 初始化MQClientInstance
                initMQClientFactory();
                // 初始化负载均衡
                initRebalanceImpl();
                // 初始化消息拉取API对象
                initPullAPIWrapper();
                // 初始化拉取偏移量
                initOffsetStore();
                // 启动MQClientInstance
                mQClientFactory.start();
                // 启动一些定时任务
                startScheduleTask();
                this.serviceState = ServiceState.RUNNING;
                log.info("the consumer [{}] start OK", this.defaultLitePullConsumer.getConsumerGroup());
                operateAfterRunning();
                break;
            case RUNNING:
            case START_FAILED:
            case SHUTDOWN_ALREADY:
                throw new MQClientException("The PullConsumer service state not OK, maybe started once, "
                    + this.serviceState
                    + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                    null);
            default:
                break;
        }
    }
}

负载均衡

拉取模式对应的负载均衡类为RebalanceLitePullImpl(推模式使用的是RebalanceService),在initRebalanceImpl方法中设置了消费者组、消费模式、分配策略等信息:

public class DefaultLitePullConsumerImpl implements MQConsumerInner {
    
    // 实例化,拉模式使用的是RebalanceLitePullImpl
    private RebalanceImpl rebalanceImpl = new RebalanceLitePullImpl(this);

    private void initRebalanceImpl() {
        // 设置消费者组
        this.rebalanceImpl.setConsumerGroup(this.defaultLitePullConsumer.getConsumerGroup());
        // 设置消费模式
        this.rebalanceImpl.setMessageModel(this.defaultLitePullConsumer.getMessageModel());
        // 设置分配策略
        this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultLitePullConsumer.getAllocateMessageQueueStrategy());
        // 设置mQClientFactory
        this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
    }
}

【RocketMQ】消息的拉取一文中已经讲到过,消费者启动后会进行负载均衡,对每个主题进行负载均衡,拉模式下处理逻辑也是如此,所以这里跳过中间的过程,进入到rebalanceByTopic方法,可以负载均衡之后如果消费者负载的ProcessQueue发生了变化,会调用messageQueueChanged方法触发变更事件:

public abstract class RebalanceImpl {
     private void rebalanceByTopic(final String topic, final boolean isOrder) {
        switch (messageModel) {
            case BROADCASTING: {
                // ...
            }
            case CLUSTERING: {
                Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
                List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
                // ...
                if (mqSet != null && cidAll != null) {
                    // ...
                    try {
                        // 分配消息队列
                        allocateResult = strategy.allocate(this.consumerGroup, this.mQClientFactory.getClientId(), mqAll, cidAll);
                    } catch (Throwable e) {
                        log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
                            e);
                        return;
                    }

                    Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
                    if (allocateResult != null) {
                        allocateResultSet.addAll(allocateResult);
                    }
                    // 更新处理队列
                    boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
                    if (changed) {
                        log.info(
                            "rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
                            strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
                            allocateResultSet.size(), allocateResultSet);
                        // 触发变更事件
                        this.messageQueueChanged(topic, mqSet, allocateResultSet);
                    }
                }
                break;
            }
            default:
                break;
        }
    }
}

触发消息队列变更事件

RebalanceLitePullImplmessageQueueChanged方法中又调用了MessageQueueListenermessageQueueChanged方法触发消息队列改变事件:

public class RebalanceLitePullImpl extends RebalanceImpl {
    @Override
    public void messageQueueChanged(String topic, Set<MessageQueue> mqAll, Set<MessageQueue> mqDivided) {
        MessageQueueListener messageQueueListener = this.litePullConsumerImpl.getDefaultLitePullConsumer().getMessageQueueListener();
        if (messageQueueListener != null) {
            try {
                // 触发改变事件
                messageQueueListener.messageQueueChanged(topic, mqAll, mqDivided);
            } catch (Throwable e) {
                log.error("messageQueueChanged exception", e);
            }
        }
    }
}

MessageQueueListenerImplDefaultLitePullConsumerImpl的内部类,在messageQueueChanged方法中,不管是广播模式还是集群模式,都会调用updatePullTask更新拉取任务:

public class DefaultLitePullConsumerImpl implements MQConsumerInner {
    class MessageQueueListenerImpl implements MessageQueueListener {
        @Override
        public void messageQueueChanged(String topic, Set<MessageQueue> mqAll, Set<MessageQueue> mqDivided) {
            MessageModel messageModel = defaultLitePullConsumer.getMessageModel();
            switch (messageModel) {
                case BROADCASTING:
                    updateAssignedMessageQueue(topic, mqAll);
                    updatePullTask(topic, mqAll); // 更新拉取任务
                    break;
                case CLUSTERING:
                    updateAssignedMessageQueue(topic, mqDivided);
                    updatePullTask(topic, mqDivided); // 更新拉取任务
                    break;
                default:
                    break;
            }
        }
    }
}

更新拉取任务

在updatePullTask方法中,从拉取任务表taskTable中取出了所有的拉取任务进行遍历,taskTable中记录了之前分配的拉取任务,负载均衡之后可能发生变化,所以需要对其进行更新,这一步主要是处理原先分配给当前消费者的消息队列,在负载均衡之后不再由当前消费者负责,所以需要从taskTable中删除,之后调用startPullTask启动拉取任务:

public class DefaultLitePullConsumerImpl implements MQConsumerInner {
    private final ConcurrentMap<MessageQueue, PullTaskImpl> taskTable =
        new ConcurrentHashMap<MessageQueue, PullTaskImpl>();

    private void updatePullTask(String topic, Set<MessageQueue> mqNewSet) {
        // 从拉取任务表中获取之前分配的消息队列进行遍历
        Iterator<Map.Entry<MessageQueue, PullTaskImpl>> it = this.taskTable.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<MessageQueue, PullTaskImpl> next = it.next();
            // 如果与重新进行负载均衡的主题一致
            if (next.getKey().getTopic().equals(topic)) {
                // 如果重新分配的消息队列集合中不包含此消息独立
                if (!mqNewSet.contains(next.getKey())) {
                    next.getValue().setCancelled(true);
                    // 从任务表移除
                    it.remove();
                }
            }
        }
        // 启动拉取任务
        startPullTask(mqNewSet);
    }
}

提交拉取任务

startPullTask方法入参中传入的是负载均衡后重新分配的消息队列集合,在startPullTask中会对重新分配的集合进行遍历,如果taskTable中不包含某个消息队列,就构建PullTaskImpl对象,加入taskTable,这一步主要是处理负载均衡后新增的消息队列,为其构建PullTaskImpl加入到taskTable,之后将拉取消息的任务PullTaskImpl提交到线程池周期性的执行:

public class DefaultLitePullConsumerImpl implements MQConsumerInner {

    private void startPullTask(Collection<MessageQueue> mqSet) {
        // 遍历最新分配的消息队列集合
        for (MessageQueue messageQueue : mqSet) {
            // 如果任务表中不包含
            if (!this.taskTable.containsKey(messageQueue)) {
                // 创建拉取任务
                PullTaskImpl pullTask = new PullTaskImpl(messageQueue);
                // 加入到任务表
                this.taskTable.put(messageQueue, pullTask);
                // 将任务提交到线程池定时执行
                this.scheduledThreadPoolExecutor.schedule(pullTask, 0, TimeUnit.MILLISECONDS);
            }
        }
    }
}

拉取消息

PullTaskImpl继承了Runnable,在run方法中的处理逻辑如下:

  1. 获取消息队列对应处理队列ProcessQueue;
  2. 获取消息拉取偏移量,也就是从何处开始拉取消息;
  3. 调用pull方法进行消息拉取;
  4. 判断拉取结果,如果拉取到了消息,将拉取到的结果封装为ConsumeRequest进行提交,也就是放到了阻塞队列中,后续消费者从队列中获取数据进行消费;
   public class PullTaskImpl implements Runnable {
        private final MessageQueue messageQueue;
        private volatile boolean cancelled = false;
        private Thread currentThread;

        @Override
        public void run() {
            // 如果未取消
            if (!this.isCancelled()) {
                this.currentThread = Thread.currentThread();
                // ...
                // 获取消息队列对应的ProcessQueue
                ProcessQueue processQueue = assignedMessageQueue.getProcessQueue(messageQueue);
                // ...  跳过一系列校验
                long offset = 0L;
                try {
                    // 获取拉取偏移量
                    offset = nextPullOffset(messageQueue);
                } catch (Exception e) {
                    log.error("Failed to get next pull offset", e);
                    scheduledThreadPoolExecutor.schedule(this, PULL_TIME_DELAY_MILLS_ON_EXCEPTION, TimeUnit.MILLISECONDS);
                    return;
                }

                if (this.isCancelled() || processQueue.isDropped()) {
                    return;
                }
                long pullDelayTimeMills = 0;
                try {
                    SubscriptionData subscriptionData;
                    // 获取主题
                    String topic = this.messageQueue.getTopic();
                    // 获取主题对应的订阅信息SubscriptionData
                    if (subscriptionType == SubscriptionType.SUBSCRIBE) {
                        subscriptionData = rebalanceImpl.getSubscriptionInner().get(topic);
                    } else {
                        subscriptionData = FilterAPI.buildSubscriptionData(topic, SubscriptionData.SUB_ALL);
                    }
                    // 拉取消息
                    PullResult pullResult = pull(messageQueue, subscriptionData, offset, defaultLitePullConsumer.getPullBatchSize());
                    if (this.isCancelled() || processQueue.isDropped()) {
                        return;
                    }
                    // 判断拉取结果
                    switch (pullResult.getPullStatus()) {
                        case FOUND: // 如果获取到了数据
                            final Object objLock = messageQueueLock.fetchLockObject(messageQueue);
                            synchronized (objLock) { // 加锁
                                if (pullResult.getMsgFoundList() != null && !pullResult.getMsgFoundList().isEmpty() && assignedMessageQueue.getSeekOffset(messageQueue) == -1) {
                                    processQueue.putMessage(pullResult.getMsgFoundList());
                                    // 将拉取结果封装为ConsumeRequest,提交消费请求
                                    submitConsumeRequest(new ConsumeRequest(pullResult.getMsgFoundList(), messageQueue, processQueue));
                                }
                            }
                            break;
                        case OFFSET_ILLEGAL:
                            log.warn("The pull request offset illegal, {}", pullResult.toString());
                            break;
                        default:
                            break;
                    }
                    updatePullOffset(messageQueue, pullResult.getNextBeginOffset(), processQueue);
                } catch (InterruptedException interruptedException) {
                    log.warn("Polling thread was interrupted.", interruptedException);
                } catch (Throwable e) {
                    pullDelayTimeMills = pullTimeDelayMillsWhenException;
                    log.error("An error occurred in pull message process.", e);
                }
                // ...
            }
        }
    }

submitConsumeRequest方法中可以看到将创建的ConsumeRequest对象放入了阻塞队列consumeRequestCache中:

public class DefaultLitePullConsumerImpl implements MQConsumerInner {
    // 阻塞队列
    private final BlockingQueue<ConsumeRequest> consumeRequestCache = new LinkedBlockingQueue<ConsumeRequest>();

    private void submitConsumeRequest(ConsumeRequest consumeRequest) {
        try {
            // 放入阻塞队列consumeRequestCache中
            consumeRequestCache.put(consumeRequest);
        } catch (InterruptedException e) {
            log.error("Submit consumeRequest error", e);
        }
    }
}

消息消费

在前面的例子中,可以看到消费者是调用poll方法获取数据的,进入到poll方法中,可以看到是从consumeRequestCache中获取消费请求的,然后从中解析出消息内容返回:

public class DefaultLitePullConsumerImpl implements MQConsumerInner {
    
    public synchronized List<MessageExt> poll(long timeout) {
        try {
            // ...
            long endTime = System.currentTimeMillis() + timeout;
            // 从consumeRequestCache中获取数据进行处理
            ConsumeRequest consumeRequest = consumeRequestCache.poll(endTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
            // ...
            if (consumeRequest != null && !consumeRequest.getProcessQueue().isDropped()) {
                // 获取消息内容
                List<MessageExt> messages = consumeRequest.getMessageExts();
                long offset = consumeRequest.getProcessQueue().removeMessage(messages);
                assignedMessageQueue.updateConsumeOffset(consumeRequest.getMessageQueue(), offset);
                this.resetTopic(messages);
                // 返回消息内容
                return messages;
            }
        } catch (InterruptedException ignore) {
        }
        return Collections.emptyList();
    }
}

参考

RocketMQ源码分析之pull模式consumer

RocketMQ版本:4.9.3

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【RocketMQ】消息拉模式分析 的相关文章

  • 如何用Java写入OS系统日志?

    Mac OS 有一个名为 Console 的应用程序 其中包含记录的消息 错误和故障 我相信 Windows 中的等效项是事件查看器 我想 Linux 上也有一个 但我不知道它是什么 也不知道它在哪里 是否可以像这样从 Java 输出获取消
  • 如何在java中压缩/解压tar.gz文件

    谁能告诉我在java中压缩和解压缩tar gzip文件的正确方法我一直在搜索 但我能找到的最多的是zip或gzip 单独 我写了一个包装器公共压缩 http commons apache org compress called jarchi
  • 指纹奇异点检测

    我正在尝试确定指纹的核心点和增量点 我正在使用庞加莱指数方法 但我无法成功检测到这一点 而且我不明白为什么 First I divide the image in 15x15 blocks then I calculate the x an
  • 在 Tomcat 上部署 Java Web 项目,无需 WAR 或 EAR

    我有一个 Java Web 项目 Struts Spring 在我的本地主机上完美运行 我必须将其部署在我的网站上 但虚拟主机提供的 Tomcat Manager 界面显示 由于安全原因 它无法上传 WAR 文件 当联系技术支持时 我被告知
  • 使用 JAXB 编组 LocalDate

    我正在构建一系列链接类 我希望能够将其实例编组到 XML 以便我可以将它们保存到文件中并稍后再次读取它们 目前我使用以下代码作为测试用例 import javax xml bind annotation import javax xml b
  • FFmpeg 不适用于 android 10,直接进入 onFailure(String message) 并显示空消息

    我在我的一个项目中使用 FFmpeg 进行视频压缩 在 Android 10 Google Pixel 3a 上 对于发送执行的任何命令 它会直接进入 onFailure String message 并显示空消息 所以我在我的应用程序 g
  • 此版本不符合 Google Play 64 位要求,添加库后仍然出现错误

    我正在 Play 商店上传一个视频编辑器应用程序 其中包含带有一些本机代码的库 所以我通过将其添加到 gradle 来使其兼容 64 位 ndk abiFilters armeabi v7a arm64 v8a x86 x86 64 添加了
  • Codility 钉板

    尝试了解 Codility NailingPlanks 的解决方案 问题链接 https app codility com programmers lessons 14 binary search algorithm nailing pla
  • 用于防止滥用的 Servlet 过滤器? (DoS、垃圾邮件等)

    我正在寻找一个 Servlet 过滤器库 它可以帮助我保护我们的 Web 服务免受未经授权的使用和 DDoS 攻击 我们的网络服务有 授权客户 因此理想情况下 过滤器将帮助检测未经授权或行为不当的客户 或检测使用同一帐户的多个人 此外 我们
  • Java 中 static 关键字如何工作?

    我正在阅读Java教程 http docs oracle com javase tutorial index html从一开始我就有一个问题static字段或变量上的关键字 作为Java said here http docs oracle
  • 在多模块项目中访问绑定适配器

    我有一个多模块项目 其中应用程序模块包含我的绑定适配器 而我的功能模块取决于我的应用程序模块 因为它是动态功能模块 应用程序 包含绑定适配器 gt 动态功能模块 存在布局的地方 我在所有模块中启用了数据绑定和 kapt 我无法成功构建应用程
  • 如何列出所有可用的 LookAndFeel 主题?

    如何列出所有可用的 LookAndFeel 主题 我想在 JComboBox 中显示以供用户选择 这真的很简单 public static UIManager LookAndFeelInfo getInstalledLookAndFeels
  • Hibernate @OneToMany 注释到底是如何工作的?

    我对 Hibernate 还很陌生 我正在通过教程学习它 我在理解到底如何一对多注释作品 所以我有这两个实体类 Student代表一个学生并且Guide代表指导学生的人 因此 每个学生都与一名向导相关联 但一名向导可以跟随多个学生 我想要一
  • C++ 中的 Java ArrayList [重复]

    这个问题在这里已经有答案了 在Java中我可以做 List
  • 在 netBeans 中运行程序时,字体看起来非常奇怪

    我在我的新 MacBook M1 上设置了 netBeans 和 SceneBuilder 除了运行程序时的字体外 一切正常 它看起来像这样 我不知道为什么 按钮应显示 Click me 标签应显示 Hello 我收到的错误消息是 M rz
  • 在方法内声明类 - Final 关键字 [重复]

    这个问题在这里已经有答案了 给定方法中的以下内部类 IsSomething public class InnerMethod private int x public class Something private int y public
  • Java:基于 Web 的应用程序中的单例类实例

    我在 Web Application 中有这个 Singleton 类 public class MyDAO private static MyDAO instance private MyDAO public static MyDAO g
  • 编译时在代码中替换Java静态最终值?

    在java中 假设我有以下内容 fileA java class A public static final int SIZE 100 然后在另一个文件中我使用这个值 fileB java import A class b Object t
  • RecyclerView 不调用 onCreateViewHolder 或 onBindView

    没有收到任何错误 所有数据似乎都有效 由于某种原因 没有调用与视图相关的方法 我已确定以下事项 getItemCount 是唯一被调用的适配器方法 并且返回一个正整数值 我知道这将是你们将要查看的区域 构造函数正在被调用 成员变量有效 Pa
  • java中的回调是什么[重复]

    这个问题在这里已经有答案了 可能的重复 什么是回调函数 https stackoverflow com questions 824234 what is a callback function 我已经阅读了回调的维基百科定义 但我仍然没有明

随机推荐

  • IDEA在Debug模式下 对象转JSON

    如果文章对你有帮助欢迎 关注 点赞 收藏 一键三连 一起努力 IDEA在debug模式下面是不能直接复制被调试的Object为Json字符串的 但是在工作中经常会用到json入参 这里用两种方式实现这个操作 方便进行其他操作和工作交流 一
  • eds能谱图分析实例_EDS那些事儿

    什么是EDS 我们通常所说的EDS全称为能量色散X射线谱仪 简称能谱仪 可同时记录所有X射线谱 用以测量X射线强度与X射线能量的函数关系 是一种不损坏试样的快速微区成分分析方法 通过测量材料被激发的特征X射线能量进行元素的定性分析 测量特征
  • fake-useragent,python爬虫伪装请求头

    在编写爬虫进行网页数据的时候 大多数情况下 需要在请求是增加请求头 下面介绍一个python下非常好用的伪装请求头的库 fake useragent 具体使用说明如下 安装fake useragent库 pip install fake u
  • 复杂美区块链溯源系统架构

    从功能架构上 复杂美将区块链存证溯源系统按照功能划分为区块链核心层 接口层 运维管理层 溯源平台层和用户端层 1 区块链基础层 面向整个存证溯源平台提供基础信息服务 主要是为上层架构组件提供基础设施 保证上层服务可靠运行 源数据从IOT设备
  • 数据结构-链式存储

    数据结构 一 数据结构的定义 一组用来保存一种或者多种特定关系的数据集合 二 数据与数据之间的关系 lt 1 gt 数据的逻辑结构 数据元素与元素之间的关系 集合 关系平等 线性结构 元素之间一对一的关系 表 队列 栈 树形结构 元素之间一
  • 模式分类识别

    模式分类识别 DBN深度置信网络数据多特征分类预测 Matlab完整程序 目录 模式分类识别 DBN深度置信网络数据多特征分类预测 Matlab完整程序 分类结果 基本介绍 程序设计 参考资料 分类结果
  • Host文件

    linux中 etc目录 配置文件 etc目录包含了系统特有的配置文件 所谓配置文件 就是用于控制程序运行的本地文件 它绝大多情况下都说 只读 的私有文件 而且是可编辑的 这里的可编辑是指能直接看懂的 所以那些二进制可执行文件是不能作为配置
  • springboot多数据源---2事务

    一 多数据源事务控制 在多数据源下 由于涉及到数据库的多个读写 一旦发生异常就可能会导致数据不一致的情况 在这种情况希望使用事务 进行回退 但是Spring的声明式事务在一次请求线程中只能使用一个数据源进行控制 但是对于多源数据库 1 单一
  • 在webstorm 中直接运行ts文件

    原文链接 在webstorm 中直接运行ts文件 上一篇 ubuntu 使用 Apache Bench 进行并发测试 下一篇 使用js解数独难题 安装插件后重启IDE Run Configuration for TypeScript
  • Notice: Use of undefined constant submit - assumed 'submit'

    Notice Use of undefined constant submit assumed submit in D wamp www ECMS insert monitors php on line 66 Notice Undefine
  • vue期望值与实际值比较:折线图

    效果图 点击上方对应按钮 下方相应的数据图可隐藏 显示 代码 一 下载echarts包 终端运行 npm install echarts 二 components HelloWorld vue
  • Python3,一行代码解析地址信息,原来物流单的地址是这样拆分。

    1行代码解析地址信息 1 引言 2 代码示例 2 1 简介 2 2 安装 2 3 实战 2 3 1 提取省市区信息 2 3 2 提取街镇乡 村或居委会信息 2 3 3 自动补全省市信息 3 总结 1 引言 小屌丝 鱼哥 你说咱们发快递时填写
  • 页式存储,段式存储,段页式存储,引入快表等访存次数

    王道的说法 页式存储 2次 第一次 访问内存中的页表 利用逻辑地址中的页号查找到页帧号 与逻辑地址中的页内偏移拼接形成物理地址 第二次 得到物理地址后 再一次访问内存 存取指令或者数据 段式存储 2次 同上 段页式存储 3次 第一次 访问内
  • 【译】Rust 实现一个 DNS 客户端,我从中学到什么

    What I learned from making a DNS client in Rust 译文 Rust 实现一个 DNS 客户端 我从中学到什么 原文链接 https blog adamchalmers com making a d
  • 大津算法的matlab实现

    大津算法详解 一 算法功能 图像分割就是把图像分成若干个特定的 具有独特性质的区域并提出感兴趣目标的技术和过程 它是由图像处理到图像分析的关键步骤 大津算法也称最大类间差法 由大津于1979年提出 被认为是图像分割中阈值选取的最佳算法 计算
  • 用开卡工具重生SSD,SM2246XT一步一步开卡成功教程

    故障现象 不能进系统 用U盘从PE进入 过程很慢 卡住 进不了PE 直接拆下硬盘 用硬盘盒连接电脑 能识别 发现C盘还已经标红 D盘正常 还不错 文件都在 直接拷贝出来 接下来就是对他直接格式化 这里出现了问题 无论是用PE的还是windo
  • 前端知识题整理第二期

    1 js中的闭包指什么 有权访问另一个函数作用域中的变量的函数 创建闭包的常见方式 就是一个函数内部创建另一个函数 2 v if和v show的区别是什么 v if是动态的向DOM树内添加或删除DOM元素 v show本质是标签displa
  • STM32 硬件IIC 控制OLED I2C卡死问题

    更新通知 2023 09 06 STM32L151 固件库 使用I2C 太难了 又宕机了 建议不要在固件库版本上尝试硬件IIC 了 一般人真用不了 直接使用软件模拟的 或者不要使用固件库了 用HAL 库吧 据说HAL 库没这么多问题 不死心
  • 【深度学习与计算机视觉】13、深度学习中的目标检测视频笔记

    文章目录 一 目标检测是什么 二 RCNN 三 SPPnet 四 Fast R CNN 五 Faster R CNN 六 R FCN 七 YOLO v1 八 YOLO v2 九 YOLO v3 一 目标检测是什么 最小外接矩形 也就是最后要
  • 【RocketMQ】消息拉模式分析

    RocketMQ有两种获取消息的方式 分别为推模式和拉模式 推模式 推模式在 RocketMQ 消息的拉取一文中已经讲过 虽然从名字上看起来是消息到达Broker后推送给消费者 实际上还是需要消费向Broker发送拉取请求获取消息内容 推模