<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Glader&apos;s Blog - 中文版</title><description>Glader的技术博客 - 中文文章RSS订阅</description><link>https://blog.mygld.top/</link><language>zh-CN</language><item><title>LangChain4j 框架的学习</title><link>https://blog.mygld.top/zh-cn/posts/langchain4j/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/langchain4j/</guid><description>学习 LangChain4j 框架的记录笔记。</description><pubDate>Sun, 28 Dec 2025 15:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. LangChain4j 简介&lt;/h2&gt;
&lt;h3&gt;概念&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;LangChain4j&lt;/strong&gt; 是一个开源的 &lt;strong&gt;Java 库/框架&lt;/strong&gt;，用来简化在 Java 应用中集成和使用&lt;strong&gt;大型语言模型&lt;/strong&gt;的过程。它的设计目标是让 Java 开发者能够像使用 Python 的 LangChain 那样，轻松构建复杂的 AI 应用（比如聊天机器人、智能助手、检索增强生成系统等）。&lt;/p&gt;
&lt;h3&gt;特点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;LLM 集成&lt;/strong&gt;：提供统一的 API，让你可以调用多种主流的大语言模型（如 OpenAI、Google、Anthropic、国产模型等），而无需直接对接各自的低级接口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模块化与扩展性&lt;/strong&gt;：框架采用模块化设计，不同的组件可以单独引入，例如模型提供商、向量数据库、记忆管理、工具调用等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RAG 支持（检索增强生成）&lt;/strong&gt;：将外部文本数据转化为向量，结合向量数据库进行语义检索，并将结果注入模型上下文以提升回答质量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工具与 Agents&lt;/strong&gt;：支持函数调用、外部工具集成以及智能体工作流（Agents），构建更复杂的 AI 任务流程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;与 Java 生态集成&lt;/strong&gt;：可以与常见的 Java 框架（如 Spring Boot、Quarkus）结合使用。&lt;/p&gt;
&lt;h2&gt;2. Java 语言整合 LangChain4j&lt;/h2&gt;
&lt;h3&gt;引入依赖&lt;/h3&gt;
&lt;p&gt;我们首先创建一个基本的 Java 项目，我这里以 Maven 项目为例。&lt;/p&gt;
&lt;p&gt;首先引入 LangChain4j 的核心框架依赖和适配实现依赖 (这里使用 OpenAI 的适配实现)，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
	&amp;lt;groupId&amp;gt;dev.langchain4j&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;langchain4j&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;${langchain4j.version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;dependency&amp;gt;
	&amp;lt;groupId&amp;gt;dev.langchain4j&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;langchain4j-open-ai&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;${langchain4j.version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体版本的特点，以及其他接口规范的依赖（如阿里、Ollama等）可以参考 &lt;a href=&quot;https://github.com/langchain4j/langchain4j&quot;&gt;官方开源仓库&lt;/a&gt; 或 &lt;a href=&quot;https://docs.langchain4j.dev/&quot;&gt;官方文档&lt;/a&gt;，他们的使用方法基本和 OpenAI 的一致。&lt;/p&gt;
&lt;p&gt;然后我们再引入 Junit 的依赖，便于进行单元测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
	&amp;lt;groupId&amp;gt;org.junit.jupiter&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;junit-jupiter&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;5.11.4&amp;lt;/version&amp;gt;
	&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;文生文快速入门 Demo&lt;/h3&gt;
&lt;p&gt;然后我们写一个单元测试就可以快速掌握 LangChain4j 的使用方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void test1() {
	ChatModel model = OpenAiChatModel.builder()
				.apiKey(System.getenv(&amp;quot;OPENAI_API_KEY&amp;quot;))
				.modelName(&amp;quot;gpt-4o-mini&amp;quot;)
				.build();
	String answer = model.chat(&amp;quot;你好&amp;quot;);
	System.out.println(answer);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你有 OpenAI 的 api 密钥，可以直接运行该测试单元，查看结果，就可以输出模型返回的结果。&lt;/p&gt;
&lt;p&gt;需要注意的是，&lt;code&gt;ChatModel&lt;/code&gt; API 是在 LangChain4j 1.0.0-beta4 版本中引入的。在此之前的版本中，应使用 &lt;code&gt;ChatLanguageModel&lt;/code&gt; 作为对话模型的规范接口。&lt;/p&gt;
&lt;p&gt;如果我们使用的是其他的一些第三方的 API，如果其也满足 OpenAI 的接口规范，我们可以使用 &lt;code&gt;OpenAiChatModel&lt;/code&gt; 中的 &lt;code&gt;baseUrl&lt;/code&gt; 方法，来填写第三方 API 接口地址，具体接口地址格式需要根据第三方接口的接口文档来进行规范，但不应包含具体的资源路径，例如， 如果我们使用&lt;a href=&quot;https://www.siliconflow.cn/&quot;&gt;硅基流动&lt;/a&gt;的接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void test1() {
	ChatModel model = OpenAiChatModel.builder()
				.baseUrl(&amp;quot;https://api.siliconflow.cn/v1&amp;quot;)
				.apiKey(System.getenv(&amp;quot;SILICONFLOW_API_KEY&amp;quot;))
				.modelName(&amp;quot;Qwen/Qwen3-30B-A3B-Instruct-2507&amp;quot;)
				.build();
	String answer = model.chat(&amp;quot;你好&amp;quot;);
	System.out.println(answer);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1766936577698_image-20251228223835701.png&quot; alt=&quot;image-20251228223835701.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，成功打印出模型返回的结果，还带着一个非常可爱的 emoji 表情 😊。&lt;/p&gt;
&lt;p&gt;上述红色警告可以暂时忽略，这是因为当前项目尚未配置具体的日志实现。从 LangChain4j 核心框架的依赖树可以看出，它仅引入了 &lt;code&gt;slf4j&lt;/code&gt; 作为日志门面，而未绑定任何具体的日志实现。后续在集成 Spring Boot 项目时，Spring Boot 默认提供的日志实现会自动补齐这一部分：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1766936591197_image-20251228224635498.png&quot; alt=&quot;image-20251228224635498.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果不打算集成 Spring Boot 项目，可以手动引入 Logback 作为日志实现来消除该警告。实际上，Spring Boot 默认已经集成并自动配置了 Logback 作为日志实现，因此在 Spring Boot 项目中通常不会遇到该问题。&lt;/p&gt;
&lt;p&gt;但是我们现在仅仅做测试，不用日志系统也是没问题的。&lt;/p&gt;
&lt;h3&gt;文生图快速入门 Demo&lt;/h3&gt;
&lt;p&gt;对于文生图，使用的是 &lt;code&gt;ImageModel&lt;/code&gt;，我这里使用阿里的 &lt;code&gt;Qwen-Image&lt;/code&gt; 的文生图模型，这里我们直接调用 &lt;code&gt;ImageModel&lt;/code&gt; 下的 &lt;code&gt;generate&lt;/code&gt; 方法，传入我们的文本描述即可，比如我想生成一个“猫耳萌妹子”的图片，我们就传入这个字符串，返回的是一个 &lt;code&gt;Response&lt;/code&gt; 数据，我们可以使用 &lt;code&gt;response.content().url()&lt;/code&gt; 获得生成的图片的直链。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void test2() {
    ImageModel model = OpenAiImageModel.builder()
            .baseUrl(&amp;quot;https://api.siliconflow.cn/v1&amp;quot;)
            .apiKey(System.getenv(&amp;quot;SILICONFLOW_API_KEY&amp;quot;))
            .modelName(&amp;quot;Qwen/Qwen-Image&amp;quot;)
            .build();
    Response&amp;lt;Image&amp;gt; response = model.generate(&amp;quot;猫耳萌妹子&amp;quot;);
    System.out.println(response.content().url());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1766936603775_image-20251228233034445.png&quot; alt=&quot;image-20251228233034445.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们把打开链接，下载图片，得到的图片如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1766936613495_outputs_3d5c294b-e6e4-4f01-80e6-0ba68f8c6340_3473e7dd60b0066b46ba8e1fef1f4198_ComfyUI_1c512918_00001_.png&quot; alt=&quot;outputs_3d5c294b-e6e4-4f01-80e6-0ba68f8c6340_3473e7dd60b0066b46ba8e1fef1f4198_ComfyUI_1c512918_00001_.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可见质量非常不错，非常符合我们的要求。&lt;/p&gt;
&lt;p&gt;在此基础上，其他模态方向的能力（例如文生语音、图生文、图生视频等）读者可以结合所选平台的 API 文档进一步探索。需要注意的是，不同厂商对多模态能力的接口设计差异较大：有的平台提供 OpenAI-compatible 的扩展端点，有的平台则采用各自的多模态协议或工作流编排接口。实际落地时建议优先选择接口稳定、文档清晰且生态成熟的方案。&lt;/p&gt;
&lt;h2&gt;3. LangChain4j 整合 Spring Boot&lt;/h2&gt;
&lt;h3&gt;快速入门 Demo&lt;/h3&gt;
&lt;p&gt;使用 LangChain4j 整合 Spring Boot，首先要求我们的 Spring Boot 版本必须 3.2 版本及以上，且 JDK 版本要求 17 及以上。&lt;a href=&quot;https://docs.langchain4j.dev/tutorials/spring-boot-integration/&quot;&gt;原文细节&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;LangChain4j 主要提供了两类 Starter，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;集成类 Starter&lt;/strong&gt;：用于对接常见的模型提供方、向量数据库、Embedding 服务等第三方组件，提供相应的自动装配与配置能力，便于在 Spring Boot 中开箱即用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;声明式 &lt;a href=&quot;https://docs.langchain4j.dev/tutorials/ai-services&quot;&gt;AI Services Starter&lt;/a&gt;&lt;/strong&gt;：用于启用和增强声明式 AI Service 能力（如 &lt;code&gt;@AiService&lt;/code&gt;），支持以接口/注解方式定义 AI 能力，并由框架完成实现生成、依赖注入与运行时编排。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;集成类 Starter&lt;/h4&gt;
&lt;p&gt;这样说比较干燥，我们来先看第一类 Starter。这一类的 Starter 的统一具体格式为：&lt;code&gt;langchain4j-{integration-name}-spring-boot-starter&lt;/code&gt;，其中 &lt;code&gt;{integration-name}&lt;/code&gt; 表示具体的集成对象（例如某个大模型提供方、Embedding 服务或向量数据库等）。例如我这里用 OpenAI 的统一接口规范，就可以引入如下依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;dev.langchain4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;langchain4j-open-ai-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.10.0-beta18&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;{integration-name}&lt;/code&gt; 就是 &lt;code&gt;open-ai&lt;/code&gt;，对于其他提供方的具体名称格式，可以参考&lt;a href=&quot;https://github.com/langchain4j/langchain4j-spring&quot;&gt;官方仓库&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;引入依赖之后，下面就是振奋人心的书写配置信息的阶段。我们可以在 &lt;code&gt;application.properties&lt;/code&gt; 中书写，也可以在 &lt;code&gt;application.yml&lt;/code&gt; 等格式文件中书写。&lt;/p&gt;
&lt;p&gt;倘若在 &lt;code&gt;application.properties&lt;/code&gt; 中书写，基本需要加入以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;langchain4j.open-ai.chat-model.api-key=${OPENAI_API_KEY}
langchain4j.open-ai.chat-model.model-name=gpt-4o
langchain4j.open-ai.chat-model.log-requests=true
langchain4j.open-ai.chat-model.log-responses=true
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里为什么要用三个点，这是上述四个是基本的配置信息，第一个是密钥信息，第二个是模型信息，第三个是开启把&lt;strong&gt;发给模型的请求内容&lt;/strong&gt;打到日志里，第四个是开启把&lt;strong&gt;模型返回的响应内容&lt;/strong&gt;打到日志里。&lt;/p&gt;
&lt;p&gt;还有一些其他的配置项也可以参与配置，具体请参考官方的 &lt;a href=&quot;https://docs.langchain4j.dev/apidocs/index.html&quot;&gt;api 文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;比如，我现在要用硅基流动的接口，所以我还需要一个 &lt;code&gt;base-url&lt;/code&gt; 的配置项，所以总体配置就是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;langchain4j.open-ai.chat-model.api-key=${SILICONFLOW_API_KEY}
langchain4j.open-ai.chat-model.model-name=Qwen/Qwen3-30B-A3B-Instruct-2507
langchain4j.open-ai.chat-model.base-url=https://api.siliconflow.cn/v1
langchain4j.open-ai.chat-model.log-requests=true
langchain4j.open-ai.chat-model.log-responses=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们写一个测试 Controller，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
public class ChatController {

    private ChatModel chatModel;

    public ChatController(ChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @GetMapping(&amp;quot;/chat&amp;quot;)
    public String model(@RequestParam(value = &amp;quot;message&amp;quot;, defaultValue = &amp;quot;Hello&amp;quot;) String message) {
        return chatModel.chat(message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种情况下，Controller 只依赖接口 &lt;code&gt;ChatModel&lt;/code&gt;，具体实现由 starter 自动创建一个实例&lt;code&gt;OpenAiChatModel&lt;/code&gt;，来实现自动装配和注入，非常的方便。&lt;/p&gt;
&lt;p&gt;然后我们编写测试单元进行测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
private ChatController chatController;
@Test
void test4(){
    System.out.println(chatController.model(&amp;quot;你好，你是谁？&amp;quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1766981637702_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，不仅有我们 &lt;code&gt;sout&lt;/code&gt; 的输出结果，还有请求与响应内容的日志打印，这是我们前面在配置文件里开启的两个 &lt;code&gt;log&lt;/code&gt; 的结果。&lt;/p&gt;
&lt;p&gt;在实际的项目开发中，我们可以根据我们的需求个性化定义工具类，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@AllArgsConstructor
@Component
public class AIUtil {
    private final ChatModel chatModel;

    public String generateText(String content){
        return chatModel.chat(content);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就非常的优雅。记得要加上 &lt;code&gt;@Component&lt;/code&gt; 注解交给 &lt;code&gt;IoC&lt;/code&gt; 容器管理。&lt;/p&gt;
&lt;p&gt;我们前面的所有测试，都是等接口完全生成完文本之后，才获得结果，然后打印，但是我们在网页中使用 AI 进行对话的时候，它的输出往往是流式的，例如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767012674563_d3vyk-ytvxb.gif&quot; alt=&quot;d3vyk-ytvxb.gif&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，他是一个词一个词的往外崩的，并不是把所有内容都获取完毕才返回给我们。这种输出方式通常被称为&lt;strong&gt;流式输出&lt;/strong&gt;，其核心思想是：模型在生成内容的过程中，每产生一个 token（或一个文本片段），就立刻返回给调用方，而不是等待完整响应生成完成。&lt;/p&gt;
&lt;p&gt;对于 Web 场景下的 AI 对话来说，流式输出几乎是“标配”，它可以显著提升用户体验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;更低的首字延迟&lt;/strong&gt;：用户几乎立刻就能看到模型开始“说话”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更自然的交互感&lt;/strong&gt;：更接近真人输入的节奏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更适合长文本生成&lt;/strong&gt;：避免长时间白屏等待&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来，我们就来看一下 &lt;strong&gt;LangChain4j + Spring Boot&lt;/strong&gt; 场景下，如何优雅地实现流式输出。&lt;/p&gt;
&lt;p&gt;在 LangChain4j 中，流式输出并不是通过 &lt;code&gt;ChatModel&lt;/code&gt; 来完成的，而是通过专门的 &lt;strong&gt;Streaming 接口&lt;/strong&gt;，例如 &lt;code&gt;StreamingChatModel&lt;/code&gt;。当使用符合对应模型提供商规范的接口时，LangChain4j 会在底层通过 &lt;strong&gt;SSE（Server-Sent Events）&lt;/strong&gt; 与模型建立流式连接，并将模型在生成过程中产生的 &lt;strong&gt;token 增量&lt;/strong&gt; 逐个回调给调用方。&lt;/p&gt;
&lt;p&gt;好在这些复杂的底层细节，我们并不需要手动处理。&lt;/p&gt;
&lt;p&gt; LangChain4j 的 Spring Boot Starter 已经为我们封装好了对应的自动配置能力，会根据配置文件自动创建并注册 &lt;code&gt;StreamingChatModel&lt;/code&gt; 相关的 Bean。&lt;/p&gt;
&lt;p&gt;首先，我们需要在 &lt;code&gt;application.properties&lt;/code&gt; 中补充流式模型的配置。与前面同步调用使用的 &lt;code&gt;chat-model&lt;/code&gt; 不同，这里使用的是 &lt;code&gt;streaming-chat-model&lt;/code&gt; 前缀，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;langchain4j.open-ai.streaming-chat-model.api-key=${SILICONFLOW_API_KEY}
langchain4j.open-ai.streaming-chat-model.model-name=Qwen/Qwen3-30B-A3B-Instruct-2507
langchain4j.open-ai.streaming-chat-model.base-url=https://api.siliconflow.cn/v1
langchain4j.open-ai.streaming-chat-model.log-requests=true
langchain4j.open-ai.streaming-chat-model.log-responses=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们改造一下工具类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@AllArgsConstructor
@Component
public class AIUtil {
    private final StreamingChatModel streamingChatModel;

    public void generateText(String content, StreamingChatResponseHandler handler){
        streamingChatModel.chat(content,handler);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是把原来的 &lt;code&gt;ChatModel&lt;/code&gt; 改成 &lt;code&gt;StreamingChatModel&lt;/code&gt;，然后 &lt;code&gt;chat&lt;/code&gt; 方法里传入一个&lt;strong&gt;回调处理器：StreamingChatResponseHandler&lt;/strong&gt;。改动非常微小。&lt;/p&gt;
&lt;p&gt;现在 &lt;strong&gt;Spring Boot 做流式返回已经不需要自己手写 SSE 细节了&lt;/strong&gt;，我们只需要引入 &lt;code&gt;Spring WebFlux&lt;/code&gt; 的依赖，即可方便地通过返回 &lt;code&gt;Flux&lt;/code&gt; 实现服务端数据的&lt;strong&gt;自动分片、实时推送与连接管理&lt;/strong&gt;，Spring 会负责底层的 SSE 协议封装与刷新逻辑。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Spring WebFlux&lt;/code&gt; 的依赖格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-webflux&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们以最快的速度写一个 &lt;code&gt;Service&lt;/code&gt; 和  &lt;code&gt;Controller&lt;/code&gt; 进行测试，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class ChatServiceImpl implements ChatService {

    @Autowired
    private AIUtil aiUtil;

    @Override
    public Flux&amp;lt;String&amp;gt; chat(String content) {
        return Flux.create(e-&amp;gt;aiUtil.generateText(content, new StreamingChatResponseHandler() {
            @Override
            public void onPartialResponse(String partialResponse) {
                e.next(partialResponse);
            }

            @Override
            public void onCompleteResponse(ChatResponse chatResponse) {
                e.next(&amp;quot; 输出完毕。&amp;quot;);
                e.complete();
            }

            @Override
            public void onError(Throwable throwable) {
                e.next(throwable.getMessage());
            }
        }));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
public class ChatController {

    @Autowired
    private ChatService chatService;

    @GetMapping(value = &amp;quot;/chat&amp;quot;, produces = &amp;quot;text/event-stream;charset=UTF-8&amp;quot;)
    public Flux&amp;lt;String&amp;gt; chat(@RequestParam(&amp;quot;message&amp;quot;) String content) {
        return chatService.chat(content);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，我们打开 ApiFox 进行测试，我们输入我们的测试 url：&lt;code&gt;http://localhost:8080/api/chat?message=你好，你是谁&lt;/code&gt;，可以看到返回的结果一个词一个词的蹦了出来：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767025808832_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;从运行结果可以看出，web 端能够&lt;strong&gt;逐段接收实时返回的内容&lt;/strong&gt;，这正是 &lt;code&gt;StreamingChatResponseHandler&lt;/code&gt; 各个回调方法协同工作的结果。下面我们逐一分析这些回调在流式输出中的作用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StreamingChatResponseHandler&lt;/code&gt; 的几个回调方法分别对应了模型生成过程中的不同阶段：在模型尚未完全生成结果时，&lt;code&gt;onPartialResponse&lt;/code&gt; 会被多次触发，每次返回一小段内容，我们在这里通过 &lt;code&gt;FluxSink.next()&lt;/code&gt; 将数据立即推送给下游，从而实现前端页面中文字的实时输出；当模型生成结束后，会回调 &lt;code&gt;onCompleteResponse&lt;/code&gt;，用于标识本次生成已经完成，实际使用中通常在这里结束流；如果在生成过程中发生异常，则会触发 &lt;code&gt;onError&lt;/code&gt;，用于将错误信号传递给前端。正是通过 &lt;code&gt;Flux.create&lt;/code&gt; 将这种回调式的流转为 &lt;code&gt;Flux&lt;/code&gt;，Spring WebFlux 才能够自动以流式的方式将数据持续写入 HTTP 响应中，最终实现无需手写 SSE 的流式输出效果。&lt;/p&gt;
&lt;p&gt;除上述三个主要的回调方法之外，还有一个 &lt;code&gt;onPartialThinking&lt;/code&gt; 方法。这个方法是做什么的呢？&lt;/p&gt;
&lt;p&gt;我们发现，现在其实很多模型都是带思考过程的，就是先进行思考推理，这个回调其实就是来处理推理过程返回的内容的，这里就不再继续讨论，原理和 &lt;code&gt;onPartialResponse&lt;/code&gt; 类似，感兴趣的朋友可以继续探究，事实上在后面我们还有&lt;strong&gt;更简洁&lt;/strong&gt;的回调写法，在后面我们再继续探究。&lt;/p&gt;
&lt;h4&gt;声明式 AI Services Starter&lt;/h4&gt;
&lt;p&gt;LangChain4j 提供了一个 Spring Boot starter，用于自动配置 &lt;a href=&quot;https://docs.langchain4j.dev/tutorials/ai-services&quot;&gt;AI 服务&lt;/a&gt;、&lt;a href=&quot;https://docs.langchain4j.dev/tutorials/rag&quot;&gt;RAG&lt;/a&gt;、&lt;a href=&quot;https://docs.langchain4j.dev/tutorials/tools&quot;&gt;工具&lt;/a&gt;等。&lt;/p&gt;
&lt;p&gt;我们已经完成了底层模型客户端的自动装配，但如果仍然停留在直接注入 &lt;code&gt;ChatModel&lt;/code&gt;、手写 Prompt 和回调逻辑这一层，整体使用方式依然偏向命令式。&lt;strong&gt;AI Services Starter&lt;/strong&gt; 的意义在于进一步抽象这些样板代码，让你可以像编写普通 Spring Service 一样，通过声明式接口来使用大模型能力。你只需要定义方法签名和注解，LangChain4j 就会在运行时自动生成实现，负责 Prompt 构建、模型调用以及结果映射等细节。配合 Spring Boot 的自动配置，模型参数和密钥都可以集中在 &lt;code&gt;application.yml&lt;/code&gt; 中统一管理，也便于在不同环境或模型之间切换。当引入 RAG 或工具调用时，这种方式的优势会更加明显，相关编排对业务代码几乎是透明的，使代码始终保持简洁、清晰和可维护。&lt;/p&gt;
&lt;p&gt;接下来，我们快速来演示一下在代码中的具体使用。&lt;/p&gt;
&lt;p&gt;首先我们要导入 &lt;code&gt;langchain4j-spring-boot-starter&lt;/code&gt; 的依赖，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;dev.langchain4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;langchain4j-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.10.0-beta18&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;!--        langchain4j-reactor 是 LangChain4j 对 Reactor（Mono / Flux）的官方适配层，--&amp;gt;
&amp;lt;!--        因为我们流式输出用到了 Flux，这里需要引入下面这个适配层依赖，如果你的项目中不需要流式--&amp;gt;
&amp;lt;!--        输出，则不需要引入下面这个依赖，只需要引入上面的依赖即可。--&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;dev.langchain4j&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;langchain4j-reactor&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.10.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们把 &lt;code&gt;ChatServiceImpl&lt;/code&gt; 注释掉或删除掉就可以了，我们不用手动去实现这么繁琐的代码，我们直接回到 &lt;code&gt;ChatService&lt;/code&gt; 接口中，给它添加上 &lt;code&gt;AiService&lt;/code&gt; 注解，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@AiService
public interface ChatService {
    Flux&amp;lt;String&amp;gt; chat(String content);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其它代码不需要改动，我们再去运行去 ApiFox 测试一下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767150345242_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到和我们料想的一样，可以正确给出流式响应，而且不需要我们自己去书写那些繁琐的实现类代码了。&lt;/p&gt;
&lt;p&gt;那我们回过头来，去看一下 &lt;code&gt;@AiService&lt;/code&gt; 的源码，看看它到底为我们做了什么。源码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AiService {
    AiServiceWiringMode wiringMode() default AiServiceWiringMode.AUTOMATIC; // 装配模式：自动或手动指定依赖
    String chatModel() default &amp;quot;&amp;quot;;                 // 同步 ChatModel 的 Bean 名称
    String streamingChatModel() default &amp;quot;&amp;quot;;        // 流式 StreamingChatModel 的 Bean 名称
    String chatMemory() default &amp;quot;&amp;quot;;                // ChatMemory Bean 名称，用于对话记忆
    String chatMemoryProvider() default &amp;quot;&amp;quot;;        // ChatMemoryProvider Bean 名称，按会话创建记忆
    String contentRetriever() default &amp;quot;&amp;quot;;          // ContentRetriever Bean 名称，用于 RAG 检索
    String retrievalAugmentor() default &amp;quot;&amp;quot;;        // RetrievalAugmentor Bean 名称，封装完整 RAG 增强流程
    String moderationModel() default &amp;quot;&amp;quot;;           // ModerationModel Bean 名称，用于内容安全审查
    String toolProvider() default &amp;quot;&amp;quot;;               // ToolProvider Bean 名称，用于统一提供工具
    String[] tools() default {};                   // 指定要注入的工具 Bean 名称列表
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，&lt;code&gt;AiService&lt;/code&gt; 中，又封装了 &lt;code&gt;Service&lt;/code&gt; 注解，因此，使用 &lt;code&gt;AiService&lt;/code&gt;，就相当于使用了 &lt;code&gt;Service&lt;/code&gt; 注解，因此会被 Spring 扫描并纳入 IoC 管理。不过当它标注在接口上时，Spring 注入的并不是接口本身，而是 LangChain4j 根据注解配置在运行时生成的代理实现，关于动态代理的知识点，可以参考我的另一篇&lt;a href=&quot;https://blog.mygld.top/zh-cn/posts/dynamic-proxy/&quot;&gt;博客&lt;/a&gt;；这个代理会负责 Prompt 组装、模型选择（chat/streaming）、记忆/RAG/工具等能力的编排，从而让接口方法看起来像普通 Service 调用，但底层实际是在调用大模型。我们来看一下它动态代理部分的源码，位于 &lt;code&gt;dev.langchain4j.service.DefaultAiServices&lt;/code&gt; 下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Internal
class DefaultAiServices&amp;lt;T&amp;gt; extends AiServices&amp;lt;T&amp;gt; {
    ...
    public T build() {
        validate();
        Object proxyInstance = Proxy.newProxyInstance(
                context.aiServiceClass.getClassLoader(),
                new Class&amp;lt;?&amp;gt;[]{context.aiServiceClass},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // Object / default 方法处理
                        if (method.isDefault()) {
                            return InvocationHandler.invokeDefault(proxy, method, args);
                        }
                        if (method.getDeclaringClass() == Object.class) {
                            ...
                        }
                        // 构建一次 AI 调用上下文
                        InvocationContext invocationContext = InvocationContext.builder()
                                .invocationId(UUID.randomUUID())
                                .interfaceName(context.aiServiceClass.getName())
                                .methodName(method.getName())
                                .build();
                        // 核心入口：将接口方法调用转为 AI 调用
                        return invoke(method, args, invocationContext);
                    }
                    public Object invoke(Method method, Object[] args, InvocationContext invocationContext) {
                        // Prompt / Memory / RAG / Guardrails 等准备
                        ...
                        // 判断是否为流式返回
                        Type returnType = method.getGenericReturnType();
                        boolean streaming =
                                returnType == TokenStream.class || canAdaptTokenStreamTo(returnType);
                        if (streaming) {
                            // 流式：返回 TokenStream 或适配后的类型（如 Flux）
                            TokenStream tokenStream = new AiServiceTokenStream(...);
                            return returnType == TokenStream.class
                                    ? tokenStream
                                    : adapt(tokenStream, returnType);
                        }
                        // 非流式：同步调用模型并解析结果
                        ChatResponse response = chatModel.execute(...);
                        return serviceOutputParser.parse(response, returnType);
                    }
                }
        );
        return (T) proxyInstance;
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，我们注意到 &lt;code&gt;AiService&lt;/code&gt; 中还提供了很多注解属性供我们使用，我们这里只做快速入门，其他属性的具体作用，后期我会单独开文章进行详细的讲解。&lt;/p&gt;
</content:encoded><category>LangChain4j</category><category>AI</category><category>Java</category><category>Spring Boot</category><author>Glader</author></item><item><title>Differentiable Auxiliary Learning for Sketch Re-Identification 的解读</title><link>https://blog.mygld.top/zh-cn/posts/sbir-02/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/sbir-02/</guid><description>Differentiable Auxiliary Learning for Sketch Re-Identification 的论文阅读。</description><pubDate>Sat, 27 Dec 2025 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇论文是 AAAI-2024 的文章，文章的标题是&amp;quot;Differentiable Auxiliary Learning for Sketch Re-Identification&amp;quot; (可微分辅助学习在草图重识别中的应用)。&lt;/p&gt;
&lt;p&gt;论文提出了一种名为 DALNet 的网络架构。该方法通过对真实照片进行背景去除和边缘检测强化，生成一个”类草图“的中间辅助模态，以此来桥接并对齐草图与照片模态 。由于负责生成这一辅助模态的模块是可训练且可微分的，支持端到端优化，因此该方法被命名为”可微分辅助学习”（&lt;strong&gt;D&lt;/strong&gt;ifferentiable &lt;strong&gt;A&lt;/strong&gt;uxiliary &lt;strong&gt;L&lt;/strong&gt;earning, 简称 DAL）。&lt;/p&gt;
&lt;h2&gt;1. 动机&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019243298_f1.png&quot; alt=&quot;f1.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图，论文的动机非常明确，可以分为两大点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;草图与行人图像之间&lt;strong&gt;模态间&lt;/strong&gt;差异太大，能否构建一个中间模态来辅助打破两种模态的差异鸿沟。&lt;/li&gt;
&lt;li&gt;草图与行人图像各自&lt;strong&gt;模态内&lt;/strong&gt;差异也很大，首先同一个行人的草图可能有不同画师绘制，风格多样，抽象程度差异大；并且同一个行人在不同摄像头下的照片，会受到背景杂波、光照变化以及视角姿态差异的严重干扰 。这导致即便是同一个人的照片，在特征空间中也可能相距甚远，增加了匹配难度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为解决第一个问题，论文构建了一个“类草图”的中间辅助模态作为桥梁。该模态通过对真实照片进行背景去除与边缘检测增强生成，从而有效地辅助建立了&lt;strong&gt;模态间&lt;/strong&gt;的特征对齐 。论文在特征学习阶段加入多模态协同约束：用跨模态的 circle loss 让草图、照片与类草图三者关系整体对齐。&lt;/p&gt;
&lt;p&gt;为解决第二个问题，论文又引入&lt;strong&gt;模态内&lt;/strong&gt;的 circle loss 专门压缩同一模态中同身份的分布、拉开不同身份的距离，从而对草图来说，减弱不同画师风格造成的影响；对真实图像来说，减弱光照背景与视角姿态变化等带来的特征漂移问题。&lt;/p&gt;
&lt;h2&gt;2. 方法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019247006_f2.png&quot; alt=&quot;f2.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;模型的总体框架图如上图所示，整体来说，训练阶段首先使用 Dynamic Auxiliary Generator (DAG) 模块生成一个类草图的辅助模态图像 (这个模块是可循练的，通过 $L_{SR}$ 损失来优化这个模块，使得生成的辅助图像自适应逼近目标草图风格)，然后把草图、辅助图像和真实图像三个模态的图像进行编码，提取特征，然后把辅助模态的特征融合到另外两个模态当中去，并通过交叉注意力机制，强化草图与真实图像之间共享语义信息、实现细粒度交互融合。最后结合分类损失与跨模态/模态内 circle loss 来共同约束三模态的特征关系与分布，下面我来分析一下各部分的细节。&lt;/p&gt;
&lt;h3&gt;Dynamic Auxiliary Generator (DAG)&lt;/h3&gt;
&lt;p&gt;这是动态辅助生成器模块，作用就是输入一张真实图像，输出一张类草图的辅助图像。其原理具体如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019255280_f3.png&quot; alt=&quot;f3.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先，将真实行人图像输入预训练的 U²-Net（该网络参数保持冻结）得到前景分割的掩码矩阵；再利用该掩码对原图进行逐像素筛选，从而保留行人主体并去除背景，得到背景被抠除的行人图像。U²-Net 的具体结构如上图右侧所示，之所以叫 U²-Net，是因为它在整体上是一个 U-Net 式的编码器-解码器结构，但其每个编码与解码阶段内部又嵌套了一个更小的 U-Net（即 RSU 模块，Residual U-block），相当于“U 中套 U”，形成两层 U 形结构的级联，因此用 U 的平方来命名。&lt;/p&gt;
&lt;p&gt;U²-Net 会在解码过程中从不同尺度的侧输出层生成多张掩码（side maps），这些掩码分别对应不同分辨率下对前景的预测：浅层侧输出更关注边缘与细节，高层侧输出更关注整体结构与语义区域；训练时通常对这些侧输出施加深度监督以稳定收敛、提升多尺度分割能力，最后再将多尺度侧输出进行融合（如拼接后卷积或加权融合）得到一张最终的高质量前景掩码，用于后续的背景抠除。&lt;/p&gt;
&lt;p&gt;得到抠除背景的行人图像后，此时仍然是一个 RGB 的彩色图像，因此首先要通过一个 1×1 的卷积块将其转化为单通道的灰度图像，然后经过一个 3×3 的卷积核，进行边缘检测，强化轮廓线条，使灰度图更加逼近于草图，从而生成类草图的辅助模态图像。这其中，这个  3×3 的卷积核是可循练的，这个也是 DAG 模块唯一可训练优化的地方，通过损失函数 $L_{SR}$ 进行约束，该损失函数的定义我会在下面的特征提取模块中进行详细的分析。卷积核初始化为中心数字为 9，周围 8 个数字为 -0.8，先提供一个稳定的“边缘增强”先验，再在训练过程中根据下游检索目标自适应调整卷积核权重，使生成的 Auxiliary 更符合草图域的轮廓表达方式。&lt;/p&gt;
&lt;h3&gt;Featrue Extraction&lt;/h3&gt;
&lt;p&gt;然后是特征提取模块，如下图所示，整体采用一个三流的 ResNet-50 作为 backbone，分别对 Photo、Auxiliary 和 Sketch 三种模态进行特征编码：每个模态先经过各自的前端 ResBlocks (使用的 ResNet-50 的前两个阶段) 提取低层局部特征，得到三路的全局表征；随后再把三路特征送入同一套“权重共享”的后续 ResBlocks  (使用的是 ResNet-50 后面剩余的阶段)，学习更高层、更接近语义的共享表示，为后面的模态交互对齐做准备。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019269713_f4.png&quot; alt=&quot;f4.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;具体来看，定义 $P={x_i^P}_{i=1}^{N_p}$ 为真实图像的样本集合，$N_p$ 表示样本数量，$A$ 与 $S$ 集合也是同理，分别表示类草图集合和草图集合。&lt;/p&gt;
&lt;p&gt;现将 Photo、Auxiliary 和 Sketch 放入各自的前端 ResBlocks 得到三个编码后的特征图，对这些特征图进行 Gem Pooling 池化操作，分别得到三个池化后的特征向量 $I^P$,$I^A$ 和 $I^S$。具体公式如上图的 $I_k^u$ 所示，其中 $u$ 表示模态，$k$ 表示当前为第 $k$ 个通道，$H$ 和 $W$ 分别代表特征图的高和宽，$p$ 是一个可学习的参数，$p$ 接近为 $1$ 时，池化会更像“平均池化”（对整幅特征图做更均匀的聚合）；而当 $p$ 逐渐变大时，聚合过程会越来越接近“最大池化”（更强调响应最强的位置）。之所以使用 GeM Pooling，是因为它用一个可学习的 $p$ 在平均池化与最大池化之间自适应折中：既能保留全局结构信息，又能突出对行人检索更关键的局部判别区域（如衣服纹理、轮廓细节等），从而在跨模态匹配时获得更稳健、判别性更强的全局表征。&lt;/p&gt;
&lt;p&gt;得到的三个特征向量后，论文计算风格细化损失函数 $$L_{SR}$$（其形式与 InfoNCE 类似），具体公式如上图所示，目的是把 DAG 生成的辅助模态 Auxiliary 从“照片风格”拉向“草图风格”，从而优化 DAG 模块的卷积核参数，但又不破坏它继承自 Photo 的人体结构信息。具体做法是：以辅助模态的风格特征 $$I^A$$ 作为锚点，把同一身份对应的草图风格特征 $$I^S$$ 当作唯一的正样本；分母中只放入 $$I^S$$ 与一组照片风格特征 $${I_i^P}_{i=1}^{N}$$ 来做对比，而不额外加入其他草图特征，是因为这里并不是要学习“草图之间”的可分性（那属于身份判别和检索损失去处理），而是要明确地把 $$I^A$$ 从 Photo 域中区分出来并向 Sketch 域靠拢：如果分母再引入大量“其他草图”，优化会变成让 $$I^A$$ 同时远离这些草图（包括与目标草图风格相近的草图），容易削弱“向草图域靠近”的牵引力，甚至引入不必要的身份与风格混杂。因而该损失的对比集合被刻意设计为“一个草图正样本 + 多个照片负样本”，通过温度系数 $$\xi$$ 的 softmax 归一化最大化 $$I^A$$ 与 $$I^S$$ 的相似度、最小化 $$I^A$$ 与各 $$I_i^P$$ 的相似度，从而实现“去照片风格化 + 向草图风格对齐”的风格迁移约束。实验细节里给出温度系数 $$\xi$$ 取值为 0.07。&lt;/p&gt;
&lt;p&gt;之后，三路的特征图送入同一套“权重共享”的后续 ResBlocks，学习更高层、更接近语义的共享表示，具体表示为 $f^P$,$f^A$ 和 $f^S$，为后面的模态交互对齐做准备。这里注意，送入到后续的 ResBlocks 的是原来三路生成的特征图，而不是池化后的 $I^u$。&lt;/p&gt;
&lt;h3&gt;Modality Interactive Attention (MIA)&lt;/h3&gt;
&lt;p&gt;然后是模态交互注意力模块。具体如下图所示。该模块又具体分为两个模块组成，一个是 Bilinear Align 模块，一个是 Auxilirary Cross-Attention 模块。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019279011_f5.png&quot; alt=&quot;f5.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;双线性对齐模块（BAM）如上图所示，其核心作用是利用辅助模态的特征图 $$f^A$$ 去和另外两个模态的特征图 $$f^P$$、$$f^S$$ 计算相似度权重 $$S_{uA}$$（其中 $u\in{P,S}$）。可以把 $$S_{uA}$$ 理解为一张“注意力打分图”：它刻画了在每个空间位置、每个通道上，当前模态特征与辅助模态特征的匹配程度，匹配高的位置会被赋予更大的权重，从而在后续对特征进行加权时突出更可靠、更一致的行人结构线索。具体计算时，先将 $$f^A$$ 与目标模态的 $$f^u$$ 在通道维进行拼接，然后送入双线性对齐单元建模两者的二阶交互关系，得到对齐后的响应，再通过 $$sigmoid$$ 函数 $$\sigma(\cdot)$$ 将其归一化到 $$[0,1]$$ 范围，形成相似度概率 $$S_{uA}$$，用于对 $$f^u$$ 做注意力加权增强并输出对齐特征。&lt;/p&gt;
&lt;p&gt;双线性对齐具体步骤如上图所示，首先要先对张量进行 reshape。原来的 $f^p$、$f^A$ 和 $f^S$，实际上都是 $(N,C,H,W)$ 形状的张量。其中 $N$ 表示批次大小，$C$ 表示通道数，$H$ 和 $W$ 分别表示特征图的高和宽。首先把形状调整成 $(N,C,HW)$，然后进行堆叠拼接，得到一个  $(N,2C,HW)$ 在通道上拼接的结果，如上图的红框内所示，我以上方的红框内的 $f^P$ 与 $f^A$ 的为例，这两个特征图 reshape 后，都用一系列的长条表示，其中向屏幕内部延伸的长度可以抽象为 $HW$，垂直方向的数量可以表示为通道数 $C$，两个特征图堆叠起来就是一共有 $2C$ 个长条 (上半部分 C 个橙色，下半部分 C 个绿色)，之后进入一个线性层，把通道压缩到 $C/4$,目的是在不丢失关键信息的前提下先“降维压缩”两模态拼接后的通道表达，做一次轻量的瓶颈变换：因为拼接后通道数变成了 $2C$，如果直接在高维空间做双线性交互，参数量和计算量都会很大，而且容易过拟合；因此先用第一层线性层把通道压到 $C/4$，相当于做特征筛选与信息蒸馏，保留最有助于两模态对齐的相关成分。然后再进入一个线性层，把通道还原回 $C$,目的是为了把压缩后的“对齐关系”重新映射回与原 backbone 特征一致的通道维度，方便后续与原特征进行融合/逐元素运算，并且输出的维度与后续模块（如相似度估计和注意力加权）保持匹配。&lt;/p&gt;
&lt;p&gt;计算得到的 $$S_{uA}$$ 会再 reshape 回 $$(N,C,H,W)$$ 的形状，以便与原始特征图 $$f^u$$ 在空间位置与通道维上一一对应。随后按照对齐公式 $$f_{ali}^u = S_{uA}\odot f^u + f^u$$ 对特征进行加权增强：其中 $$S_{uA}$$ 作为注意力权重，对与辅助模态更一致的区域赋予更高响应、抑制不一致或噪声区域，并通过残差项保留原始信息。最终得到的 $f_{ali}^P$ 和 $f_{ali}^S$ 就是经过 Auxiliary 引导对齐后的 Photo 和 Sketch 特征图，为后续 ACA 的跨注意力交互提供更“干净”、更可对齐的表示。&lt;/p&gt;
&lt;p&gt;同时，为了让后续 ACA 做跨注意力交互时，辅助模态也能以“对齐后的、更干净的表示”参与双向匹配（例如把 $$f_{ali}^A$$ 作为 Key/Query 与 $$f_{ali}^P$$ 或 $$f_{ali}^S$$ 进行交互），论文也对辅助模态本身做了一次同样的加权残差增强，得到对齐后的辅助特征 $$f_{ali}^A$$。具体定义为用草图与辅助模态的相似度权重 $$S_{SA}$$ 去加权 $$f^A$$：$$f_{ali}^A = S_{SA}\odot f^A + f^A$$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019284989_150ddc67-7a3d-4eec-af71-15cdf1af13d4.png&quot; alt=&quot;150ddc67-7a3d-4eec-af71-15cdf1af13d4.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;最终得到的结果就是这样的了，深浅表示对特征的关注程度，越深关注越强，越浅则越弱。至于为什么有的是虚线框，论文里没有解释，我查阅一些画图的资料，了解到虚线可能表示三种模态下的一些公共特征。&lt;/p&gt;
&lt;h4&gt;Auxilirary Cross-Attention Module (ACA)&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019295101_f6.png&quot; alt=&quot;f6.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;ACA 的目的不是再去“算一个相似度分数”，而是基于 BAM 得到的对齐特征，真正让三种模态之间发生信息交换与融合：论文强调它“利用辅助模态去引导模型学习模态共享表征的分布”，并且能在 photo 与 sketch 之间实现显著的信息交互与融合。&lt;/p&gt;
&lt;p&gt;如上图所示，具体计算以 “photo 与 auxiliary 的交互” 为例：把 BAM 输出的对齐特征 $$f_{ali}^P$$ 和 $$f_{ali}^A$$ 分别视为 Query 与 Key（记作 $$Q^P$$ 和 $$K^A$$），然后用标准的缩放点积注意力得到从 photo 指向 auxiliary 的匹配权重 $W_{P\rightarrow A} = \text{Softmax}\left(\frac{Q^P (K^A)^T}{\sqrt{d_K}}\right)$，其中 $$d_K$$ 是 Key 的通道维度。
同时再交换 Query与Key 得到反向的匹配权重$W_{A\rightarrow P} = \text{Softmax}\left(\frac{Q^A (K^P)^T}{\sqrt{d_K}}\right)$。&lt;/p&gt;
&lt;p&gt;有了双向权重后，论文用一个“往返一致”的方式来精炼 photo 特征：以 $$V^P=f_{ali}^P$$ 作为 Value，将 $$W_{P\rightarrow A}$$ 与 $$W_{A\rightarrow P}$$ 相乘后再去加权 $$V^P$$，最后做 LayerNorm，得到被辅助模态突出后的 photo 特征 $\hat f^P = \text{Norm}\left(W_{P\rightarrow A} W_{A\rightarrow P} V^P\right),\quad V^P=f_{ali}^P.$
这里可以理解为：先用 $$W_{P\rightarrow A}$$ 找到 photo 中哪些位置或模式能在 auxiliary 中找到对应，再用 $$W_{A\rightarrow P}$$ 把这种对应关系“映射回来”确认一致性，从而更稳健地强化两者共有的结构语义，抑制各自的噪声与不一致区域。&lt;/p&gt;
&lt;p&gt;同理，把 $$f_{ali}^S$$ 与 $$f_{ali}^A$$ 做同样的双向 cross-attention，就能得到在 auxiliary 潜在表征引导下精炼后的 sketch 特征 $\hat f^S$。&lt;/p&gt;
&lt;h3&gt;Multi-Modality Collaborative Learning&lt;/h3&gt;
&lt;p&gt;多模态协同学习，这部分主要围绕两类损失来设计：一类是类别损失，用于保证三种模态的特征具备清晰的身份判别性；另一类是 circle loss 系列的度量学习损失，其中跨模态 circle loss 用来约束不同模态但同一身份的样本在特征空间中相互靠近、不同身份相互分离，从整体上实现模态间对齐；同时再引入模态内 circle loss，专门压缩同一模态内部同身份样本的分布、拉开不同身份距离，以缓解草图风格差异和照片视角/光照/背景变化带来的类内离散问题。两者配合，使模型同时完成模态间与模态内的对齐。&lt;/p&gt;
&lt;h4&gt;类别损失&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019299719_f7.png&quot; alt=&quot;f7.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，类别（身份）损失用的是 $$L_{ID}$$，目的是用“身份监督”把三种模态拉到同一个可分的身份空间里，让同一人的 Photo / Auxiliary / Sketch 学到一致的身份模式。论文把它写成两部分相加：$$L_{ID}=L_{id}(F)+L_{id}(\hat F)$$，其中 $$F={f^P,f^A,f^S}$$ 表示三种模态经过共享 ResBlocks 输出的特征图集合，$$\hat F={\hat f^P,\hat f^S}$$ 表示经过 MIA 细粒度交互增强后的 Photo 与 Sketch 特征图集合，而 $$L_{id}$$ 就是标准的交叉熵分类损失（用真实身份标签做 softmax 分类监督）。这样设计的直观含义是：一方面直接约束共享骨干提取到的三模态基础表征在身份层面对齐；另一方面也约束经过辅助模态引导交互后的增强表征仍然保持正确的身份判别性，避免注意力交互把特征“对齐”到错误的人或引入身份无关的偏移。&lt;/p&gt;
&lt;h4&gt;Circle Loss 损失&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019307867_f8.png&quot; alt=&quot;f8.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，论文基于 2020 年提出的 circle loss，进行一波改进，适用于草图和图像，并设计了两个损失函数，一个是 $L_{CM}$，这个是用于模态间的对齐；另一个是 $L_{IM}$ 这个是用于模态内的对齐。&lt;/p&gt;
&lt;p&gt;论文用 circle loss 来做度量学习约束，而不是常见的 triplet loss，核心原因是：Sketch Re-ID 同时存在“跨模态差异巨大且每个身份样本极少”的问题，单纯用 triplet 往往只在少量三元组上做局部约束，容易出现优化不稳定、对难样本依赖强、以及只顾跨模态而忽略模态内分布的现象；circle loss 则以“成对相似度优化”的统一形式，把一个 batch 中的多对正负样本一起纳入优化，并通过自适应权重 $$\alpha_i^+,\alpha_j^-$$ 更强调那些“更难”的正负对，从而更适合在有限样本下学习更稳健的度量空间。&lt;/p&gt;
&lt;p&gt;我先说一下我对原始 circle loss 的一个理解，原始公式如上图的红框内所示。它把“相似度”当作优化对象：对同一身份形成的正样本对，相似度记为 $Z_i^+$（希望它尽可能大）；对不同身份形成的负样本对，相似度记为 $Z_j^-$（希望它尽可能小）。因此它的核心就是同时做两件事：一边把所有 $Z_i^+$ 往 1 拉（拉近同类），一边把所有 $Z_j^-$ 往 0 甚至更小拉（推远异类）。它把正对和负对分别放进指数函数里做加权求和：正对那一项的指数部分大致是 $-\gamma \alpha_i^+(Z_i^+ - \delta(+))$，当某个正对相似度 $Z_i^+$ 还不够大、低于期望阈值 $\delta(+)$ 时，括号是负的，指数项就会变大，对损失贡献变大，从而反向传播会强力推动这个正对去变得更相似；反过来如果 $Z_i^+$ 已经很大，超过 $\delta(+)$，这项贡献就变小，说明“容易正样本”不会再被过度优化。负对那一项的指数部分则是 $\gamma \alpha_j^-(Z_j^- - \delta(-))$，当某个负对相似度 $Z_j^-$ 偏大、超过阈值 $\delta(-)$ 时，这项会迅速变大，损失会更关注这些“难负样本”，从而推动它们的相似度下降；如果负对本来就很小（已经被分开），它的贡献就会被自动压低。这里的 $\delta(+)=1-m$、$\delta(-)=m$ 由间隔 $m$ 控制，相当于给正负对各自设定一个“达标线”，要求正对相似度尽量超过 $1-m$，负对相似度尽量低于 $m$；$\gamma$ 是尺度系数，用来控制优化的“力度/陡峭程度”。而最关键的自适应权重就是 $\alpha_i^+=[1+m-Z_i^+]&lt;em&gt;+$ 和 $\alpha_j^-=[Z_j^-+m]&lt;/em&gt;+$：它会让相似度不理想的正对（$Z_i^+$ 小）和最容易混淆的负对（$Z_j^-$ 大）获得更大的权重，从而把训练重点自然转移到“难样本对”上，这就是 circle loss 同时实现“拉近同类、推远异类”且更稳定的原因。&lt;/p&gt;
&lt;p&gt;在此基础上，论文把 circle loss 的“成对相似度优化”思想进一步推广到三模态场景，并针对草图 Re-ID 的两个核心矛盾分别做了对应设计。首先是模态间对齐的 $L_{CM}$：它不再只在同一模态里构造正负对，而是直接在不同模态之间计算相似度来构造正负对，也就是把 $Z_i^+$ 和 $Z_j^-$ 变成跨模态的余弦相似度 $Z_i^{uv+}(f^u,f^v)$、$Z_j^{uv-}(f^u,f^v)$（$u\neq v,;u,v\in{S,P,A}$），并对 $AS$、$AP$、$PS$ 三组模态两两施加 circle loss 约束后相加。这样做的直观目的就是：同一身份的 sketch、photo、auxiliary 在特征空间里要彼此靠近，不同身份要分开；而且因为 auxiliary 处在“类草图”的中间态，它同时参与 $AS$ 和 $AP$ 的对齐，会在优化过程中起到桥梁作用，间接降低 $PS$ 的对齐难度。具体如上图里的绿色方框里的公式所示。&lt;/p&gt;
&lt;p&gt;但如果只用 $L_{CM}$，模型往往会把优化重心放在“跨模态差异”上，导致每个模态内部同一身份的分布仍然可能很散（比如同一个人不同摄像头的 photo 仍相距很远，或者不同画师的 sketch 风格差异仍很大），从而学到次优的潜在空间。因此论文又加了模态内对齐的 $L_{IM}$：它仍然使用 circle loss 的形式，但构造正负对时是在同一模态内部做约束，并且为了在样本少的情况下制造更有力度的监督，它把同一模态里交互前的特征 $f^u$与交互后的特征 $\hat f^u$配对来构造正样本对（同一身份应当一致），同时选取最容易混淆的异身份配对作为负样本对。这样一来，$L_{CM}$ 负责把不同模态拉到同一度量空间、完成模态间对齐，$L_{IM}$ 负责在各自模态内部压缩类内离散、拉开类间间隔；两者配合，就能同时缓解“跨模态鸿沟”和“模态内差异大”这两个问题。具体如上图里的蓝色方框里的公式所示。&lt;/p&gt;
&lt;p&gt;在实验的具体细节里，各参数的设置为 $m_{cm}=0.25$，$m_{im}=0.5$，$\gamma=64$。&lt;/p&gt;
&lt;p&gt;总的损失函数就是上述四个损失函数之和，即 $L=L_{ID}+L_{CM}+L_{IM}+λL_{SR}$，其中 $λ = 0.6$。&lt;/p&gt;
&lt;h2&gt;3. 实验&lt;/h2&gt;
&lt;h3&gt;数据集配置&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019321060_42b8aa22-7bf6-404e-9665-46e9c970f16e.png&quot; alt=&quot;42b8aa22-7bf6-404e-9665-46e9c970f16e.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图，论文用到了如上 5 个数据集。&lt;/p&gt;
&lt;h3&gt;消融实验&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019325296_f9.png&quot; alt=&quot;f9.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上表1表示在 PKU-Sketch 和 ShoeV2 数据集上的进行消融实验，其中 B 表示 Baseline。论文说明这里的 Baseline 是“只用 identity loss 训练的 ResNet-50”；Aux. 表示是否引入辅助模态。$L_{Cir}$ 表示原始的 circle loss，后面的就是论文设计的损失函数和模块，可以发现，使用论文所提出的完整的模型和损失函数时，所有指标最高。&lt;/p&gt;
&lt;p&gt;如上表2是为了验证 DAG 模块是否参与联合训练更新对整个模型的影响，$G_f$ 表示固定参数，$G_j$ 表示参数参与训练优化，这里就是指的是那个卷积核的参数。$L_{SR}$ 指的就是那个风格细化损失。可以发现，当使用 $L_{SR}$ 损失函数在训练时优化 DAG 模块的参数才能使得模型效果最好，指标最高。&lt;/p&gt;
&lt;h3&gt;对比实验&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019337408_f10.png&quot; alt=&quot;f10.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上表3~表5是在不同数据集上与当前的 SoTA 模型进行对比，可以看到不管是在哪个数据集上，本文提出的模型各个指标最高。&lt;/p&gt;
&lt;h3&gt;可视化展示&lt;/h3&gt;
&lt;p&gt;图3是在做“注意力可视化对比”。论文用 XGrad-CAM 把两条不同视角的草图 query，以及各自 top-4 的检索照片结果的注意力热力图画出来，并用红框和绿框分别标记错误检索和正确检索。 结论很直观：Baseline 的注意力容易忽视草图与照片之间真正相关的区域，尤其在场景和姿态变化时会被一些相似局部干扰；而 DALNet 能同时关注到更“跨模态共享”的人体线索（比如脸部关键区域、衣服纹理、包、胸牌等），所以正确检索（绿框）更多。&lt;/p&gt;
&lt;p&gt;图4是在做“特征分布对齐过程的可视化”。论文随机选了 PKU-Sketch 的 10 个身份，用 t-SNE 把三种模态的特征分布在训练不同 epoch 下画出来（亮度区分不同身份）。 现象是：epoch=0 时 photo（橙）和 sketch（灰）分布差异很大；训练推进后 auxiliary（绿）像“桥”一样把两者连起来；到 epoch=50 时 photo 和 sketch 逐渐收敛，同时类内更紧、类间更开；最终 epoch=100 时 auxiliary 特征会聚到各自身份中心，说明模型学到了更强的身份判别性，并把三模态分布对齐起来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1767019353003_f11.png&quot; alt=&quot;f11.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;本文针对草图行人检索中跨模态鸿沟大与模态内变化剧烈的问题，提出 DALNet：先由 DAG 从真实照片生成“类草图”辅助模态作为桥梁，再用三流共享骨干提取特征，并通过 MIA 在辅助模态引导下实现细粒度跨模态交互融合，最后用分类损失与改进的 circle loss（同时包含跨模态与模态内）联合优化，实现模态间与模态内的同步对齐。&lt;/p&gt;
&lt;p&gt;创新点在于引入可训练的动态辅助模态生成与风格细化约束来缩小模态差异，并设计“辅助引导的交互注意力和跨模态与模态内的 circle loss”这一整套协同学习机制，使特征分布对齐更稳定、检索性能更强。&lt;/p&gt;
</content:encoded><category>SBIR</category><category>多模态</category><author>Glader</author></item><item><title>Modalities collaboration and granularities interaction for fine–grained sketch-based image retrieval 的解读</title><link>https://blog.mygld.top/zh-cn/posts/sbir-01/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/sbir-01/</guid><description>细粒度草图图像检索中的模态协同与粒度交互的论文阅读。</description><pubDate>Sat, 22 Nov 2025 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;论文的题目为：Modalities collaboration and granularities interaction for fine–grained sketch-based image retrieval 。&lt;/p&gt;
&lt;p&gt;可以翻译为：细粒度草图图像检索中的模态协同与粒度交互。&lt;/p&gt;
&lt;p&gt;这篇论文发表在 Pattern Recognition 2026 上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805776882_image-20251122170327558.png&quot; alt=&quot;image-20251122170327558.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;1. 动机&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805846546_f1.png&quot; alt=&quot;f1.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图，论文动机就两个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;如图 a，现有方法只关注单模态特征提取，忽略了草图（结构轮廓）和照片（纹理颜色）之间的互补性，且同实例跨模态差异大、不同实例单模态内差异小的特点使直接对齐困难。草图和真实图像之间的互补信息未充分利用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;如图 c，固定大小的块划分会产生边界噪声并割裂完整特征，同时缺少跨粒度特征交互，无法利用多尺度上下文信息增强判别性。&lt;/p&gt;
&lt;p&gt;整篇论文就是为了解决这两个问题的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. 方法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805843159_f2.png&quot; alt=&quot;f2.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;上面是论文提出的方法总体框架图，框架名称简称为 MCGI，大致可以分为 3 个大模块：CMFL、CMIC和 CGPI。其中 CGPI 又分为三个小模块：MGPM、CGII 和 MGPC。&lt;/p&gt;
&lt;h3&gt;CMFL (Cross-Modality Feature Learning)&lt;/h3&gt;
&lt;p&gt;跨模态特征学习模块，这个不是论文的创新点，可以说这就是一个基线模块，这部分论文用的 ViT-B/16 编码器，草图或图像切割成 token 加上位置编码和 [CLS] token 后分别进入编码器的 L 个层（12 层），然后输出的 $f_{cls}^{s,L}$ 和  $f_{cls}^{p,L}$  这两个 [CLS] token (前者表示草图的，后者表示真实图像的)，然后把这两个 [CLS] token 分别放入 $W_s$ 和 $W_p$ 这两个分类器，对分类结果计算交叉熵损失 $L_{ce}^s$ 和 $L_{ce}^{p}$。同时进行基本的跨模态对齐，论文使用的是三元组损失并且进行互对齐，计算得到的三元组损失分别为 $L_{tri}^s$ 和 $L_{tri}^{p}$，具体公式如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805848709_f3.png&quot; alt=&quot;f3.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;CMIC (Cross-Modality Information Compensation)&lt;/h3&gt;
&lt;p&gt;跨模态信息补偿模块，这是论文提出的第一个创新点，用来解决动机里面的问题 1。方法是对第 l 层（论文取 l = 11）的输出 token 进行交换，交换方法时保留 [CLS] token 不交换，只交换 N 个 patch token，交换前的 token 集合表示为 $F^{s,l}$ 和 $F^{p,l}$（前者表示草图的，后者表示真实图像的），交换后的 token 集合表示为 $\bar{F}^{s,l}$ 和  $\bar{F}^{p,l}$。 然后交换完的 token 集合通过剩下的 $L - l$ 层得到输出，分别取两侧的 [CLS] token，分别为的 $\bar{f}&lt;em&gt;{cls}^{s,L}$ 和  $\bar{f}&lt;/em&gt;{cls}^{p,L}$ ，用这两个 token 再去如法炮制按照 CMFL 那样计算交叉熵损失得到 $L_{ce}^{s,2}$ 和 $L_{ce}^{p,2}$。&lt;/p&gt;
&lt;p&gt;但是有一个问题，我们在训练阶段虽然知道草图和图像是一一配对的，但是在推理阶段不知道配对关系，因此论文设计了一个关系知识蒸馏，把 CMFL 模块输出的两个 [CLS] token: $f_{cls}^{s,L}$ 和  $f_{cls}^{p,L}$  分别传入两个 MLP 来模拟交换 token 后的输出，并设计损失函数 $L_{rkd}^{s}$ 和 $L_{rkd}^p$ 进行蒸馏，具体公式如下，$\psi_D$ 表示距离函数，$\mu$ 表示距离的归一化因子，$l_{\delta}$ 表示 Huber 损失函数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805849352_f4.png&quot; alt=&quot;f4.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;CGPI (Cross-Granularity Prototype Interaction)&lt;/h2&gt;
&lt;p&gt;跨粒度原型交互模块，这是论文的第 2 个创新点，这个就是来解决动机里的问题 2 的。该模块又分为以下三个小模块。&lt;/p&gt;
&lt;h4&gt;MGPM (Multi-Granularity Prototype Learning Module)&lt;/h4&gt;
&lt;p&gt;多粒度原型学习模块。该模块论文中简称有两种，分别是 MGPM 和 MGPL。论文的框架图中写的是 MGPM，后面的文字介绍中写的 MGPL，为了统一且便于讲解，我这里使用 MGPM 这种简写方式。&lt;/p&gt;
&lt;p&gt;该模块取第 L - 1 层的 patch token 输出 (不含 [CLS] token)，草图和真实图像的输出分别为 $[f_1^{s,L-1},f_2^{s,L-1},...,f_n^{s,L-1}]$ 和  $[f_1^{p,L-1},f_2^{p,L-1},...,f_n^{p,L-1}]$，然后分别平均分成 $M$ 组 (论文里取 $M = 7$)，每组有 $\frac{n}{M}$ 个 token，然后，逐步聚合这些组形成不同粒度的特征序列 $[f_1^{s,L-1},f_{1:2}^{s,L-1},...,f_{1:M}^{s,L-1}]$ 和  $[f_1^{p,L-1},f_{1:2}^{p,L-1},...,f_{1:M}^{p,L-1}]$，其中 $1 : i$ 表示前 $i$ 个组的 token 拼接在一起。&lt;/p&gt;
&lt;p&gt;为了增强这些多粒度特征的判别性，论文引入可学习的多粒度共享原型 $P = [P_1, P_2, ..., P_M]$，将每个粒度的原型与对应的 local tokens 拼接：$Z_s = [C(P_1, f_1^{s,L-1}), C(P_2, f_{1:2}^{s,L-1}), ..., C(P_M, f_{1:M}^{s,L-1})]$ 和 $Z_p = [C(P_1, f_1^{p,L-1}), C(P_2, f_{1:2}^{p,L-1}), ..., C(P_M, f_{1:M}^{p,L-1})]$，其中 $C(·)$ 表示拼接操作。这些原型的作用类似于 [CLS] token，每个原型 $P_i$ 负责聚合第 $i$ 个粒度的全局特征表示。&lt;/p&gt;
&lt;p&gt;然后把 $Z_s$ 和 $Z_p$ 分别送入第 $L$ 层 Transfomer Encoder 得到 $F^s$ 和 $F^p$，具体详细图解流程与公式如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805848921_f5.png&quot; alt=&quot;f5.png&quot;&gt;&lt;/p&gt;
&lt;h4&gt;CGII (Cross-granularity information interaction)&lt;/h4&gt;
&lt;p&gt;跨粒度信息交互模块，这个模块是用来继续处理上一个模块的输出的，该模块通过引入两个外部记忆单元 $M_k$ 和 $M_v $作为 Key 和 Value，将多粒度原型特征 $F_s$ 和 $F_p$ 作为 Query，计算跨粒度的注意力权重来捕获不同粒度之间的上下文关联。通过多头注意力机制聚合不同粒度的信息，输出增强后的多粒度特征 $\tilde{F}_s = [\tilde{P}_1^s, \tilde{P}_2^s, ..., \tilde{P}_M^s]$ 和 $\tilde{F}_p = [\tilde{P}_1^p, \tilde{P}_2^p, ..., \tilde{P}&lt;em&gt;M^p]$，最后与全局 [CLS] token 拼接得到完整的增强特征集 $f_s^{en} = [f&lt;/em&gt;{cls}^{s,L}, \tilde{P}_1^s, \tilde{P}_2^s, ..., \tilde{P}&lt;em&gt;M^s]$ 和 $f_p^{en} = [f&lt;/em&gt;{cls}^{p,L}, \tilde{P}_1^p, \tilde{P}_2^p, ..., \tilde{P}_M^p]$。具体流程如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805863744_f6.png&quot; alt=&quot;f6.png&quot;&gt;&lt;/p&gt;
&lt;h4&gt;MGPC (Multi-Granularity Prototype-aware Contrastive Loss)&lt;/h4&gt;
&lt;p&gt;多粒度原型感知对比损失模块，该模块使用上一个模块输出的 $f_{en}^s$ 和 $f_{en}^p$，计算对比损失 $L_{mgpc}$ 来进一步对齐跨模态的多粒度特征表示。该损失通过增加正样本对（配对的草图-照片）在多粒度特征空间中的相似度，同时减小负样本对的相似度，促进草图和照片在多粒度层面的特征对齐。具体流程和公式如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805866459_f7.png&quot; alt=&quot;f7.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;总体损失，便是上述所有损失加权求和，其中取 $\lambda_1 = 0.6$，$\lambda_2 = 1.0$。这两个参数调优在后面实验会进行探讨。&lt;/p&gt;
&lt;h2&gt;3. 实验&lt;/h2&gt;
&lt;h3&gt;数据集设置&lt;/h3&gt;
&lt;p&gt;实验设置如下，用到了四个数据集：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805970138_image-20251122170645389.png&quot; alt=&quot;image-20251122170645389.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;分别是椅子数据集、鞋子数据集、衣服数据集和行人数据集。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805863128_f8.png&quot; alt=&quot;f8.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;与当前先进的方法对比&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805867741_f9.png&quot; alt=&quot;f9.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图三个表展示了在不同数据集下，与当前先进的方法进行的对比，可以发现改论文的模型总体效果更好，尤其是在椅子和鞋子数据集上，在所有指标上都具有提升。&lt;/p&gt;
&lt;h3&gt;消融实验&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805864365_f10.png&quot; alt=&quot;f10.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图，第一个表是在鞋子数据集上进行的消融实验分析，探讨论文提出的四个创新性的模块的作用，可以发现只有所有模块都使用时，指标是最高的，这说明每个模块都不可或缺。&lt;/p&gt;
&lt;p&gt;第二个表是对各损失函数进行消融分析，也是在鞋子数据集上进行实验分析，可以发现，移除任意一种损失函数都会导致模型性能出现不同程度的下降。这些结果验证了所提出的每个损失函数对模型整体性能的有效贡献。&lt;/p&gt;
&lt;h3&gt;超参数分析&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805869685_f11.png&quot; alt=&quot;f11.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，(a) 图展示了在不同层进行 token 交换对模型性能的影响。实验结果表明，随着层数 $l$ 的增加，各准确率和 mAP 均呈现出逐步上升的趋势，并在 $l = 11$ 时达到峰值，因此论文 $l$ 取 $11$。&lt;/p&gt;
&lt;p&gt;(b) 图展示了多粒度数量 $M$ (也就是前面的分组数量) 对模型性能的影响。实验结果显示，当 $M$ 取 $7$ 时，各项指标整体最好，且 mAP 最高。&lt;/p&gt;
&lt;p&gt;(c) 图对比了本文提出的 MGPM 模块与传统固定块划分策略（PCB）的性能差异。实验结果表明，MGPL 在所有评价指标上均显著优于 PCB，验证了层次化粒度划分策略的有效性，因此论文采用 MGPM 作为多粒度特征学习方法。&lt;/p&gt;
&lt;p&gt;后面两个折线图分别是来实验 $\lambda_1$ 与 $\lambda_2$ 的取值对实验的影响的，可以发现 $\lambda_1=0.6$，$\lambda_2=1.0$ 时效果最好。&lt;/p&gt;
&lt;h3&gt;实验展示&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763805843210_f12.png&quot; alt=&quot;f12.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图展示了该论文提出的方法在四个数据集上的检索结果可视化，其中绿色方框表示检索正确的图像，红色方框表示检索错误的图像。方法能够将正确的目标图像排在更靠前的位置。这一改进在椅子、鞋子、服装和行人等不同类型的数据集上均有体现，证明了该论文框架具有良好的泛化能力。&lt;/p&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;p&gt;MCGI 框架通过利用跨模态互补信息和跨粒度上下文关联来解决细粒度草图图像检索问题，从而弥合模态差异并增强特征判别性。&lt;/p&gt;
&lt;p&gt;跨模态信息补偿（CMIC）模块通过 token 交换和知识蒸馏整合草图和照片的互补信息，以学习模态鲁棒的特征表示。&lt;/p&gt;
&lt;p&gt;跨粒度原型交互（CGPI）模块分层提取多粒度特征并建模其上下文交互，以捕获判别性的细粒度信息。&lt;/p&gt;
&lt;p&gt;在四个基准数据集上的大量实验证明了最先进的性能，在 QMUL-Chair-V2 上达到 78.6% 的 Rank-1 准确率，在 QMUL-Shoe-V2 上达到 44.5%，在 Clothes-V1 上达到 96.0%，在 Sketch Re-ID 上达到 91.5%。&lt;/p&gt;
&lt;p&gt;消融研究验证了各个组件的有效性，其中 CMIC 提高了模态鲁棒性，CGPI 通过分层特征学习和跨粒度交互增强了细粒度判别性。&lt;/p&gt;
</content:encoded><category>SBIR</category><category>多模态</category><author>Glader</author></item><item><title>略微探讨 Spring 事务</title><link>https://blog.mygld.top/zh-cn/posts/transaction/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/transaction/</guid><description>Spring 中的事务的相关知识点。</description><pubDate>Wed, 19 Nov 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 什么是事务&lt;/h2&gt;
&lt;h3&gt;事务的定义&lt;/h3&gt;
&lt;p&gt;事物通常是指一组要被视为&lt;strong&gt;整体执行&lt;/strong&gt;的活动或事件。&lt;/p&gt;
&lt;p&gt;举个例子，比如银行转账，A 账户向 B 账户进行转账 100 元，此时要求下面两种操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A 账户余额减少 100 元&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;B 账户余额增加 100 元&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个操作必须一起成功或一起失败。如果 A 账户余额减少了，B 账户没有增加，亦或者 A 账户余额没变，B 账户余额增加了，这就会出现&lt;strong&gt;数据不一致的问题&lt;/strong&gt;，也就是银行系统出现了错误，账务记录不再可靠。&lt;/p&gt;
&lt;p&gt;事务就是来解决类似于上述情况的问题，这便就是&lt;strong&gt;事务存在的意义&lt;/strong&gt;：确保操作要么全部成功，要么全部失败，避免部分完成导致的数据错误，从而保证数据的一致性与完整性。&lt;/p&gt;
&lt;h3&gt;事务的性质&lt;/h3&gt;
&lt;p&gt;事务具有 ACID 四种特性，即原子性（Atomicity）、一致性（Consistency）、隔离性（Isolation）、持久性（Durability）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原子性&lt;/strong&gt;：事务不可分割。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一致性&lt;/strong&gt;：事务前后数据保持有效状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;隔离性&lt;/strong&gt;：多个事务互不干扰。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;持久性&lt;/strong&gt;：提交后数据永久保存。&lt;/p&gt;
&lt;h2&gt;2. MySQL 中的事务&lt;/h2&gt;
&lt;h3&gt;注意事项&lt;/h3&gt;
&lt;p&gt;MySQL 并非所有存储引擎都支持事务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;支持事务&lt;/strong&gt;：InnoDB（默认）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不支持事务&lt;/strong&gt;：MyISAM（不支持事务，数据操作无法回滚）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用事务前，请确保表是 &lt;strong&gt;InnoDB 引擎&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;基本事务操作&lt;/h3&gt;
&lt;p&gt;在 MySQL 中，我们可以使用 &lt;code&gt;START TRANSACTION&lt;/code&gt; 或 &lt;code&gt;BEGIN&lt;/code&gt; 进行开启事务，然后执行 SQL 操作。然后使用 &lt;code&gt;COMMIT&lt;/code&gt; 进行事务的提交或使用 &lt;code&gt;ROLLBACK&lt;/code&gt; 进行事务的回滚。&lt;/p&gt;
&lt;p&gt;下面我们以上面的银行转账进行举例，首先创建一张简化的用户余额数据表，并初始化一定的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE accounts (
    id CHAR(1) PRIMARY KEY,
    balance DECIMAL(10,2) NOT NULL
) ENGINE=InnoDB;

INSERT INTO accounts (id, balance) VALUES
(&amp;#39;A&amp;#39;, 1000.00),
(&amp;#39;B&amp;#39;, 500.00);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意：正常项目中主键通常用整数类型，这里为了符合上面的题目例子，方便解释设置为 char 类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763568602728_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来我们在 console 中输入 &lt;code&gt;START TRANSACTION&lt;/code&gt; 开启事务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763568677995_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在我们我们创建一个转账的操作，并执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 扣减 A 账户余额
UPDATE accounts
SET balance = balance - 100
WHERE id = &amp;#39;A&amp;#39;;

-- 增加 B 账户余额
UPDATE accounts
SET balance = balance + 100
WHERE id = &amp;#39;B&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763568758800_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时我们使用 &lt;code&gt;SELECT * FROM accounts&lt;/code&gt; 来查看当前的表状态：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763568836709_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以发现已经操作成功了，A 与 B 的余额发生了变化，但是我们此时再回到 IDEA 的数据库工具面板中查看表格数据，却发现余额没有变化：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763568884300_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这便是&lt;strong&gt;事务隔离性&lt;/strong&gt;的体现。这些修改只在&lt;strong&gt;事务内部&lt;/strong&gt;可见。在 &lt;strong&gt;同一个会话&lt;/strong&gt;中，事务未提交前对数据的修改是可见的，但对于其他会话，这些修改仍然不可见，直到事务提交后才会对外生效。我们在 IDEA 中打开数据库工具面板查看表格数据，这相当于开启了一个新的会话，这和刚才的事务是相互独立的，因此数据并没有发生变化。&lt;/p&gt;
&lt;p&gt;此时如果我们使用 &lt;code&gt;COMMIT&lt;/code&gt; 提交事务，此时数据才正式的写到数据库表中去持久化，这便是事务的&lt;strong&gt;持久性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763569321757_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时，我们就会发现在 IDEA 的数据库工具中查看表格数据就被修改了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763569293218_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;同理，使用 &lt;code&gt;ROLLBACK&lt;/code&gt; 就可以&lt;strong&gt;撤销事务中所有未提交的操作&lt;/strong&gt;，将数据恢复到事务开始之前的状态。这样，即使在事务内进行了多次修改，也不会影响数据库的实际数据，实现 &lt;strong&gt;原子性&lt;/strong&gt; 和 &lt;strong&gt;一致性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763569536600_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763569568256_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3.  Spring 中的事务&lt;/h2&gt;
&lt;h3&gt;示例 Demo&lt;/h3&gt;
&lt;p&gt;我们编写如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Mapper
public interface AccountsMapper {
    @Update(&amp;quot;UPDATE accounts &amp;quot; +
            &amp;quot;SET balance = balance + #{delta} &amp;quot; +
            &amp;quot;WHERE id = #{id}&amp;quot;)
    void updateByDelta(@Param(&amp;quot;id&amp;quot;) Character id, @Param(&amp;quot;delta&amp;quot;) BigDecimal delta);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class AccountsService {
    @Autowired
    private AccountsMapper accountsMapper;

    public void transfer(Character id1, Character id2, BigDecimal delta) {
        accountsMapper.updateByDelta(id1,BigDecimal.ZERO.subtract(delta));
        System.out.println(1 / 0);
        accountsMapper.updateByDelta(id2,delta);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;AccountsService&lt;/code&gt; 中我们定义了一个简易的 &lt;code&gt;transfer&lt;/code&gt; 转账方法。在该方法中，我们故意写了一个 &lt;code&gt;1/0&lt;/code&gt;，让其抛出运行时异常，运行后我们查看抛出异常后，数据库中的数据有没有发生变化。&lt;/p&gt;
&lt;p&gt;我们创建一个测试单元：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@SpringBootTest
class DemoApplicationTests {
    @Autowired
    private AccountsService accountsService;
    @Test
    void  testTransaction(){
        accountsService.transfer(&amp;#39;A&amp;#39;,&amp;#39;B&amp;#39;,new BigDecimal(&amp;quot;100&amp;quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后我们发现，抛出了一个异常：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763605878341_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时我们去查看数据库表，我们发现，出问题了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763606126461_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;A 账户的余额减少了 100 元，B 账户的余额却没有增加，总的余额 1500 缺无缘无故少了 100 元，这就对数据的一致性与完整性造成了问题。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;AccountsService&lt;/code&gt; 代码中，首先成功运行了 &lt;code&gt;accountsMapper.updateByDelta(id1,BigDecimal.ZERO.subtract(delta));&lt;/code&gt;,但是之后抛出异常后，下面的 &lt;code&gt;accountsMapper.updateByDelta(id2,delta);&lt;/code&gt; 就不执行了，因此就造成了上述问题。而我们希望的是，这两个 &lt;code&gt;update&lt;/code&gt; 要么同时成功，要么同时失败才可以。因此，必须把这两个 &lt;code&gt;update&lt;/code&gt; 封成一个事务去执行，一旦其中一个失败，则全部 &lt;code&gt;ROLLBACK&lt;/code&gt; 回滚。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;@Transactional&lt;/code&gt; 的基本用法&lt;/h3&gt;
&lt;p&gt;在 Spring 框架中，事务管理是通过 &lt;strong&gt;Spring 的事务抽象&lt;/strong&gt;来实现的，它可以让我们在业务代码中方便地管理事务，而不需要直接操作数据库的 &lt;code&gt;START TRANSACTION&lt;/code&gt;、&lt;code&gt;COMMIT&lt;/code&gt; 或 &lt;code&gt;ROLLBACK&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 Spring 框架中，我们往往使用 &lt;code&gt;@Transactional&lt;/code&gt; 注解来声明某个方法或类需要在事务中执行。Spring 会在方法执行前自动开启事务，方法执行成功后提交事务，如果在执行过程中出现 &lt;strong&gt;RuntimeException&lt;/strong&gt; 或 &lt;strong&gt;Error&lt;/strong&gt;，事务会自动回滚，从而保证操作的&lt;strong&gt;原子性&lt;/strong&gt;和&lt;strong&gt;一致性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt; 既可以加在方法上，又可以加在类上。如果加在方法上，则只对该方法生效，事务仅在执行该方法时开启；如果加在类上，则作用于类中所有公共方法，相当于给每个公共方法都加了事务。但在实际开发中，通常是只加在需要用的方法上，而不是直接加在类上。&lt;/p&gt;
&lt;p&gt;下面我们把刚才的 &lt;code&gt;AccountsService&lt;/code&gt; 中的 &lt;code&gt;transfer&lt;/code&gt; 方法上，加上 &lt;code&gt;@Transactional&lt;/code&gt; 的注解，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class AccountsService {
    @Autowired
    private AccountsMapper accountsMapper;

    @Transactional
    public void transfer(Character id1, Character id2, BigDecimal delta) {
        accountsMapper.updateByDelta(id1,BigDecimal.ZERO.subtract(delta));
        System.out.println(1 / 0);
        accountsMapper.updateByDelta(id2,delta);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了对比，我先把数据库的数据还原成 900 与 600。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763569293218_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;再次运行单元测试，抛出异常后，查看数据库结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763606647857_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;刷新后，可以看到，数据没有任何变动修改,这说明事务成功回滚了。当 &lt;code&gt;transfer&lt;/code&gt; 方法执行到 &lt;code&gt;1/0&lt;/code&gt; 抛出 &lt;code&gt;ArithmeticException&lt;/code&gt; 异常时，Spring 检测到运行时异常，自动触发了事务回滚，撤销了之前对 A 账户的扣款操作，保证了数据的一致性。&lt;/p&gt;
&lt;p&gt;如果我们此时把 &lt;code&gt;1/0&lt;/code&gt; 的代码删掉，然后再去运行，此时没有任何异常，因此会 &lt;code&gt;COMMIT&lt;/code&gt; 成功，刷新数据库表如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763606959856_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;@Transactional&lt;/code&gt; 的高级用法&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;rollBackFor&lt;/code&gt; 属性&lt;/h4&gt;
&lt;p&gt;OK，刚才前面说的是遇到运行时异常时自动回滚，那如果异常不是运行时异常，还会不会自动回滚？我们来测试一下。我们修改 &lt;code&gt;transfer&lt;/code&gt; 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class AccountsService {
    @Autowired
    private AccountsMapper accountsMapper;
    @Transactional
    public void transfer(Character id1, Character id2, BigDecimal delta) throws Exception {
        accountsMapper.updateByDelta(id1,BigDecimal.ZERO.subtract(delta));
        if(true){
            throw new Exception(&amp;quot;手动抛出异常~&amp;quot;);
        }
        accountsMapper.updateByDelta(id2,delta);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;if(true)&lt;/code&gt; 可以让编译器认为下面的语句可能会被执行到，因此不会编译报错，相当于“骗”了一下编译器。&lt;/p&gt;
&lt;p&gt;单元测试里我们捕获异常，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@SpringBootTest
class DemoApplicationTests {
    @Autowired
    private AccountsService accountsService;
    @Test
    void  testTransaction(){
        try {
            accountsService.transfer(&amp;#39;A&amp;#39;,&amp;#39;B&amp;#39;,new BigDecimal(&amp;quot;100&amp;quot;));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行单元测试后刷新并查看数据库表我们发现，又出问题了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763607383019_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;还是和最初始的那样，即使加了 &lt;code&gt;@Transactional&lt;/code&gt;，也出现了之前的情况，A 的余额减少，B 的余额没增加，这是因为什么？&lt;/p&gt;
&lt;p&gt;就像我们刚才所说的，&lt;code&gt;@Transactional&lt;/code&gt; 默认只能对运行时异常自动回滚，因为 &lt;code&gt;ArithmeticException&lt;/code&gt; 是 &lt;code&gt;RuntimeException&lt;/code&gt; 的子类，因此可以自动回滚，但是 &lt;code&gt;Exception&lt;/code&gt; 不是运行时异常，因此无法自动回滚。&lt;/p&gt;
&lt;p&gt;怎么办，难道我们就没有办法解决这个问题了吗？我们马上去看一下 &lt;code&gt;@Transactional&lt;/code&gt; 的源码，来看看这个注解里有没有什么属性我们可以设置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor(&amp;quot;transactionManager&amp;quot;)
    String value() default &amp;quot;&amp;quot;;

    @AliasFor(&amp;quot;value&amp;quot;)
    String transactionManager() default &amp;quot;&amp;quot;;

    String[] label() default {};

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    String timeoutString() default &amp;quot;&amp;quot;;

    boolean readOnly() default false;

    Class&amp;lt;? extends Throwable&amp;gt;[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class&amp;lt;? extends Throwable&amp;gt;[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现了 &lt;code&gt; Class&amp;lt;? extends Throwable&amp;gt;[] rollbackFor() default {};&lt;/code&gt;，听这名字一看 &lt;code&gt;rollbackFor&lt;/code&gt;，为...而回滚，这不正是我们想要的吗？他的传入值是一个 Class 数组的形式，我们立马回到我们的 &lt;code&gt;transfer&lt;/code&gt; 方法，添加上这个属性，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class AccountsService {
    @Autowired
    private AccountsMapper accountsMapper;

    @Transactional(rollbackFor = {Exception.class})
    public void transfer(Character id1, Character id2, BigDecimal delta) throws Exception {
        accountsMapper.updateByDelta(id1,BigDecimal.ZERO.subtract(delta));
        if(true){
            throw new Exception(&amp;quot;手动抛出异常~&amp;quot;);
        }
        accountsMapper.updateByDelta(id2,delta);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们把数据库数据还原成 800 和 700，再去进行单元测试，结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763607945642_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这下就非常完美了，异常也抛出了，数据也没有被修改，说明 &lt;code&gt;ROLLBACK&lt;/code&gt; 回滚了。&lt;/p&gt;
&lt;h4&gt;事务传播行为&lt;/h4&gt;
&lt;p&gt;好，但是我们又有一个问题。我们在项目中，往往会有储存操作日志的业务逻辑，下面我们创建一个 &lt;code&gt;accounts_log&lt;/code&gt; 表。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE accounts_log (
    id BigInt PRIMARY KEY AUTO_INCREMENT,
    message TEXT NOT NULL
) ENGINE=InnoDB;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;请注意，我这是简化的 log 表，正常项目开发没这么简单，还有很多别的字段，如日志类型等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后创建一个实体类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Data
@AllArgsConstructor
@NoArgsConstructor
public class AccountsLog {
    private Long id;
    private String message;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个 Mapper：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Mapper
public interface AccountsLogMapper {
    @Insert(&amp;quot;INSERT INTO accounts_log VALUES(#{id},#{message})&amp;quot;)
    void insert(AccountsLog accountsLog);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个 Service：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class AccountsLogService {
    @Autowired
    private AccountsLogMapper accountsLogMapper;
    
    public void insert(AccountsLog accountsLog) {
        accountsLogMapper.insert(accountsLog);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改造 transfer 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class AccountsService {
    @Autowired
    private AccountsMapper accountsMapper;

    @Autowired
    private AccountsLogService accountsLogService;

    @Transactional(rollbackFor = {Exception.class})
    public void transfer(Character id1, Character id2, BigDecimal delta) throws Exception {
        String message = id1 + &amp;quot;给&amp;quot;
                + id2 + &amp;quot;发起转账&amp;quot; + delta + &amp;quot;元, &amp;quot;;
        try {
            accountsMapper.updateByDelta(id1,BigDecimal.ZERO.subtract(delta));
            if(true){
                throw new Exception(&amp;quot;手动抛出异常~&amp;quot;);
            }
            accountsMapper.updateByDelta(id2,delta);
            message = &amp;quot;成功了！&amp;quot;;
        } catch (Exception e){
            message += &amp;quot;但是失败了！&amp;quot;;
            throw e;//继续抛出异常，要不然 @Transactional 检测不到
        }finally {
            accountsLogService.insert(new AccountsLog(null,message));
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们要求转账不论是否成功，都要记录日志，如果只这样写，我们运行单元测试后发现：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763609907018_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;异常抛出了，但是 &lt;code&gt;accounts_log&lt;/code&gt; 表里却没有数据，这是因为虽然 &lt;code&gt;accountsLogService.insert&lt;/code&gt; 是在 &lt;code&gt;finally&lt;/code&gt; 里被调用，但是该操作仍然在同一个事务方法下被约束，只要有异常，所有修改都会被回滚，包括 &lt;code&gt;finally&lt;/code&gt; 中的操作。那么我们如何解决这个问题呢？&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Transactional&lt;/code&gt; 的源码中我们看到有一个属性 &lt;code&gt;Propagation propagation() default Propagation.REQUIRED;&lt;/code&gt;，这个属性用于控制&lt;strong&gt;事务的传播行为&lt;/strong&gt;，即当一个事务方法被另一个事务方法调用时，如何处理事务的问题。&lt;/p&gt;
&lt;p&gt;Spring 定义了 7 种事务传播行为:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;传播行为&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;REQUIRED&lt;/strong&gt; (默认)&lt;/td&gt;
&lt;td&gt;如果当前存在事务，则加入该事务；如果没有事务，则创建一个新事务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;REQUIRES_NEW&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;总是创建新事务，如果当前存在事务，则将当前事务挂起&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SUPPORTS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;如果当前存在事务，则加入该事务；如果没有事务，则以非事务方式执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NOT_SUPPORTED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;以非事务方式执行，如果当前存在事务，则将当前事务挂起&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MANDATORY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;如果当前存在事务，则加入该事务；如果没有事务，则抛出异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NEVER&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;以非事务方式执行，如果当前存在事务，则抛出异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NESTED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;如果当前存在事务，则在嵌套事务内执行；如果没有事务，则创建一个新事务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;对于我们的日志记录需求，应该使用 &lt;strong&gt;&lt;code&gt;Propagation.REQUIRES_NEW&lt;/code&gt;&lt;/strong&gt;，它会创建一个&lt;strong&gt;完全独立的新事务&lt;/strong&gt;，不受外层事务回滚的影响。&lt;/p&gt;
&lt;p&gt;我们修改 &lt;code&gt;AccountsLogService&lt;/code&gt; 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class AccountsLogService {
    @Autowired
    private AccountsLogMapper accountsLogMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insert(AccountsLog accountsLog) {
        accountsLogMapper.insert(accountsLog);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们再去运行单元测试查看数据库表结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1763610392093_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;相当完美，我们的日志添加成功了。对于当前这个例子，在进行内层事务时，外层事务会挂起，等待内层事务声明周期结束后，才重新唤醒外层事务。&lt;/p&gt;
&lt;p&gt;但是在正式的项目开发中，我们大部分情况是使用默认的传播方式的（约 85 % ~ 90 % 的情景），其次使用较多的就是 &lt;code&gt;REQUIRES_NEW&lt;/code&gt; (约 5 % ~10 %)，其他传播方式使用的概率不是很大，感兴趣的小伙伴可以自行去了解。&lt;/p&gt;
</content:encoded><category>Java</category><category>事务</category><category>MySQL</category><category>Spring</category><author>Glader</author></item><item><title>Spring Bean 的作用域与生命周期</title><link>https://blog.mygld.top/zh-cn/posts/beanpro/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/beanpro/</guid><description>Spring Bean 的作用域与生命周期相关知识点。</description><pubDate>Mon, 20 Oct 2025 15:31:53 GMT</pubDate><content:encoded>&lt;p&gt;Spring Bean 的作用域默认是单例 (Singleton)，同时支持修改为多例 (Prototype)。那么怎么具体来实现呢？&lt;/p&gt;
&lt;h2&gt;Singleton&lt;/h2&gt;
&lt;p&gt;先来了解一下什么是单例。&lt;/p&gt;
&lt;p&gt;我们举一个不太恰当的例子，我们在写一个配置类 &lt;code&gt;ArrayListConfig&lt;/code&gt;，用来把 &lt;code&gt;ArrayList&lt;/code&gt; 交给 &lt;code&gt;IOC&lt;/code&gt; 容器管理，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class ArrayListConfig {

    @Bean
    public ArrayList&amp;lt;String&amp;gt; arrayList() {
        return new ArrayList&amp;lt;String&amp;gt;();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意，事实上，我们正常开发做项目的时候是不会让 &lt;code&gt;IOC&lt;/code&gt; 来管理 &lt;code&gt;ArrayList&lt;/code&gt; 的生命周期的，这里仅仅为了方便演示，便于学习。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;好，我们现在创建好了这个配置类，然后我们写一个单元测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
private ArrayList&amp;lt;String&amp;gt; arrayList1;
@Autowired
private ArrayList&amp;lt;String&amp;gt; arrayList2;
@Test
void test1(){
    arrayList1.add(&amp;quot;1&amp;quot;);
    arrayList2.add(&amp;quot;2&amp;quot;);
    System.out.println(arrayList1);
    System.out.println(arrayList2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么大家思考一下，打印输出的结果会是什么呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1760974541072_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;他输出的都是 &lt;code&gt;[1, 2]&lt;/code&gt;，这就是 &lt;code&gt;Singleton&lt;/code&gt;。在 &lt;code&gt;IOC&lt;/code&gt; 容器启动时，就会创建一个 &lt;code&gt;Bean&lt;/code&gt; 实例，之后我们自动装配后获得的对象，都是指向同一个 &lt;code&gt;Bean&lt;/code&gt; 实例，因此才会输出同样的内容。&lt;/p&gt;
&lt;p&gt;那么我们如何才能够实现这两个 &lt;code&gt;ArrayList&lt;/code&gt; 指向不同的 &lt;code&gt;Bean&lt;/code&gt; 实例呢？这就要求我们使用多例 &lt;code&gt;Prototype&lt;/code&gt; 了。&lt;/p&gt;
&lt;h2&gt;Prototype&lt;/h2&gt;
&lt;p&gt;多例很简单，我们只需要使用注解 &lt;code&gt;@Scope&lt;/code&gt; 即可，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class ArrayListConfig {

    @Bean
    @Scope(&amp;quot;prototype&amp;quot;)
    public ArrayList&amp;lt;String&amp;gt; arrayList() {
        return new ArrayList&amp;lt;String&amp;gt;();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1760975018552_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图，此时便达到我们想要的效果了，这就是多例。&lt;/p&gt;
&lt;p&gt;实际上，单例是 &lt;code&gt;@Scope(&amp;quot;singleton&amp;quot;)&lt;/code&gt;，但我们一般省略不写。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Scope&lt;/code&gt; 也可以结合 &lt;code&gt;@Compoment&lt;/code&gt; 等在定义类时直接实现单例或多例，这里便不再演示。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意，&lt;strong&gt;多例 Bean &lt;strong&gt;有时候也称为&lt;/strong&gt;原型 Bean&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bean 的生命周期&lt;/h2&gt;
&lt;p&gt;不知道大家有没有思考过 &lt;code&gt;Bean&lt;/code&gt; 的生命周期这一问题。&lt;/p&gt;
&lt;p&gt;就比如，我们交给 &lt;code&gt;IOC&lt;/code&gt; 容器管理的 &lt;code&gt;Bean&lt;/code&gt;，他都是什么时候创建的呢？就比如我们上面的那个 &lt;code&gt;ArrayList&lt;/code&gt;，他是在我们第一次使用的时候创建的，还是容器一启动的时候就创建好了？&lt;/p&gt;
&lt;p&gt;为了便于测试，现在我先手动创建一个类，并交给 &lt;code&gt;IOC&lt;/code&gt; 容其管理，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component
public class Kunkun {
    public Kunkun() {
        System.out.println(&amp;quot;坤坤诞生了！&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们写个单元测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
void test2(){
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1760975922695_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上，我们发现，即使没有使用 &lt;code&gt;Kunkun&lt;/code&gt;（我们也没有 @Autowired 一个 Kunkun），&lt;code&gt;Kunkun&lt;/code&gt; 的构造函数也被调用了，这说明默认情况下，容器启动时初始化单例 Bean。&lt;/p&gt;
&lt;p&gt;那么有些时候，提前把所有 &lt;code&gt;Bean&lt;/code&gt; 对象都创建好是比较浪费资源的，那么我们能不能这样，就是我们第一次使用这个 &lt;code&gt;Bean&lt;/code&gt; 的时候，再去创建，之后再用直接就从 &lt;code&gt;IOC&lt;/code&gt; 容器中拿就可以了，能否这样呢？是可以的，我们只需要加上 &lt;code&gt;@Lazy&lt;/code&gt; 这个注解就可以了，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component
@Lazy
public class Kunkun {
    public Kunkun() {
        System.out.println(&amp;quot;坤坤诞生了！&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候我们再去运行刚才的单元测试 &lt;code&gt;Test2&lt;/code&gt; 就不会有任何输出了。&lt;/p&gt;
&lt;p&gt;那么此时我们修改一下单元测试，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
private Kunkun kunkun;

@Test
void test2(){
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们此时运行发现：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1760976350481_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这说明，此时 &lt;code&gt;Kunkun&lt;/code&gt; 的 &lt;code&gt;Bean&lt;/code&gt; 对象在使用时，才进行了创建。&lt;/p&gt;
&lt;p&gt;请注意，我们上述讲的都是针对单例 &lt;code&gt;Bean&lt;/code&gt; 的情况，如果是多例 &lt;code&gt;Bean&lt;/code&gt; 呢？&lt;/p&gt;
&lt;p&gt;我们不妨在刚才的类里加一个 &lt;code&gt;@Scope(&amp;quot;prototype&amp;quot;)&lt;/code&gt;，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;

import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(&amp;quot;prototype&amp;quot;)
public class Kunkun {
    public Kunkun() {
        System.out.println(&amp;quot;坤坤诞生了！&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们仍然不声明并创建 &lt;code&gt;Kunkun&lt;/code&gt;，来看看启动时有没有出现 &lt;code&gt;Kunkun&lt;/code&gt; 的构造方法。&lt;/p&gt;
&lt;p&gt;经过测试，发现的确没有任何关于 &lt;code&gt;Kunkun&lt;/code&gt; 构造方法的输出，这说明对于多例 &lt;code&gt;Bean&lt;/code&gt;，不会在 &lt;code&gt;IOC&lt;/code&gt; 容器启动时就被初始化创建。&lt;/p&gt;
&lt;p&gt;那么我们 &lt;code&gt;@Autowired&lt;/code&gt; 两个 &lt;code&gt;Kunkun&lt;/code&gt;，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
private Kunkun kunkun1;
@Autowired
private Kunkun kunkun2;
@Test
void test3(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后输出：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1760977093828_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，构造方法调用成功了，我们不难得出结论，对于多例 &lt;code&gt;Bean&lt;/code&gt;，默认就是懒加载方式，如果你还想加 &lt;code&gt;@Lazy&lt;/code&gt;，当然也是不会报错的，但是加上没什么用。&lt;/p&gt;
&lt;p&gt;好了，聊完了单例和多例 &lt;code&gt;Bean&lt;/code&gt; 的创建，我们再聊聊销毁。&lt;/p&gt;
&lt;p&gt;先看单例 &lt;code&gt;Bean&lt;/code&gt; 的销毁：&lt;/p&gt;
&lt;p&gt;有一个注解 &lt;code&gt;@PreDestroy&lt;/code&gt;，被它所注解的方法，在 &lt;code&gt;Bean&lt;/code&gt; 销毁时会自动执行，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component
public class Kunkun {
    public Kunkun() {
        System.out.println(&amp;quot;坤坤诞生了！&amp;quot;);
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println(&amp;quot;Kunkun 被销毁~&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们随便写个单元测试，&lt;code&gt;@Autowired&lt;/code&gt; 一个 &lt;code&gt;Kunkun&lt;/code&gt; 看看效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1760977566189_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以发现，对于单例 &lt;code&gt;Bean&lt;/code&gt;，在容器被销毁时，&lt;code&gt;Bean&lt;/code&gt; 实例也会被销毁。&lt;/p&gt;
&lt;p&gt;那么我们再把他变成多例 &lt;code&gt;Bean&lt;/code&gt; 实验一下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;

import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;

@Component
@Scope(&amp;quot;prototype&amp;quot;)
public class Kunkun {
    public Kunkun() {
        System.out.println(&amp;quot;坤坤诞生了！&amp;quot;);
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println(&amp;quot;Kunkun 被销毁~&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在单元测试中 &lt;code&gt;@Autowired&lt;/code&gt; 两个 &lt;code&gt;Kunkun&lt;/code&gt;，并测试：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1760977765546_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们发现，只有构造方法被调用了，但容器被销毁后，&lt;code&gt;Bean&lt;/code&gt; 对象并没有被销毁。这是因为对于多例 &lt;code&gt;Bean&lt;/code&gt;，容器并不会去管理它的销毁，必须手动强制去销毁，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
private Kunkun kunkun1;
@Autowired
private Kunkun kunkun2;
@Test
void test3(){
    kunkun1.preDestroy();
    kunkun2.preDestroy();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;默认作用域&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Spring Bean 默认是 &lt;strong&gt;单例（Singleton）&lt;/strong&gt;，整个容器共享一个实例。&lt;/li&gt;
&lt;li&gt;可以通过 &lt;code&gt;@Scope(&amp;quot;prototype&amp;quot;)&lt;/code&gt; 或“多例（Prototype）”来每次获取都创建新实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建时机&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单例 Bean&lt;/strong&gt;：容器启动时创建（非 &lt;code&gt;@Lazy&lt;/code&gt; 时）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多例 Bean&lt;/strong&gt;：每次获取时创建，默认就是按需创建（懒加载）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Lazy&lt;/code&gt;：用于延迟单例 Bean 初始化，对多例 Bean 无实际意义&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;销毁机制&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单例 Bean&lt;/strong&gt;：容器关闭时自动销毁，支持 &lt;code&gt;@PreDestroy&lt;/code&gt; 或 &lt;code&gt;DisposableBean.destroy()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多例 Bean&lt;/strong&gt;：容器不管理销毁，需要手动调用销毁方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用场景&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;单例：全局共享对象、服务类、配置类&lt;/li&gt;
&lt;li&gt;多例：临时对象、短期使用对象、频繁创建的计算或任务对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键点&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;单例 Bean 生命周期长，容器全权管理&lt;/li&gt;
&lt;li&gt;多例 Bean 生命周期短，开发者负责资源释放&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Lazy&lt;/code&gt; 主要优化单例 Bean 的初始化，减少启动开销&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>Java</category><category>Spring</category><category>Bean</category><author>Glader</author></item><item><title>第三方 Bean 注入 IoC 容器</title><link>https://blog.mygld.top/zh-cn/posts/thid-bean/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/thid-bean/</guid><description>第三方 Bean 注入 IoC 容器的一些方法。</description><pubDate>Mon, 13 Oct 2025 16:00:30 GMT</pubDate><content:encoded>&lt;p&gt;在实际开发过程中，我们往往需要集成各类第三方库来提高开发效率或扩展系统功能。然而，在使用 Spring 或 Spring Boot 框架时，如何将这些第三方库中的组件正确地注册到 IoC 容器中，常常成为需要解决的问题。&lt;/p&gt;
&lt;h2&gt;手动注册 @Bean&lt;/h2&gt;
&lt;p&gt;以第三方工具包 &lt;code&gt;HuTool&lt;/code&gt; 举例，首先我们先引入 &lt;code&gt;HuTool&lt;/code&gt; 的 &lt;code&gt;Maven&lt;/code&gt; 依赖，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;    
    &amp;lt;groupId&amp;gt;cn.hutool&amp;lt;/groupId&amp;gt;    
    &amp;lt;artifactId&amp;gt;hutool-all&amp;lt;/artifactId&amp;gt;    
    &amp;lt;version&amp;gt;5.8.41&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;HuTool&lt;/code&gt; 中，如果我们要使用 &lt;code&gt;cn.hutool.http.HttpUtil&lt;/code&gt; 工具类，我们希望能够像这样使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
private HttpUtil httpUtil;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但很明显，直接这样写会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;No qualifying bean of type &amp;#39;cn.hutool.http.HttpUtil&amp;#39; available
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 &lt;code&gt;Hutool&lt;/code&gt; 的类都是静态方法，没有注册到 &lt;code&gt;Spring&lt;/code&gt; 容器中。&lt;/p&gt;
&lt;p&gt;我们可以手动在配置类中创建 Bean，使用 &lt;code&gt;@Bean&lt;/code&gt; 注解，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class HutoolConfig {
    @Bean
    public HttpUtil getHttpUtil() {
        return new HttpUtil();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们进行单元测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
private HttpUtil httpUtil;
@Test
void test1(){
    System.out.println(httpUtil.get(&amp;quot;https://www.baidu.com&amp;quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1760370708748_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果在配置类中创建 Bean 时，你有需要传入形参，如果 &lt;code&gt;IOC&lt;/code&gt; 容器中有这个形参的类型，则可以自动注入。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class SecurityConfig {
    @Bean
    public AuthenticationManager getAuthenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                   .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;他相当于省略了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
HttpSecurity http;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;@ComponentScan —— 扩展扫描路径&lt;/h2&gt;
&lt;p&gt;如果第三方库中的某些类已经使用了 &lt;code&gt;@Component&lt;/code&gt;、&lt;code&gt;@Service&lt;/code&gt;、&lt;code&gt;@Repository&lt;/code&gt; 等注解，但这些类不在我们默认的扫描路径下，可以通过 &lt;code&gt;@ComponentScan&lt;/code&gt; 来扩展扫描范围。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@SpringBootApplication
@ComponentScan(basePackages = {
    &amp;quot;com.example.myapp&amp;quot;,           // 你的项目包路径
    &amp;quot;com.baomidou.mybatisplus.extension.plugins&amp;quot;  // 第三方库包路径
})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;basePackages 是一个数组，一旦使用 &lt;code&gt;@ComponentScan&lt;/code&gt;，&lt;code&gt;@SpringBootApplication&lt;/code&gt; 中的默认扫描路径便失效，我们必须在 basePackages 配置上我们项目的包路径，然后再去添加第三方库的包路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;@Import 直接导入配置类&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;@Import&lt;/code&gt; 可以直接导入配置类或普通类到 Spring 容器中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 第三方配置类
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
              .setAddress(&amp;quot;redis://127.0.0.1:6379&amp;quot;)
              .setPassword(&amp;quot;your-password&amp;quot;);
        return Redisson.create(config);
    }
}

// 在主配置类中导入
@Configuration
@Import(RedissonConfig.class)
public class ApplicationConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者直接在启动类上使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@SpringBootApplication
@Import({RedissonConfig.class, OtherConfig.class})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;导入普通类&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@Import&lt;/code&gt; 还可以直接导入没有任何注解的普通类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 第三方普通类
public class DataConverter {
    public String convert(Object data) {
        return JSON.toJSONString(data);
    }
}

// 导入配置
@Configuration
@Import(DataConverter.class)
public class ConverterConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入后可以直接注入使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class DataService {
    @Autowired
    private DataConverter dataConverter;
    
    public String processData(Object data) {
        return dataConverter.convert(data);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ImportSelector 动态注册 Bean&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ImportSelector&lt;/code&gt; 允许我们根据条件动态选择要导入的配置类，这在需要根据环境或配置动态注册 Bean 时非常有用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 自定义 ImportSelector
public class ElasticsearchImportSelector implements ImportSelector {
    
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // 可以根据注解属性、环境变量等动态决定导入哪些配置类
        Map&amp;lt;String, Object&amp;gt; attributes = importingClassMetadata
            .getAnnotationAttributes(EnableElasticsearch.class.getName());
        
        String mode = (String) attributes.get(&amp;quot;mode&amp;quot;);
        
        if (&amp;quot;rest&amp;quot;.equals(mode)) {
            return new String[]{
                &amp;quot;com.example.config.ElasticsearchRestClientConfig&amp;quot;
            };
        } else if (&amp;quot;transport&amp;quot;.equals(mode)) {
            return new String[]{
                &amp;quot;com.example.config.ElasticsearchTransportConfig&amp;quot;
            };
        }
        
        // 默认配置
        return new String[]{
            &amp;quot;com.example.config.ElasticsearchDefaultConfig&amp;quot;
        };
    }
}

// 配置类
public class ElasticsearchRestClientConfig {
    @Bean
    public ElasticsearchClient elasticsearchClient() {
        RestClient restClient = RestClient.builder(
            new HttpHost(&amp;quot;localhost&amp;quot;, 9200)
        ).build();
        
        ElasticsearchTransport transport = new RestClientTransport(
            restClient, new JacksonJsonpMapper()
        );
        
        return new ElasticsearchClient(transport);
    }
}

// 在配置类中使用
@Configuration
@Import(ElasticsearchImportSelector.class)
public class SearchConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更复杂的示例：条件导入&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ConditionalImportSelector implements ImportSelector {
    
    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        List&amp;lt;String&amp;gt; imports = new ArrayList&amp;lt;&amp;gt;();
        
        // 检查类路径中是否存在某个类
        if (ClassUtils.isPresent(&amp;quot;com.example.ThirdPartyClass&amp;quot;, null)) {
            imports.add(&amp;quot;com.example.config.ThirdPartyConfig&amp;quot;);
        }
        
        // 检查系统属性
        if (&amp;quot;true&amp;quot;.equals(System.getProperty(&amp;quot;enable.cache&amp;quot;))) {
            imports.add(&amp;quot;com.example.config.CacheConfig&amp;quot;);
        }
        
        // 检查环境变量
        String profile = System.getenv(&amp;quot;SPRING_PROFILES_ACTIVE&amp;quot;);
        if (&amp;quot;prod&amp;quot;.equals(profile)) {
            imports.add(&amp;quot;com.example.config.ProdConfig&amp;quot;);
        }
        
        return imports.toArray(new String[0]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用 DeferredImportSelector&lt;/h3&gt;
&lt;p&gt;如果需要在所有 &lt;code&gt;@Configuration&lt;/code&gt; 类处理完之后再进行导入，可以实现 &lt;code&gt;DeferredImportSelector&lt;/code&gt; 接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DeferredElasticsearchImportSelector implements DeferredImportSelector {
    
    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        // 延迟导入逻辑
        return new String[]{&amp;quot;com.example.config.ElasticsearchConfig&amp;quot;};
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;@EnableXxx 组合注解&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@EnableXxx&lt;/code&gt; 是一种高层封装的配置方式，它通常组合了 &lt;code&gt;@Import&lt;/code&gt; 和其他注解，为用户提供一个简单的开关来启用某个功能模块。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@EnableXxx&lt;/code&gt;  可以放在启动类上，也可以放在配置类上。&lt;/p&gt;
</content:encoded><category>Bean</category><category>Java</category><author>Glader</author></item><item><title>Spring AOP</title><link>https://blog.mygld.top/zh-cn/posts/aop/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/aop/</guid><description>Spring 中的 AOP 的相关知识点。</description><pubDate>Wed, 08 Oct 2025 11:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Spring AOP 示例注解解释&lt;/h2&gt;
&lt;h3&gt;@Before&lt;/h3&gt;
&lt;p&gt;在目标方法执行前运行。常用于记录日志、参数校验、权限控制等。
当匹配到定义的切点表达式后，Spring 会在目标方法调用前先执行该方法。&lt;/p&gt;
&lt;h3&gt;@Around&lt;/h3&gt;
&lt;p&gt;环绕通知，包裹目标方法的整个执行过程。通过 &lt;code&gt;ProceedingJoinPoint.proceed()&lt;/code&gt; 显式调用目标方法，可以在调用前后添加自定义逻辑。
这是最灵活的通知类型，可以控制方法是否执行、修改参数或处理返回值。&lt;/p&gt;
&lt;h3&gt;@After&lt;/h3&gt;
&lt;p&gt;在目标方法执行结束后执行，无论是否发生异常都会触发。类似于 finally 代码块，常用于资源释放、通用日志等场景。&lt;/p&gt;
&lt;h3&gt;@AfterReturning&lt;/h3&gt;
&lt;p&gt;在目标方法正常返回后执行，如果目标方法抛出了异常则不会调用。常用于处理返回结果或输出执行成功日志。&lt;/p&gt;
&lt;h3&gt;@AfterThrowing&lt;/h3&gt;
&lt;p&gt;当目标方法抛出异常时执行。可用于记录异常信息、发送报警、异常统计等。&lt;/p&gt;
&lt;h3&gt;@Pointcut&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@Pointcut&lt;/code&gt; 用于&lt;strong&gt;抽取切点表达式&lt;/strong&gt;，即将原本写在每个注解中的 &lt;code&gt;execution(...)&lt;/code&gt; 规则提取出来，单独定义成一个可复用的方法。
 这样在多个通知中就可以直接通过方法名来引用，避免重复书写表达式，使切面更加清晰易维护。&lt;/p&gt;
&lt;p&gt;一般定义方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Pointcut(&amp;quot;execution(* top.mygld.demo.service.impl..*(..))&amp;quot;)
public void serviceMethods() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在其他通知中直接使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Before(&amp;quot;serviceMethods()&amp;quot;)
@After(&amp;quot;serviceMethods()&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Spring AOP 代码示例（使用 @Pointcut 抽取）&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class TestAspect {

    // 抽取切点表达式，匹配 service.impl 包下所有方法
    @Pointcut(&amp;quot;execution(* top.mygld.demo.service.impl..*(..))&amp;quot;)
    public void serviceMethods() {}

    @Before(&amp;quot;serviceMethods()&amp;quot;)
    public void before() {
        log.info(&amp;quot;before ....&amp;quot;);
    }

    @Around(&amp;quot;serviceMethods()&amp;quot;)
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&amp;quot;around ... before ....&amp;quot;);
        Object result = joinPoint.proceed();
        log.info(&amp;quot;around ... after ....&amp;quot;);
        return result;
    }

    @After(&amp;quot;serviceMethods()&amp;quot;)
    public void after() {
        log.info(&amp;quot;after ....&amp;quot;);
    }

    @AfterReturning(&amp;quot;serviceMethods()&amp;quot;)
    public void afterReturning() {
        log.info(&amp;quot;afterReturning ....&amp;quot;);
    }

    @AfterThrowing(&amp;quot;serviceMethods()&amp;quot;)
    public void afterThrowing() {
        log.info(&amp;quot;afterThrowing ....&amp;quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些注解的执行顺序大致为：
 当方法正常执行时：&lt;code&gt;@Around → @Before → 方法执行 → @AfterReturning → @After → @Around&lt;/code&gt;
 当方法抛出异常时：&lt;code&gt;@Around → @Before → 方法执行 → @AfterThrowing → @After → @Around&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;通过这些注解的组合，可以灵活地在不同阶段切入业务逻辑，实现统一的日志、监控或安全控制。
而使用 &lt;code&gt;@Pointcut&lt;/code&gt; 则让多个通知共享同一套匹配规则，结构更清晰、可维护性更高。&lt;/p&gt;
&lt;p&gt;当然如果每个方法要单独配置不同的切入点表达式，就可以分开写，例如:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@AfterReturning(&amp;quot;execution(* top.mygld.demo.dao.impl..*(..))&amp;quot;)
public void afterReturning(){
    log.info(&amp;quot;afterReturning ....&amp;quot;);
}

@AfterThrowing(&amp;quot;execution(* top.mygld.demo.service.impl..*(..))&amp;quot;)
public void afterThrowing(){
    log.info(&amp;quot;afterThrowing ....&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 多个通知的执行顺序&lt;/h2&gt;
&lt;p&gt;在 Spring AOP 中，&lt;strong&gt;多个通知的执行顺序&lt;/strong&gt;遵循以下规则：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同一切面类中&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认按照通知类型的自然调用顺序执行：
 &lt;code&gt;@Around → @Before → 目标方法 → @AfterReturning/@AfterThrowing → @After → @Around&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;若存在多个同类型通知（例如多个 &lt;code&gt;@Before&lt;/code&gt;），则按照&lt;strong&gt;方法名的字母顺序&lt;/strong&gt;执行。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;@Before&lt;/code&gt; 中，方法名字母顺序&lt;strong&gt;越小越先&lt;/strong&gt;执行；在 &lt;code&gt;@After&lt;/code&gt; 中，方法名字字母顺序&lt;strong&gt;越小越后&lt;/strong&gt;执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;不同切面类之间&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;默认按类名的字母顺序执行，规则和 1 中相同。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;可通过 &lt;code&gt;@Order&lt;/code&gt; 注解或实现 &lt;code&gt;Ordered&lt;/code&gt; 接口来控制优先级。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@Order&lt;/code&gt; 数值越小，优先级越高（即越早执行 &lt;code&gt;@Before&lt;/code&gt;，越晚执行 &lt;code&gt;@After&lt;/code&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Aspect
@Component
@Order(5)
public class LogAspect1 {}

@Aspect
@Component
@Order(7)
public class LogAspect2 {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;执行过程概览&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;外层切面 @Around
    ↓
  内层切面 @Around
      ↓
    @Before（由外到内）
        ↓
     目标方法执行
        ↓
    @AfterReturning / @AfterThrowing（由内到外）
        ↓
    @After（由内到外）
    ↓
  内层 @Around 结束
↓
外层 @Around 结束
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一机制确保在存在多个切面或通知时，AOP 的执行顺序是&lt;strong&gt;可预测且可精确控制&lt;/strong&gt;的。&lt;/p&gt;
&lt;h2&gt;4. 切入点表达式&lt;/h2&gt;
&lt;h3&gt;execution&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;execution&lt;/code&gt; 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配，语法为：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;带 ? 的表示可以省略的部分。&lt;/p&gt;
&lt;p&gt;1.访问修饰符：可省略（比如：public、protected)
2.包名.类名：可省略（但是不建议）
3.throws异常：可省略（注意是方法上声明抛出的异常，不是实际抛出的异常）&lt;/p&gt;
&lt;p&gt;*：单个独立的任意符号，可以通配任意返回值、包名、类名、方法名、任意类型的参数，也可以通配包、类、方法名的一部分&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;execution(* top.*.service.*.update*(*))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;.. ：多个连续的任意符号，可以通配任意层级的包，或任意类型、任意个数的参数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;execution(* top.mygld..UserService.*(..))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;annotation&lt;/h3&gt;
&lt;p&gt;根据注解去匹配方法，只对添加对应注解的方法有效，例如我们先创建一个注解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把注解加在要匹配的方法上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@LogOperation
@Override
public List&amp;lt;User&amp;gt; findAll() {
    return userDao.findAll();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在切面类中使用 &lt;code&gt;@annotation(注解引用)&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Before(&amp;quot;@annotation(top.mygld.demo.anno.LogOperation)&amp;quot;)
public void before(){
    log.info(&amp;quot;before&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样在 idea 中可以点击前面的 &lt;code&gt;m&lt;/code&gt; 图标，便可验证匹配成功。&lt;/p&gt;
&lt;h2&gt;5. 连接点&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;Spring&lt;/code&gt; 中用 &lt;code&gt;JoinPoint&lt;/code&gt; 抽象了连接点，用它可以获得方法执行时的相关信息，如目标类名、方法名、方法参数等。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;@Around&lt;/code&gt; 通知，获取连接点信息只能使用 &lt;code&gt;ProceedingJoinPoint&lt;/code&gt;。
对于其它四种通知，获取连接点信息只能使用 &lt;code&gt;JoinPoint&lt;/code&gt;，它是 &lt;code&gt;ProceedingJoinPoint&lt;/code&gt; 的父类型。&lt;/p&gt;
&lt;p&gt;下面以 &lt;code&gt;@Before&lt;/code&gt; 进行演示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Before(&amp;quot;@annotation(top.mygld.demo.anno.LogOperation)&amp;quot;)
public void before(JoinPoint jp){
    log.info(&amp;quot;before&amp;quot;);
    //1. 获取目标对象
    Object target = jp.getTarget();
    log.info(&amp;quot;target: {}&amp;quot;, target);
        
    //2. 获取目标类
    String name = target.getClass().getName();
    log.info(&amp;quot;name: {}&amp;quot;, name);
        
    //3. 获取目标方法
    String methodName = jp.getSignature().getName();
    log.info(&amp;quot;methodName: {}&amp;quot;, methodName);
        
    //4. 获取目标方法参数
    Object[] args = jp.getArgs();
    log.info(&amp;quot;args: {}&amp;quot;, args);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ProceedingJoinPoint&lt;/code&gt; 可以执行目标方法，而 &lt;code&gt;JoinPoint&lt;/code&gt; 不可以，前者是后者的子类。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Java</category><category>AOP</category><category>Spring</category><author>Glader</author></item><item><title>volatile 关键字</title><link>https://blog.mygld.top/zh-cn/posts/volatile/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/volatile/</guid><description>Java 中的 volatile 关键字相关知识点。</description><pubDate>Wed, 01 Oct 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;volatile 关键字&lt;/h2&gt;
&lt;p&gt;现在有如下两个代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MyThread extends Thread{
    public static int a = 0;
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(&amp;quot;线程开始执行，当前 a = &amp;quot; + a);
        a = 1;
        System.out.println(&amp;quot;线程执行结束，当前 a = &amp;quot; + a);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        while(true){
            if(MyThread.a == 1){
                System.out.println(&amp;quot;检测到 a = 1&amp;quot;);
                break;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么你认为，如果我运行主类，得到的结果是什么？&lt;/p&gt;
&lt;p&gt;根据我们先前的知识，我们肯定会认为，代码运行后，首先创建了 &lt;code&gt;t&lt;/code&gt; 线程，然后 &lt;code&gt;t&lt;/code&gt; 线程启动，&lt;code&gt;t&lt;/code&gt; 线程与主线程同时执行，此时主线程处于无限循环，等待并监听 &lt;code&gt;MyThread&lt;/code&gt; 中的共享成员 &lt;code&gt;a&lt;/code&gt; 的值，当线程 &lt;code&gt;t&lt;/code&gt; 睡眠 &lt;code&gt;1&lt;/code&gt; 秒后修改 &lt;code&gt;a&lt;/code&gt; 的值变为 &lt;code&gt;1&lt;/code&gt;，之后主线程监听到 &lt;code&gt;a&lt;/code&gt; 的值变成 &lt;code&gt;1&lt;/code&gt;，跳出循环，结束整个程序。也就是说整个程序依次打印输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;线程开始执行，当前 a = 0
线程开始执行，当前 a = 1
检测到 a = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后程序结束运行。这是我们根据代码思考出来的运行结果，但事实真的是这样吗？我们尝试一下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1759329721308_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们发现，程序只只输出了线程 &lt;code&gt;t&lt;/code&gt; 中的内容，输出完成后程序并没有结束，而是仍然在一直运行，即使我们等了很久很久，也没有停止运行，这是为什么呢？&lt;/p&gt;
&lt;p&gt;其实，这种问题，我们称之为多线程的&lt;strong&gt;可见性&lt;/strong&gt;，指的是一个线程对共享变量的修改，其他线程能否立即看到。&lt;/p&gt;
&lt;p&gt;可见性的原因产生有很多，当前这种情况主要是因为：现代 CPU 一般会将变量值缓存在寄存器或本地缓存中，而不是每次都从主内存读取。&lt;/p&gt;
&lt;p&gt;简单来说，对于 &lt;code&gt;static&lt;/code&gt; 修饰的这种共享变量，为了避免多个线程同时直接去修改主内存中的数据而导致一系列的问题，Java 会在读取这个共享变量的时候，先复制一份这个变量的副本到当前线程的工作内存中，如果当前线程不对这个副本的数据进行修改，则不会重新进行获取；只有当前线程对这个副本的数据进行了修改，线程才会去同步这个修改到这个真正的共享变量中去。&lt;/p&gt;
&lt;p&gt;所以，上面的问题就很好分析了，这是因为 &lt;code&gt;t&lt;/code&gt; 线程首先睡眠了 &lt;code&gt;1&lt;/code&gt; 秒，这保证了主线程先去执行了 &lt;code&gt;if(MyThread.a == 1)&lt;/code&gt;，这使得主线程获得了一份 &lt;code&gt;a&lt;/code&gt; 变量的副本，&lt;code&gt;a&lt;/code&gt; 的值是 &lt;code&gt;0&lt;/code&gt;。当 &lt;code&gt;t&lt;/code&gt; 线程睡眠结束后把 &lt;code&gt;a&lt;/code&gt; 改成 &lt;code&gt;1&lt;/code&gt; 后，虽然同步到了共享变量里，但主线程因为没有对 &lt;code&gt;a&lt;/code&gt; 进行过修改，所以主线程一直没有去重新获取 &lt;code&gt;a&lt;/code&gt; 的最新数据，一直都是用的原来 &lt;code&gt;a = 0&lt;/code&gt; 的副本，这就导致了主线程的死循环。&lt;/p&gt;
&lt;p&gt;那么我们容易想到，我们重新让主线程获取一下 &lt;code&gt;a&lt;/code&gt; 的值不就行了？这显然是可以的，那么如何重新获得 &lt;code&gt;a&lt;/code&gt; 的值呢？其实有以下几种方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        while(true){
            Thread.sleep(1);
            if(MyThread.a == 1){
                System.out.println(&amp;quot;检测到 a = 1&amp;quot;);
                break;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以在 &lt;code&gt;while&lt;/code&gt; 里面加上一句 &lt;code&gt;Thread.sleep(毫秒数)&lt;/code&gt;，其中这个 &lt;code&gt;毫秒数&lt;/code&gt; 可以任意填写，这不重要，重要的是我们使用了 &lt;code&gt;sleep&lt;/code&gt; 这个方法，首先会让主线程进行阻塞，一旦阻塞结束进入就绪态后，就会重新获取共享变量中的数据，结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1759331150903_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这次我们可以看到，程序达到了我们的运行的目的，并且程序也正常结束了。除此之外，我们还有别的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        while(true){
            synchronized (Main.class) {}
            if(MyThread.a == 1){
                System.out.println(&amp;quot;检测到 a = 1&amp;quot;);
                break;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        while(true){
            Thread.currentThread().resume();
            if(MyThread.a == 1){
                System.out.println(&amp;quot;检测到 a = 1&amp;quot;);
                break;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两种方法分别是运用了同步代码块运行结束后会重新获取共享变量数据的特点和强制刷新线程后重新获取共享变量的数据特点，且第二种方法已经被淘汰了。&lt;/p&gt;
&lt;p&gt;那么有没有直接解决共享变量数据不一致问题的方法？当然有，就是 &lt;code&gt;volatile&lt;/code&gt; 关键字，只要共享变量被 &lt;code&gt;volatile&lt;/code&gt; 修饰，线程在写入时会将值刷新到主内存，读取时会强制从主内存获取最新值，然后存入线程的工作内存（副本）中使用，而不是一直使用旧的本地副本。&lt;/p&gt;
&lt;p&gt;如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MyThread extends Thread{
    public static volatile int a = 0;
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(&amp;quot;线程开始执行，当前 a = &amp;quot; + a);
        a = 1;
        System.out.println(&amp;quot;线程执行结束，当前 a = &amp;quot; + a);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        while(true){
            if(MyThread.a == 1){
                System.out.println(&amp;quot;检测到 a = 1&amp;quot;);
                break;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1759331150903_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到成功达到了要求。&lt;/p&gt;
&lt;p&gt;当然，&lt;code&gt;volatile&lt;/code&gt; 对于未被 &lt;code&gt;static&lt;/code&gt; 修饰的普通成员也有效果，大家可以自行尝试。&lt;/p&gt;
&lt;p&gt;实际上，&lt;code&gt;volatile&lt;/code&gt; 还可以禁止指令重排，虽然重排对多线程的执行影响远远不如随机性大，但这个作用也需要记一下。&lt;/p&gt;
</content:encoded><category>Java</category><category>volatile</category><author>Glader</author></item><item><title>实词与成语积累</title><link>https://blog.mygld.top/zh-cn/posts/idioms/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/idioms/</guid><description>gwy 考试实词与成语积累。</description><pubDate>Tue, 30 Sep 2025 09:00:50 GMT</pubDate><content:encoded>&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;筚路蓝缕&lt;/strong&gt;：架着简陋的柴车，穿着破烂衣服去开辟山林，形容创业艰苦。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栉风沐雨&lt;/strong&gt;：形容人经常在外面不顾风雨地辛苦奔波。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;艰苦卓绝&lt;/strong&gt;：坚忍刻苦的精神超过寻常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;餐风宿露&lt;/strong&gt;：形容旅途或野外生活艰苦的联合式成语。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;倒逼&lt;/strong&gt;：反向推动，逆向促进。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消弭&lt;/strong&gt;：消除。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对冲&lt;/strong&gt;：降低。&lt;/li&gt;
&lt;li&gt;并列结构两个必须都适合搭配才可以。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;融会贯通&lt;/strong&gt;：各方面的知识或道理融合贯穿起来，全面透彻理解。强调融合起来容易透彻理解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;蔚然成风&lt;/strong&gt;：形容一种事物逐渐发展盛行，形成一种良好风气。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无出其右&lt;/strong&gt;：没有能超过他的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独树一帜&lt;/strong&gt;：比喻与众不同，独成一家。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三足鼎立&lt;/strong&gt;：比喻三方面对立的局势。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;盛极一时&lt;/strong&gt;：形容一时特别兴盛和流行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尔虞我诈&lt;/strong&gt;：相互欺骗。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;万马奔腾&lt;/strong&gt;：群众性活动声势浩大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经天纬地&lt;/strong&gt;：主要形容人，形容人的才能极大，能做非常伟大的事业。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;汪洋恣肆&lt;/strong&gt;：形容文章，言论，书法气势豪放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纵横捭阖&lt;/strong&gt;：政治或外交作用手段分化或拉拢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;金戈铁马&lt;/strong&gt;：战士持枪驰马的英姿。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;秣马厉兵&lt;/strong&gt;：形容喂饱战马，磨快兵器，指做好战斗准备或事前准备工作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;戎马倥偬&lt;/strong&gt;：形容军务繁忙，置身战场。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;制度护佑&lt;/strong&gt;：固定搭配，制度上保护劳动者。&lt;/li&gt;
&lt;li&gt;固定搭配，&lt;strong&gt;延误时机&lt;/strong&gt;，&lt;strong&gt;荒废学业&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;激浊扬清&lt;/strong&gt;：冲去污水，让清水上来，比喻清除坏的，发扬好的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去芜存菁&lt;/strong&gt;：除去杂质，保留精华。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长处&lt;/strong&gt;一词无比较之意，&lt;strong&gt;优势&lt;/strong&gt;则有。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;相形见绌&lt;/strong&gt;：与同类事物相比显露出不足。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回天乏术&lt;/strong&gt;：比喻局势或病情严重到无法挽救的程度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;左支右绌&lt;/strong&gt;：原指弓射箭的姿势，左手支持，右手屈曲，指力量不足，应付了这方面，那一方面又出了问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独木难支&lt;/strong&gt;：单个主体力量单薄，维持不住全局。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;捉襟见肘&lt;/strong&gt;：整理一下衣襟，就漏出了胳膊肘，形容衣服破烂，生活窘困，也比喻顾此失彼，应付不过来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;力所不逮&lt;/strong&gt;：能力达不到，心有余而力不足之意。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简捷&lt;/strong&gt;：简单快捷。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鳞次栉比&lt;/strong&gt;：强调排列有次序，多用于房屋等整齐的东西，数量多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;星罗棋布&lt;/strong&gt;：强调分布范围广，应形状整齐与不整齐都能用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;波诡云谲&lt;/strong&gt;：形容房屋的构造像云彩波浪一样千姿百态，后形容事物变化莫测。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;浩如烟海&lt;/strong&gt;：形容典籍，图书等极为丰富。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空洞&lt;/strong&gt;：没有内容或内容不切实际。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;雅俗共赏&lt;/strong&gt;：文艺作品既通俗又优美，任何层次的人都能欣赏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;曲高和寡&lt;/strong&gt;：比喻言论或作品不通俗，能了解的人很少。讽刺意味。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视觉体验&lt;/strong&gt;是观众感受，&lt;strong&gt;视觉呈现&lt;/strong&gt;是客观表现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;收效甚微&lt;/strong&gt;：指付出努力但没有什么结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;山重水复&lt;/strong&gt;：重重山河阻隔。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;卧薪尝胆&lt;/strong&gt;：在逆境中磨炼意志，刻苦自厉，发愤图强。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;厚积薄发&lt;/strong&gt;：多多积蓄，慢慢放出，形容只有充分准备才能办好事情。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自强不息&lt;/strong&gt;：努力上进，永不懈怠。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兢兢业业&lt;/strong&gt;：形容小心谨慎，认真踏实，丝毫不敢懈怠。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;步步为营&lt;/strong&gt;：形容防守严密，行动谨慎。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有的放矢&lt;/strong&gt;：比喻做事有明确的目的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;举足轻重&lt;/strong&gt;：所处地位非常重要，一举一动对全局有重大影响。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尾大不掉&lt;/strong&gt;：比喻属下势力强大，或组织庞大，涣散，难以指挥调度，现比喻机构庞大，指挥不灵。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;血本无归&lt;/strong&gt;：形容经济活动导致本金完全损失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;责无旁贷&lt;/strong&gt;：自身应承担的责任不可推卸给他人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;洞若观火&lt;/strong&gt;：清楚的就像看火一样，形容事物透彻分明。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当仁不让&lt;/strong&gt;：应该做的事情，主动承当，不推辞，不退避。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;义不容辞&lt;/strong&gt;：形容为了正义事业，敢于挺身而出，不做推辞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;弃若敝屣&lt;/strong&gt;：像扔掉破鞋一样把它抛弃，比喻毫不可惜。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;故步自封&lt;/strong&gt;：比喻守着老一套，不求进步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;束之高阁&lt;/strong&gt;：把东西捆起来，放在高高的架子上，指弃置不用或置之不理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一锤定音&lt;/strong&gt;：比喻做事干脆，说了算数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标本兼治&lt;/strong&gt;：对事物的表象和本质方面的问题都进行治理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开源节流&lt;/strong&gt;：比喻增加收入，节省开支。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;素昧平生&lt;/strong&gt;：彼此一向不了解，也指与某人从来不认识。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;曲意逢迎&lt;/strong&gt;：违背自己的意愿，想方设法奉承讨好别人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;沽名钓誉&lt;/strong&gt;：用不正当手段捞取名誉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检视&lt;/strong&gt;：检验查看。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首鼠两端&lt;/strong&gt;：迟疑不决或动摇不定（语出《史记·魏其武安侯列传》。首鼠：踌躇）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推波助澜&lt;/strong&gt;：借助外力加剧矛盾&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;莫衷一是&lt;/strong&gt;：形容事物的情况或观点多种多样，没有一个一致的结论或意见&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;春风化雨&lt;/strong&gt;：比喻良好的教育和适宜的环境。多用来称颂老师和长辈对学生及晚辈潜移默化的教诲。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;和风细雨&lt;/strong&gt;：比喻以和缓态度进行劝说或批评的行为方式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔靴搔痒&lt;/strong&gt;：原指隔着靴子抓挠皮肤瘙痒处，现比喻言语或行动未触及本质、未解决关键问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扬汤止沸&lt;/strong&gt;：处理问题仅采取表面措施而未触及根本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;洋洋盈耳&lt;/strong&gt;：形容读书声或讲话声、乐曲声等悦耳动听&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;舌灿莲花&lt;/strong&gt;：形容人口才好，口齿伶俐，能言善道，有如莲花般地美妙。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;旁征博引&lt;/strong&gt;：指说话、写文章引用材料作为依据或例证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;口若悬河&lt;/strong&gt;：形容人善于言谈，能言善辩，把话说得很流利，而且滔滔不绝。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无可厚非&lt;/strong&gt;：没有什么可以过分指责的，表示有错误，但可以原谅，也比喻还有一定的道理，不能全部否定&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无可非议&lt;/strong&gt;：没有什么可以指摘的，表示言行合乎情理，没有错误&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无可讳言&lt;/strong&gt;：没有什么不可以直说的。指可以坦率地说。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;匠心独运&lt;/strong&gt;：独创性地运用精巧的心思。多形容文学、艺术方面的独特构思。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;罔顾&lt;/strong&gt;：不顾及&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标新立异&lt;/strong&gt;：为了显示自己，故意显得与众不同&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;贪大求全&lt;/strong&gt;：过分贪图规模大而全面&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;好大喜功&lt;/strong&gt;：一心想做大事，立大功&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;出神入化&lt;/strong&gt;：形容技艺达到了绝妙的境界，没有体现逼真&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栩栩如生&lt;/strong&gt;：形容文学、艺术作品描摹、刻画人或物的形象十分生动逼真&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;惟妙惟肖&lt;/strong&gt;：形容描写或模仿得十分逼真&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;面面俱到&lt;/strong&gt;：指各方面都能照顾到&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;百无一失&lt;/strong&gt;：形容有充分把握&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鞭长莫及&lt;/strong&gt;：比喻距离太远而无能为力，比喻力量达不到&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作壁上观&lt;/strong&gt;：指双方交战，自己站在壁垒上旁观。后多比喻在一旁观望，不给予帮助&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正本清源&lt;/strong&gt;：从源头上清理、根本上整顿，比喻彻底解决问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如履薄冰&lt;/strong&gt;：比喻随时都会发生危险，做事极为小心谨慎&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;根深蒂固&lt;/strong&gt;：比喻基础稳固，不容易动摇。有时也指黑恶势力基础牢固，难以铲除。常用于中性或消极语境&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交相辉映&lt;/strong&gt;：各种光亮、色彩等互相映照，多用于形容美好的景象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;休戚与共&lt;/strong&gt;：形容关系密切，利害相关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;块垒&lt;/strong&gt;：泛指郁积之物，比喻胸中郁结的愁闷或气愤。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参差向背&lt;/strong&gt;：一般用来形容 &lt;strong&gt;高低不齐、方向不一&lt;/strong&gt; 的状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;丘壑&lt;/strong&gt;：指山峰与河谷，也比喻深远的意境。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;情愫&lt;/strong&gt;：只可意会不可言传的心境。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;稗官野史&lt;/strong&gt;：泛称小说及记载不见经传的轶闻琐事的著述。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;遗闻轶事&lt;/strong&gt;：未被广泛知晓却引发人们兴趣的传闻与故事&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如火如荼&lt;/strong&gt;：原比喻军容之盛，现多形容大规模行动气势旺盛、气氛热烈&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;壁垒&lt;/strong&gt;：比喻对立的事物和界限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;寅吃卯粮&lt;/strong&gt;：寅年吃了卯年的粮食。比喻经济困难,入不敷出,预先挪用眼下只能亏空着的财物或还没到手的收入,不顾将来&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标杆&lt;/strong&gt;：比喻榜样&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;杀鸡取卵&lt;/strong&gt;：贪图眼前利益，损害长远利益。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大相径庭&lt;/strong&gt;：着重突出相差很远, 还可表示彼此矛盾很大, 多指事物、举止或言论等区别明显&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;以讹传讹&lt;/strong&gt;：把本来就不正确的东西又错误地传开去。结果越传越错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;妄下雌黄&lt;/strong&gt;：在没有充分了解事实真相之前，我们不能妄下雌黄，随意对他人进行评判。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不着边际&lt;/strong&gt;：指没有着落。也形容言论空泛，不切实际或离题太远。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;乍暖还寒&lt;/strong&gt;：刚刚变暖，依旧带有几分寒意。形容冬末春初时天气忽冷忽热，冷热不定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大马金刀&lt;/strong&gt;：骑着大马，举着金刀。形容豪爽，气派大。也形容说话直率锋利，不留情面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;揶揄&lt;/strong&gt;：嘲笑，讥讽&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不齿&lt;/strong&gt;：是不愿意提到，表示极端鄙视&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不耻&lt;/strong&gt;：不顾羞耻；不以为有失体面；不以为耻。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;侧目&lt;/strong&gt;： 不敢从正面看，斜着眼睛看，形容畏惧而又愤恨。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;诟病&lt;/strong&gt;：意思为侮辱，后引申为指责或嘲骂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;苛求&lt;/strong&gt;：过严地要求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;噤声&lt;/strong&gt;：闭口不做声,即禁声。 例噤声不语。 住口;不许再说下去。 &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;讳言&lt;/strong&gt;：不敢或不愿说。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;悸动&lt;/strong&gt;：表示因恐惧、紧张或情绪激动引发的心跳加速现象&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;律动&lt;/strong&gt;：有规律地行动;有节奏地跳动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一蹶不振&lt;/strong&gt;：比喻一遭到挫折就不能再振作起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引导与诱导&lt;/strong&gt;：前者侧重帮助别人思考，后者侧重出于自身利益故意引导别人的思路&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;胸无城府&lt;/strong&gt;：形容心胸坦率、坦白，没有什么隐藏。也形容人真诚坦率而&lt;strong&gt;没有社会经验&lt;/strong&gt;。后喻指为人&lt;strong&gt;胸怀坦荡&lt;/strong&gt;，不用心机。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;化解风险&lt;/strong&gt;：固定搭配&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;汲取经验&lt;/strong&gt;：固定搭配&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;难以为继&lt;/strong&gt;：难以继续下去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;积重难返&lt;/strong&gt;：长期形成的不良习惯、弊端，不易改变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;辗转&lt;/strong&gt;：1.也做展转，躺在床上翻来覆去；2.经过许多人的手或经过许多地方。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;粉墨登场&lt;/strong&gt;：原指演员化妆上台演戏。现多比喻坏人经过一番乔装打扮，登上政治舞台（含贬义）或社会生活中演戏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;俨然&lt;/strong&gt;：主要表示庄重、严肃的样子，或者非常地像某物/某人，还可以形容整齐的样子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已然&lt;/strong&gt;：表示事情已经成为事实，已经如此。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;晴雨表&lt;/strong&gt;：反映形势的变化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;群策群力&lt;/strong&gt;：大家共同出主意，共同出力量。指集中并发挥众人的智慧和力量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从善如流&lt;/strong&gt;：比喻善于听取别人意见好建议或乐于做好事。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拾人牙慧&lt;/strong&gt;：拾取人家的一言半语当做自己的话。比喻自己无真知灼见，只是因袭或重复别人的语言或文字。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邯郸学步&lt;/strong&gt;：比喻模仿不成，反把自己原有的长处失去了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;夸夸其谈&lt;/strong&gt;：指说话或写文章浮夸、不切实际，浮夸而又滔滔不绝地乱说。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;刻舟求剑&lt;/strong&gt;：比喻拘泥于成例,不知变通。亦比喻思想行动脱离实际,徒劳无获。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;耳提面命&lt;/strong&gt;：谓不但当着面讲,而且是提着他的耳朵向他讲。形容恳切热忱地教诲。今义后谓教诲殷切，要求严格。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;望眼欲穿&lt;/strong&gt;：极目远望，眼珠都快要破了。形容盼望非常急切。也作“眼欲穿”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;望穿秋水&lt;/strong&gt;：把眼睛都望穿了。形容盼望的程度。秋水：比喻人的眼睛像秋水一样晶莹。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;润物无声&lt;/strong&gt;：引申为通过潜移默化的方式对人施加积极影响，&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;呆若木鸡&lt;/strong&gt;：形容呆笨或因恐惧、惊讶而发呆的样子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;钳制&lt;/strong&gt;：用强力限制，使不能自由行动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;珠联璧合&lt;/strong&gt;：意思是珍珠联串在一起，美玉结合在一块，比喻杰出的人才或美好的事物聚集在一起的美好样子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风生水起&lt;/strong&gt;：风从水面吹过，水面掀起波澜。形容事情做得有生气，蓬勃兴旺。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐逸&lt;/strong&gt;：1.避世隐居。2.指隐居的人&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;飘逸&lt;/strong&gt;：飘浮轻飞，引申指洒脱自然的气质或艺术作品的清新风格。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;孜孜矻矻&lt;/strong&gt;：形容勤勉不懈的样子。强调不懈怠。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;孜孜不倦&lt;/strong&gt;：勤奋努力，不知疲倦。强调不知疲倦。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;皓首穷经&lt;/strong&gt;：直到年老白头还在钻研经籍。形容勤勉好学，至老不倦。也称白首穷经。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;旷日持久&lt;/strong&gt;：耽搁时日，持续很久。多表示处于胶着状态，空耗时日而无进展。中性词。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绵延不绝&lt;/strong&gt;：其核心语义指代自然景观连续不断出现的状态，常用于描述山脉、河流等地理景观的连续性特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;巡回&lt;/strong&gt;：按一定路线到各处（活动）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;洄游&lt;/strong&gt;：海洋中一些动物（主要是鱼类）因为产卵、觅食或受季节变化的影响，沿着一定路线有规律地往返迁移。也作回游。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;巡游&lt;/strong&gt;：出外游玩；游逛；巡行察看，来回游弋。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逡巡&lt;/strong&gt;：有所顾虑而徘徊或不敢前进。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;货真价实&lt;/strong&gt;：形容实实在在，一点不假。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;差强人意&lt;/strong&gt;：勉强使人满意。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;彻头彻尾&lt;/strong&gt;：从头到尾；贯彻始终；完完全全。中性词，可做贬义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实至名归&lt;/strong&gt;：有了真正的学识、本领或功业，自然就有声誉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不折不扣&lt;/strong&gt;：没有折扣，表示完全、十足的意思。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一脉相通&lt;/strong&gt;：指事物之间相互关联，犹如一条脉络贯穿下来可以互通。强调事物间的关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一脉相承&lt;/strong&gt;：一个血统或派别世代传承下来。泛指思想、文化、学术等的继承关系。强调继承。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;唇齿相依&lt;/strong&gt;：比喻关系密切，互相依存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虎视眈眈&lt;/strong&gt;：像老虎那样凶狠地盯着。形容心怀不善，伺机攫取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;明察秋毫&lt;/strong&gt;：形容目光敏锐，微小的细节也能捕捉到，也形容洞察一切。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日薄西山&lt;/strong&gt;：多比喻衰老、病重的人或衰微的事物临近死亡、终结。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;百无聊赖&lt;/strong&gt;：精神上无所寄托，感到什么都没意思。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;因势利导&lt;/strong&gt;：指顺着事情发展的趋势向有利的方向引导。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不卑不亢&lt;/strong&gt;：既不自卑也不高傲。形容待人的态度和言行恰如其分，自然得体。也形容人说话办事有恰当的分寸。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;筚门圭窦&lt;/strong&gt;：荆条竹苇编的门，穿壁而成、形状如圭（上锐下方）的户；柴门小户。指穷人居住的简陋房舍；也指贫苦人家。&lt;/li&gt;
&lt;li&gt;捉襟见肘指力量不够应付不过来，顾此失彼是慌张或忙乱导致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兰艾同焚&lt;/strong&gt;：比喻好人坏人同归于尽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;桑枢瓮牖（sang shu weng you）&lt;/strong&gt;：用桑树做门轴，用瓦罐做窗户。喻义比喻贫苦之家。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;钟鸣鼎食&lt;/strong&gt;：击钟列鼎而食。形容贵族的豪华排场。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;岸芷汀兰&lt;/strong&gt;：岸边的香草，小洲上的兰花，香气浓郁，颜色青葱。岸芷和汀兰都是指水边的美丽的花。岸和汀都是指岸边的意思。形容人像芳兰一样品德高尚，谦让有礼。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;粉墙黛瓦&lt;/strong&gt;：指白色墙壁与青黑色屋瓦，多用以描写传统建筑的外观特征。该词现常见于对徽派建筑及江南民居的景观描述。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空中楼阁&lt;/strong&gt;：悬在半空中的阁楼。比喻虚幻的事物或脱离实际的空想。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;荡气回肠&lt;/strong&gt;：使意气激荡，情绪回旋。形容音乐、文辞等十分动人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日新月异&lt;/strong&gt;：每日每月都出现新的情况，呈现新的面貌。形容新事物不断涌现，面貌不断更新，发展极为迅速。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;竿头日上&lt;/strong&gt;：比喻学业进步很快。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风起云涌&lt;/strong&gt;：大风起来，乌云涌现。形容气势雄伟。比喻许多事物相继兴起，声势浩大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如日中天&lt;/strong&gt;：像太阳正处于天空中央。比喻事物正发展到十分兴盛的阶段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;薪火相传&lt;/strong&gt;：比喻师生传授，学问和技艺一代代地继承下去。也比喻种族、文化等代代相传。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常备不懈&lt;/strong&gt;：时刻准备着，毫不忪懈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自省&lt;/strong&gt;：自我反省。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;仰仗&lt;/strong&gt;：依靠；依赖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依仗&lt;/strong&gt;：凭借、倚靠。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;仰仗&lt;/strong&gt;偏正式、带感激或尊重；&lt;strong&gt;依仗&lt;/strong&gt;偏中性，强调依靠或借助力量，也有轻微贬义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;离弦走板&lt;/strong&gt;：比喻言行偏离公认的准则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返本还原&lt;/strong&gt;：指返回原来的状况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;随俗浮沉&lt;/strong&gt;：自己没有一定的想法，随着潮流走。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一举两得&lt;/strong&gt;：指做一件事情同时得到两方面的好处。记住强调得到两方面好处，而不是起到效果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应运而生&lt;/strong&gt;：1.后泛指顺应某种时机、条件、需要等而产生。 2.本指顺应天命而产生。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异军突起&lt;/strong&gt;：比喻与众不同的新派别或势力一下子崛起，独树一帜。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;秉笔直书&lt;/strong&gt;：写史书根据事实记录，不隐讳。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不动声色&lt;/strong&gt;：在紧急情况下，说话、神态仍跟平时一样没有变化。形容非常镇静。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抽丝剥茧&lt;/strong&gt;：丝得一根一根地抽,茧得一层一层地剥开。形容对事物分析细致且层次分明。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安身立命&lt;/strong&gt;：意为生活有着落且精神有寄托，含褒义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思考与思索&lt;/strong&gt;：思考是考虑，仅是思维层面，思索是反复思考与探索，有思考后行动的含义。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>实词</category><category>成语</category><category>gwy</category><author>Glader</author></item><item><title>线程池</title><link>https://blog.mygld.top/zh-cn/posts/thread-pool/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/thread-pool/</guid><description>Java 线程池总结。</description><pubDate>Mon, 29 Sep 2025 07:07:53 GMT</pubDate><content:encoded>&lt;h2&gt;线程池主要核心原理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;创建一个池子，池子中是空的&lt;/li&gt;
&lt;li&gt;提交任务时，池子会创建新的线程对象，任务执行完毕，线程归还给池子下回再次提交任务时，不需要创建新的线程，直接复用已有的线程即可&lt;/li&gt;
&lt;li&gt;但是如果提交任务时，池子中没有空闲线程，也无法创建新的线程，任务就会排队等待&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;线程池的创建&lt;/h2&gt;
&lt;p&gt;线程池的创建，主要使用 &lt;code&gt;Executors&lt;/code&gt; 工具类中的两个方法，一个是 &lt;code&gt;newCachedThreadPool&lt;/code&gt;，一个是 &lt;code&gt;newFixedThreadPool&lt;/code&gt;，前者是用来创建一个无长度上限的线程池，后者是用来创建一个有长度上限的线程池，他们都会返回一个 &lt;code&gt;ExecutorService&lt;/code&gt; 对象，具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
ExecutorService e2 = Executors.newFixedThreadPool(3); //创建一个最多可以容纳5个线程的线程池
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;向线程池中提交任务&lt;/h2&gt;
&lt;p&gt;接下来我们测试线程池是否真的能达到像它所说的那样的效果，提交任务常使用的方法是 &lt;code&gt;submit&lt;/code&gt; 或 &lt;code&gt;execute&lt;/code&gt; 方法，通常通过 &lt;code&gt;execute(Runnable)&lt;/code&gt; 提交任务仅执行而不返回结果，而 &lt;code&gt;submit(Runnable)&lt;/code&gt; 会返回 &lt;code&gt;Future&lt;/code&gt;，可用于获取执行状态或异常信息。&lt;/p&gt;
&lt;p&gt;我们做如下测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args){
        //创建线程
        ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
        //ExecutorService e2 = Executors.newFixedThreadPool(5); //创建一个最多可以容纳5个线程的线程池
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + &amp;quot; 在运行。&amp;quot;);
            }
        };
        e1.submit(r);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1759131804943_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上，线程已经在执行了，可以发现程序并没有结束，因此在后面我们要在线程池不需要使用的时候进行销毁，但一般线程池是不需要关闭的。&lt;/p&gt;
&lt;p&gt;我们接下来多测试几个线程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args){
        //创建线程
        ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
        //ExecutorService e2 = Executors.newFixedThreadPool(5); //创建一个最多可以容纳5个线程的线程池
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + &amp;quot; 在运行。&amp;quot;);
            }
        };
        e1.submit(r);
        e1.submit(r);
        e1.submit(r);
        e1.submit(r);
        e1.submit(r);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1759132017346_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如图，我们添加了 &lt;code&gt;5&lt;/code&gt; 个任务。为了进一步测试，我们在添加人物之前进行一小段时间的睡眠，验证已经执行完的线程池里的线程对象能不能得到复用，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        //创建线程
        ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
        //ExecutorService e2 = Executors.newFixedThreadPool(5); //创建一个最多可以容纳5个线程的线程池
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + &amp;quot; 在运行。&amp;quot;);
            }
        };
        e1.submit(r);
        Thread.sleep(500);
        e1.submit(r);
        Thread.sleep(500);
        e1.submit(r);
        Thread.sleep(500);
        e1.submit(r);
        Thread.sleep(500);
        e1.submit(r);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1759132181483_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上，线程 1 执行完毕后，被多次复用。&lt;/p&gt;
&lt;p&gt;接下来再测试一下 &lt;code&gt;newFixedThreadPool&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        //创建线程
        //ExecutorService e1 = Executors.newCachedThreadPool(); //创建一个无上限的线程池
        ExecutorService e2 = Executors.newFixedThreadPool(3); //创建一个最多可以容纳5个线程的线程池
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + &amp;quot; 在运行。&amp;quot;);
            }
        };
        e2.submit(r);
        e2.submit(r);
        e2.submit(r);
        e2.submit(r);
        e2.submit(r);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1759132350198_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;非常完美，如上图，线程最多只能被使用 &lt;code&gt;3&lt;/code&gt; 个。&lt;/p&gt;
&lt;h2&gt;线程池的销毁&lt;/h2&gt;
&lt;p&gt;已经没有用的线程池，只需要使用  &lt;code&gt;shutdown&lt;/code&gt; 方法销毁即可，这里不再演示。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;自定义线程池&lt;/h2&gt;
&lt;p&gt;除了使用 &lt;code&gt;Executors&lt;/code&gt; 提供的工厂方法，我们也可以通过 &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; 来创建一个更加灵活、可控的线程池。它的构造方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public ThreadPoolExecutor(
        int corePoolSize,        // 核心线程数
        int maximumPoolSize,     // 最大线程数
        long keepAliveTime,      // 非核心线程的最大存活时间
        TimeUnit unit,           // 存活时间的单位
        BlockingQueue&amp;lt;Runnable&amp;gt; workQueue, // 任务队列
        ThreadFactory threadFactory,       // 线程工厂，用于创建新线程
        RejectedExecutionHandler handler   // 拒绝策略
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参数含义&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;corePoolSize&lt;/strong&gt;
 核心线程数，线程池会始终保持的线程数量，即使这些线程处于空闲状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;maximumPoolSize&lt;/strong&gt;
 最大线程数，当任务过多时，线程池最多能扩展到的线程数量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;keepAliveTime &amp;amp; unit&lt;/strong&gt;
 非核心线程（超过 &lt;code&gt;corePoolSize&lt;/code&gt; 的部分）在空闲时的存活时间，超时后会被回收。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;workQueue&lt;/strong&gt;
 存放等待执行任务的阻塞队列。常见选择：&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ArrayBlockingQueue&lt;/code&gt;（有界队列，数组实现）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LinkedBlockingQueue&lt;/code&gt;（无界队列，链表实现）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SynchronousQueue&lt;/code&gt;（直接提交任务，不存储）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;threadFactory&lt;/strong&gt;
 定义线程的创建方式，比如设置线程名、是否为守护线程等。默认工厂即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;handler&lt;/strong&gt;
 当线程池和队列都满了时的拒绝策略：&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AbortPolicy&lt;/code&gt;（默认，抛异常）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CallerRunsPolicy&lt;/code&gt;（由调用者线程执行任务）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DiscardPolicy&lt;/code&gt;（直接丢弃任务）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DiscardOldestPolicy&lt;/code&gt;（丢弃队列中最老的任务）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;示例：自定义线程池&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo;

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 自定义线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                2,                      // 核心线程数
                5,                      // 最大线程数
                10, TimeUnit.SECONDS,   // 非核心线程空闲存活时间
                new ArrayBlockingQueue&amp;lt;&amp;gt;(3),  // 有界任务队列，最多存 3 个任务
                Executors.defaultThreadFactory(), // 默认线程工厂
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略：直接抛异常
        );

        Runnable task = () -&amp;gt; {
            System.out.println(Thread.currentThread().getName() + &amp;quot; 在执行任务&amp;quot;);
            try {
                Thread.sleep(2000); // 模拟耗时任务
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        // 提交 10 个任务，观察线程池如何调度
        for (int i = 1; i &amp;lt;= 10; i++) {
            try {
                pool.execute(task);
            } catch (RejectedExecutionException e) {
                System.out.println(&amp;quot;任务 &amp;quot; + i + &amp;quot; 被拒绝了！&amp;quot;);
            }
        }

        pool.shutdown(); // 关闭线程池
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1759135722020_c11effa6-8ac4-4dab-acd1-bf8f0085253c.png&quot; alt=&quot;c11effa6-8ac4-4dab-acd1-bf8f0085253c.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;执行流程说明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;前两个任务会直接启动 &lt;strong&gt;核心线程&lt;/strong&gt; 处理。&lt;/li&gt;
&lt;li&gt;接下来的三个任务会放入 &lt;strong&gt;任务队列&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;当队列满了之后，线程池会继续创建新线程（最多到 &lt;code&gt;maximumPoolSize=5&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;如果线程池和队列都满了，第 9、10 个任务会触发 &lt;strong&gt;拒绝策略&lt;/strong&gt;，直接抛异常并提示被拒绝。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最大并行数&lt;/h2&gt;
&lt;h3&gt;最大并行数的含义&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU 层面&lt;/strong&gt;：
 对于计算密集型任务，最大并行数通常受 CPU 核心数限制（包括超线程）。比如你的 CPU 有 8 核（16 线程），理论上同时执行 16 个计算任务是最优的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程池层面&lt;/strong&gt;：
 对于 Java 中的 &lt;code&gt;ExecutorService&lt;/code&gt; 或 &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;，最大并行数就是线程池 &lt;code&gt;maximumPoolSize&lt;/code&gt; 的值。线程池会根据这个值控制同时活跃线程的数量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作系统限制&lt;/strong&gt;：
 JVM 的线程数也受操作系统限制，通常线程数太多会导致内存溢出或调度开销过大。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;获取最大并行数可以通过下述代码获取：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) {
        int i = Runtime.getRuntime().availableProcessors();
        System.out.println(i);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;线程池多大合适&lt;/h2&gt;
&lt;h3&gt;CPU 密集型运算&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;最大并行数 + 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;I/O 密集型运算&lt;/h3&gt;
&lt;p&gt;$最大并行数 * 期望 CPU 利用率 * \frac{总时间(CPU计算时间+等待时间)}{CPU计算时间}$&lt;/p&gt;
&lt;h2&gt;面试八股文重点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;volatile&lt;/li&gt;
&lt;li&gt;JMM&lt;/li&gt;
&lt;li&gt;悲观锁、乐观锁、CAS&lt;/li&gt;
&lt;li&gt;原子性&lt;/li&gt;
&lt;li&gt;并发工具类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些内容是面试经常会提问的点，后期再做整理。&lt;/p&gt;
</content:encoded><category>线程池</category><category>Java</category><author>Glader</author></item><item><title>Java 的动态代理</title><link>https://blog.mygld.top/zh-cn/posts/dynamic-proxy/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/dynamic-proxy/</guid><description>Java 的动态代理相关知识点。</description><pubDate>Sun, 14 Sep 2025 05:40:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 概念&lt;/h2&gt;
&lt;p&gt;所谓 Java 的动态代理，我个人的理解其实就是通过反射机制，创建一个接口的临时实现类，并返回一个实例化对象。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;只能代理接口&lt;/strong&gt;：JDK动态代理只能为接口创建代理，不能为具体类创建代理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;反射调用&lt;/strong&gt;：每次方法调用都会通过反射机制转发到 InvocationHandler&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;临时类&lt;/strong&gt;：代理类是运行时生成的，存在于内存中，不会持久化到磁盘&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通俗来讲，比如坤坤是一位大明星，要开演唱会，坤坤只负责唱歌和跳舞，那么布置舞台这些事情，肯定不是坤坤自己来做，所以就需要代理人来做这些。&lt;/p&gt;
&lt;p&gt;于是我们可以这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;

public class KunKun {
    public String sing(String name){
        System.out.println(&amp;quot;坤坤唱了：&amp;quot; + name);
        return &amp;quot;坤坤唱完了&amp;quot;;
    }
    public void dance(String name){
        System.out.println(&amp;quot;坤坤跳了：&amp;quot; + name + &amp;quot;舞蹈！&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;

public class ProxyPeople {
    KunKun kunKun = new KunKun();
    public String sing(String name){
        System.out.println(&amp;quot;代理人布置场地。&amp;quot; );
        System.out.println(&amp;quot;代理人布置场地完成。&amp;quot; );
        return kunKun.sing(name);
    }
    public void dance(String name){
        System.out.println(&amp;quot;坤坤跳了：&amp;quot; + name + &amp;quot;舞蹈！&amp;quot;);
        kunKun.dance(name);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们只需要在主类中运行:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ProxyPeople proxyPeople = new ProxyPeople();
proxyPeople.sing(&amp;quot;只因你太美&amp;quot;);
proxyPeople.dance(&amp;quot;芭蕾舞&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可。&lt;/p&gt;
&lt;p&gt;但是这样写显然不够优雅，于是你想到了用多态的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;

public interface People {
    String sing(String name);
    void dance(String name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;

public class KunKun implements People{
    @Override
    public String sing(String name){
        System.out.println(&amp;quot;坤坤唱了：&amp;quot; + name);
        return &amp;quot;坤坤唱完了&amp;quot;;
    }
    @Override
    public void dance(String name){
        System.out.println(&amp;quot;坤坤跳了：&amp;quot; + name + &amp;quot;舞蹈！&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;

public class ProxyPeople implements People{
    KunKun kunKun = new KunKun();
    @Override
    public String sing(String name){
        System.out.println(&amp;quot;代理人布置场地。&amp;quot; );
        System.out.println(&amp;quot;代理人布置场地完成。&amp;quot; );
        return kunKun.sing(name);
    }
    @Override
    public void dance(String name){
        System.out.println(&amp;quot;代理人布置场地。&amp;quot; );
        System.out.println(&amp;quot;代理人布置场地完成。&amp;quot; );
        kunKun.dance(name);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就优雅了许多，但是还不够，这样的话我们需要单独去写一个实现类 &lt;code&gt;ProxyPeople&lt;/code&gt;，显得有些多余，因为我们的语句比较简单，那有没有不用去单独写一个实现类的方法？动态代理就是做这个的。&lt;/p&gt;
&lt;h2&gt;2. Proxy 类&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;java.lang.reflect&lt;/code&gt; 包下，有一个 &lt;code&gt;Proxy&lt;/code&gt; 类，它有一个 &lt;code&gt;newProxyInstance(ClassLoader loader, Class&amp;lt;?&amp;gt;[] interfaces, InvocationHandler h)&lt;/code&gt; 方法，可以临时生成实现类并返回一个对象。&lt;/p&gt;
&lt;p&gt;其中，&lt;code&gt;newProxyInstance&lt;/code&gt; 第一个参数是一个类加载器，第二个是一个接口的 Class 数组，第三个参数是用来指定代理对象要做什么事情，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyUtil {
    public static People createProxyPeople(KunKun kunKun) {
        return (People) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
                new Class[]{People.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                        System.out.println(&amp;quot;代理人布置场地。&amp;quot; );
                        System.out.println(&amp;quot;代理人布置场地完成。&amp;quot; );
                        if (method.getName().equals(&amp;quot;sing&amp;quot;)) {
                            return method.invoke(kunKun, objects);
                        }
                        else{
                            method.invoke(kunKun,objects);
                            return null;
                        }
                    }
                });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后 &lt;code&gt;KunKun&lt;/code&gt; 的类不需要改动，直接在主类中运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;People proxyPeople = ProxyUtil.createProxyPeople(new KunKun());
System.out.println(proxyPeople.sing(&amp;quot;只因你太美&amp;quot;));
proxyPeople.dance(&amp;quot;芭蕾舞&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757831269327_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里做一个补充，为什么 &lt;code&gt;newProxyInstance&lt;/code&gt; 第二个参数是一个数组呢？因为&lt;strong&gt;一个代理对象可以同时实现多个接口&lt;/strong&gt;，例如：&lt;/p&gt;
&lt;p&gt;假设设你有多个接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interface UserService {
    void saveUser(String name);
}

interface LogService {
    void log(String message);
}

interface CacheService {
    void cache(String key, Object value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以创建一个代理对象，&lt;strong&gt;同时实现这三个接口&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;InvocationHandler handler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(&amp;quot;调用方法: &amp;quot; + method.getName());
        
        // 根据方法名或接口类型进行不同处理
        if (method.getName().equals(&amp;quot;saveUser&amp;quot;)) {
            System.out.println(&amp;quot;保存用户: &amp;quot; + args[0]);
        } else if (method.getName().equals(&amp;quot;log&amp;quot;)) {
            System.out.println(&amp;quot;记录日志: &amp;quot; + args[0]);
        } else if (method.getName().equals(&amp;quot;cache&amp;quot;)) {
            System.out.println(&amp;quot;缓存数据: &amp;quot; + args[0] + &amp;quot; -&amp;gt; &amp;quot; + args[1]);
        }
        
        return null;
    }
};

// 同时实现多个接口
Object proxy = Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class, LogService.class, CacheService.class},  // 多个接口
    handler
);

// 可以转换成任意一个接口使用
UserService userService = (UserService) proxy;
LogService logService = (LogService) proxy;
CacheService cacheService = (CacheService) proxy;

userService.saveUser(&amp;quot;张三&amp;quot;);
logService.log(&amp;quot;操作完成&amp;quot;);
cacheService.cache(&amp;quot;user:1&amp;quot;, &amp;quot;张三&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你传入多个接口时，JVM会生成一个同时实现所有接口的代理类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 动态生成的代理类会实现所有接口
public final class $Proxy0 extends Proxy 
    implements UserService, LogService, CacheService {
    
    // 实现UserService的方法
    public void saveUser(String name) throws Throwable {
        super.h.invoke(this, saveUserMethod, new Object[]{name});
    }
    
    // 实现LogService的方法
    public void log(String message) throws Throwable {
        super.h.invoke(this, logMethod, new Object[]{message});
    }
    
    // 实现CacheService的方法
    public void cache(String key, Object value) throws Throwable {
        super.h.invoke(this, cacheMethod, new Object[]{key, value});
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 实际使用场景&lt;/h2&gt;
&lt;p&gt;这种设计在框架中很常见，比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Spring AOP&lt;/strong&gt;：代理对象需要实现原始接口 + 额外的增强接口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事务管理&lt;/strong&gt;：业务接口 + 事务管理接口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存代理&lt;/strong&gt;：业务接口 + 缓存管理接口&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以数组设计是为了支持&lt;strong&gt;多接口实现&lt;/strong&gt;的灵活性，即使你只传入一个接口，也要用数组的形式 &lt;code&gt;new Class[]{YourInterface.class}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这样就可以了，以上就是 Java 动态代理的相关知识点。&lt;/p&gt;
</content:encoded><category>Java</category><category>动态代理</category><author>Glader</author></item><item><title>Java 反射复习</title><link>https://blog.mygld.top/zh-cn/posts/java-fanshe/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/java-fanshe/</guid><description>Java 反射复习知识点整理。</description><pubDate>Thu, 11 Sep 2025 12:03:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Java 反射的概念&lt;/h2&gt;
&lt;p&gt;Java 的反射（reflection）机制是指在程序的运行状态中，可以构造任意一个类的对象，可以了解任意一个对象所属的类，可以了解任意一个类的成员变量和方法，可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为 Java 语言的反射机制。反射被视为动态语言的关键。&lt;/p&gt;
&lt;h2&gt;2. 反射 API&lt;/h2&gt;
&lt;h3&gt;(1) 反射中常用的类&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;java.lang.Class&lt;/code&gt;：Java 中类也可以看作一种特殊的&lt;strong&gt;对象&lt;/strong&gt;，Class 便表示的是类的对象。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java.lang.reflect.Constructor&lt;/code&gt;：类的构造器。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java.lang.reflect.Field&lt;/code&gt;：类的成员变量。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java.lang.reflect.Method&lt;/code&gt;：类的成员方法。&lt;/p&gt;
&lt;h3&gt;(2) 获取类对象的三种方法&lt;/h3&gt;
&lt;p&gt;现在我在 &lt;code&gt;top.mygld.demo.pojo&lt;/code&gt; 包下创建 &lt;code&gt;User&lt;/code&gt; 类如下，方便接下来的演示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.pojo;
public class User {
    private String name;
    private int age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后接下来在主类中演示类对象的获取。&lt;/p&gt;
&lt;p&gt;i）直接通过 &lt;code&gt;类名.class&lt;/code&gt; 获取，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class c = User.class;   //假设User是我们封装好的类
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ii）通过 &lt;code&gt;Class.forName(&amp;quot;类全名&amp;quot;)&lt;/code&gt; 获取，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class c = Class.forName(&amp;quot;top.mygld.demo.pojo.User&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;iii）通过 &lt;code&gt;具体对象.getClass()&lt;/code&gt; 获取，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;User user = new User(&amp;quot;小明&amp;quot;,30);
Class c = user.getClass();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上述三种方法获得的对象 &lt;code&gt;c&lt;/code&gt; 便是 &lt;code&gt;User&lt;/code&gt; 类所对应的类对象，接下来我们可以通过调用 &lt;code&gt;c&lt;/code&gt; 中的一些方法，来获取 &lt;code&gt;User&lt;/code&gt; 类中的内部信息，进而对内部信息进行操控，这便是 &lt;code&gt;Java&lt;/code&gt; 的反射。&lt;/p&gt;
&lt;h3&gt;(3) 类对象的基本信息&lt;/h3&gt;
&lt;p&gt;在第（2）步中，我们获取到了类对象 &lt;code&gt;c&lt;/code&gt;，那么如果我们直接打印输出 &lt;code&gt;c&lt;/code&gt; 会得到什么？我们尝试一下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;
public class ReflectionDemo {
    public static void main(String[] args){
        Class c = User.class;
        System.out.println(c);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757600592486_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图，是输出了 &lt;code&gt;class + 类全名&lt;/code&gt;。这说明 &lt;code&gt;Class&lt;/code&gt; 类重写了 &lt;code&gt;toString&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;此外，&lt;code&gt;Class&lt;/code&gt; 类还提供了两个主要的成员方法：&lt;code&gt;getName&lt;/code&gt; 和 &lt;code&gt;getSimpleName&lt;/code&gt;，分别用来获得类对象的&lt;strong&gt;类全名&lt;/strong&gt;和&lt;strong&gt;类简名&lt;/strong&gt;效果如下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;
public class ReflectionDemo {
    public static void main(String[] args){
        Class c = User.class;
        System.out.println(c.getName());
        System.out.println(c.getSimpleName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757600943657_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;(4) 通过反射获得目标类的构造器信息&lt;/h3&gt;
&lt;p&gt;通过反射机制，我们可以获得目标类的构造器（Constuctor）信息，如构造器的个数、构造器的参数情况（无参构造器、有参构造器）、有参构造器的参数个数、参数类型、访问修饰符等等。&lt;/p&gt;
&lt;p&gt;为了便于测试，我接下来对 &lt;code&gt;User&lt;/code&gt; 类进行改写，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.pojo;
public class User {
    private String name;
    private int age;

    public User(){}

    private User(String name){
        this.name = name;
    }

    protected User(int age){
        this.age = age;
    }

    public User(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return &amp;quot;User{&amp;quot; +
                &amp;quot;name=&amp;#39;&amp;quot; + name + &amp;#39;\&amp;#39;&amp;#39; +
                &amp;quot;, age=&amp;quot; + age +
                &amp;#39;}&amp;#39;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;Class&lt;/code&gt; 中的 &lt;code&gt;getDeclaredConstructors()&lt;/code&gt; 方法可以获得目标类中的所有构造器的构造器对象数组，使用 &lt;code&gt;Constructor&lt;/code&gt; 中的 &lt;code&gt;getParameterCount()&lt;/code&gt; 可以获得对应构造器中的参数个数，具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;

import java.lang.reflect.Constructor;

public class ReflectionDemo {
    public static void main(String[] args){
        Class c = User.class;
        Constructor[] cons = c.getDeclaredConstructors();
        System.out.println(&amp;quot;共有 &amp;quot; + cons.length + &amp;quot; 个构造器&amp;quot;) ;
        for(Constructor con : cons){
            System.out.println(con + &amp;quot; 参数个数：&amp;quot; + con.getParameterCount());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757602300065_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然如果只想获得 &lt;code&gt;public&lt;/code&gt; 修饰的构造器，使用 &lt;code&gt;Class&lt;/code&gt; 中的 &lt;code&gt;getConstructors()&lt;/code&gt; 方法即可，这里将不再演示。&lt;/p&gt;
&lt;p&gt;那么有一个问题，如果我想获得单个指定的构造器对象，该如何获取呢？其实只需要调用 &lt;code&gt;getDeclaredConstructor&lt;/code&gt; 的重载方法 &lt;code&gt;getDeclaredConstructor(Class&amp;lt;?&amp;gt;... parameterTypes)&lt;/code&gt; 即可，其中 &lt;code&gt;parameterTypes&lt;/code&gt; 只需要传入 &lt;code&gt;类名.class&lt;/code&gt; 或 &lt;code&gt;基本数据类型.class&lt;/code&gt; 即可，具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;

public class ReflectionDemo {
    public static void main(String[] args) throws NoSuchMethodException {
        Class c = User.class;
        System.out.println(c.getDeclaredConstructor(int.class));
        System.out.println(c.getDeclaredConstructor(String.class, int.class));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757602790702_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然，如果想获得某个 &lt;code&gt;public&lt;/code&gt; 修饰的构造器，也可以使用 &lt;code&gt;Class&lt;/code&gt; 中的 &lt;code&gt;getConstructor(Class&amp;lt;?&amp;gt;... parameterTypes)&lt;/code&gt; 来获得，其中它的参数可以为空，表示获得 &lt;code&gt;public&lt;/code&gt; 修饰的无参构造。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意：&lt;code&gt;getDeclaredConstructors&lt;/code&gt; 和&lt;code&gt;getDeclaredConstructor&lt;/code&gt; 不受访问修饰符限制，&lt;code&gt;getConstructors&lt;/code&gt; 和&lt;code&gt;getConstructor&lt;/code&gt; 只能获取 &lt;code&gt;public&lt;/code&gt; 修饰的构造器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(5) 通过反射创建目标类实例对象&lt;/h3&gt;
&lt;p&gt;已经获得到了目标类的构造器的对象，此时有人会有疑问了，用这个构造器对象我们能做什么？能做的其实非常多。&lt;/p&gt;
&lt;p&gt;首先，通过调用 &lt;code&gt;Constructor&lt;/code&gt; 中的 &lt;code&gt;newInstance(Object... initargs)&lt;/code&gt; 方法，我们可以实例化一个目标类的对象，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;
import java.lang.reflect.Constructor;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception{
        Class c = User.class;
        Constructor con = c.getDeclaredConstructor(String.class,int.class);
        User kun = (User) con.newInstance(&amp;quot;坤坤&amp;quot;, 18);
        System.out.println(kun);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757603302511_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;那有人要问了，这不脱裤子放屁多此一举吗？我直接 &lt;code&gt;new User(&amp;quot;坤坤&amp;quot;,18)&lt;/code&gt; 不就行了？还需要这么麻烦吗？&lt;/p&gt;
&lt;p&gt;那接下来我要说点有意思的，我们观察到，在 &lt;code&gt;User&lt;/code&gt; 类中，有一个 &lt;code&gt;private&lt;/code&gt; 修饰的的构造方法，我们正常去执行如下代码一定会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;User user = new User(&amp;quot;坤坤&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是因为 &lt;code&gt;private User(String name)&lt;/code&gt; 是私有的，不可以被外部直接访问，但是，通过反射我们可以临时绕过访问权限，使得能够调用这个构造器。调用 &lt;code&gt;Constructor&lt;/code&gt; 中的 &lt;code&gt;setAccessible(boolean)&lt;/code&gt; 即可，具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;
import java.lang.reflect.Constructor;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception{
        Class c = User.class;
        Constructor con = c.getDeclaredConstructor(String.class);
        con.setAccessible(true);
        System.out.println(con.newInstance(&amp;quot;坤坤&amp;quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意，使用 &lt;code&gt;con.setAccessible(true);&lt;/code&gt; 仅仅是临时修改了访问权限，并不会对目标类造成影响，该构造器仍然是 &lt;code&gt;private&lt;/code&gt; 的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(6) 通过反射获取目标类成员变量&lt;/h3&gt;
&lt;p&gt;既然通过反射能获得目标类的构造方法信息，那么肯定也能获取目标类的成员变量（Field）信息。&lt;/p&gt;
&lt;p&gt;其实，类比于构造器的获取，成员变量的获取基本类似：&lt;/p&gt;
&lt;p&gt;通过调用 &lt;code&gt;Field&lt;/code&gt; 中的 &lt;code&gt;getDeclaredFields()&lt;/code&gt; 方法可以获得目标类下所有的成员变量信息；调用 &lt;code&gt;Field&lt;/code&gt; 中的 &lt;code&gt;getDeclaredField(String name)&lt;/code&gt;  可以获得成员变量中变量名为 &lt;code&gt;name&lt;/code&gt; 的指定变量；通过调用 &lt;code&gt;Field&lt;/code&gt; 中的 &lt;code&gt;getFields()&lt;/code&gt; 方法可以获得目标类下所有的 &lt;code&gt;public&lt;/code&gt; 修饰的成员变量信息；调用 &lt;code&gt;Field&lt;/code&gt; 中的 &lt;code&gt;getField(String name)&lt;/code&gt;  可以获得 &lt;code&gt;public&lt;/code&gt; 修饰的成员变量中变量名为 &lt;code&gt;name&lt;/code&gt; 的指定变量，具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception{
        Class c = User.class;
        System.out.println(c.getConstructor());

        Field[] fields = c.getDeclaredFields();
        for (Field f : fields) {
            System.out.println(f);
        }

        Field name = c.getDeclaredField(&amp;quot;name&amp;quot;);
        System.out.println(name);

        System.out.println(c.getFields().length);
        //因为User中未设置public修饰的成员变量，这里不再演示getField()方法，大家可以自行尝试
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意：&lt;code&gt;getDeclaredField&lt;/code&gt; 和 &lt;code&gt;getField&lt;/code&gt; 方法参数列表均不能为空。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(7) 通过反射修改和获取目标类对象的成员变量的值&lt;/h3&gt;
&lt;p&gt;在第(6)步中，我们获得了目标类成员变量信息，那么我们能否可以根据这个成员变量对象，去修改或获取目标类对象的成员变量的值？答案是可以的，我们只需要使用 &lt;code&gt;Field&lt;/code&gt; 中的 &lt;code&gt;set(Object obj, Object value)&lt;/code&gt; 和 &lt;code&gt;get(Object obj)&lt;/code&gt; 方法即可，具体如下（由于 &lt;code&gt;User&lt;/code&gt; 中成员变量都是 &lt;code&gt;private&lt;/code&gt; 的，不能直接修改，需要先通过调用 &lt;code&gt;Field&lt;/code&gt; 中的 &lt;code&gt;setAccessible(true)&lt;/code&gt; 来临时绕过访问权限）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;
import java.lang.reflect.Field;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception{
        Class c = User.class;
        Field name = c.getDeclaredField(&amp;quot;name&amp;quot;);
        name.setAccessible(true);

        User user = new User(&amp;quot;坤坤&amp;quot;,18);
        System.out.println(user);

        name.set(user,&amp;quot;鸽鸽&amp;quot;);
        System.out.println(user);
        System.out.println(name.get(user));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757605971267_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;(9) 通过反射获取目标类成员方法&lt;/h3&gt;
&lt;p&gt;如果你详细看了上述 8 个部分内容，你应该能够猜到，获取成员方法（Method）信息也主要有四个方法：&lt;/p&gt;
&lt;p&gt;分别是 &lt;code&gt;Method&lt;/code&gt; 中的 &lt;code&gt;getDeclaredMethods&lt;/code&gt;、&lt;code&gt;getDeclaredMethod&lt;/code&gt;、&lt;code&gt;getMethods&lt;/code&gt; 和 &lt;code&gt;getMethod&lt;/code&gt;，他们的功能与注意事项应该不言而喻了吧，为了方便演示，我先在 &lt;code&gt;User&lt;/code&gt; 中添加若干方法，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.pojo;
public class User {
    private String name;
    private int age;

    public User(){}

    private User(String name){
        this.name = name;
    }

    protected User(int age){
        this.age = age;
    }

    public User(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return &amp;quot;User{&amp;quot; +
                &amp;quot;name=&amp;#39;&amp;quot; + name + &amp;#39;\&amp;#39;&amp;#39; +
                &amp;quot;, age=&amp;quot; + age +
                &amp;#39;}&amp;#39;;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    private void hello(){
        System.out.println(&amp;quot;你好，User&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我再在主类中测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception{
        Class c = User.class;
        Method[] declaredMethods = c.getDeclaredMethods();
        for(Method m : declaredMethods){
            System.out.println(m);
        }

        System.out.println(c.getMethod(&amp;quot;setName&amp;quot;, String.class));

        Method[] methods = c.getMethods();
        for(Method m : methods){
            System.out.println(m);
        }

        System.out.println(c.getDeclaredMethod(&amp;quot;hello&amp;quot;));

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757606750629_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意：&lt;code&gt;getDeclaredMethod&lt;/code&gt; 和 &lt;code&gt;getMethod&lt;/code&gt; 中，第一个参数是成员方法的名字，后面从第二个参数开始，表示这个成员方法的的参数列表的参数类型，如果没有参数列表，则上述方法只写第一个参数即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(10) 通过反射调用目标类对象的成员方法&lt;/h3&gt;
&lt;p&gt;通过反射，我们也可以调用 &lt;code&gt;Method&lt;/code&gt; 中的 &lt;code&gt;invoke(目标类对象,参数列表)&lt;/code&gt; 方法来调用目标类对象中的成员方法，具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;
import java.lang.reflect.Method;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception{
        Class c = User.class;
        Method setName = c.getMethod(&amp;quot;setName&amp;quot;, String.class);

        User user = (User) c.newInstance();
        System.out.println(user);

        setName.invoke(user, &amp;quot;坤坤&amp;quot;);
        System.out.println(user);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757607221301_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3. 反射的作用&lt;/h2&gt;
&lt;p&gt;i) 基本作用：可以得到一个类的全部成分然后操作。（在上述步骤中已经体现）&lt;/p&gt;
&lt;p&gt;ii) 可以破坏封装性。（因为可以修改类内部的成员的访问权限，因此会破坏类的封装性）&lt;/p&gt;
&lt;p&gt;iii) 可以绕过泛型的约束，例如下述代码的执行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        ArrayList&amp;lt;String&amp;gt; a = new ArrayList&amp;lt;&amp;gt;();
        a.add(&amp;quot;hello&amp;quot;);
        Class c = a.getClass();
        Method m = c.getDeclaredMethod(&amp;quot;add&amp;quot;, Object.class);
        m.invoke(a, 123);
        System.out.println(a);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757646014301_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;p&gt;综上，Java 反射的核心知识点主要包括四个方面：&lt;strong&gt;类对象&lt;/strong&gt;（获取 &lt;code&gt;Class&lt;/code&gt; 对象及其信息）、&lt;strong&gt;构造器&lt;/strong&gt;（获取和操作类的构造方法）、&lt;strong&gt;成员变量&lt;/strong&gt;（获取和操作类的字段）、以及&lt;strong&gt;成员方法&lt;/strong&gt;（获取和调用类的方法）。&lt;/p&gt;
&lt;p&gt;很多框架都是通过反射的方法实现的，例如 &lt;code&gt;Spring Boot&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;通过反射 ，我们还可以自主设计一些框架，假如我要获取任意类的成员变量信息，我可以写一个比较简单的框架函数，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package top.mygld.demo.test;
import top.mygld.demo.pojo.User;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        User user = new User(&amp;quot;坤坤&amp;quot;,18);
        getInformation(user);
    }
    public static void getInformation(Object object) throws IllegalAccessException {
        Class c = object.getClass();
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            System.out.println(field.getName() + &amp;quot;:&amp;quot; + field.get(object));
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1757646240966_image.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然，上述只是举了个例子，这个函数代码可能对某些其他的类不适用。&lt;/p&gt;
&lt;p&gt;”反射“为什么叫”反射“，我认为，正常调用某个对象的成员属性或成员方法，都是 &lt;code&gt;对象.成员&lt;/code&gt; 的格式，而在反射中的格式基本都是 &lt;code&gt;成员.方法(对象)&lt;/code&gt; 的形式，和正常的形式是反着的，所以叫 &lt;code&gt;反射&lt;/code&gt;。当然这只是我的一个个人理解，置于为啥真的叫”反射“，感兴趣的可以去查一下资料了~&lt;/p&gt;
</content:encoded><category>反射</category><category>Java</category><author>Glader</author></item><item><title>人工智能大判官</title><link>https://blog.mygld.top/zh-cn/posts/aipg/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/aipg/</guid><description>基于 AI 的判官小游戏。</description><pubDate>Mon, 19 May 2025 07:31:53 GMT</pubDate><content:encoded>&lt;p&gt;写了个纯基于前端的简陋 AI 判官小游戏，一个沙雕小游戏：&lt;a href=&quot;https://glader-judge.netlify.app/&quot;&gt;游戏链接&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>AI</category><category>娱乐</category><author>Glader</author></item><item><title>仓促完成的毕设</title><link>https://blog.mygld.top/zh-cn/posts/bysj/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/bysj/</guid><description>仓促完成的毕设。</description><pubDate>Fri, 28 Mar 2025 15:31:53 GMT</pubDate><content:encoded>&lt;p&gt;因为要交本科毕业论文初稿了，仓促完成了这个基于 Vue3 + SpringBoot 的智能问卷系统，欢迎大家测试。&lt;/p&gt;
&lt;p&gt;AI 接口用的硅基流动邀请赠送的余额，可能随时会用完，如果 AI 生成不能用了，多半是没余额了。&lt;/p&gt;
&lt;p&gt;因为写的比较仓促，bug比较多，&lt;del&gt;这是正常的...&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;测试网站：&lt;a href=&quot;http://zhw.mygld.top&quot;&gt;毕设测试网站&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>毕设</category><category>Vue</category><category>SpringBoot</category><author>Glader</author></item><item><title>使用 Netlify 和 Astro 构建静态博客</title><link>https://blog.mygld.top/zh-cn/posts/netlify-astro-blog/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/netlify-astro-blog/</guid><description>基于 Netlify 和 Astro 构建简易静态博客。</description><pubDate>Sun, 26 Jan 2025 17:07:51 GMT</pubDate><content:encoded>&lt;h2&gt;1.注册 NetLify&lt;/h2&gt;
&lt;p&gt;首先打开 NetLify 的官方网站&lt;a href=&quot;https://app.netlify.com/&quot;&gt;点击此处跳转&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;选择一种方式注册登录即可，这里我选择 GitHub 登录，即第一种方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421447708_image-20250126141222970.png&quot; alt=&quot;image-20250126141222970&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们授权使用 GitHub 登录&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421469705_image-20250126141411824.png&quot; alt=&quot;image-20250126141411824&quot;&gt;&lt;/p&gt;
&lt;p&gt;之后会弹出填写信息的一些页面，里面的内容依次代表姓名、计划、角色等内容，我们大致写写，并且勾选一下标签即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421498412_image-20250126142026081.png&quot; alt=&quot;image-20250126142026081&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果我们在&lt;code&gt;How are you planning to use Netlify?&lt;/code&gt;中选择了&lt;code&gt;Personal&lt;/code&gt;选项，即个人项目选项，在最后会让我们起一个项目名称，之后可以用于&lt;code&gt;URL&lt;/code&gt;中作为标识，这里我们可以根据自己的个性化需求自行设计。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421519660_image-20250126142309986.png&quot; alt=&quot;image-20250126142352840&quot;&gt;&lt;/p&gt;
&lt;p&gt;完成后，点击&lt;code&gt;Continue to deploy&lt;/code&gt;，会弹出让我们设置第一个项目，这里我们点击&lt;code&gt;Skip this step for now&lt;/code&gt;，即跳过该步骤。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421520957_image-20250126142514856.png&quot; alt=&quot;image-20250126142514856&quot;&gt;&lt;/p&gt;
&lt;p&gt;用过上述步骤，我们注册 NetLify 成功。&lt;/p&gt;
&lt;h2&gt;2.安装 Astro&lt;/h2&gt;
&lt;p&gt;首先我们访问 Astro 的官方文档网站，&lt;a href=&quot;https://docs.astro.build/zh-cn/getting-started/&quot;&gt;点击此处跳转&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;然后我们点击“安装 Astro”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421542376_image-20250126150134573.png&quot; alt=&quot;image-20250126150134573&quot;&gt;&lt;/p&gt;
&lt;p&gt;在新的页面中，显示了有三个前提条件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421547221_image-20250126150232186.png&quot; alt=&quot;image-20250126150232186&quot;&gt;&lt;/p&gt;
&lt;p&gt;根据文档的前提条件表示，我们需要 Node.js 环境，一个文本编辑器和终端。&lt;/p&gt;
&lt;p&gt;Node.js 环境的安装配置可以参考下述教程：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/m0_74825135/article/details/145189303&quot;&gt;Node.js 安装教程，点击跳转&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;后两者直接使用系统自带的即可，考虑到更舒适的写代码的环境，我们可以下载使用 VSCode 这款文本编辑器，下载与安装方式可以参考如下教程：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/ZHOUPUYU/article/details/143952444&quot;&gt;VSCode安装教程，点击跳转&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;上述前提条件都完成后，我们正式开始 Astro 的安装。&lt;/p&gt;
&lt;p&gt;我们首先新建一个空白文件夹&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421549982_image-20250126151529771.png&quot; alt=&quot;image-20250126151529771&quot;&gt;&lt;/p&gt;
&lt;p&gt;紧接着在地址栏处输入&lt;code&gt;cmd&lt;/code&gt;并点击回车&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421556736_image-20250126151626715.png&quot; alt=&quot;image-20250126151626715&quot;&gt;&lt;/p&gt;
&lt;p&gt;在弹出的终端中，输入下述指令，并且在弹出的提示中输入&lt;code&gt;y&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421574744_image-20250126152541823.png&quot; alt=&quot;image-20250126152541823&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-npm&quot;&gt;npm create astro@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待一段时间后，会提示选择新项目的目录，我们可以输入&lt;code&gt;./glader-blog&lt;/code&gt;，表示在当前目录下创建一个新的文件夹&lt;code&gt;glader-blog&lt;/code&gt;，将项目存在这里。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421584326_image-20250126152711786.png&quot; alt=&quot;image-20250126152711786&quot;&gt;&lt;/p&gt;
&lt;p&gt;在之后我们连点 3 次回车，进入等待项目初始化&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421582577_image-20250126152753352.png&quot; alt=&quot;image-20250126152753352&quot;&gt;&lt;/p&gt;
&lt;p&gt;当提示下述内容时，表示项目已经初始化结束，此时我们的文件夹下就会多出了&lt;code&gt;glader-blog&lt;/code&gt;目录，我们的项目就存储在其中：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421596917_image-20250126153013217.png&quot; alt=&quot;image-20250126153013217&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421596281_image-20250126153020024.png&quot; alt=&quot;image-20250126153020024&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们进入创建好的项目，并使用&lt;code&gt;npm install&lt;/code&gt;安装依赖，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421621324_image-20250126153505274.png&quot; alt=&quot;image-20250126153505274&quot;&gt;&lt;/p&gt;
&lt;p&gt;Astro 内置了服务器，等待安装完成后，我们运行指令&lt;code&gt;npm run dev&lt;/code&gt;即可运行服务器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421623642_image-20250126160811452.png&quot; alt=&quot;image-20250126160811452&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们在浏览器中输入&lt;code&gt;http://localhost:4321&lt;/code&gt;即可访问预览。&lt;/p&gt;
&lt;p&gt;在浏览器中输入该网址运行后结果如下，这表示 Astro 项目成功安装。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421630379_image-20250126161025204.png&quot; alt=&quot;image-20250126161025204&quot;&gt;&lt;/p&gt;
&lt;h2&gt;3.熟悉 Astro 的编写环境&lt;/h2&gt;
&lt;p&gt;为了我们方便地编写代码，我们使用&lt;code&gt;VSCode&lt;/code&gt;这款文本编辑器，首先我们打开&lt;code&gt;VSCode&lt;/code&gt;,并使用&lt;code&gt;VSCode&lt;/code&gt;打开刚才创建好的新项目，即&lt;code&gt;glader-blog&lt;/code&gt;，如下，此时右下角会弹出安装 Astro 的扩展，我们点击安装：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421632300_image-20250126161445199.png&quot; alt=&quot;image-20250126161445199&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果没有弹出该提示，我们可以点击扩展图标，手中搜索安装：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421635189_image-20250126161611608.png&quot; alt=&quot;image-20250126161611608&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421638243_image-20250126161657370.png&quot; alt=&quot;image-20250126161657370&quot;&gt;&lt;/p&gt;
&lt;p&gt;安装完成后，我们返回项目目录，在左下角会发现多了一个&lt;code&gt;npm&lt;/code&gt;脚本选项，我们直接点击里面的内容就可以帮助我们运行 npm 脚本，例如里面有一个&lt;code&gt;dev&lt;/code&gt;选项，点击这个就相当于刚才在项目目录下运行了&lt;code&gt;npm run dev&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421647329_image-20250126161756711.png&quot; alt=&quot;image-20250126161756711&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421654641_image-20250126162817282.png&quot; alt=&quot;image-20250126162817282&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421668628_image-20250126162829988.png&quot; alt=&quot;image-20250126162829988&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，效果和刚才直接在小黑窗中输入命令相同。&lt;/p&gt;
&lt;h2&gt;4.编写简易博客&lt;/h2&gt;
&lt;p&gt;在项目目录下的&lt;code&gt;src/pages/&lt;/code&gt;下，有一个&lt;code&gt;index.astro&lt;/code&gt;文件，这个是网页的主页&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421671425_image-20250126163327161.png&quot; alt=&quot;image-20250126163327161&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以尝试将这个文件中的代码全部删除，然后在这个文件中重新编写一段代码，代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;html lang=&amp;quot;zh-cn&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;utf-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type =&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;generator&amp;quot; content = {Astro.generator} &amp;gt;
    &amp;lt;title&amp;gt;Astro&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Astro&amp;lt;/h1&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们在浏览器中可以发现，网页中的内容变成了我们代码中的内容了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421684815_image-20250126163611792.png&quot; alt=&quot;image-20250126163611792&quot;&gt;&lt;/p&gt;
&lt;p&gt;Astro 本质上是一个静态网站生成器，他可以兼容 html 的原生代码，为了方便演示，在这里，我们先粗糙的写一个博客的主界面，代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;zh&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;
    &amp;lt;meta http-equiv=&amp;quot;X-UA-Compatible&amp;quot; content=&amp;quot;ie=edge&amp;quot;&amp;gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;/styles.css&amp;quot;&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;!-- 导航栏 --&amp;gt;
    &amp;lt;header&amp;gt;
      &amp;lt;nav&amp;gt;
        &amp;lt;ul&amp;gt;
          &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;/&amp;quot;&amp;gt;首页&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
          &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;/about&amp;quot;&amp;gt;关于我&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
          &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;/posts&amp;quot;&amp;gt;博客文章&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
          &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;/contact&amp;quot;&amp;gt;联系我们&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
      &amp;lt;/nav&amp;gt;
    &amp;lt;/header&amp;gt;

    &amp;lt;!-- 主内容区 --&amp;gt;
    &amp;lt;main&amp;gt;
      &amp;lt;section class=&amp;quot;intro&amp;quot;&amp;gt;
        &amp;lt;h1&amp;gt;欢迎来到我的博客！&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;这里记录了我对编程、技术和生活的一些思考与分享。&amp;lt;/p&amp;gt;
      &amp;lt;/section&amp;gt;

      &amp;lt;!-- 最新文章列表 --&amp;gt;
      &amp;lt;section class=&amp;quot;posts&amp;quot;&amp;gt;
        &amp;lt;h2&amp;gt;最新文章&amp;lt;/h2&amp;gt;
        &amp;lt;ul&amp;gt;
          &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;&amp;quot;&amp;gt;测试文章1&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
      &amp;lt;/section&amp;gt;
    &amp;lt;/main&amp;gt;

    &amp;lt;!-- 底部 --&amp;gt;
    &amp;lt;footer&amp;gt;
      &amp;lt;p&amp;gt;&amp;amp;copy; 2025 我的博客. All Rights Reserved.&amp;lt;/p&amp;gt;
    &amp;lt;/footer&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421690090_image-20250126164447301.png&quot; alt=&quot;image-20250126165954940&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时假如我们要写一篇&lt;code&gt;MarkDown&lt;/code&gt;格式的博客，我们在&lt;code&gt;src/pages/&lt;/code&gt;下，新建一个文件夹&lt;code&gt;posts&lt;/code&gt;用来存放我们的文章。然后我们在&lt;code&gt;src/pages/posts/&lt;/code&gt;下新建&lt;code&gt;post-1.md&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421752871_image-20250126170422874.png&quot; alt=&quot;image-20250126170422874&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 我的第一篇博客文章

 发表于：2022-07-01

 欢迎来到我学习关于 Astro 的新博客！在这里，我将分享我建立新网站的学习历程。

 ## 我做了什么

 1. **安装 Astro**：首先，我创建了一个新的 Astro 项目并设置好了我的在线账号。

 2. **制作页面**：然后我学习了如何通过创建新的 `.astro` 文件并将它们保存在 `src/pages/` 文件夹里来制作页面。

 3. **发表博客文章**：这是我的第一篇博客文章！我现在有用 Astro 编写的页面和用 Markdown 写的文章了！

 ## 下一步计划

 我将完成 Astro 教程，然后继续编写更多内容。关注我以获取更多信息。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写完博客后，我们在&lt;code&gt;index.astro&lt;/code&gt;中，给“测试文章1”标签添加超链接，链接到刚才的md文章，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421762744_image-20250126170355396.png&quot; alt=&quot;image-20250126170355396&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意：不要加.md，直接写文件名即可，即不要写成&lt;code&gt;/posts/post-1.md&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;此时在网页中我们点击&lt;code&gt;测试文章1&lt;/code&gt;，即可显示我们的博客内容：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421778675_image-20250126170559846.png&quot; alt=&quot;image-20250126170559846&quot;&gt;&lt;/p&gt;
&lt;p&gt;更多语法、组件等相关内容，读者可以自行到&lt;a href=&quot;https://docs.astro.build/zh-cn/tutorial/0-introduction/&quot;&gt;Astro开发文档官网&lt;/a&gt;学习。&lt;/p&gt;
&lt;h2&gt;5.上传项目到 GitHub&lt;/h2&gt;
&lt;p&gt;上传项目到 GitHub，首先要先下载安装&lt;code&gt;Git&lt;/code&gt;，安装方法可以参考下述教程：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_63701294/article/details/143861827&quot;&gt;Git安装教程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;安装完 Git 后，我们要给 VSCode 配置 Git，并将刚才写的项目上传到 GitHub 仓库，具体教程可参考如下教程：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/m0_51185558/article/details/126181439&quot;&gt;VSCode 配置 Git，并上传代码至 GitHub 教程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;然后我们点击发布分支，然后再点击上传至私有仓库即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421782733_image-20250126172016705.png&quot; alt=&quot;image-20250126172016705&quot;&gt;&lt;/p&gt;
&lt;p&gt;上传完成后，我们在GitHub上就拥有了这样的一个私有仓库&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421791351_image-20250126173832736.png&quot; alt=&quot;image-20250126173832736&quot;&gt;&lt;/p&gt;
&lt;p&gt;此外，我们我们要改动我们的代码，我们也可以在这里提交修改&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421794872_image-20250126174055339.png&quot; alt=&quot;image-20250126174055339&quot;&gt;&lt;/p&gt;
&lt;p&gt;至此，我们就可以在本地编辑我们的博客，然后使用 Git 将我们的代码实时保存到 GitHub 上。&lt;/p&gt;
&lt;h2&gt;6.将代码托管到 NetLify&lt;/h2&gt;
&lt;p&gt;我们再次打开 NetLify 的官网，登录好我们刚才注册的账号，我们点击&lt;code&gt;Import from Git&lt;/code&gt;，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421803647_image-20250126174502965.png&quot; alt=&quot;image-20250126174502965&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后选择 GitHub：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421801345_image-20250126174530132.png&quot; alt=&quot;image-20250126174530132&quot;&gt;&lt;/p&gt;
&lt;p&gt;在弹出来的新窗口中授权本次操作，并选择要上传的仓库：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421811147_image-20250126174725799.png&quot; alt=&quot;image-20250126174725799&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后点击&lt;code&gt;Install&lt;/code&gt;进行安装，之后会弹出输入密码的提示，输入 GitHub 的密码点击确定即可。&lt;/p&gt;
&lt;p&gt;此时如下图所示，已上传成功，我们点击该仓库进行配置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421813119_image-20250126174935190.png&quot; alt=&quot;image-20250126174935190&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们只需要在 Site name 中起个名字就行，这个名字以后会放在我们要访问的域名中去，其他的空着不写，直接点击&lt;code&gt;Deploy&lt;/code&gt;即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421817068_image-20250126175747946.png&quot; alt=&quot;image-20250126175747946&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们只需等待部署即可，这个可能需要几分钟的时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421825570_image-20250126180043018.png&quot; alt=&quot;image-20250126180043018&quot;&gt;&lt;/p&gt;
&lt;p&gt;部署完成后，结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421825129_image-20250126180215411.png&quot; alt=&quot;image-20250126180215411&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时我们访问网站&lt;code&gt;https://glader.netlify.app&lt;/code&gt;即可显示我们的博客。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.mygld.top/file/1765421878678_image-20250126180340011.png&quot; alt=&quot;image-20250126180340011&quot;&gt;&lt;/p&gt;
&lt;p&gt;至此，我们的博客部署完成。&lt;/p&gt;
&lt;p&gt;此外，在 GitHub 中，很多人已经基于 Astro 框架开发了很多便于操作的博客主题，我们上述操作仅仅是学习 Astro 的基本使用方法，在实际博客的制作中，我们可以去 GitHub 中选择自己心仪的博客主题作为模板部署即可。&lt;/p&gt;
</content:encoded><category>NetLify</category><category>Astro</category><category>Blog</category><author>Glader</author></item><item><title>域名迁移</title><link>https://blog.mygld.top/zh-cn/posts/update-domain/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/update-domain/</guid><description>将博客迁移到了：blog.mygld.top。</description><pubDate>Mon, 13 Jan 2025 02:34:50 GMT</pubDate><content:encoded>&lt;p&gt;将博客迁移到了：&lt;a href=&quot;https://blog.mygld.top&quot;&gt;blog.mygld.top&lt;/a&gt;。&lt;/p&gt;
</content:encoded><category>域名</category><author>Glader</author></item><item><title>整理 SpringBoot 知识点（二）</title><link>https://blog.mygld.top/zh-cn/posts/springboot-02/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/springboot-02/</guid><description>SpringBoot 知识点整理（二）</description><pubDate>Wed, 01 Jan 2025 18:10:10 GMT</pubDate><content:encoded>&lt;h2&gt;yml 的配置&lt;/h2&gt;
&lt;p&gt;除了（一）中总结的内容，yml 还有其他的语法，例如可以在 yml 中继续参数引用，例如以下这个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: Bob
person:
  name: ${name}
  age: 20
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Value(&amp;quot;${person.name}&amp;quot;)
private String name;
@Value(&amp;quot;${person.age}&amp;quot;)
private int age;

@Test
void test2(){
	System.out.println(name + &amp;quot;:&amp;quot; + age);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后输出结果为 &lt;code&gt;Bob:20&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这说明在 yml 中还可以继续参数引用，person.name 赋值为了 name 的值，也就是 &lt;code&gt;Bob&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;另外，yml 中的数组也可以写成行内数组的格式，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;list: [hello,world]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;yml 中还有一个重要的知识点，就是纯量。比如现在在 yml 中定义如下参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;msg1: &amp;#39;hello\n world&amp;#39; #不会识别转义字符，会原样输出
msg2: &amp;quot;hello\n world&amp;quot; #会识别转义字符
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述参数的唯一区别就是 msg1 使用的是单引号，而 msg2 使用的是双引号，我们编写如下代码进行测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Value(&amp;quot;${msg1}&amp;quot;)
private String msg1;
@Value(&amp;quot;${msg2}&amp;quot;)
private String msg2;

@Test
void test3(){
	System.out.println(msg1);
	System.out.println(msg2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tex&quot;&gt;hello\n world
hello 
 world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明，如果我们使用单引号引用字符串，那么不会识别转义字符，会原样输出；如果使用双引号，则会识别转义字符。&lt;/p&gt;
&lt;p&gt;那么思考一下，如果定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;msg3: hello\n world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会有什么效果？经过本人实验，效果其实加单引号效果一样，不会识别转义字符。&lt;/p&gt;
&lt;p&gt;另外，再补充一点，即使我们不使用 &lt;code&gt;@Value&lt;/code&gt; 和 &lt;code&gt;@ConfigurationProperties&lt;/code&gt; 这两个注解，也有别的办法在代码中随时获得配置中的参数，我们可以使用 &lt;code&gt;Environment&lt;/code&gt; 这个类中的 &lt;code&gt;getProperty(参数名)&lt;/code&gt; 即可，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Autowired
private Environment env; //import org.springframework.core.env.Environment;

@Test
void test4(){
	System.out.println(env.getProperty(&amp;quot;msg1&amp;quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;参数名直接用字符串形式就可以，不用再加 &lt;code&gt;$&lt;/code&gt; 或 &lt;code&gt;{}&lt;/code&gt; 了。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Environment&lt;/code&gt; 导的是 &lt;code&gt;org.springframework.core.env.Environment;&lt;/code&gt; 不要导入错了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Profile 的使用&lt;/h2&gt;
&lt;p&gt;我们在开发 SpringBoot 应用时，通常同一套程序会被安装到不同环境，比如:开发、测试、生产等。其中数据库地址、服务器端口等等配置都不同，如果每次打包时，都要修改配置文件，那么非常麻烦。profile 功能就是来进行动态配置切换的。&lt;/p&gt;
&lt;h3&gt;多 Profile 文件方式&lt;/h3&gt;
&lt;p&gt;如下图，我现在新建如下三个 yml 文件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i1.wp.com/dev.ruom.top/i/2025/02/16/531394.webp&quot; alt=&quot;image-20250216225659712&quot;&gt;&lt;/p&gt;
&lt;p&gt;我在 &lt;code&gt;application-dev.yml&lt;/code&gt; 文件中写入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;server:
  port: 8082
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;code&gt;application-test.yml&lt;/code&gt; 文件中写入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;server:
  port: 8081
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我 &lt;code&gt;application.yml&lt;/code&gt; 中什么也不写，运行后结果如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i1.wp.com/dev.ruom.top/i/2025/02/16/609475.webp&quot; alt=&quot;image-20250216225901594&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到当前并没有设置 Profile，并且当前在 8080 端口运行。&lt;/p&gt;
&lt;p&gt;那么此时我如果在 &lt;code&gt;application.yml&lt;/code&gt; 中写入以下语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  profiles:
    active: dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么运行后的结果如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i1.wp.com/dev.ruom.top/i/2025/02/16/342994.webp&quot; alt=&quot;image-20250216230054561&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到此时的 Profile 被设置了，此时激活的是 &lt;code&gt;dev&lt;/code&gt;，并且发现项目在 8082 端口运行，这说明 &lt;code&gt;application-dev.yml&lt;/code&gt; 中的配置生效了，同理我如果将 &lt;code&gt;application.yml&lt;/code&gt; 换成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  profiles:
    active: test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么项目就会运行在 8081 端口，切换成激活了 &lt;code&gt;test&lt;/code&gt; 的 Profile，即 &lt;code&gt;application-test.yml&lt;/code&gt; 中的配置生效。&lt;/p&gt;
&lt;p&gt;通过这个方法，我们可以快速切换适用于不同环境下的配置。&lt;/p&gt;
&lt;h3&gt;yml 多文档方式&lt;/h3&gt;
&lt;p&gt;在多 Profile 文件方式中我们发现，我们需要新建多个 &lt;code&gt;yml&lt;/code&gt; 或 &lt;code&gt;properties&lt;/code&gt; 文件，这样有些许麻烦，那么是否可以将不同环境下的配置写在同一个配置文件中，并用特殊的符号隔开呢？答案是有的，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
server:
  port: 8081
spring:
  profiles: test
---
server:
  port: 8082
spring:
  profiles: dev
---
spring:
  profiles:
    active: dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不同的配置文件我们只需要使用 &lt;code&gt;---&lt;/code&gt; 隔开即可。&lt;/p&gt;
</content:encoded><category>SpringBoot</category><category>yml</category><category>profile</category><author>Glader</author></item><item><title>整理 SpringBoot 知识点（一）</title><link>https://blog.mygld.top/zh-cn/posts/springboot-01/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/springboot-01/</guid><description>SpringBoot 知识点整理（一）</description><pubDate>Fri, 27 Dec 2024 06:18:10 GMT</pubDate><content:encoded>&lt;h2&gt;SpringBoot 中 yml 的配置与获取&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;application.yml&lt;/code&gt;文件中，自定义配置可以如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;student:
  name: 小坤
  age: 18
  hobbies:
    - 唱
    - 跳
    - rap
    - 篮球
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意：层次之间缩进要对齐，并且冒号后面一定要空格后再写入值，如果是数组，则换行后每一行是一个值，前面加上&lt;code&gt;-&lt;/code&gt;，并且&lt;code&gt;-&lt;/code&gt;后面也要加上一个空格。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;获取配置方法1：使用&lt;code&gt;@Value(&amp;quot;${属性}&amp;quot;)&lt;/code&gt;，在&lt;code&gt;pojo&lt;/code&gt;实体类中的运用如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Data
@NoArgsConstructor
@Component
public class Student {
    @Value(&amp;quot;${student.name}&amp;quot;)
    private String name;
    @Value(&amp;quot;${student.age}&amp;quot;)
    private Integer age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取配置方法2：使用&lt;code&gt;@ConfigurationProperties(prefix = &amp;quot;前缀&amp;quot;)&lt;/code&gt;，在&lt;code&gt;pojo&lt;/code&gt;实体类中的运用如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Data
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = &amp;quot;student&amp;quot;)
public class Student {
    private String name;
    private Integer age;
    private List&amp;lt;String&amp;gt; hobbies;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意：倘若使用方法2，则&lt;code&gt;pojo&lt;/code&gt;实体类中的属性名称必须和&lt;code&gt;yml&lt;/code&gt;配置文件中的名称一模一样。&lt;/li&gt;
&lt;li&gt;注意：&lt;code&gt;@Value&lt;/code&gt;仅支持简单的字符串解析，不能解析&lt;code&gt;List&lt;/code&gt;这种复杂的数据结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SpringBoot 整合 MyBatis&lt;/h2&gt;
&lt;p&gt;MyBatis的起步依赖如下，将其加入到&lt;code&gt;pom.xml&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
     &amp;lt;groupId&amp;gt;org.mybatis.spring.boot&amp;lt;/groupId&amp;gt;
     &amp;lt;artifactId&amp;gt;mybatis-spring-boot-starter&amp;lt;/artifactId&amp;gt;
     &amp;lt;version&amp;gt;3.0.3&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时也要引入MySQL的驱动依赖，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.mysql&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mysql-connector-j&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在配置文件&lt;code&gt;application.yml&lt;/code&gt;中写入数据源配置，格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/{数据库名}
    username: {用户名}
    password: {密码}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如我们现在有一个数据库名字叫做&lt;code&gt;mybatis&lt;/code&gt;，并且其中有一张表&lt;code&gt;user&lt;/code&gt;如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;age&lt;/th&gt;
&lt;th&gt;gender&lt;/th&gt;
&lt;th&gt;phone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;小坤&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;18800000001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;小徐&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;18800000002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;小菜&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;18800000003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;小蔡&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;18800000004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;小鸡&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;18800000005&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;小美&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;18800000006&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;接下来，我们要新建一个&lt;code&gt;pojo&lt;/code&gt;实体类&lt;code&gt;User&lt;/code&gt;，代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String name;
    private Short age;
    private Integer gender;
    private String phone;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在&lt;code&gt;mapper&lt;/code&gt;包下新建接口&lt;code&gt;UserMapper&lt;/code&gt;,如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Mapper
public interface UserMapper {
    @Select(&amp;quot;select * from user where id = #{id}&amp;quot;)
    public User selectUserById(Integer id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意：一定不要忘记加&lt;code&gt;@Mapper&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再然后，在&lt;code&gt;service&lt;/code&gt;包下新建接口&lt;code&gt;UserService&lt;/code&gt;,如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface UserService {
    public User selectUserById(Integer id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;紧接着，在&lt;code&gt;service&lt;/code&gt;包的&lt;code&gt;impl&lt;/code&gt;包下，新建实现类&lt;code&gt;UserServiceImpl&lt;/code&gt;，实现&lt;code&gt;UserService&lt;/code&gt;接口，并加入&lt;code&gt;@Service&lt;/code&gt;注解交给&lt;code&gt;IOC&lt;/code&gt;容器处理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User selectUserById(Integer id) {
        return userMapper.selectUserById(id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，在&lt;code&gt;controller&lt;/code&gt;包下新建&lt;code&gt;UserController&lt;/code&gt;，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
public class UserController {
    
    @Autowired
    private UserService userService;

    @RequestMapping(&amp;quot;/selectUserById&amp;quot;)
    public User selectUserById(Integer id) {
        return userService.selectUserById(id);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意：上述操作属于MVC架构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随后，运行项目，在浏览器中输入&lt;code&gt;http://localhost:8080/selectUserById?id=1&lt;/code&gt;,查看效果，发现浏览器中输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&amp;quot;id&amp;quot;:1,&amp;quot;name&amp;quot;:&amp;quot;小坤&amp;quot;,&amp;quot;age&amp;quot;:19,&amp;quot;gender&amp;quot;:1,&amp;quot;phone&amp;quot;:&amp;quot;18800000001&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>SpringBoot</category><category>MyBatis</category><author>Glader</author></item><item><title>测试 latex 是否可以正常渲染</title><link>https://blog.mygld.top/zh-cn/posts/test-latex/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/test-latex/</guid><description>测试 latex 是否可以正常渲染</description><pubDate>Thu, 26 Dec 2024 07:22:30 GMT</pubDate><content:encoded>&lt;p&gt;本文章仅用于测试 latex 公式是否可以正常渲染，测试内容由 AI 生成：&lt;/p&gt;
&lt;h2&gt;1. 内联公式&lt;/h2&gt;
&lt;p&gt;例如，我们可以写出著名的爱因斯坦质能公式：$E=mc^2$，该公式说明能量和质量的关系。&lt;/p&gt;
&lt;h2&gt;2. 独立显示公式&lt;/h2&gt;
&lt;p&gt;对于二次方程：
$$
ax^2 + bx + c = 0
$$
其求根公式为：
$$
x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
$$&lt;/p&gt;
&lt;h2&gt;3. 求和与极限&lt;/h2&gt;
&lt;p&gt;无穷级数的经典结果：
$$
\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}
$$
同时，我们可以测试一下极限公式：
$$
\lim_{x\to 0} \frac{\sin x}{x} = 1
$$&lt;/p&gt;
&lt;h2&gt;4. 矩阵与特殊公式&lt;/h2&gt;
&lt;p&gt;以下是一个 2x2 矩阵的例子：
$$
\mathbf{A} =
\begin{bmatrix}
a &amp;amp; b \
c &amp;amp; d
\end{bmatrix}
$$
以及举世闻名的欧拉公式：
$$
e^{i\pi} + 1 = 0
$$&lt;/p&gt;
</content:encoded><category>test</category><category>latex</category><author>Glader</author></item><item><title>将 Markdown 和 LaTeX 转为图片的脚本开发</title><link>https://blog.mygld.top/zh-cn/posts/markdown-to-image/</link><guid isPermaLink="true">https://blog.mygld.top/zh-cn/posts/markdown-to-image/</guid><description>将 chatgpt 生成的 Markdown 格式的文本转化为图片的一个脚本。</description><pubDate>Mon, 09 Dec 2024 23:54:53 GMT</pubDate><content:encoded>&lt;p&gt;前几天写了个 QQ 的机器人玩玩，接了一个第三方的 AI 接口。虽然在 LiteLoaderQQNT 中有相关的插件可以渲染 Markdown 和 LaTeX 等格式的文本，但大部分人是没有这些插件的。因此，我想着能否写一个将含有 Markdown 和 LaTeX 格式的文本转化为图片的脚本，于是便有了以下操作。&lt;/p&gt;
&lt;h2&gt;实现思路&lt;/h2&gt;
&lt;p&gt;经过查阅资料，了解到可以通过以下步骤实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将生成的文本转化为 HTML 网页；&lt;/li&gt;
&lt;li&gt;使用 MathJax 将其中的 LaTeX 格式渲染出来；&lt;/li&gt;
&lt;li&gt;对页面进行截图并保存。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;JavaScript 脚本：网页截图&lt;/h2&gt;
&lt;p&gt;首先，写一个 JS 脚本，用于将给定的 HTML 网页进行截图，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const puppeteer = require(&amp;#39;puppeteer&amp;#39;);
const path = require(&amp;#39;path&amp;#39;);
const fs = require(&amp;#39;fs&amp;#39;);

// 从命令行参数获取输入和输出路径
const args = process.argv.slice(2);
const inputPath = args[0];
const outputPath = args[1];

// 检查是否提供了参数
if (!inputPath || !outputPath) {
  console.error(&amp;#39;用法: node script.js &amp;lt;输入路径&amp;gt; &amp;lt;输出路径&amp;gt;&amp;#39;);
  process.exit(1);
}

// 将输入路径解析为绝对路径
const resolvedInputPath = path.resolve(__dirname, inputPath);

// 检查输入文件是否存在
if (!fs.existsSync(resolvedInputPath)) {
  console.error(&amp;#39;输入文件未找到:&amp;#39;, resolvedInputPath);
  process.exit(1);
}

(async () =&amp;gt; {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setViewport({ width: 1200, height: 100 });

  // 打开页面
  await page.goto(&amp;#39;file://&amp;#39; + resolvedInputPath, { waitUntil: &amp;#39;networkidle0&amp;#39; });

  // 尝试等待 MathJax 渲染完成，最多等待 2 秒
  try {
    await page.waitForFunction(
      &amp;#39;window.MathJax &amp;amp;&amp;amp; window.MathJax.Hub &amp;amp;&amp;amp; window.MathJax.Hub.getAllJax().length &amp;gt; 0&amp;#39;,
      { timeout: 2000 }
    );
  } catch (e) {
    console.log(&amp;#39;MathJax 未完全加载，但继续生成图片...&amp;#39;);
  }

  // 截图并保存到指定输出路径
  const resolvedOutputPath = path.resolve(__dirname, outputPath);
  await page.screenshot({ path: resolvedOutputPath, fullPage: true });

  console.log(&amp;#39;Screenshot Successfully:&amp;#39;, resolvedOutputPath);

  await browser.close();
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将上述代码保存为 &lt;code&gt;screenshot.js&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;配置依赖环境&lt;/h3&gt;
&lt;p&gt;上述代码依赖 Node.js 环境，首先要配置好 Node.js 的环境变量，同时使用以下命令在相同文件夹下安装 Puppeteer 包：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install puppeteer
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Python 脚本：Markdown 转 HTML&lt;/h2&gt;
&lt;p&gt;在相同文件夹下新建一个 &lt;code&gt;md2img.py&lt;/code&gt; 的 Python 程序，用于将 Markdown 和 LaTeX 转换为图片。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import markdown
from markdown.extensions import Extension
from markdown.treeprocessors import Treeprocessor

# MathJax
class MathJaxExtension(Extension):
    def extendMarkdown(self, md):
        md.treeprocessors.register(MathJaxProcessor(md), &amp;#39;mathjax&amp;#39;, 175)

class MathJaxProcessor(Treeprocessor):
    def run(self, root):
        for element in root.iter():
            if element.tag == &amp;#39;span&amp;#39; and &amp;#39;class&amp;#39; in element.attrib and &amp;#39;math&amp;#39; in element.attrib[&amp;#39;class&amp;#39;]:
                element.tag = &amp;#39;script&amp;#39;
                element.attrib[&amp;#39;type&amp;#39;] = &amp;#39;math/tex&amp;#39;
                element.text = element.text

def replace_brackets(input_string):
    input_string = input_string.replace(&amp;#39;\[&amp;#39;, &amp;#39;?gzl?&amp;#39;)
    input_string = input_string.replace(&amp;#39;\]&amp;#39;, &amp;#39;?gzr?&amp;#39;)
    input_string = input_string.replace(&amp;#39;\(&amp;#39;, &amp;#39;?gxl?&amp;#39;)
    input_string = input_string.replace(&amp;#39;\)&amp;#39;, &amp;#39;?gxr?&amp;#39;)
    return input_string

def covert_brackets(input_string):
    input_string = input_string.replace(&amp;#39;?gzl?&amp;#39;, &amp;#39;\[&amp;#39;)
    input_string = input_string.replace(&amp;#39;?gzr?&amp;#39;, &amp;#39;\]&amp;#39;)
    input_string = input_string.replace(&amp;#39;?gxl?&amp;#39;, &amp;#39;\(&amp;#39;)
    input_string = input_string.replace(&amp;#39;?gxr?&amp;#39;, &amp;#39;\)&amp;#39;)
    return input_string

def convert_markdown_to_html_with_latex(md_text):
    md = markdown.Markdown(extensions=[&amp;#39;codehilite&amp;#39;, &amp;#39;fenced_code&amp;#39;, MathJaxExtension()])
    html = md.convert(md_text)
    html = covert_brackets(html)
    return f&amp;quot;&amp;quot;&amp;quot;
    &amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
        &amp;lt;script type=&amp;quot;text/javascript&amp;quot; src=&amp;quot;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
        &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/default.min.css&amp;quot;&amp;gt;
        &amp;lt;script src=&amp;quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
        &amp;lt;script&amp;gt;hljs.highlightAll();&amp;lt;/script&amp;gt;
        &amp;lt;style&amp;gt;body {{ font-size: 30px; }}&amp;lt;/style&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        {html}
    &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
    &amp;quot;&amp;quot;&amp;quot;

def markdown_to_html(md_text, html_out_path):
    html_output = convert_markdown_to_html_with_latex(replace_brackets(md_text))
    with open(html_out_path, &amp;#39;w&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:
        f.write(html_output)
    print(&amp;quot;HTML has finished.&amp;quot;)

def html_to_image(html_path, img_path):
    import subprocess
    import os

    html_path = os.path.abspath(html_path)
    img_path = os.path.abspath(img_path)

    if not os.path.exists(html_path):
        raise FileNotFoundError(f&amp;quot;HTML 文件未找到: {html_path}&amp;quot;)

    node_command = [&amp;quot;node&amp;quot;, &amp;quot;screenshot.js&amp;quot;, html_path, img_path]

    try:
        result = subprocess.run(
            node_command,
            check=True,
            text=True,
            capture_output=True
        )
        print(&amp;quot;screenshot.js 输出:&amp;quot;, result.stdout)
    except subprocess.CalledProcessError as e:
        print(&amp;quot;screenshot.js 执行失败:&amp;quot;, e.stderr)
        raise

    if not os.path.exists(img_path):
        raise FileNotFoundError(f&amp;quot;图片生成失败: {img_path}&amp;quot;)

    print(f&amp;quot;图片生成成功: {img_path}&amp;quot;)

import os
import time

def markdown_to_image(md_text, img_out_path):
    tmp_dir = os.path.join(os.getcwd(), &amp;#39;tmp&amp;#39;, &amp;#39;cache&amp;#39;)
    os.makedirs(tmp_dir, exist_ok=True)
    timestamp = int(time.time())
    html_file_path = os.path.join(tmp_dir, f&amp;quot;temp_{timestamp}.html&amp;quot;)
    markdown_to_html(md_text, html_file_path)
    try:
        html_to_image(html_file_path, img_out_path)
        print(f&amp;quot;图片生成成功: {img_out_path}&amp;quot;)
    except Exception as e:
        print(f&amp;quot;图片生成失败: {e}&amp;quot;)
        raise
    finally:
        if os.path.exists(html_file_path):
            os.remove(html_file_path)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置依赖环境&lt;/h3&gt;
&lt;p&gt;上述 Python 程序需要安装 &lt;code&gt;markdown&lt;/code&gt; 包，运行以下命令安装：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install markdown
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调用示例&lt;/h2&gt;
&lt;p&gt;新建一个 &lt;code&gt;test.py&lt;/code&gt;，在里面调用 &lt;code&gt;md2img.markdown_to_image(md_text, img_out_path)&lt;/code&gt; 即可实现 Markdown 和 LaTeX 转图片。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数说明：&lt;ul&gt;
&lt;li&gt;&lt;code&gt;md_text&lt;/code&gt; 表示含有 Markdown 和 LaTeX 的文本；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;img_out_path&lt;/code&gt; 表示图片的生成路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Markdown</category><category>LaTex</category><category>Image</category><author>Glader</author></item></channel></rss>