当前位置: 首页 > news >正文

Spring kafka源码分析——消息是如何消费的

文章目录

    • 概要
    • 端点注册
    • 创建监听容器
    • 启动监听容器
    • 消息拉取与消费
    • 小结

概要

本文主要从Spring Kafka的源码来分析,消费端消费流程;从spring容器启动到消息被拉取下来,再到执行客户端自定义的消费逻辑,大致概括为以下4个部分:

在这里插入图片描述

源码分析主要也是从以上4个部分进行分析;

环境准备

maven依赖如下:

 	<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId></dependency>

消费端代码:

 @KafkaListener(topics = KafkaController.TOPIC_TEST_ERROR, groupId = "${spring.application.name}")public void replicatedTopicConsumer2(ConsumerRecord<String, String> recordInfo) {int partition = recordInfo.partition();System.out.println("partition:" + partition);String value = recordInfo.value();System.out.println(value);}

参数配置使用默认配置

端点注册

KafkaAutoConfiguration

与其他组件相同,spring-kafka的入口加载入口类也是以AutoConfiguration结尾,即:KafkaAutoConfiguration,由于本文重点分析消费者流程,自动类这里主要关注以下几个地方:
在这里插入图片描述
在这里插入图片描述
kafka启动后,会自动将ConcurrentKafkaListenerContainerFactory加载到容器中。

一般来说,消费端会使用到@KafkaListener注解或者@KafkaListeners注解,所以,我们的重点就是只要是关注,这两个注解是如何被识别,并且起到监听作用的,以下是类的加载流程:

在这里插入图片描述
Bean在执行init方法后会调用,初始化后置处理方法,而KafkaListenerAnnotationBeanPostProcessor实现了BeanPostProcessor,KafkaListenerAnnotationBeanPostProcessor#postProcessAfterInitialization就会被触发执行,在该方法中,会读取该bean中标注了@KafkaListener@KafkaListeners的方法
在这里插入图片描述

protected void processKafkaListener(KafkaListener kafkaListener, Method method, Object bean, String beanName) {Method methodToUse = checkProxy(method, bean);MethodKafkaListenerEndpoint<K, V> endpoint = new MethodKafkaListenerEndpoint<>();endpoint.setMethod(methodToUse);processListener(endpoint, kafkaListener, bean, methodToUse, beanName);}

从上面看出,每个标注了KafkaListener注解的方法都会创建一个MethodKafkaListenerEndpoint,接着调用KafkaListenerEndpointRegistrar#registerEndpoint(KafkaListenerEndpoint,KafkaListenerContainerFactory<?>)进行注册

在这里插入图片描述
MethodKafkaListenerEndpoint又得到KafkaListenerEndpointDescriptor,最后将有的KafkaListenerEndpointDescriptor放到endpointDescriptors集合中

这里需要注意的是,KafkaListenerAnnotationBeanPostProcessor中的KafkaListenerEndpointRegistrar registrar属性是new出来的,并没有在spring容器中,而后面的创建监听器时还会再用到。
在这里插入图片描述
以上就是kafka端点注册流程。

创建监听容器

spring kafka把每个标注了KafkaListener注解的方法称为Endpoint,为每个方法生成了一个MethodKafkaListenerEndpoint对象,同时又为每个端点生成了一个MessageListenerContainer;以下是具体的生成流程

在这里插入图片描述
KafkaListenerAnnotationBeanPostProcessor实现了SmartInitializingSingleton,其中的方法afterSingletonsInstantiated会在bean初始化后进行执行

