首页 文章 精选 留言 我的

精选列表

搜索[职场成长],共7789篇文章
优秀的个人博客,低调大师

滥用微软原生 OAuth 认证体系的 Teams 职场钓鱼攻击机理与防御研究

传统网络钓鱼依托仿冒域名伪造微软登录页面实现凭证窃取,相关防护手段已形成成熟体系,但 2026 年 6—7 月 Check Point 披露的规模化钓鱼活动完全摒弃虚假站点构造思路,直接复用微软login.microsoftonline.com官方 OAuth 授权链路,依托 Teams 人力资源任务通知社工诱饵诱导用户向攻击者管控第三方应用授予全域 Microsoft 365 权限,形成可绕过多层传统防护的新型云身份威胁。本次攻击覆盖全球 120 余家不同行业机构,具备标准化邮件模板、统一跳转链路、云服务中转窃取令牌、产业化可租赁攻击工具四大工业化特征,突破域名信誉筛查、多因素认证、终端行为检测等主流安全管控机制,一旦用户完成许可确认,攻击者可长期读取企业邮件、云文档、Teams 会话、SharePoint 资料库等全部办公资产,衍生商务邮件劫持、内部数据批量泄露等高风险次生危害。本文以 Check Point 公开的完整攻击调查材料为核心实证样本,系统还原本次 OAuth 许可钓鱼的邮件社工设计、官方认证链路复用、令牌劫持、权限滥用全杀伤链,剖析 OAuth 协议原生信任假设缺陷、企业现有安全架构多层失效根源;结合反网络钓鱼技术专家芦笛的专业研判,梳理工业制造、法律服务、公共事业等重点行业受害分布特征,构建 “邮件语义前置拦截 —Entra ID 授权行为审计 — 条件访问强策略约束 — 人员分层安全运营” 四层闭环纵深防御框架,明确各层级可落地的技术管控规则与管理规范。研究证实,依托厂商合法身份基础设施的协议滥用型钓鱼已成为云办公场景首要身份安全风险,仅依靠域名黑白名单、静态页面特征匹配的被动防护模式无法形成有效对抗,防御重心必须转向授权上下文、令牌生命周期、第三方应用权限动态管控维度,相关研究结论可为国内政企 Microsoft 365 租户身份安全治理、协同软件钓鱼专项防护提供完整理论依据与实操方案。

优秀的个人博客,低调大师

一份职场开发人员提升指南正在空投中,请注意查收

建立一个实质性的项目 从不同的视角,端点功能触发, 构建具有独特性且能展示开发人员创造力的应用 web后端框架 -了解如何构建框架 利用主要使用的编程语言构建一个程序包,并在多个项目中使用 围绕自己超级感兴趣的事物构建一个更大的项目 构建一个完全超乎你当前理解范围的事物 向开源做贡献 构建自己的开源软件包,应用程序 向现有的代码库贡献代码 开源软件包 构建自己的开源软件包,要确保不是来自某个教程,是真正想发布给别人的东西 记录创作过程 记录创建的原因以及思考过程 从新的角度思考问题并获得新的见解 别人的代码库 查看GitHub上流行软件包和插件的代码库,很可能这些软件包中含有很多未解决的问题 首先仔细阅读代码库制定的代码贡献指南,即README.md文件,然后开始解决问题,进一步添加一些实用的功能: 证明自己能够遵循编程准则 尽快成为开源社区的一员 熟练掌握分叉,拉取请求和提交 了解社区维护的软件包,有机会采用一些更好的编程实践 阅读高级代码 努力理解代码,并和自己的代码进行比较,设法寻找可以改善自己的实现的模式,同时记录下所有能够提高自己工作流程的技术细节 可以从社区寻找开源项目,阅读并理解代码 在阅读其他开发人员的代码时,尽量将遇到的问题向开发者提出来,如果有人能够回答你的问题,就更好了,你需要让他们知道你在努力理解高级代码,请求他们回答你的问题 应该阅读什么代码 阅读你真正用得着的代码 阅读那些你已经在使用的插件或者库文件源码 为阅读的代码添加注释,文档 阅读印象深刻的代码 遇到的印象深刻的项目 了解项目所基于的开源技术 阅读你尊敬的大师写出的代码 关注那些优秀的程序员 阅读给你带来灵感的代码 在初期选择一个小项目进行阅读,这样可以关注细节,学习到更多知识 如何阅读源码 先看大框架 在宏观层面上熟悉代码是如何实现的(README.md), 项目的网站,教程,文档 大致浏览项目的结构 注意项目的文件结构 目标: 熟悉源代码 找出哪些文件包含,请求,加载其他文件 大部分代码在哪里 命名空间以及相关性质 记录阅读心得 当开始理解程序流时,为代码添加注释,记录你的假设和结论 随着理解的进展,删除无关紧要的注释 边阅读边测试 当阅读别人代码时,测试是最佳理解代码程序流的方式:记录了代码应该完成的任务 在阅读时,试着让测试套件成功运行 这样确保开发环境配置恰当,在进行更改优化时更加得心应手 亲自动手做 为代码的实现添加或者更改逻辑但不要破坏代码 尝试给代码中添加功能 设置项目范围的日志记录 了解更多的编程设计模式和原理 仔细阅读每个模型的定义 搜索学习每个模型的示例 快速阅读设计模式和原理

