<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title><![CDATA[Heck's  Blog]]></title> 
<link>https://www.heckjj.com/index.php</link> 
<description><![CDATA[一瞬间的决定，往往可以改变很多，事实上，让自己成功的往往不是知识，是精神！ 如果你总是为自己找借口，那只好让成功推迟。执行力，今天！]]></description> 
<language>zh-cn</language> 
<copyright><![CDATA[Heck's  Blog]]></copyright>
<item>
<link>https://www.heckjj.com/post/687/</link>
<title><![CDATA[解决flux获取元数据时流式输出被阻塞的问题]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Fri, 05 Jun 2026 08:31:25 +0000</pubDate> 
<guid>https://www.heckjj.com/post/687/</guid> 
<description>
<![CDATA[ 
	&nbsp;&nbsp; Flux&lt;Object&gt; result = aOneAgentOpenApiService.chatRun(aOneAgentRunParam)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.replay()&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 允许多个订阅者同时消费<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.autoConnect(2);&nbsp;&nbsp;&nbsp;&nbsp;// 至少 2 个订阅者时开始（前台SSE + 后台元数据提取）<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// 后台异步处理：提取元数据 + 保存历史记录<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String userId = chatParam.getUserId();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String query = aOneAgentRunParam.getQuery();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CompletableFuture.runAsync(() -&gt; &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// 提取 AGENT_RUN_STARTED 事件<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;AOneAgentRunResponseVO resultVO = AOneSkillUtil.getAnswerFluxByEventType(<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result, List.of(AOneAgentMessageEventTypeEnum.AGENT_RUN_STARTED));<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ChatHistoryParam chatHistoryParam = ChatHistoryParam.builder()<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.chatId(resultVO.getMessageGroupId())<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.role(MessageRole.ASSISTANT.getValue())<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.messageId(resultVO.getMessageId())<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.sectionId(resultVO.getMessageGroupId())<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.conversationId(resultVO.getConversationId())<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.userId(userId)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.startTime(DateUtil.date(createdAt))<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.platform(this.getPlatformType().getCode())<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.input(query)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.latencyFirstResp(DateUtil.current() - createdAt)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.build();<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// 提取 AGENT_ROUND_COMPLETED 元数据<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;resultVO = AOneSkillUtil.getAnswerFluxByEventType(<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result, List.of(AOneAgentMessageEventTypeEnum.AGENT_ROUND_COMPLETED));<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;JSONObject metadata = resultVO.getMetadata();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (ObjUtil.isNotNull(metadata)) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;chatHistoryParam.setInputTokens(Convert.toLong(metadata.getLong(INPUT_TOKEN)));<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;chatHistoryParam.setOutputTokens(Convert.toLong(metadata.getLong(OUTPUT_TOKEN)));<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;chatHistoryParam.setTotalTokens(Convert.toLong(metadata.getLong(TOTAL_TOKEN)));<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// 提取完整回答<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;resultVO = AOneSkillUtil.getAnswerCompleteFlux(result);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (ObjUtil.isNotNull(resultVO)) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String answer = resultVO.getAnswer();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (StrUtil.isBlank(answer) &amp;&amp; StrUtil.isNotBlank(resultVO.getErrorCode())) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;answer = resultVO.getErrorMsg();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.error(&quot;AOne领域智能体平台错误：&#123;&#125;,&#123;&#125;&quot;, resultVO.getErrorCode(), answer);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;chatHistoryParam.setOutput(answer);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;chatHistoryParam.setRequestIp(chatParam.getRequestIp());<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;chatHistoryParam.setIsFallback(ZERO);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;chatHistoryParam.setLatency(DateUtil.current() - createdAt);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;chatHistoryEventPublisher.publishEvent(new ChatHistoryEvent(chatHistoryParam));<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125; catch (Exception e) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.error(&quot;保存对话历史记录失败&quot;, e);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;);<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// 立即返回流给前端，不等待元数据提取完成<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return result;
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/682/</link>
<title><![CDATA[从 JDK 8 到 JDK 21 的“握手”血泪史]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Thu, 09 Apr 2026 09:02:37 +0000</pubDate> 
<guid>https://www.heckjj.com/post/682/</guid> 
<description>
<![CDATA[ 
	随着 Java 生态的演进，我们将核心项目从 JDK 8 迁移至 JDK 21。然而，在升级过程中，我们遇到了一个令人费解的现象：原本在 JDK 8 上运行正常的第三方接口调用，在 JDK 21 上却频繁抛出 SSLHandshakeException: Received fatal alert: handshake_failure。本文将详细复盘这次排查过程，揭示 JDK 21 在 SSL/TLS 握手层面的底层变化，并提供终极解决方案。<br/>一、 故障现象：版本升级后的“断连”<br/>在将开发环境切换至 JDK 21 后，我们的应用在调用特定政府网站接口（https://www.xxx.gov.cn）时发生了故障。<br/>错误日志：<br/>javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure<br/>诡异现象：<br/>JDK 8： 运行完美，无任何报错。<br/>JDK 21： 死活连不上，且不报证书错误（CertificateException），直接握手失败。<br/>浏览器/Postman： 可以正常访问。<br/>这种“代码没变，环境变了”的差异，直接指向了 JDK 版本间的底层安全策略差异。<br/>二、 排查之路：层层递进的“破案”过程<br/>为了定位问题，我们开启 -Djavax.net.debug=ssl:handshake 调试模式，通过分析握手日志，我们经历了三个阶段的认知升级。<br/>第一阶段：误判为“弱加密算法” —— 方案失效<br/>直觉： JDK 21 更安全，默认禁用了弱算法。我们猜测是服务器使用了 MD5 或 1024 位密钥。<br/>行动： 修改 java.security 文件，删除 jdk.tls.disabledAlgorithms 中的限制，甚至加上了 ECDH（误以为是禁用了 ECDH 导致的）。<br/>结果： 无效。 即使放宽了所有限制，握手依然失败。这说明问题不在“禁用列表”，而在“协商过程”。<br/>第二阶段：聚焦“椭圆曲线” —— 关键线索<br/>在仔细比对 ClientHello（客户端发起）和 ServerHello（服务器回复）的日志时，我们发现了异常。<br/>在服务器的 ECDHServerKeyExchange 消息中，出现了如下关键字段：<br/><br/>&quot;ECDH ServerKeyExchange&quot;: &#123;<br/>&nbsp;&nbsp;&quot;parameters&quot;: &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&quot;named group&quot;: &quot;x448&quot;&nbsp;&nbsp;// 服务器强制要求使用 x448 曲线<br/>&nbsp;&nbsp;&#125;<br/>&#125;<br/>分析：<br/>JDK 21 的 ClientHello： 默认优先发送 x25519 和 secp256r1（P-256）。<br/>服务器的回应： 该政府服务器配置较为特殊（可能为了高性能或特定合规），它无视了客户端的偏好，强制选择了 x448 (Curve448) 算法进行密钥交换。<br/>矛盾点： 虽然 JDK 21 理论上支持 x448，但在某些构建版本或特定的握手上下文中，客户端无法处理服务器强制下发的 x448 参数，导致计算共享密钥失败。<br/>三、 根本原因 (Root Cause)<br/>这次故障的根本原因并非简单的“证书错误”或“协议不支持”，而是“算法偏好不匹配”：<br/>服务器端的“死板”： 目标服务器（szpsq.gov.cn）在握手时，强制指定了 named group 为 x448。<br/>客户端的“不适”： JDK 21 客户端在收到此指令后，由于环境配置或算法库实现的细微差异，无法完成基于 x448 的密钥计算。<br/>JDK 8 为什么能行？ Java 8 根本不认识 x448（Java 11+ 才引入），服务器检测到 JDK 8 的 ClientHello 后，会自动降级使用传统的 secp256r1，从而避开了这个坑。<br/>四、 解决方案：强制“降级”协商<br/>既然问题出在服务器强制使用 x448，那么解决方案就是不让服务器有机会选择 x448。我们通过 JVM 参数，强制客户端只支持通用的 secp256r1。<br/>✅ 最终生效方案<br/>在 IDEA 运行配置或 Tomcat 启动脚本的 VM Options 中，添加以下参数：<br/>-Djdk.tls.namedGroups=&quot;secp256r1&quot;<br/>方案解析<br/>原理： 该参数强制 JVM 在发送 ClientHello 时，只在 supported_groups 扩展中声明支持 secp256r1。<br/>效果： 服务器收到请求后，发现自己想用的 x448 客户端不支持，无奈之下只能选择次优但通用的 secp256r1 进行握手。<br/>结果： 握手成功，数据正常获取。<br/>五、 经验总结与建议<br/>不要盲目放宽限制： 遇到 handshake_failure，不要第一时间去修改 java.security 里的 disabledAlgorithms，这往往是性能或兼容性问题，而非安全性拦截。<br/>善用 Debug 日志： -Djavax.net.debug=ssl:handshake 是排查 SSL 问题的瑞士军刀。重点关注 ClientHello 发了什么，ServerHello 回了什么，以及 named group 的协商过程。<br/>老旧服务器的兼容性： 国内部分政府或银行网站（.gov.cn）服务器配置更新滞后或使用了特殊算法（如 x448 或国密），在迁移到 JDK 11/17/21 时，大概率需要通过 -Djdk.tls.namedGroups 或代码层定制 SSLParameters 来强制兼容。<br/><br/>注： 技术升级往往伴随着“暗坑”，这次排查虽然曲折，但也让我们更深入理解了 TLS 握手的底层细节。希望这篇复盘能帮后来者节省几个小时的排查时间。
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/675/</link>
<title><![CDATA[当你还在一个词一个词地调教 Prompt 时，硅谷的大佬们已经在布局上下文工程（Context Engineering）啦]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Thu, 17 Jul 2025 01:11:54 +0000</pubDate> 
<guid>https://www.heckjj.com/post/675/</guid> 
<description>
<![CDATA[ 
	当你还在一个词一个词地调教 Prompt 时，硅谷的大佬们已经在布局上下文工程（Context Engineering）了。<br/><br/>推特上的 AI 架构师 @HeyNina101 一针见血地指出：「提示词工程是给爱好者玩的，上下文工程（Context Engineering）才是专业生产力。」这条推文迅速引爆了硅谷技术圈，Shopify CEO Tobi Lutke 和前特斯拉 AI 总监 Andrej Karpathy 等大佬纷纷点赞。<br/><br/>Prompt vs 上下文<br/><br/>你在对话框里敲下的指令，就是 Prompt。<br/><br/>但一个能打的 AI 应用，需要的远不止 Prompt。还需要为模型构建整个世界观，这就是「上下文工程」。<br/><br/>上下文工程包括：<br/><br/>任务设定：明确告诉 AI 要干嘛<br/>案例示范：给它几个栗子学学<br/>外部文档：挂载知识库，让它变身专家<br/>对话历史：让 AI 拥有短期记忆<br/>工具输出：调用 API 后的结果反馈<br/>系统状态：应用当前的运行情况<br/>长期记忆：跨越对话周期的记忆<br/>信息压缩：在有限的上下文中高效传递信息<br/>多模态：不止文本，还有图片、声音等等<br/><br/>上下文给的太少，AI 就是人工智障；给的太多，系统就运行变慢、或者烧光你的预算。如何拿捏好这个平衡，不仅仅是简单的指令艺术，更是一个系统设计的挑战。<br/><br/>真正的LLM应用，远不止套壳<br/><br/>一个生产级的 LLM 应用，不仅仅是上面这些，还需要：<br/><br/>控制流：设计 AI 的思考和行动路径<br/>上下文打包：智能筛选、组合信息喂给模型<br/>模型路由：根据任务，自动切换最合适的模型<br/>UI 与生成验证：确保输出的内容能看、能用<br/>安全护栏、评估、预取、并行计算...<br/><br/>这个复杂的系统，我们称之为 LLM 软件工程，而「上下文」正是这个新世界的核心层。<br/><br/>到底是谁在控制 LLM？<br/><br/>Google DeepMind 的工程师 Philipp Schmid 用一张图给出了答案：真正决定 LLM 行为的，是一个由指令、用户输入、历史、文档、工具等共同构成的庞大上下文系统。<br/><br/>所以，别再只盯着那个小小的输入框了。<br/><br/>你认为上下文工程会是下一个风口吗？<br/>独立开发者如何抓住这个机会？<br/>评论区聊聊你的看法<br/><a href="https://www.heckjj.com/attachment.php?fid=264" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=264" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0" width="400" height="400"/></a><a href="https://www.heckjj.com/attachment.php?fid=265" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=265" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0" width="400" height="400"/></a><a href="https://www.heckjj.com/attachment.php?fid=266" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=266" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0" width="400" height="400"/></a><a href="https://www.heckjj.com/attachment.php?fid=267" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=267" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0" width="400" height="400"/></a><br/><br/>
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/674/</link>
<title><![CDATA[一文搞懂Fine-tuning大模型微调]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Thu, 17 Jul 2025 01:11:49 +0000</pubDate> 
<guid>https://www.heckjj.com/post/674/</guid> 
<description>
<![CDATA[ 
	本篇文章深入分析了大型模型微调的基本理念和多样化技术，细致介绍了LoRA、适配器调整(Adapter Tuning)、前缀调整(Prefix Tuning)等多个微调方法。详细讨论了每一种策略的基本原则、主要优点以及适宜应用场景，使得读者可以依据特定的应用要求和计算资源限制，挑选最适合的微调方案。<br/><br/>一、大型模型微调的基础理论<br/>大型语言模型(LLM)的训练过程通常分为两大阶段：<br/><br/>阶段一：预训练阶段<br/>在这个阶段，大型模型会在大规模的无标签数据集上接受训练，目标是使模型掌握语言的统计特征和基础知识。此期间，模型将掌握词汇的含义、句子的构造规则以及文本的基本信息和上下文。<br/>需特别指出，预训练实质上是一种无监督学习过程。完成预训练的模型，亦即基座模型(Base Model)，拥有了普遍适用的预测能力。例如，GLM-130B模型、OpenAI的四个主要模型均属于基座模型。<br/><br/>阶段二：微调阶段<br/>预训练完成的模型接下来会在针对性的任务数据集上接受更进一步的训练。这一阶段主要涉及对模型权重的细微调整，使其更好地适配具体任务。最终形成的模型将具备不同的能力，如gpt code系列、gpt text系列、ChatGLM-6B等。<br/><br/>那么，何为大型模型微调？<br/>直观上，大型模型微调即是向模型“输入”更多信息，对模型的特定功能进行“优化”，通过输入特定领域的数据集，使模型学习该领域知识，从而优化大模型在特定领域的NLP任务中的表现，如情感分析、实体识别、文本分类、对话生成等。<br/><br/><br/>为何微调至关重要？<br/>其核心理由是，微调能够“装备”大模型以更精细化的功能，例如整合本地知识库进行搜索、针对特定领域问题构建问答系统等。<br/>以VisualGLM为例，作为一个通用多模态模型，当应用于医学影像判别时，就需要输入医学影像领域的数据集以进行微调，以此提升模型在医学影像图像识别方面的表现。<br/>这与机器学习模型的超参数优化类似，只有在调整超参数后，模型才能更好地适应当前数据集；同时，大型模型可以经历多轮微调，每次微调都是对模型能力的优化，即我们可以在现有的、已经具备一定能力的大模型基础上进一步进行微调。<br/><br/>二、大型模型的经典网络结构<br/>以GPT系列中的Transformer为例，这种深度学习模型结构通过自注意力机制等技巧解决了相关问题。正是得益于Transformer架构，基于GPT的大型语言模型取得了显著的进展。<br/>Transformer模型架构包含了众多模块，而我们讨论的各种微调技术通常是对这些模块中的特定部分进行优化，以实现微调目的。<br/><br/>要深入理解各类微调手段，首先需要对网络架构有一个基本的认识。以下以Transformer为例，阐述各个模块的作用：<br/><br/>输入嵌入层（Input Embedding）<br/>输入（Inputs）：模型的输入环节，通常为单词或符号序列。<br/>输入嵌入（Input Embedding）：此步骤将输入序列（例如句中的每个单词）转化为嵌入表示，即能够表征单词语义信息的高维向量。<br/>位置编码（Positional Encoding）：鉴于Transformer不依赖序列，位置编码旨在提供序列中单词位置的信息，这些编码添加到输入嵌入中，确保模型即便同时处理输入也能够利用单词的顺序信息。<br/>编码器层（Encoder，左边）<br/>Nx：指示有N个相同的编码器层叠加而成。每个编码器层包括两个主要子层：多头自注意力机制和前馈神经网络。<br/>多头自注意力（Multi-Head Attention）：注意力机制允许模型在处理每个单词时考虑到输入序列中的所有单词。多头部分表示模型并行学习输入数据的不同表示。<br/>残差连接和归一化（Add &amp; Norm）：注意力层后面跟着残差连接和层归一化，有助于防止深层网络中的梯度消失问题，并稳定训练过程。<br/>前馈神经网络（Feed Forward）：全连接神经网络处理自注意力层的输出，包含两个线性变换和一个非线性激活函数。<br/>解码器层（Decoder，右侧）<br/>解码器亦包含多个相同的层，每层包括三个主要子层：掩蔽的多头自注意力机制、多头自注意力机制和前馈神经网络。<br/>掩蔽多头自注意力（Masked Multi-Head Attention）：与编码器的多头自注意力机制类似，但为确保解码顺序性，掩蔽操作确保预测仅依赖于之前的输出。<br/>前馈神经网络（Feed Forward）：与编码器相同，每个子层之后也有加法和归一化步骤。<br/>输出嵌入层和输出过程<br/>解码器端的嵌入层将目标序列转换为向量形式。<br/>线性层（Linear）和Softmax层：解码器的输出通过线性层映射到一个更大的词汇空间，Softmax函数将输出转换为概率分布。<br/><br/>三、大型模型微调的技术手段<br/><br/>大型模型的全面微调（Fine-tuning）涉及调整所有层和参数，以适配特定任务。此过程通常采用较小的学习率和特定任务的数据，可以充分利用预训练模型的通用特征，但可能需要更多计算资源。<br/><br/>参数高效微调（Parameter-Efficient Fine-Tuning，PEFT）旨在通过最小化微调参数数量和计算复杂度，提升预训练模型在新任务上的表现，从而减轻大型预训练模型的训练负担。<br/><br/>即使在计算资源受限的情况下，PEFT技术也能够利用预训练模型的知识快速适应新任务，实现有效的迁移学习。因此，PEFT不仅能提升模型效果，还能显著缩短训练时间和计算成本，使更多研究者能够参与到深度学习的研究中。<br/><br/>PEFT包括LoRA、QLoRA、适配器调整(Adapter Tuning)、前缀调整(Prefix Tuning)、提示调整(Prompt Tuning)、P-Tuning及P-Tuning v2等多种方法。<br/><br/>以下图表示了7种主流微调方法在Transformer网络架构中的作用位置及其简要说明，接下来将详细介绍每一种方法。<br/><br/>1、LoRA<br/>LoRA（Low-Rank Adaptation）是一种旨在微调大型预训练语言模型（如GPT-3或BERT）的技术。其核心理念在于，在模型的决定性层次中引入小型、低秩的矩阵来实现模型行为的微调，而无需对整个模型结构进行大幅度修改。<br/>这种方法的优势在于，在不显著增加额外计算负担的前提下，能够有效地微调模型，同时保留模型原有的性能水准。<br/>LoRA的操作流程如下：<br/><br/>确定微调目标权重矩阵：首先在大型模型（例如GPT）中识别出需要微调的权重矩阵，这些矩阵一般位于模型的多头自注意力和前馈神经网络部分。<br/>引入两个低秩矩阵：然后，引入两个维度较小的低秩矩阵A和B。假设原始权重矩阵的尺寸为dd，则A和B的尺寸可能为dr和r*d，其中r远小于d。<br/>计算低秩更新：通过这两个低秩矩阵的乘积AB来生成一个新矩阵，其秩（即r）远小于原始权重矩阵的秩。这个乘积实际上是对原始权重矩阵的一种低秩近似调整。<br/>结合原始权重：最终，新生成的低秩矩阵AB被叠加到原始权重矩阵上。因此，原始权重经过了微调，但大部分权重维持不变。这个过程可以用数学表达式描述为：新权重 = 原始权重 + AB。<br/>以一个具体实例来说，假设我们手头有一个大型语言模型，它通常用于执行广泛的自然语言处理任务。现在，我们打算将其微调，使其在处理医疗健康相关的文本上更为擅长。<br/>采用LoRA方法，我们无需直接修改模型现有的大量权重。相反，只需在模型的关键部位引入低秩矩阵，并通过这些矩阵的乘积来进行有效的权重调整。这样一来，模型就能更好地适应医疗健康领域的专业语言和术语，同时也避免了大规模权重调整和重新训练的必要。<br/><br/>2、QLoRA<br/>QLoRA（Quantized Low-Rank Adaptation）是一种结合了LoRA（Low-Rank Adaptation）方法与深度量化技术的高效模型微调手段。QLoRA的核心在于：<br/><br/>量化技术：QLoRA采用创新的技术将预训练模型量化为4位。这一技术包括低精度存储数据类型（4-bit NormalFloat，简称NF4）和计算数据类型（16-bit BrainFloat）。这种做法极大地减少了模型存储需求，同时保持了模型精度的最小损失。<br/>量化操作：在4位量化中，每个权重由4个比特表示，量化过程中需选择最重要的值并将它们映射到16个可能的值之一。首先确定量化范围（例如-1到1），然后将这个范围分成16个区间，每个区间对应一个4-bit值。然后，原始的32位浮点数值将映射到最近的量化区间值上。<br/>微调阶段：在训练期间，QLoRA先以4-bit格式加载模型，训练时将数值反量化到bf16进行训练，这样大幅减少了训练所需的显存。例如，33B的LLaMA模型可以在24 GB的显卡上进行训练。<br/>量化过程的挑战在于设计合适的映射和量化策略，以最小化精度损失对性能的影响。在大型模型中，这种方法可以显著减少内存和计算需求，使得在资源有限的环境下部署和训练成为可能。<br/><br/>3、适配器调整（Adapter Tuning）<br/>与LoRA技术类似，适配器调整的目标是在保留预训练模型原始参数不变的前提下，使模型能够适应新的任务。适配器调整的方法是在模型的每个层或选定层之间插入小型神经网络模块，称为“适配器”。这些适配器是可训练的，而原始模型的参数则保持不变。<br/><br/>适配器调整的关键步骤包括：<br/><br/>以预训练模型为基础：初始阶段，我们拥有一个已经经过预训练的大型模型，如BERT或GPT，该模型已经学习了丰富的语言特征和模式。<br/>插入适配器：在预训练模型的每个层或指定层中，我们插入适配器。适配器是小型的神经网络，一般包含少量层次，并且参数规模相对较小。<br/>维持预训练参数不变：在微调过程中，原有的预训练模型参数保持不变。我们不直接调整这些参数，而是专注于适配器的参数训练。<br/>训练适配器：适配器的参数会根据特定任务的数据进行训练，使适配器能够学习如何根据任务调整模型的行为。<br/>针对任务的调整：通过这种方式，模型能够对每个特定任务进行微调，同时不影响模型其他部分的通用性能。适配器有助于模型更好地理解和处理与特定任务相关的特殊模式和数据。<br/>高效与灵活：由于只有部分参数被调整，适配器调整方法相比于全模型微调更为高效，并且允许模型迅速适应新任务。<br/>例如，如果我们有一个大型文本生成模型，它通常用于执行广泛的文本生成任务。若要将其微调以生成专业的金融报告，我们可以在模型的关键层中加入适配器。在微调过程中，仅有适配器的参数会根据金融领域的数据进行更新，使得模型更好地适应金融报告的写作风格和术语，同时避免对整个模型架构进行大幅度调整。<br/><br/>LoRA与适配器调整的主要区别在于：<br/><br/>LoRA：在模型的权重矩阵中引入低秩矩阵来实现微调。这些低秩矩阵作为原有权重矩阵的修改项，在实际计算时对原有权重矩阵进行调整。<br/>适配器调整：通过在模型各层中添加小型神经网络模块，即“适配器”，来实现微调。适配器独立于模型的主体结构，仅适配器的参数在微调过程中更新，而模型的其他预训练参数保持不变。<br/>4、前缀调整（Prefix Tuning）<br/>与传统的微调范式不同，前缀调整提出了一种新的策略，即在预训练的语言模型（LM）输入序列前添加可训练、任务特定的前缀，从而实现针对不同任务的微调。这意味着我们可以为不同任务保存不同的前缀，而不是为每个任务保存一整套微调后的模型权重，从而节省了大量的存储空间和微调成本。<br/><br/>前缀实际上是一种连续可微的虚拟标记（Soft Prompt/Continuous Prompt），与离散的Token相比，它们更易于优化并且效果更佳。这种方法的优势在于不需要调整模型的所有权重，而是通过在输入中添加前缀来调整模型的行为，从而节省大量的计算资源，同时使得单一模型能够适应多种不同的任务。前缀可以是固定的（即手动设计的静态提示）或可训练的（即模型在训练过程中学习的动态提示）。<br/><br/>5、提示调整（Prompt Tuning）<br/>提示调整是一种在预训练语言模型输入中引入可学习嵌入向量作为提示的微调方法。这些可训练的提示向量在训练过程中更新，以指导模型输出更适合特定任务的响应。<br/><br/>提示调整与前缀调整都涉及在输入数据中添加可学习的向量，这些向量是在输入层添加的，但两者的策略和目的不同：<br/><br/>提示调整：旨在模仿自然语言中的提示形式，将可学习向量（通常称为提示标记）设计为模型针对特定任务生成特定类型输出的引导。这些向量通常被视为任务指导信息的一部分，倾向于使用较少的向量来模仿传统的自然语言提示。<br/>前缀调整：可学习前缀更多地用于提供输入数据的直接上下文信息，作为模型内部表示的一部分，可以影响整个模型的行为。<br/>以下是两者的训练示例，以说明它们的不同：<br/><br/>提示调整示例：<br/>输入序列: [Prompt1][Prompt2] “这部电影令人振奋。”<br/>问题: 评价这部电影的情感倾向。<br/>答案: 模型需要预测情感倾向（例如“积极”）<br/>提示: 没有明确的外部提示，[Prompt1][Prompt2]作为引导模型的内部提示，这里的问题是隐含的，即判断文本中表达的情感倾向。<br/>前缀调整示例：<br/>输入序列: [Prefix1][Prefix2][Prefix3] “I want to watch a movie.”<br/>问题: 根据前缀生成后续的自然语言文本。<br/>答案: 模型生成的文本，如“that is exciting and fun.”<br/>提示: 前缀本身提供上下文信息，没有单独的外部提示。<br/>6、P-Tuning<br/>P-Tuning（基于提示的微调）和提示调整都是为了调整大型预训练语言模型（如GPT系列）以适应特定任务而设计的技术。两者都利用预训练的语言模型执行特定的下游任务，如文本分类、情感分析等，并使用某种形式的“提示”或“指导”来引导模型输出，以更好地适应特定任务。<br/><br/>提示调整与P-Tuning的主要区别在于：<br/><br/>提示调整：使用静态的、可训练的虚拟标记嵌入，在初始化后保持固定，除非在训练过程中更新。这种方法相对简单，因为它只涉及调整一组固定的嵌入参数，在处理多种任务时表现良好，但可能在处理特别复杂或需要细粒度控制的任务时受限。<br/>P-Tuning：使用一个可训练的LSTM模型（称为提示编码器prompt_encoder）来动态生成虚拟标记嵌入，允许根据输入数据的不同生成不同的嵌入，提供更高的灵活性和适应性，适合需要精细控制和理解复杂上下文的任务。这种方法相对复杂，因为它涉及一个额外的LSTM模型来生成虚拟标记嵌入。<br/>P-Tuning中使用LSTM（长短期记忆网络）作为生成虚拟标记嵌入的工具，利用了LSTM的以下优势：<br/><br/>更好的适应性和灵活性：LSTM可以捕捉输入数据中的时间序列特征，更好地理解和适应复杂的、顺序依赖的任务，如文本生成或序列标注。<br/>改进的上下文理解：LSTM因其循环结构，擅长处理和理解长期依赖关系和复杂的上下文信息。<br/>参数共享和泛化能力：在P-Tuning中，LSTM模型的参数可以在多个任务之间共享，这提高了模型的泛化能力，并减少了针对每个单独任务的训练需求。而在提示调整中，每个任务通常都有其独立的虚拟标记嵌入，这可能限制了跨任务泛化的能力。<br/>这些特性使得LSTM特别适合处理复杂任务和需要细粒度控制的应用场景。然而，这些优势也伴随着更高的计算复杂度和资源需求，因此在实际应用中需要根据具体需求和资源限制来权衡使用LSTM的决策。<br/><br/>7、P-Tuning v2<br/>P-Tuning v2是P-Tuning的进一步改进版，在P-Tuning中，连续提示被插入到输入序列的嵌入层中，除了语言模型的输入层，其他层的提示嵌入都来自于上一层。这种设计存在两个问题：<br/><br/>第一，它限制了优化参数的数量。由于模型的输入文本长度是固定的，通常为512，因此提示的长度不能过长。<br/>第二，当模型层数很深时，微调时模型的稳定性难以保证；模型层数越深，第一层输入的提示对后面层的影响难以预测，这会影响模型的稳定性。<br/>P-Tuning v2的改进在于，不仅在第一层插入连续提示，而是在多层都插入连续提示，且层与层之间的连续提示是相互独立的。这样，在模型微调时，可训练的参数量增加了，P-Tuning v2在应对复杂的自然语言理解(NLU)任务和小型模型方面，相比原始P-Tuning具有更出色的效能。<br/><br/>除了以上PEFT，当前还存在PILL（Pluggable Instruction Language Learning）、SSF（Scaling &amp; Shifting Your Features）等其他类型的微调方法。<br/><br/>PILL是PEFT的一个特定实现，特别关注于如何通过插入可训练的模块或插件来提升模型的任务适应性。这些插件被设计为与原始模型协同工作，以提高模型在处理特定任务时的效率和效果。<br/><br/>SSF核心思想是对模型的特征（即模型层的输出）进行缩放（Scaling）和位移（Shifting）。简单来说，就是通过调整特征的比例和偏移量来优化模型的性能。<br/><br/>这种方法可以在改善模型对特定任务的响应时，不需要调整或重新训练模型中的所有参数，从而在节省计算资源的同时保持或提升模型性能。这对于处理大规模模型特别有效，因为它减少了训练和调整所需的资源和时间。<br/><br/>四、大模型的微调策略<br/><br/>综上所述，微调是一种强大的工具，它能够使大型预训练模型适应于特定的任务和应用场景。正确选择和应用微调策略对于实现高效且有效的模型性能至关重要。<br/><br/>1、微调与迁移学习：微调实际上是迁移学习的一个实例，其中预训练的模型（通常在大型通用数据集上训练）被用作特定任务的起点。这种方法使得即使是对于小数据集的任务，也可以实现高效的学习<br/><br/>2、选择微调策略：选择哪种微调方法取决于多个因素，包括任务的复杂性、可用的数据量、计算资源和期望的性能。<br/><br/>例如，对于需要细粒度控制的复杂任务，P-Tuning v2或LSTM基础的P-Tuning可能更适合。而对于计算资源有限的情况，可以选择LoRA或Adapter Tuning等方法。<br/><br/>3、微调与模型泛化能力：微调时需要注意的一个关键问题是保持模型的泛化能力。过度的微调可能会导致模型对特定训练数据过拟合，而忽略了其在实际应用中的泛化能力。
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/673/</link>
<title><![CDATA[Elasticsearch拼音分词]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Thu, 29 May 2025 02:57:48 +0000</pubDate> 
<guid>https://www.heckjj.com/post/673/</guid> 
<description>
<![CDATA[ 
	elasticsearch的分词器对于文本分析至关重要。对于中文等语言，合适的分词器可以显著提高搜索相关性和结果的准确性。拼音分词器不仅支持基于拼音的搜索，还能实现拼音自动补全等功能。本文将介绍如何在Elasticsearch中安装拼音分词器，以及如何配置和测试它。<br/><br/>分词器的三要素<br/>在Elasticsearch中，分词器（Analyzer）由以下三个主要部分组成：<br/><br/>Character Filters（字符过滤器）：在Tokenizer处理之前对文本进行预处理，如删除或替换特定字符。<br/>Tokenizer（分词器）：按照一定的规则将文本切分成词条（Term），例如ik_max_word就是智能切分中文。<br/>Token Filters（词条过滤器）：对Tokenizer输出的词条进行进一步处理，如转换为小写、同义词替换、拼音转换等。<br/>安装拼音分词器插件<br/>步骤1：下载插件<br/>访问<a href="https://release.infinilabs.com/analysis-pinyin/stable/" target="_blank">elasticsearch-analysis-pinyin</a> 下载页面，下载与您的Elasticsearch版本相匹配的插件。<br/><br/>步骤2：上传插件<br/>将下载的插件压缩包上传到Elasticsearch的plugins目录。<br/><br/>步骤3：检查插件列表<br/>curl -XGET &quot;http://localhost:9200/_cat/plugins?v&quot;<br/>预期输出：<br/>name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;component&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; version<br/>node-master analysis-pinyin 7.17.6<br/>包含 analysis-pinyin 插件。<br/><br/>步骤4：测试插件<br/>curl -XPOST &quot;http://localhost:9200/_analyze&quot; -H &quot;Content-Type: application/json&quot; -d&#039;<br/>&#123;<br/>&nbsp;&nbsp;&quot;analyzer&quot;: &quot;pinyin&quot;,<br/>&nbsp;&nbsp;&quot;text&quot;: &quot;姜军&quot;<br/>&#125;&#039;<br/>预期输出：<br/>&#123;<br/>&nbsp;&nbsp;&quot;tokens&quot;: [<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#123; &quot;token&quot;: &quot;a&quot;, &quot;position&quot;: 0 &#125;,<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#123; &quot;token&quot;: &quot;li&quot;, &quot;position&quot;: 1 &#125;,<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#123; &quot;token&quot;: &quot;ba&quot;, &quot;position&quot;: 2 &#125;,<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#123; &quot;token&quot;: &quot;ba&quot;, &quot;position&quot;: 3 &#125;<br/>&nbsp;&nbsp;]<br/>&#125;<br/><br/>Easy-Es 中使用 pinyin 分词器<br/>确保实体类中正确引用 Analyzer.PINYIN：<br/>@IndexField(<br/>&nbsp;&nbsp;&nbsp;&nbsp;fieldType = FieldType.TEXT,<br/>&nbsp;&nbsp;&nbsp;&nbsp;analyzer = Analyzer.PINYIN,<br/>&nbsp;&nbsp;&nbsp;&nbsp;searchAnalyzer = Analyzer.PINYIN,<br/>&nbsp;&nbsp;&nbsp;&nbsp;fieldData = true<br/>)<br/>private String name;
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/669/</link>
<title><![CDATA[SpringBoot启用HTTPS及问题解决]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Wed, 04 Dec 2024 09:49:00 +0000</pubDate> 
<guid>https://www.heckjj.com/post/669/</guid> 
<description>
<![CDATA[ 
	配置证书<br/>打开CMD输入命令：<br/><br/>keytool -genkey -alias heck -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore heck-ssl-key.p12 -validity 3650<br/><br/>证书会下载到当前目录下：<br/>server:<br/>&nbsp;&nbsp;port: 443<br/>&nbsp;&nbsp;ssl:<br/>&nbsp;&nbsp;&nbsp;&nbsp;# 证书的路径，可用绝对路径，如果放到项目资源文件路径需要添加 classpath:<br/>&nbsp;&nbsp;&nbsp;&nbsp;key-store: classpath:heck-ssl-key.p12<br/>&nbsp;&nbsp;&nbsp;&nbsp;key-store-password: 123456<br/>&nbsp;&nbsp;&nbsp;&nbsp;key-store-type: PKCS12<br/><br/><br/>可能还会报异常DerInputStream.getLength(): lengthTag=111, too big.&nbsp;&nbsp;<br/>Could not load key store &#039;classpath:heck-ssl-key.p12&#039;：<br/><br/><br/>可以pom.xml 把这个文件加入编译，<br/><br/> &lt;plugins&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;plugin&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;artifactId&gt;maven-resources-plugin&lt;/artifactId&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;version&gt;3.2.0&lt;/version&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;configuration&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;nonFilteredFileExtensions&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;!--&lt;nonFilteredFileExtension&gt;p12&lt;/nonFilteredFileExtension&gt;--&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;nonFilteredFileExtension&gt;jks&lt;/nonFilteredFileExtension&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/nonFilteredFileExtensions&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/configuration&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/plugin&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/plugins&gt;<br/><br/>刷新 Maven，clean 后重新 compile。<br/><br/>
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/667/</link>
<title><![CDATA[Linux开机出现 Generating “/run/initramfs/rdsosreport.txt“解决方案]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Tue, 22 Oct 2024 11:42:10 +0000</pubDate> 
<guid>https://www.heckjj.com/post/667/</guid> 
<description>
<![CDATA[ 
	Linxu开机出现 Generating "/run/initramfs/rdsosreport.txt"解决方案<br/><a href="https://www.heckjj.com/attachment.php?fid=260" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=260" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0"/></a><br/><br/>解决：<br/>一、找这个-root结尾的文件也不一样。<br/><br/>大家可以用ls /dev/mapper查看到自己装的镜像对应的以-root结尾的文件是哪个。<br/><a href="https://www.heckjj.com/attachment.php?fid=262" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=262" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0" width="500" height="150"/></a><br/><br/>二、所以我们运行的是：xfs_repair /dev/mapper/cl-root<br/><a href="https://www.heckjj.com/attachment.php?fid=261" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=261" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0" width="800" height="150"/></a><br/> 还是报错的话需要<br/><br/>需要卸载掉 （会丢数据，没办法，总比重做系统方便吧）
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/664/</link>
<title><![CDATA[idea在service窗口误删服务恢复中显示多个服务]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Tue, 12 Mar 2024 03:26:32 +0000</pubDate> 
<guid>https://www.heckjj.com/post/664/</guid> 
<description>
<![CDATA[ 
	IDEA的Service窗口误删服务(也就是点到了Hide Configuration选项)后如何恢复<br/><br/>看下到底是做了什么操作导致的服务被隐藏&nbsp;&nbsp;(赶时间直接到最后看解决方案就行)<br/>正常情况下, 在开启窗口的情况下, 会显示我们所有的微服务启动类, 如下图<br/><a href="https://www.heckjj.com/attachment.php?fid=255" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=255" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0"/></a><br/><br/><br/>idea在service窗口中显示多个服务如下：在这里插入图片描述<br/>.idea &gt; workspace.xml 中找到 RunDashboard 替换成如下<br/><br/><br/>&nbsp;&nbsp;&lt;component name=&quot;RunDashboard&quot;&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;option name=&quot;configurationTypes&quot;&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;set&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;option value=&quot;SpringBootApplicationConfigurationType&quot; /&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/set&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;/option&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;option name=&quot;ruleStates&quot;&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;list&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;RuleState&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;option name=&quot;name&quot; value=&quot;ConfigurationTypeDashboardGroupingRule&quot; /&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/RuleState&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;RuleState&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;option name=&quot;name&quot; value=&quot;StatusDashboardGroupingRule&quot; /&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/RuleState&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/list&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;/option&gt;<br/>&nbsp;&nbsp;&lt;/component&gt;<br/>
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/658/</link>
<title><![CDATA[spring security+redis序列化失败no way to handle typed deser with setterless yet]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Thu, 23 Nov 2023 09:49:30 +0000</pubDate> 
<guid>https://www.heckjj.com/post/658/</guid> 
<description>
<![CDATA[ 
	用 security加redis写项目时一直报这个错误，说是没有set方法？我用的data注解怎么可能<br/><br/>这个bug找了一下午，终于找到原因了：redis序列化会查询所有以get和set开头的方法，而我的user继承了security的UserDetails，多了一个集合类型的getAuthorities方法，所有导致无法序列化，使用的是jackson的序列化器<br/><br/>解决办法：<br/>加上@JsonIgnore注解设置不序列化<br/><br/> @JsonIgnore<br/>&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>&nbsp;&nbsp;&nbsp;&nbsp;public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ArrayList&lt;NdpAuthority&gt; grantedAuthorities = CollectionUtil.newArrayList();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (ObjUtil.isNotEmpty(roles)) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;roles.forEach(dict -&gt; &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String roleName = dict.getStr(CommonConstant.NAME);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;NdpAuthority ndpAuthority = new NdpAuthority(roleName);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;grantedAuthorities.add(ndpAuthority);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return grantedAuthorities;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/><br/>加个属性，写个对应的set方法 
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/657/</link>
<title><![CDATA[spring boot启动后自动执行代码方式汇总]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[编程杂谈]]></category>
<pubDate>Thu, 23 Nov 2023 06:27:23 +0000</pubDate> 
<guid>https://www.heckjj.com/post/657/</guid> 
<description>
<![CDATA[ 
	在实际项目开发过程中，我们有时候需要让项目在启动时执行特定方法。如要实现这些功能：<br/>提前加载相应的数据到缓存中；<br/>检查当前项目运行环境；<br/>检查程序授权信息，若未授权则不能使用后续功能；<br/>执行某个特定方法；<br/><br/>一直想整理一下Springboot启动后自执行某段代码或者方法相关的点，囿于时间问题及担心理解不是太全面所以没整理，后来一想先整理出来，将这一方面的东西记录下来。其实这篇文章不应该体现spring boot，因为自动执行方式不仅限于spring boot，java spring细化到bean的初始化都可以完成。<br/><br/>一、java自身的启动时加载方式<br/><br/>1、static代码块<br/>static静态代码块，在类加载的时候即自动执行。 由于静态代码块没有名字，我们并不能主动调用，它会在类加载的时候，自动执行。所以静态代码块，可以更早的给类中的静态属性，进行初始化赋值操作。并且，静态代码块只会自动被执行一次，因为JVM在一次运行中，对一个类只会加载一次！<br/><br/>2、构造函数constructor<br/>在对象初始化时执行。执行顺序在static静态代码块之后。<br/><br/>3、@PostConstruct注解<br/>@PostConstruct<br/>public void testInit() &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;System.out.printin(&quot;postConstruct“);<br/>&#125;<br/>@PostConstruct注解使用在方法上，这个方法在对象依赖注入初始化之后执行。执行节点在BeanPostProcessor的postProcessBeforeInitialization之后，在postProcessAfterInitialization之前。<br/>很多人以为该注解是Spring提供的。其实是Java自己的注解。Java中该注解的说明：@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行，并且只会被服务器执行一次。PostConstruct在构造函数之后执行，init()方法之前执行。<br/>注意：加了postconstruct注解的方法，如果执行失败，整个程序会无法正常启动！这个方法执行不完，整个程序也启动不了！也不建议将耗时逻辑放到这里面来。<br/><br/>二、Servlet加载的方式<br/>1、实现ServletContextListener接口contextInitialized方法<br/>import lombok.extern.slf4j.Slf4j;<br/>import org.springframework.stereotype.Component;<br/>import javax.servlet.ServletContextEvent;<br/>import javax.servlet.ServletContextListener;<br/><br/>@Slf4j<br/>@Component<br/>public class ServletContextListenerImpl implements ServletContextListener &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;/**<br/>&nbsp;&nbsp;&nbsp;&nbsp; * 在初始化Web应用程序中的任何过滤器或Servlet之前，将通知所有ServletContextListener上下文初始化。<br/>&nbsp;&nbsp;&nbsp;&nbsp; */<br/>&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>&nbsp;&nbsp;&nbsp;&nbsp;public void contextInitialized(ServletContextEvent sce) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;启动时自动执行 ServletContextListener 的 contextInitialized 方法&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/>注意：该方法会在填充完普通Bean的属性，但是还没有进行Bean的初始化之前执行<br/><br/>三、Spring启动时加载方式<br/>1、实现ServletContextAware接口setServletContext 方法<br/>@Component<br/>public class StartInitServletContext implements ServletContextAware &#123;<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>&nbsp;&nbsp;&nbsp;&nbsp;public void setServletContext(ServletContext servletContext) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(&quot;StartInitServletContext: 开始处理事情&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/><br/>2、ApplicationListener监听器<br/>创建Listener的方式有两种：<br/>方式1：编程实现ApplicationListener接口<br/>方式2：使用@EventListener注解<br/>注意监听的事件，通常是ApplicationStartedEvent 或者ApplicationReadyEvent，其他的事件可能无法注入bean。<br/><br/>编程实现ApplicationListener接口<br/>/**<br/> * 监听的是 ApplicationStartedEvent 事件，<br/> * 则 ApplicationListener 一定会在 CommandLineRunner 和 ApplicationRunner 之前执行；<br/> */<br/>@Component<br/>public class StartInitStartedEvent implements ApplicationListener&lt;ApplicationStartedEvent&gt; &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>&nbsp;&nbsp;&nbsp;&nbsp;public void onApplicationEvent(ApplicationStartedEvent event) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(&quot;StartInitStartedEvent：开始处理事情&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/>/**<br/> * 监听的是 ApplicationReadyEvent 事件，<br/> * 则 ApplicationListener 一定会在 CommandLineRunner 和 ApplicationRunner 之后执行；<br/> */<br/>@Component<br/>public class StartInitReadyEvent implements ApplicationListener&lt;ApplicationReadyEvent&gt; &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>&nbsp;&nbsp;&nbsp;&nbsp;public void onApplicationEvent(ApplicationReadyEvent event) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(&quot;StartInitReadyEvent: 开始处理事情&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/>实现 public interface ApplicationListener&lt;E extends ApplicationEvent&gt; 接口，监听 ApplicationEvent 及其下面的子事件：<br/>/**<br/> * 事件监听器一般是由开发者自己定义<br/> * 定义事件监听器<br/> */<br/>@Component<br/>//@Lazy<br/>public class MyApplicationListener implements ApplicationListener&#123;<br/> <br/>&nbsp;&nbsp;@Override<br/>&nbsp;&nbsp;public void onApplicationEvent(ApplicationEvent event) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;event = (PayloadApplicationEvent)event;<br/>&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(((PayloadApplicationEvent&lt;?&gt;) event).getPayload());<br/>&nbsp;&nbsp;&#125;<br/>&#125;<br/><br/>@EventListener方式<br/>将要执行的方法所在的类交个Spring容器扫描(@Component),并且在要执行的方法上添加@EventListener注解执行<br/>import lombok.extern.slf4j.Slf4j;<br/>import org.springframework.context.event.ContextRefreshedEvent;<br/>import org.springframework.context.event.EventListener;<br/>import org.springframework.stereotype.Component;<br/><br/>@Slf4j<br/>@Component<br/>public class EventListenerTest &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;@EventListener<br/>&nbsp;&nbsp;&nbsp;&nbsp;public void onApplicationEvent(ContextRefreshedEvent event) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;启动时自动执行&nbsp;&nbsp;@EventListener 注解方法&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/><br/>3、InitializingBean接口<br/>@Component<br/>@Data<br/>public class TestInit implements InitializingBean,eanFactoryAware，ApplicationContextAware&#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;private BeanFactory beanFactory:<br/>&nbsp;&nbsp;&nbsp;&nbsp;private ApplicationContext applicationContext;<br/>&nbsp;&nbsp;&nbsp;&nbsp;private String name;<br/>@Override<br/>public voidafterPropertiesSet(hrows Exception &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;System.out.printin(&quot;InIt heck&quot;);<br/> &#125;<br/>&#125;<br/>Spring为bean提供了两种初始化bean的方式，实现InitializingBean接口，实现afterPropertiesSet方法，或者在配置文件中通过init-method指定，两种方式可以同时使用。<br/>实现InitializingBean接口是直接调用afterPropertiesSet方法，比通过反射调用init-method指定的方法效率要高一点，但是init-method方式消除了对spring的依赖。先调用afterPropertieSet()方法，然后再调用init-method中指定的方法。<br/><br/>4、ApplicationRunner和CommandLineRunner<br/>SpringBoot提供了两个接口来实现Spring容器启动完成后执行的功能，两个接口分别为CommandLineRunner和ApplicationRunner。这两个接口需要实现一个run方法，将代码在run中实现即可。这两个接口功能基本一致，其区别在于run方法的入参。ApplicationRunner的run方法入参为ApplicationArguments，为CommandLineRunner的run方法入参为String数组。<br/>当有多个类实现了CommandLineRunner和ApplicationRunner接口时，可以通过在类上添加@Order注解来设定运行顺序。<br/>注意:使用ApplicationRunner或者CommandLineRunner时如果报错或者出现超时的情况会导致整个程序崩溃。可以将run内的逻辑单独开一个线程使用。<br/>import lombok.extern.slf4j.Slf4j;<br/>import org.springframework.boot.ApplicationArguments;<br/>import org.springframework.boot.ApplicationRunner;<br/>import org.springframework.stereotype.Component;<br/>import java.util.Set;<br/><br/>@Slf4j<br/>@Component<br/>public class ApplicationRunnerImpl implements ApplicationRunner &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;/**<br/>&nbsp;&nbsp;&nbsp;&nbsp; * 用于指示bean包含在SpringApplication中时应运行的接口。可以定义多个ApplicationRunner bean<br/>&nbsp;&nbsp;&nbsp;&nbsp; * 在同一应用程序上下文中，可以使用有序接口或@order注释对其进行排序。<br/>&nbsp;&nbsp;&nbsp;&nbsp; */<br/>&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>&nbsp;&nbsp;&nbsp;&nbsp;public void run(ApplicationArguments args) throws Exception &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;启动时自动执行 ApplicationRunner 的 run 方法&quot;);<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Set&lt;String&gt; optionNames = args.getOptionNames();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (String optionName : optionNames) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;这是传过来的参数[&#123;&#125;]&quot;, optionName);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String[] sourceArgs = args.getSourceArgs();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (String sourceArg : sourceArgs) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;这是传过来sourceArgs[&#123;&#125;]&quot;, sourceArg);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/><br/>通过实现Ordered接口并重写getOrder方法实现，数字越小越先执行<br/><br/>import lombok.extern.slf4j.Slf4j;<br/>import org.springframework.boot.CommandLineRunner;<br/>import org.springframework.stereotype.Component;<br/><br/>@Slf4j<br/>@Component<br/>public class CommandLineRunnerImpl implements CommandLineRunner &#123;<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;@Override<br/>&nbsp;&nbsp;&nbsp;&nbsp;public void run(String... args) throws Exception &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;启动时自动执行 CommandLineRunner 的 run 方法&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/>总结一下：<br/>Spring应用启动过程中，肯定是要自动扫描有@Component注解的类，加载类并初始化对象进行自动注入。加载类时首先要执行static静态代码块中的代码，之后再初始化对象时会执行构造方法。最新 Spring Boot 面试题整理好了，点击Java面试库小程序在线刷题。<br/>在对象注入完成后，调用带有@PostConstruct注解的方法。当容器启动成功后，再根据@Order注解的顺序调用CommandLineRunner和ApplicationRunner接口类中的run方法。<br/>说明：<br/>　　1）默认执行顺序ApplicationContextInitializer、ApplicationRunner、CommandLineRunner<br/>　　2）ApplicationRunner与CommandLineRunner可通过@Order改变顺序使CommandLineRunner早于ApplicationRunner执行<br/>　　3）两个Runner的区别：CommandLineRunner参数是原始的，ApplicationRunner是对原始参数的封装<br/><br/>执行顺序为static&gt;constructor&gt;初始化前&gt;ServletContextListener&gt;@PostConstruct&gt;ServletContextAware&gt;@EventListener&gt;InitializingBean&gt;初始化后&gt;ApplicationRunner和CommandLineRunner。<br/>
]]>
</description>
</item>
</channel>
</rss>