@Override
public void afterSingletonsInstantiated() {// 这个registrar没有放入到spring 容器中this.registrar.setBeanFactory(this.beanFactory);if (this.beanFactory instanceof ListableBeanFactory) {Map<String, KafkaListenerConfigurer> instances =((ListableBeanFactory) this.beanFactory).getBeansOfType(KafkaListenerConfigurer.class);for (KafkaListenerConfigurer configurer : instances.values()) {configurer.configureKafkaListeners(this.registrar);}}if (this.registrar.getEndpointRegistry() == null) {if (this.endpointRegistry == null) {Assert.state(this.beanFactory != null,"BeanFactory must be set to find endpoint registry by bean name");this.endpointRegistry = this.beanFactory.getBean(KafkaListenerConfigUtils.KAFKA_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME,KafkaListenerEndpointRegistry.class);}this.registrar.setEndpointRegistry(this.endpointRegistry);}if (this.defaultContainerFactoryBeanName != null) {this.registrar.setContainerFactoryBeanName(this.defaultContainerFactoryBeanName);}// Set the custom handler method factory once resolved by the configurerMessageHandlerMethodFactory handlerMethodFactory = this.registrar.getMessageHandlerMethodFactory();if (handlerMethodFactory != null) {this.messageHandlerMethodFactory.setHandlerMethodFactory(handlerMethodFactory);}else {addFormatters(this.messageHandlerMethodFactory.defaultFormattingConversionService);}// 主要方法,注册端点并创建容器this.registrar.afterPropertiesSet();
}
···
**KafkaListenerEndpointRegistrar**```java
@Override
public void afterPropertiesSet() {registerAllEndpoints();
}protected void registerAllEndpoints() {synchronized (this.endpointDescriptors) {// 上一个阶段已经把所有的端点放入了endpointDescriptors集合中for (KafkaListenerEndpointDescriptor descriptor : this.endpointDescriptors) {this.endpointRegistry.registerListenerContainer(// 注意这个resolveContainerFactorydescriptor.endpoint, resolveContainerFactory(descriptor));}this.startImmediately = true;  // trigger immediate startup}
}// 如果在KafkaListener注解中的属性containerFactory没有配置容器工厂的名字,就会默认获取ConcurrentKafkaListenerContainerFactory实现类作为容器工厂
private KafkaListenerContainerFactory<?> resolveContainerFactory(KafkaListenerEndpointDescriptor descriptor) {if (descriptor.containerFactory != null) {return descriptor.containerFactory;}else if (this.containerFactory != null) {return this.containerFactory;}else if (this.containerFactoryBeanName != null) {Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name");this.containerFactory = this.beanFactory.getBean(this.containerFactoryBeanName, KafkaListenerContainerFactory.class);return this.containerFactory;  // Consider changing this if live change of the factory is required}else {throw new IllegalStateException("Could not resolve the " +KafkaListenerContainerFactory.class.getSimpleName() + " to use for [" +descriptor.endpoint + "] no factory was given and no default is set.");}}

在这里插入图片描述
以上截图是真正创建容器的地方,并把创建好的容器添加到Map<String, MessageListenerContainer> listenerContainers,后面起动时会用到。
至此,kafka监听容器创建完成,整理下主要类之间的关系,如下

在这里插入图片描述

一个@KafkaListener注解标注的方法,就可以得到一个MethodKafkaListenerEndpoint,再使用默认的ConcurrentKafkaListenerContainerFactory就会创建出一个MessageListenerContainer监听容器,有几个方法标注了@KafkaListener 就可以得到几个ConcurrentMessageListenerContainer

启动监听容器

上面的流程知道,所有创建的容器放到了`Map<String, MessageListenerContainer> listenerContainers``

在这里插入图片描述
KafkaListenerEndpointRegistry实现了Lifecycle,其中的start()方法会在bean加载的最后一个阶段中被执行到

在这里插入图片描述
以下是执行流程
在这里插入图片描述
其中org.springframework.kafka.listener.KafkaMessageListenerContainer#doStart如下