优秀的个人博客,低调大师

🔥 神级成长性!Solon v2.9 发布

Solon 框架! Java “纯血国产”应用开发框架。开放原子开源基金会,孵化项目。从零开始构建(非 java-ee 架构),有灵活的接口规范与开放生态。 追求: 更快、更小、更简单 提倡: 克制、简洁、高效、开放、生态 有什么特点? 特点 描述 更高的计算性价比 并发高 300%;内存省 50% 更快的开发效率 代码少;入门快;调试重启快 10 倍 更好的生产与部署体验 打包小 90% 更大的兼容范围 非 java-ee 架构;同时支持 java8 ~ java22,graalvm native image 入门探索视频(用户录制): 最近更新了什么? 新增 solon.boot.vertx 插件 新增 solon.cloud.gateway 插件 新增 solon.rx 插件 添加 solon.data 配置节solon.dataSources(用于自动构建数据源),支持 ENC 加密符 添加 solon.docs 配置节solon.docs(用于自动构建文档摘要) 添加 solon.view.prefix 配置项支持 "file:" 前缀(支持体外目录) 添加 solon.scheduling.simple SimpleScheduler::isStarted 方法 添加 solon@Condition(onBean, onBeanName)条件属性 添加 solon.validation ValidUtils 工具类 添加 solon LifecycleBean:postStart 方法 添加 solon MethodInterceptor 接口,替代 Interceptor(旧接口保留) 添加 solon.net.httputils 扩展机制,并与 solon.cloud 自动整合 添加 solon.net.httputils HttpResponse::headerNames 方法 添加 solon.cloud CloudDiscoveryService:findServices 方法 添加 solonsolon.plugin.exclude应用属性配置 添加 solonsolon.app.enabled应用属性配置(Solon.cfg().appEnabled()可获取) 添加 solon${.url}应用属性配置本级引用 添加 solon--cfg启动参数支持(便于内嵌场景开发) 添加 托管类构造参数注入支持(对 kotlin 更友好) 调整 solon.cloud.httputils 标为弃用,由 solon.net.httputils 替代 调整 smarthttp,jetty,undertow 的非标准方法的 FormUrlencoded 预处理时机 调整 solon.auth maven 包更名为 solon.security.auth (原 maven 包保留) 调整 solon.validation maven 包更名为 solon.security.validation (原 maven 包保留) 调整 solon.vault maven 包更名为 solon.security.vault (原 maven 包保留) 优化 AppContext::beanMake 保持与 beanSacn 相同的类处理 优化 solon.serialization.jackson 兼容 @JsonFormat 注解时间格式和时间格式配置并存 优化 solon Context::body 的兼容性,避免不可读情况 优化 solon 调试模式与 gradle 的兼容性 优化 solon.boot FormUrlencodedUtils 预处理把 post 排外 优化 solon.web.rx 允许多次渲染输出 优化 kafka-solon-cloud-plugin 添加 username, password 简化配置支持(简化有账号的连接体验) 优化 solon.boot 413 状态处理 优化 solon.boot.smarthttp 适配的 maxRequestSize 设置(取 fileSize 和 bodySize 的大值) 优化 solon AppContext 注册和查找时以 rawClz 为主(避免以接口注册时,实例类型查不到) 优化 solon.mvc kotlin data class 带默认值的注入支持(表单模式下) 优化 solon PathAnalyzer 添加 addStarts 参数选择,支持域名匹配 优化 solon LifecycleBean 和 Lifecycle 设计 修复 solon.view.thymeleaf 模板不存在时没有输出 500 的问题 修复 solon.serialization.jackson 泛型注入失效的问题 修复 solon.boot.smarthttp 适配在 chunked 下不能读取 body string 的问题 修复 solon-openapi2-knife4j 没有配置时不能启动的问题(默认改为不启用) wood 升为 1.3.0 snack3 升为 3.2.109 socket.d 升为 2.5.11 zookeeper 升为 3.9.2 dromara-plugins 升为 0.1.2 kafka_2.13 升为 3.8.0 beetlsql 升为 3.30.10-RELEASE beetl 升为 3.17.0.RELEASE mybatis 升为 3.5.16 mybatis-flex 升为 1.9.6 sqltoy 升为 5.6.20 dbvisitor 升为 5.4.3 bean-searcher 升为 4.3.0 liteflow 升为 2.12.2 aws.s3 升为 1.12.769 powerjob 升为 5.1.0 netty 升为 4.1.112.Final reactor-core 升为 3.6.9 reactor-netty-http 升为 1.1.22 vertx 升为 4.5.9 undertow 升为 2.2.34.Final jetty 升为 9.4.55.v20240627 smarthttp 升为 1.5.9 项目仓库地址? gitee:https://gitee.com/opensolon/solon github:https://github.com/opensolon/solon 官网? https://solon.noear.org