@Override
protected void doStart() {if (isRunning()) {return;}if (this.clientIdSuffix == null) { // stand-alone containercheckTopics();}ContainerProperties containerProperties = getContainerProperties();checkAckMode(containerProperties);Object messageListener = containerProperties.getMessageListener();if (containerProperties.getConsumerTaskExecutor() == null) {SimpleAsyncTaskExecutor consumerExecutor = new SimpleAsyncTaskExecutor((getBeanName() == null ? "" : getBeanName()) + "-C-");containerProperties.setConsumerTaskExecutor(consumerExecutor);}GenericMessageListener<?> listener = (GenericMessageListener<?>) messageListener;ListenerType listenerType = determineListenerType(listener);// ListenerConsumer的构造函数中创建了真正的Consumer<K, V> consumerthis.listenerConsumer = new ListenerConsumer(listener, listenerType);setRunning(true);this.startLatch = new CountDownLatch(1);// ListenerConsumer 实现了Runnable,调用submitListenable是就会开启新的线程执行其中的run方法this.listenerConsumerFuture = containerProperties.getConsumerTaskExecutor().submitListenable(this.listenerConsumer);try {if (!this.startLatch.await(containerProperties.getConsumerStartTimout().toMillis(), TimeUnit.MILLISECONDS)) {this.logger.error("Consumer thread failed to start - does the configured task executor "+ "have enough threads to support all containers and concurrency?");publishConsumerFailedToStart();}}catch (@SuppressWarnings(UNUSED) InterruptedException e) {Thread.currentThread().interrupt();}
}

每个监听容器ConcurrentMessageListenerContainer中都会创建一个出一个ListenerConsumer或多个(跟concurrency参数配置有关)ListenerConsumer,真正从kafka服务端拉去消息的逻辑在ListenerConsumerrun方法中。
到这里,主要类跟参数之间的对应关系如下
在这里插入图片描述

消息拉取与消费

这一阶段只要关注,消息的拉取到触发用户自定义方法流程与自动位移提交

不断循环拉去消息,并反射调用用户自定义方法:
在这里插入图片描述

protected void pollAndInvoke() {if (!this.autoCommit && !this.isRecordAck) {processCommits();}idleBetweenPollIfNecessary();if (this.seeks.size() > 0) {processSeeks();}pauseConsumerIfNecessary();this.lastPoll = System.currentTimeMillis();this.polling.set(true);// 调用kafka原生api进行拉取ConsumerRecords<K, V> records = doPoll();if (!this.polling.compareAndSet(true, false) && records != null) {/** There is a small race condition where wakeIfNecessary was called between* exiting the poll and before we reset the boolean.*/if (records.count() > 0) {this.logger.debug(() -> "Discarding polled records, container stopped: " + records.count());}return;}resumeConsumerIfNeccessary();debugRecords(records);if (records != null && records.count() > 0) {if (this.containerProperties.getIdleEventInterval() != null) {this.lastReceive = System.currentTimeMillis();}// 获取消息后,触发@KafkaListener标注地方法invokeListener(records);}else {checkIdle();}
}

下面先关注消费者位移在dopoll方法中什么时候触发提交地
在这里插入图片描述

在这里插入图片描述

// 每个消费组都有一个消费者协调器coordinator,在coordinator.poll方法中会判断是否需要自动提交位移
boolean updateAssignmentMetadataIfNeeded(final Timer timer) {if (coordinator != null && !coordinator.poll(timer)) {return false;}return updateFetchPositions(timer);}public boolean poll(Timer timer) {maybeUpdateSubscriptionMetadata();invokeCompletedOffsetCommitCallbacks();if (subscriptions.partitionsAutoAssigned()) {// Always update the heartbeat last poll time so that the heartbeat thread does not leave the// group proactively due to application inactivity even if (say) the coordinator cannot be found.// 唤醒心跳检测线程,触发一次心跳检测pollHeartbeat(timer.currentTimeMs());if (coordinatorUnknown() && !ensureCoordinatorReady(timer)) {return false;}if (rejoinNeededOrPending()) {// due to a race condition between the initial metadata fetch and the initial rebalance,// we need to ensure that the metadata is fresh before joining initially. This ensures// that we have matched the pattern against the cluster's topics at least once before joining.if (subscriptions.hasPatternSubscription()) {// For consumer group that uses pattern-based subscription, after a topic is created,// any consumer that discovers the topic after metadata refresh can trigger rebalance// across the entire consumer group. Multiple rebalances can be triggered after one topic// creation if consumers refresh metadata at vastly different times. We can significantly// reduce the number of rebalances caused by single topic creation by asking consumer to// refresh metadata before re-joining the group as long as the refresh backoff time has// passed.if (this.metadata.timeToAllowUpdate(timer.currentTimeMs()) == 0) {this.metadata.requestUpdate();}if (!client.ensureFreshMetadata(timer)) {return false;}maybeUpdateSubscriptionMetadata();}if (!ensureActiveGroup(timer)) {return false;}}} else {// For manually assigned partitions, if there are no ready nodes, await metadata.// If connections to all nodes fail, wakeups triggered while attempting to send fetch// requests result in polls returning immediately, causing a tight loop of polls. Without// the wakeup, poll() with no channels would block for the timeout, delaying re-connection.// awaitMetadataUpdate() initiates new connections with configured backoff and avoids the busy loop.// When group management is used, metadata wait is already performed for this scenario as// coordinator is unknown, hence this check is not required.if (metadata.updateRequested() && !client.hasReadyNodes(timer.currentTimeMs())) {client.awaitMetadataUpdate(timer);}}// 判断是否需要自动提交位移maybeAutoCommitOffsetsAsync(timer.currentTimeMs());return true;
}

在这里插入图片描述
以下就是消息拉去义位移自动提交地处理流程
在这里插入图片描述
记录返回后,会调用用户自定义地处理逻辑
在这里插入图片描述
以下时具体地调用流程

在这里插入图片描述

小结

1、kafka spring消费者端点注册、创建监听容器、启动监听容器阶段,有两个重要的类KafkaListenerAnnotationBeanPostProcessorKafkaListenerEndpointRegistry,他们对应的方法postProcessAfterInitializationstart在spring容器启动时会被执行,从而实现了kafka的监听容器的创建与启动

在这里插入图片描述
2、kafka自动提交位移时在poll方法中进行的,也就是每次获取新消息时,会先提交上次消费完成的消息;
3、拉取消息跟用户标注了@KafkaListener注解方法的处理逻辑用的是同一个线程,自动提交时间auto.commit.interval.ms默认是5s,假如用户的方法逻辑处理时长是10s,那么位移自动提交是在10s后再次调用poll方法时才会提交,而不是5s后就准时提交。

相关文章:

Spring kafka源码分析——消息是如何消费的

文章目录 概要端点注册创建监听容器启动监听容器消息拉取与消费小结 概要 本文主要从Spring Kafka的源码来分析&#xff0c;消费端消费流程&#xff1b;从spring容器启动到消息被拉取下来&#xff0c;再到执行客户端自定义的消费逻辑&#xff0c;大致概括为以下4个部分&#x…...

汽车电子的发展对国产32位MCU的影响

32位MCU是目前产品市场的主流&#xff0c;尤其是在汽车电子发展起来后&#xff0c;32位MCU在市场的比重迅速增涨&#xff0c;国内32位MCU厂商抓住风口&#xff0c;推出一系列汽车电子芯片从而稳定国内市场。如芯海的CS32F030系列就是主要应用汽车电子市场的32位MCU。 根据市场…...

Vue 目录结构 vite 项目

Vue3 项目常用的目录结构和每个文件的作用【通过 vite 创建的项目】 vite目录结构&#xff1a; dist // 打包后生成的文件目录 node_modules // 环境依赖 public // 公共资源目录 favicon.ico …...

TB/TM-商品详情原数据(APP)

一、接口参数说明&#xff1a; item_get_app-获得TB/TMapp商品详情原数据&#xff0c;点击更多API调试&#xff0c;请移步注册API账号点击获取测试key和secret 公共参数 请求地址: https://api-gw.onebound.cn/taobao/item_get_app 名称类型必须描述keyString是调用key&…...

小米发布会:雷军成长故事与创新壮举,AI大模型技术引领未来,雷军探索之路之从创业波折到小米AI领航,成就高端化传奇!

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…...

春秋云镜 CVE-2021-41947

春秋云镜 CVE-2021-41947 Subrion CMS v4.2.1 存在sql注入 靶标介绍 Subrion CMS v4.2.1 存在sql注入。 启动场景 漏洞利用 exp http://localhost/panel/visual-mode.json?getaccess&typeblocks UNION ALL SELECT username, password FROM sbr421_members -- -&o…...

面试题-React(一):React是什么?它的主要特点是什么?

探索React&#xff1a;前端开发中的重要角色与主要特点 引言&#xff1a; 在现代前端开发领域&#xff0c;React已经成为最受欢迎和广泛使用的JavaScript库之一。它由Facebook开发并于2013年首次发布。随着时间的推移&#xff0c;React在开发社区中获得了强大的支持和认可。本…...

算法笔试 java 输入输出练习

在线编程题刷题训练 所有答案 scancer函数的用法 输入输出总结top&#xff01;&#xff01;&#xff01;&#xff01; java如何调用函数&#xff08;方法&#xff09; java刷acm的各种输入输出 vscode配置java环境 子函数的调用&#xff0c;直接定义一个static子函数调用就…...

5.内置构造函数

在JavaScript中最主要的数据类型有6种: 1.基本数据类型: 字符串、数值、布尔、undefined、 null 2.引用类型&#xff1a;对象 但是&#xff0c;我们会发现有些特殊情况: //普通字符串 const str andy console.1og(str.length) // 4其实字符串、数值、布尔、等基本类型也都有…...

DG故障切换及DG Broker失效配置清理

DG故障切换及DG Broker失效配置清理 DG故障强制切主DG Broker原有配置清理 DG故障强制切主 主库发生故障无法在短时间内恢复时&#xff0c;需要执行主备切换。此时由于DG Broker无法连接到主库&#xff0c;故不能通过Broker切换&#xff0c;只能手动在备库进行切主。 --断开备…...

地毯(暴力+差分两种方法)

题目描述 在 nx n 的格子上有 m 个地毯。 给出这些地毯的信息&#xff0c;问每个点被多少个地毯覆盖。 输入格式 第一行&#xff0c;两个正整数 n,m。意义如题所述。 接下来 m 行&#xff0c;每行两个坐标 (x_1,y_1) 和 (x_2,y_2)&#xff0c;代表一块地毯&#xff0c;左上…...

最新智能AI系统+ChatGPT源码搭建部署详细教程+知识库+附程序源码

近期有网友问宝塔如何搭建部署AI创作ChatGPT&#xff0c;小编这里写一个详细图文教程吧。 使用Nestjs和Vue3框架技术&#xff0c;持续集成AI能力到AIGC系统&#xff01; 增加手机端签到功能、优化后台总计绘画数量逻辑&#xff01;新增 MJ 官方图片重新生成指令功能同步官方 …...

记一次Kafka重复消费解决过程

起因&#xff1a;车联网项目开发&#xff0c;车辆发生故障需要给三个系统推送消息&#xff0c;故障上报较为频繁&#xff0c;所以为了不阻塞主流程&#xff0c;采用了使用kafka。消费方负责推送并保存推送记录&#xff0c;但在一次压测中发现&#xff0c;实际只发生了10次故障&…...

人工智能在公检系统中的应用:校对软件助推刑事侦查工作

人工智能在公检系统中的应用&#xff0c;尤其是校对软件的应用&#xff0c;可以有效地助推刑事侦查工作。 以下是校对软件在刑事侦查工作中的一些应用方面&#xff1a; 1.自动校对和纠错&#xff1a;校对软件可以自动检测和纠正刑事侦查报告中的语法、拼写和标点错误等问题。通…...

OSI七层模型和TCP/IP四层模型

OSI七层模型和TCP/IP四层模型 七层模型(OSI) OSI七层模型&#xff08;Open Systems Interconnection Reference Model&#xff09;是一个用于计算机网络体系结构的标准化框架&#xff0c;旨在定义网络通信中不同层次的功能和协议。 各个层次具体如下&#xff1a; 物理层&am…...

vant金额输入框

1.在components中新建文件夹currency&#xff0c;新建index.js import Currency from ./src/currency.vueCurrency.install function (Vue) {Vue.component(Currency.name, Currency) }export default Currency 2.在currency中新建文件夹src&#xff0c;在src中间currency.v…...

uni-app base64转图片

pathToBase64 pathToBase64(path).then(base64 > {console.log(base64)}).catch(error > {console.error(error)})base64ToPath base64ToPath(base64).then(path > {console.log(path)}).catch(error > {console.error(error)})首先将插件引入项目。按照image-to…...

【webpack】自定义loader

&#x1f4dd;个人主页&#xff1a;爱吃炫迈 &#x1f48c;系列专栏&#xff1a;前端工程化 &#x1f9d1;‍&#x1f4bb;座右铭&#xff1a;道阻且长&#xff0c;行则将至&#x1f497; 文章目录 loaderloader引入方式loader传入/接收参数传入参数接收参数 loader返回值retur…...

【kubernetes】在k8s集群环境上,部署kubesphere

部署kubesphere 学习于尚硅谷kubesphere课程 前置环境配置-部署默认存储类型 这里使用nfs #所有节点安装 yum install -y nfs-utils# 在master节点执行以下命令 echo "/nfs/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports # 执行以下命令&#xff…...

STM32 F103C8T6学习笔记4:时钟树、滴答计时器、定时器定时中断

今日理解一下STM32F103 C8T6的时钟与时钟系统、滴答计时器、定时器计时中断的配置&#xff0c;文章提供原理&#xff0c;代码&#xff0c;测试工程下载。 目录 时钟树与时钟系统&#xff1a; 滴答计时器&#xff1a; 定时器计时中断&#xff1a; 测试结果&#xff1a; 测…...

树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频

使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...

K8S认证|CKS题库+答案| 11. AppArmor

目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、切换节点 3&#xff09;、切换到 apparmor 的目录 4&#xff09;、执行 apparmor 策略模块 5&#xff09;、修改 pod 文件 6&#xff09;、…...

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?

在建筑行业&#xff0c;项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升&#xff0c;传统的管理模式已经难以满足现代工程的需求。过去&#xff0c;许多企业依赖手工记录、口头沟通和分散的信息管理&#xff0c;导致效率低下、成本失控、风险频发。例如&#…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介 前面两期文章我们介绍了I2S的读取和写入&#xff0c;一个是通过INMP441麦克风模块采集音频&#xff0c;一个是通过PCM5102A模块播放音频&#xff0c;那如果我们将两者结合起来&#xff0c;将麦克风采集到的音频通过PCM5102A播放&#xff0c;是不是就可以做一个扩音器了呢…...

C# 类和继承(抽象类)

抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...

Java多线程实现之Thread类深度解析

Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...

Reasoning over Uncertain Text by Generative Large Language Models

https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...

免费PDF转图片工具

免费PDF转图片工具 一款简单易用的PDF转图片工具&#xff0c;可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件&#xff0c;也不需要在线上传文件&#xff0c;保护您的隐私。 工具截图 主要特点 &#x1f680; 快速转换&#xff1a;本地转换&#xff0c;无需等待上…...

Java数值运算常见陷阱与规避方法

整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...