优秀的个人博客,低调大师

Kafka成长3:Producer 元数据拉取源码原理(上)

上一节我们分析了Producer的核心组件,我们得到了一张关键的组件图。你还记得么? 简单概括下上面的图就是: 创建了Metadata组件,内部通过Cluster维护元数据 初始化了发送消息的内存缓冲器RecordAccumulator 创建了NetworkClient,内部最重要的是创建了NIO的Selector组件 启动了一个Sender线程,Sender引用了上面的所有组件,开始执行run方法。 图的最下方可以看到,上一节截止到了run方法的执行,这一节我们首先会看看run方法核心脉络做了什么。接着分析下Producer第一个核心流程:元数据拉取的源码原理。 让我们开始吧! Sender的run方法在做什么? 这一节我们就继续分析下,sender的run方法开始执行会做什么。 public void run() { log.debug("Starting Kafka producer I/O thread."); // main loop, runs until close is called while (running) { try { run(time.milliseconds()); } catch (Exception e) { log.error("Uncaught error in kafka producer I/O thread: ", e); } } log.debug("Beginning shutdown of Kafka producer I/O thread, sending remaining records."); // okay we stopped accepting requests but there may still be // requests in the accumulator or waiting for acknowledgment, // wait until these are completed. while (!forceClose && (this.accumulator.hasUnsent() || this.client.inFlightRequestCount() > 0)) { try { run(time.milliseconds()); } catch (Exception e) { log.error("Uncaught error in kafka producer I/O thread: ", e); } } if (forceClose) { // We need to fail all the incomplete batches and wake up the threads waiting on // the futures. this.accumulator.abortIncompleteBatches(); } try { this.client.close(); } catch (Exception e) { log.error("Failed to close network client", e); } log.debug("Shutdown of Kafka producer I/O thread has completed."); } 这个run方法的核心脉络很简单。主要就是2个while循环+线程的close,而2个while循环,他们都调用了run(long time)的这个方法。 通过注释你可以看到,第二个while是处理特殊情况的,当第一个while退出后,还有未发送的请求,需要第二个while循环处理完成,才会关闭线程。 整体脉络如下图所示: 接着其实就该看下run方法主要在干什么了? /** * Run a single iteration of sending * * @param now * The current POSIX time in milliseconds */ void run(long now) { Cluster cluster = metadata.fetch(); // get the list of partitions with data ready to send RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now); // if there are any partitions whose leaders are not known yet, force metadata update if (result.unknownLeadersExist) this.metadata.requestUpdate(); // remove any nodes we aren't ready to send to Iterator<Node> iter = result.readyNodes.iterator(); long notReadyTimeout = Long.MAX_VALUE; while (iter.hasNext()) { Node node = iter.next(); if (!this.client.ready(node, now)) { iter.remove(); notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now)); } } // create produce requests Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster, result.readyNodes, this.maxRequestSize, now); if (guaranteeMessageOrder) { // Mute all the partitions drained for (List<RecordBatch> batchList : batches.values()) { for (RecordBatch batch : batchList) this.accumulator.mutePartition(batch.topicPartition); } } List<RecordBatch> expiredBatches = this.accumulator.abortExpiredBatches(this.requestTimeout, now); // update sensors for (RecordBatch expiredBatch : expiredBatches) this.sensors.recordErrors(expiredBatch.topicPartition.topic(), expiredBatch.recordCount); sensors.updateProduceRequestMetrics(batches); List<ClientRequest> requests = createProduceRequests(batches, now); // If we have any nodes that are ready to send + have sendable data, poll with 0 timeout so this can immediately // loop and try sending more data. Otherwise, the timeout is determined by nodes that have partitions with data // that isn't yet sendable (e.g. lingering, backing off). Note that this specifically does not include nodes // with sendable data that aren't ready to send since they would cause busy looping. long pollTimeout = Math.min(result.nextReadyCheckDelayMs, notReadyTimeout); if (result.readyNodes.size() > 0) { log.trace("Nodes with data ready to send: {}", result.readyNodes); log.trace("Created {} produce requests: {}", requests.size(), requests); pollTimeout = 0; } for (ClientRequest request : requests) client.send(request, now); // if some partitions are already ready to be sent, the select time would be 0; // otherwise if some partition already has some data accumulated but not ready yet, // the select time will be the time difference between now and its linger expiry time; // otherwise the select time will be the time difference between now and the metadata expiry time; this.client.poll(pollTimeout, now); } 上面的代码,你如果第一次看,你肯定会觉得,这个脉络非常不清晰,不知道重点在哪里。不过还好有些注释,你能大体猜到他在干嘛。 accumulator的ready,networkclient的ready、networkclient的send、networkclient的poll 这些好像是在准备内存区域、准备网络连接的node节点、发送数据、拉取响应结果的意思。 可是如果你猜不到,该怎么办呢? 这时候就可以祭出debug这个杀器了。由于是producer,我们可以在Hellowolrd的这个客户端打断点,一步一步看下。 当你对run方法一步一步打了断点之后你会发现: accumulator的ready,networkclient的ready、networkclient的send 这些的逻辑几乎都没有执行,全部都是初始化空对象,或者方法内部直接return。 直接一路执行到了client.poll方法。如下图所示: 那么,你可以得出一个结论,while第一次循环这个run方法的核心逻辑,其实只有一句话: client.poll(pollTimeout, now) 整体脉络如下所示: 看来接下来,这个NetworkClient的poll方法,就是关键中的关键了: /** * Do actual reads and writes to sockets. * 对套接字进行实际读取和写入 * * @param timeout The maximum amount of time to wait (in ms) for responses if there are none immediately, * must be non-negative. The actual timeout will be the minimum of timeout, request timeout and * metadata timeout * @param now The current time in milliseconds * @return The list of responses received */ @Override public List<ClientResponse> poll(long timeout, long now) { long metadataTimeout = metadataUpdater.maybeUpdate(now); try { this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs)); } catch (IOException e) { log.error("Unexpected error during I/O", e); } // process completed actions long updatedNow = this.time.milliseconds(); List<ClientResponse> responses = new ArrayList<>(); handleCompletedSends(responses, updatedNow); handleCompletedReceives(responses, updatedNow); handleDisconnections(responses, updatedNow); handleConnections(); handleTimedOutRequests(responses, updatedNow); // invoke callbacks for (ClientResponse response : responses) { if (response.request().hasCallback()) { try { response.request().callback().onComplete(response); } catch (Exception e) { log.error("Uncaught error in request completion:", e); } } } return responses; } 这个方法的脉络就清晰多了,通过方法名和注释,我们几乎可以猜出他的一些作用主要有: 1)注释说:对套接字进行实际读取和写入 2)metadataUpdater.maybeUpdate(),你还记得NetworkClient的组件DefaultMetadataUpdater么,方法名意思是可能进行元数据更新。这个好像很关键的样子 3)接着执行了Selector的poll方法,这个是NetworkClient的另一个组件Selector,还记得么?它底层封装了原生的NIO Selector。这个方法应该也比较关键。 4)后续对response执行了一系列的方法,从名字上看, handleCompletedSends 处理完成发送的请求、handleCompletedReceives处理完成接受的请求、handleDisconnections处理断开连接的请求、handleConnections处理连接成功的请求、处理超时的请求handleTimedOutRequests。根据不同情况有不同的处理。 5)最后还有一个response的相关的回调处理,如果注册了回调函数,会执行下。这个应该不是很关键的逻辑 也就是简单的说就是NetworkClient执行poll方法,主要通过selector处理请求的读取和写入,对响应结果做不同的处理而已。 如下图所示: 到这里其实我们基本摸清出了run方法主要在做的一件事情了,由于是第一次循环,之前的accumulator的ready,networkclient的ready、networkclient的send 什么都没做,第一次while循环run方法核心执行的是networkclient.poll方法。而poll方法的主要逻辑就是上面图中所示的了。 maybeUpdate可能在在拉取元数据? 刚才我们分析到,poll方法首先执行的是DefaultMetadataUpdater的maybeUpdate方法,它是可能更新的意思。我们来一起看下他的逻辑吧。 public long maybeUpdate(long now) { // should we update our metadata? long timeToNextMetadataUpdate = metadata.timeToNextUpdate(now); long timeToNextReconnectAttempt = Math.max(this.lastNoNodeAvailableMs + metadata.refreshBackoff() - now, 0); long waitForMetadataFetch = this.metadataFetchInProgress ? Integer.MAX_VALUE : 0; // if there is no node available to connect, back off refreshing metadata long metadataTimeout = Math.max(Math.max(timeToNextMetadataUpdate, timeToNextReconnectAttempt), waitForMetadataFetch); if (metadataTimeout == 0) { // Beware that the behavior of this method and the computation of timeouts for poll() are // highly dependent on the behavior of leastLoadedNode. Node node = leastLoadedNode(now); maybeUpdate(now, node); } return metadataTimeout; } /** * The next time to update the cluster info is the maximum of the time the current info will expire and the time the * current info can be updated (i.e. backoff time has elapsed); If an update has been request then the expiry time * is now */ public synchronized long timeToNextUpdate(long nowMs) { long timeToExpire = needUpdate ? 0 : Math.max(this.lastSuccessfulRefreshMs + this.metadataExpireMs - nowMs, 0); long timeToAllowUpdate = this.lastRefreshMs + this.refreshBackoffMs - nowMs; return Math.max(timeToExpire, timeToAllowUpdate); } 原来这里有一个时间的判断,当判断满足才会执行maybeUpdate。 这个时间计算好像比较复杂,但是大体可以看出来,metadataTimeout是根据三个时间综合判断出来的,如果是0才会执行真正的maybeUpdate()。 像这种时候,我们可以直接在metadataTimeout这里打一个断点,看下它的值是如何计算的,比如下图: 你会发现,当第一次执行while循环,执行到poll方法,执行到这个maybeUpdate的时候,决定metadataTimeout的3个值,有两个是0,其中一个是非0,是一个299720的值。最终导致metadataTimeout也是非0,是299720。 也就是说,第一次while循环不会执行maybeUpdate的任何逻辑。 那么接着向下执行 Selector的poll()方法。 /** * Do whatever I/O can be done on each connection without blocking. This includes completing connections, completing * disconnections, initiating new sends, or making progress on in-progress sends or receives. * 在不阻塞的情况下,在每个连接上做任何可以做的 I/O。这包括完成连接完成、断开连接,启动新的发送,或在进行中的发送或接收请求 */ @Override public void poll(long timeout) throws IOException { if (timeout < 0) throw new IllegalArgumentException("timeout should be >= 0"); clear(); if (hasStagedReceives() || !immediatelyConnectedKeys.isEmpty()) timeout = 0; /* check ready keys */ long startSelect = time.nanoseconds(); //这个方法是NIO底层Selector.select(),会阻塞监听 int readyKeys = select(timeout); long endSelect = time.nanoseconds(); currentTimeNanos = endSelect; this.sensors.selectTime.record(endSelect - startSelect, time.milliseconds()); //如果监听到有操作的SelectionKeys,也就是readyKeys>0< 会执行一些操作 if (readyKeys > 0 || !immediatelyConnectedKeys.isEmpty()) { pollSelectionKeys(this.nioSelector.selectedKeys(), false); pollSelectionKeys(immediatelyConnectedKeys, true); } addToCompletedReceives(); long endIo = time.nanoseconds(); this.sensors.ioTime.record(endIo - endSelect, time.milliseconds()); maybeCloseOldestConnection(); } private int select(long ms) throws IOException { if (ms < 0L) throw new IllegalArgumentException("timeout should be >= 0"); if (ms == 0L) return this.nioSelector.selectNow(); else return this.nioSelector.select(ms); } 上面的脉络主要是2步: 1)select(timeout): NIO底层selector.select(),会阻塞监听 2)pollSelectionKeys(): 监听到有操作的SelectionKeys,做了一些操作 也就是说,最终,Sender线程的run方法,第一次while循环执行poll方法,最后什么都没干,会被selector.select()阻塞住。 如下图所示: new KafkaProducer之后 分析完了run方法的执行 ,我们分析的KafkaProducerHelloWorld第一步new KafkaProducer()基本就完成了。 大家经历了一节半的时间,终于分析清楚了KafkaProducer创建的原理。不不知道你对Kafka的Producer是不是有了更深的理解了。 分析了new KafkaProducer()之后呢? 我们继续接着KafkaProducerHelloWorld往下分析,你还记得KafkaProducerHelloWorld的代码么? public class KafkaProducerHelloWorld { public static void main(String[] args) throws Exception { //配置Kafka的一些参数 Properties props = new Properties(); props.put("bootstrap.servers", "mengfanmao.org:9092"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 创建一个Producer实例 KafkaProducer<String, String> producer = new KafkaProducer<>(props); // 封装一条消息 ProducerRecord<String, String> record = new ProducerRecord<>( "test-topic", "test-key", "test-value"); // 同步方式发送消息,会阻塞在这里,直到发送完成 // producer.send(record).get(); // 异步方式发送消息,不阻塞,设置一个监听回调函数即可 producer.send(record, new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception exception) { if(exception == null) { System.out.println("消息发送成功"); } else { exception.printStackTrace(); } } }); Thread.sleep(5 * 1000); // 退出producer producer.close(); } KafkaProducerHelloWorld主要就3步: 1)new KafkaProducer 这个我们已经分析完了,主要分析了配置文件的解析、各个组件是什么、有什么,还有就是刚才分析的run线程第一次循环到底执行了什么。 2) new ProducerRecord 创建待发送的消息 3) producer.send() 发送消息 首先创建待发送的消息: ProducerRecord<String, String> record = new ProducerRecord<>("test-topic", "test-key", "test-value"); public ProducerRecord(String topic, K key, V value) { this(topic, null, null, key, value); } /** * Creates a record with a specified timestamp to be sent to a specified topic and partition * 创建具有指定时间戳的记录以发送到指定主题和分区 * @param topic The topic the record will be appended to * @param partition The partition to which the record should be sent * @param timestamp The timestamp of the record * @param key The key that will be included in the record * @param value The record contents */ public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) { if (topic == null) throw new IllegalArgumentException("Topic cannot be null"); if (timestamp != null && timestamp < 0) throw new IllegalArgumentException("Invalid timestamp " + timestamp); this.topic = topic; this.partition = partition; this.key = key; this.value = value; this.timestamp = timestamp; } 我们之前提过,Record表示了一条消息的抽象封装。这个ProducerRecord其实就表示了一条消息。 从构造函数的注释可以看出来**,ProducerRecord可以指定往哪个topic,哪一个分区partition,并且消息可以设置一个时间戳。分区和时间戳默认可以不指定** 其实看这块源码,我们主要得到的信息就是这些了,这些都比较简单。就不画图了。 发送消息时的元数据拉取触发 当Producer和Record都创建好了之后,可以用同步或者异步的方式发送消息。 // 同步方式发送消息,会阻塞在这里,直到发送完成 // producer.send(record).get(); // 异步方式发送消息,不阻塞,设置一个监听回调函数即可 producer.send(record, new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception exception) { if(exception == null) { System.out.println("消息发送成功"); } else { exception.printStackTrace(); } } }); //同步发送 @Override public Future<RecordMetadata> send(ProducerRecord<K, V> record) { return send(record, null); } //异步发送 public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) { // intercept the record, which can be potentially modified; this method does not throw exceptions ProducerRecord<K, V> interceptedRecord = this.interceptors == null ? record : this.interceptors.onSend(record); return doSend(interceptedRecord, callback); } 同步和异步的整个发送逻辑如下图所示: 从上图你会发现,但是无论同步发送还是异步底层都会调用同一个方法doSend()。区别就是有没有callBack回调函数而已,他们还都在调用前注册一些拦截器,这里我们抓大放小下,我们重点还是关注doSend方法。 doSend方法如下: /** * Implementation of asynchronously send a record to a topic. Equivalent to <code>send(record, null)</code>. * See {@link #send(ProducerRecord, Callback)} for details. */ private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) { TopicPartition tp = null; try { // first make sure the metadata for the topic is available long waitedOnMetadataMs = waitOnMetadata(record.topic(), this.maxBlockTimeMs); long remainingWaitMs = Math.max(0, this.maxBlockTimeMs - waitedOnMetadataMs); byte[] serializedKey; try { serializedKey = keySerializer.serialize(record.topic(), record.key()); } catch (ClassCastException cce) { throw new SerializationException("Can't convert key of class " + record.key().getClass().getName() + " to class " + producerConfig.getClass(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG).getName() + " specified in key.serializer"); } byte[] serializedValue; try { serializedValue = valueSerializer.serialize(record.topic(), record.value()); } catch (ClassCastException cce) { throw new SerializationException("Can't convert value of class " + record.value().getClass().getName() + " to class " + producerConfig.getClass(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG).getName() + " specified in value.serializer"); } int partition = partition(record, serializedKey, serializedValue, metadata.fetch()); int serializedSize = Records.LOG_OVERHEAD + Record.recordSize(serializedKey, serializedValue); ensureValidRecordSize(serializedSize); tp = new TopicPartition(record.topic(), partition); long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp(); log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition); // producer callback will make sure to call both 'callback' and interceptor callback Callback interceptCallback = this.interceptors == null ? callback : new InterceptorCallback<>(callback, this.interceptors, tp); RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs); if (result.batchIsFull || result.newBatchCreated) { log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition); this.sender.wakeup(); } return result.future; // handling exceptions and record the errors; // for API exceptions return them in the future, // for other exceptions throw directly } catch (ApiException e) { log.debug("Exception occurred during message send:", e); if (callback != null) callback.onCompletion(null, e); this.errors.record(); if (this.interceptors != null) this.interceptors.onSendError(record, tp, e); return new FutureFailure(e); } catch (InterruptedException e) { this.errors.record(); if (this.interceptors != null) this.interceptors.onSendError(record, tp, e); throw new InterruptException(e); } catch (BufferExhaustedException e) { this.errors.record(); this.metrics.sensor("buffer-exhausted-records").record(); if (this.interceptors != null) this.interceptors.onSendError(record, tp, e); throw e; } catch (KafkaException e) { this.errors.record(); if (this.interceptors != null) this.interceptors.onSendError(record, tp, e); throw e; } catch (Exception e) { // we notify interceptor about all exceptions, since onSend is called before anything else in this method if (this.interceptors != null) this.interceptors.onSendError(record, tp, e); throw e; } } 这个方法的脉络虽然比较长,但是脉络还是比较清晰,主要先执行了: 1)waitOnMetadata 应该是等待元数据拉取 2)keySerializer.serialize和valueSerializer.serialize,很明显就是将Record序列化成byte字节数组 3)通过partition进行路由分区,按照一定路由策略选择Topic下的某个分区 4)accumulator.append将消息放入缓冲器中 5)唤醒Sender线程的selector.select()的阻塞,开始处理内存缓冲器中的数据。 用图来表示如下所示: 这两节我们重点分析元数据拉取的这个场景的源码原理。 所以这里我们着重先看下步骤1 ,之后的4步我们之后会分析到的。 waitOnMetadata 如何等待元数据拉取的? 既然send的第一步是执行waitOnMetadata方法,首先看下它的代码: /** * Wait for cluster metadata including partitions for the given topic to be available. * @param topic The topic we want metadata for * @param maxWaitMs The maximum time in ms for waiting on the metadata * @return The amount of time we waited in ms */ private long waitOnMetadata(String topic, long maxWaitMs) throws InterruptedException { // add topic to metadata topic list if it is not there already. if (!this.metadata.containsTopic(topic)) this.metadata.add(topic); if (metadata.fetch().partitionsForTopic(topic) != null) return 0; long begin = time.milliseconds(); long remainingWaitMs = maxWaitMs; while (metadata.fetch().partitionsForTopic(topic) == null) { log.trace("Requesting metadata update for topic {}.", topic); int version = metadata.requestUpdate(); sender.wakeup(); metadata.awaitUpdate(version, remainingWaitMs); long elapsed = time.milliseconds() - begin; if (elapsed >= maxWaitMs) throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms."); if (metadata.fetch().unauthorizedTopics().contains(topic)) throw new TopicAuthorizationException(topic); remainingWaitMs = maxWaitMs - elapsed; } return time.milliseconds() - begin; } /** * Get the current cluster info without blocking */ public synchronized Cluster fetch() { return this.cluster; } public synchronized int requestUpdate() { this.needUpdate = true; return this.version; } /** * Wait for metadata update until the current version is larger than the last version we know of */ public synchronized void awaitUpdate(final int lastVersion, final long maxWaitMs) throws InterruptedException { if (maxWaitMs < 0) { throw new IllegalArgumentException("Max time to wait for metadata updates should not be < 0 milli seconds"); } long begin = System.currentTimeMillis(); long remainingWaitMs = maxWaitMs; while (this.version <= lastVersion) { if (remainingWaitMs != 0) wait(remainingWaitMs); long elapsed = System.currentTimeMillis() - begin; if (elapsed >= maxWaitMs) throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms."); remainingWaitMs = maxWaitMs - elapsed; } } 这个方法核心就是判断了是否有Cluster元数据信息,如果没有,进行了如下操作: 1)metadata.requestUpdate(); 更新了一个needUpdate标记,这个值会影响之前maybeUpdate的metadataTimeout的计算,可以让metadataTimeout为0 2)sender.wakeup();唤醒之前nioSelector.select()的阻塞,继续执行 3)metadata.awaitUpdate(version, remainingWaitMs); 主要进行了版本比较,如果不是最新版本,调用了Metadata.wait()方法(wait方法是每个Object都会有的方法,一般和notify或者notifyAll组合使用) 整个过程我直接用图给大家表示一下,如下所示: 整个图就是今天我们分析的关键结果了,**这里通过两种阻塞和唤醒机制,一个是NIO中Selector的select()和wakeUp(),一个是MetaData对象的wait()和notifyAll()机制。**所以这里要结合之前Sender线程的阻塞逻辑一起来理解。 是不是很有意思一种使用,这里没有用任何线程的join、sleep、wait、park、unpark、notify这些方法。 小结 最后我们简单小结下,这里一节我们主要分析了如下Producer的源码原理: 初始化KafkaProducer时并没有去拉取元数据,但是创建了Selector组件,启动了Sender线程,select阻塞等待请求响应。由于还没有发送任何请求,所以初始化时并没有去真正拉取元数据。 真正拉取元数据是在第一次send方法调用时,会唤醒唤醒Selector之前阻塞的select(),进入第二次while循环,从而发送拉取元数据请求,并且通过Obejct.wait的机制等待60s,等到从Broker拉取元数据成功后,才会继续执行真正的生产消息的请求,否则会报拉取元数据超时异常。 这一节我们只是看到了进行了wait如何等待元数据拉取。 而唤醒Selector的select之后应该会进入第二次while循环 那第二次while循环如何发送请求拉取元数据请求,并且在成功后notifyAll()进行唤醒操作的呢? 让我们下一节继续分析,大家敬请期待! 我们下一节见! 本文由博客一文多发平台 OpenWrite 发布!

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册