【Spring】Spring AI 核心知识(一)

1. 自定义 Advisor

实际上,Advisor 可以看做是 Servlet 当中的“拦截器”,在大模型接收到 prompt 之前进行前置拦截增强(比如敏感词校验、记录日志、鉴权),并在大模型返回响应之后进行后置拦截增强(比如记录日志)官方已经提供了一系列 Advisor 可供插拔使用,但是接下来我们需要学习如何自定义 Advisor

1.1 自定义 Advisor 步骤

我们接下来就参考官方文档来学习如何自定义 Advisor 在项目中使用:

📖 参考文档:https://docs.spring.io/spring-ai/reference/api/advisors.html

1)步骤一:选择合适的接口实现

  • CallAroundAdvisor:用于处理同步的请求与响应(非流式)
  • StreamAroundAdvisor:用于处理流式的请求与响应

更加建议两者同时实现

2)步骤二:实现接口核心方法

  • 对于非流式接口 CallAroundAdvisor 来说,需要实现 nextAroundCall 方法
  • 对于流式接口 StreamAroundAdvisor 来说,需要实现 nextAroundStream 方法

3)步骤三:设置执行顺序

通过重写 getOrder 方法,返回的值越低,越先被执行

4)步骤四:设置唯一名称

通过重写 getName 方法,返回唯一标识符

1.2 实现 Logging Advisor

其实官方提供了一个 SimpleLoggerAdvisor 用于记录日志,但是出于以下两方面原因并没有在项目中采用

  • SimpleLoggerAdvisor 源码中使用 debug 级别输出日志,而 SpringBoot 默认忽略 debug 以下级别日志
  • 自定义的 LoggerAdvisor 更加灵活,便于自定义日志

我们可以参考官方的示例,实现自定义的 Logging Advisor,参考代码如下:

/*** 自定义日志Advisor* @author ricejson*/
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {private static final Logger logger = LoggerFactory.getLogger(MyLoggerAdvisor.class);@Overridepublic String getName() {return this.getClass().getSimpleName();}@Overridepublic int getOrder() {return 0;}@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {// 调用前记录请求日志logger.info("before req:{}", advisedRequest);// 调用执行链AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);// 调用后记录响应日志logger.info("after resp:{}", advisedResponse);return advisedResponse;}@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {// 调用前记录请求日志logger.info("before req:{}", advisedRequest);// 调用执行链Flux<AdvisedResponse> flux = chain.nextAroundStream(advisedRequest);return new MessageAggregator().aggregateAdvisedResponse(flux, (advisedResponse) -> {// 调用后记录响应日志logger.info("after resp:{}", advisedResponse);});}
}

需要特别注意的是 MessageAggregator消息聚合器对象将 Flux 类型聚合成单个 AdvisedResponse,是一种流式处理模式,我们在 ResumeApp 当中应用我们编写的自定义 Advisior 观察效果

public ResumeApp(ChatModel dashscopeChatModel) {ChatMemory chatMemory = new InMemoryChatMemory();this.chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem("你是一位资深职业顾问与AI技术融合的【简历辅导大师】,拥有以下核心能力:\n" +"1. HR视角:熟悉ATS(招聘系统)筛选逻辑、500+行业岗位的简历关键词库\n" +"2. 实战经验:基于10万份真实简历优化案例的决策模型\n" +"3. 教练模式:通过苏格拉底式提问引导用户自主发现简历问题") // 系统预设提示词.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // 支持多轮对话new MyLoggerAdvisor()) // 自定义日志Advisor.build();
}

最终运行测试代码,可以发现自定义的日志 Advisor 已经生效了!

1.3 实现 Re2 Advisor

让我们继续趁热打铁,再来学习官方文档中的示例,再来实现一个 Re2 的自定义 Advisor 叭!Re2(全称为Re-Reading)机制,简单来说就是让 AI 大模型重新阅读用户提示词,来提升大模型的推理能力,例如:

{input_prompt}
reading the question again: {input_prompt}

💡 注意:虽然 Re2 技术能够有效提升大模型的推理能力,但是带来的是双倍的成本(双倍token)对于 C 端产品来说需要控制成本

自定义 Re2 Advisor 相关代码如下:

/*** 自定义 ReReading Advisor* @author ricejson*/
public class MyReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {@Overridepublic String getName() {return this.getClass().getSimpleName();}@Overridepublic int getOrder() {return -1;}/*** 实现ReReading逻辑* @return 返回ReReading之后的请求*/private AdvisedRequest reReading(AdvisedRequest advisedRequest) {Map<String, Object> userParams = new HashMap<>(advisedRequest.userParams());// 设置替换模板userParams.put("input_prompt", advisedRequest.userText());return AdvisedRequest.from(advisedRequest).userText("""{input_prompt}reading the question again: {input_prompt}""").userParams(userParams).build();}@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {return chain.nextAroundCall(reReading(advisedRequest));}@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {return chain.nextAroundStream(reReading(advisedRequest));}
}

我们继续在 ResumeApp 当中应用我们编写的自定义的 Advisor 观察效果

public ResumeApp(ChatModel dashscopeChatModel) {ChatMemory chatMemory = new InMemoryChatMemory();this.chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem("你是一位资深职业顾问与AI技术融合的【简历辅导大师】,拥有以下核心能力:\n" +"1. HR视角:熟悉ATS(招聘系统)筛选逻辑、500+行业岗位的简历关键词库\n" +"2. 实战经验:基于10万份真实简历优化案例的决策模型\n" +"3. 教练模式:通过苏格拉底式提问引导用户自主发现简历问题") // 系统预设提示词.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // 支持多轮对话new MyLoggerAdvisor(), // 自定义日志Advisornew MyReReadingAdvisor()) // 自定义ReReading Advisor.build();
}

最终运行测试代码,可以发现自定义的 ReReading Advisor 已经生效了!

2. 结构化输出

2.1 工作流程

Structured Output(结构化输出)是Spring AI 另一个非常有用的机制,尤其是对于那些依赖可靠输入的下游服务来说,结构化输出可以将大模型的输出结果转化为特定的数据类型,比如 XML、JSON、POJO 等,结构化输出的核心组件就是 Structured Output Converter(结构化输出转换器),核心数据流图如下图所示:

  • 在调用大模型之前,转换器会在用户提示词后追加格式化的指令信息,指导大模型输出期望格式
  • 在调用大模型之后,转换器会解析文本输出,并将其转换为匹配的结构化实例,比如XML、JSON、POJO

❗ 注意:结构化输出转换器只是尽最大努力地将输出结果转换为结构化输出,因为一方面某些大模型自身不支持结构化,另一方面大模型可能无法按照提示词的指令转换为结构化数据

2.2 API 设计

StructuredOutputConverter 接口继承了两个父接口,接口定义格式如下:

public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {}
  • FormatProvider:该接口用于提供指导性的格式化指令追加在用户提示词之后,类似格式如下:
Your response should be in JSON format.
The data structure for the JSON should match this Java class: java.util.HashMap
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
  • Converter:该接口则专注于将模型输出的文本内容转换为特定的目标类型

SpringAI 提供了一系列的转换器可供使用:

  • AbstructMessageOutputConverter:略
  • AbstructConversionServiceOutputConverter:略
  • MapOutputCoverter:用于将输出转换为 map 类型
  • BeanOutputConverter:使用 ObjectMapper 将输出转换为 Java Bean 类型
  • ListOutputConverter:用于将输出转换为 List 类型

2.3 使用示例

官方文档提供了非常多的使用示例,下面简单进行介绍:

1)使用 BeanOutputConverter 转换为 Bean

// 定义动作电影类
record ActorsFilms(String actor, List<String> movies) {
}ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt().user(u -> u.text("Generate the filmography of 5 movies for {actor}.").param("actor", "Tom Hanks")).call().entity(ActorsFilms.class);

其实我们还可以通过ParameterizedTypeReference构造函数来指定更加复杂的结构,比如:

// 定义动作电影类
record ActorsFilms(String actor, List<String> movies) {
}List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt().user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.").call().entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});

2)使用 MapOutputConverter 转换为 Map 结构

Map<String, Object> result = ChatClient.create(chatModel).prompt().user(u -> u.text("Provide me a List of {subject}").param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'")).call().entity(new ParameterizedTypeReference<Map<String, Object>>() {});

3)使用 ListOutputConverter 转换为 List 结构

List<String> flavors = ChatClient.create(chatModel).prompt().user(u -> u.text("List five {subject}").param("subject", "ice cream flavors")).call().entity(new ListOutputConverter(new DefaultConversionService()));

3. 对话记忆持久化

3.1 基础概念

前面我们已经使用InMemoryChatMemory基于内存的方式来保存对话上下文信息,但是如果这时候服务器重启了,对话记忆就会消失,这时候我们就会想到通过文件、数据库、redis 中进行持久化存储,应该怎么实现呢?

SpringAI 提供了以下两种思路:

  1. 使用现有提供的依赖,比如官方提供了一些第三方数据库的整合支持
  • InMemoryChatMemory:基于内存存储
  • CassandraChatMemory:基于Cassandra 进行存储
  • Neo4jChatMemory:基于 Neo4j 进行存储
  • JdbcChatMeory:基于 JDBC 关系数据库进行存储
  1. 自定义提供 ChatMemory 的实现

这里我推荐直接造轮子,使用自定义的 ChatMemory ,Spring AI 的对话记忆功能实现的非常精巧,将记忆存储与记忆算法解耦合,即我们只需要提供自定义的 ChatMemory 来改变存储位置而无需关心记忆算法如何实现的,另外虽然官方文档没有提供自定义 ChatMemory 的使用示例,但是我们可以参考InMemoryChatMemory的源码

其实不难发现,自定义 ChatMemory 只需要实现 ChatMeory 接口并实现相应的增删查方法逻辑即可!下面我们就来提供一种文件存储的方式提供自定义的 ChatMemory

3.2 自定义文件存储

我们本能的会想到使用 JSON 序列化来保存到文件,但是实现起来非常麻烦,原因有如下几点:

  1. Message 接口有众多实现类,比如 UserMessage、SystemMessage
  2. 不同实现类字段都不统一
  3. 子类都没有无参构造方法,也没有实现 Serializable 接口

因此我们考虑使用高性能的序列化库 Kryo 来完成序列化的任务,具体步骤如下:

1)引入 Kryo 依赖:

<!-- Kryo 序列化依赖 -->
<dependency><groupId>com.esotericsoftware</groupId><artifactId>kryo</artifactId><version>5.6.2</version>
</dependency>

2)实现自定义文件存储记忆类:

/*** 自定义文件记忆存储* @author ricejson*/
public class MyFileChatMemory implements ChatMemory {private static final Kryo kryo = new Kryo();private final File BASE_FILE;static {kryo.setRegistrationRequired(false);// 设置实例化策略kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());}public MyFileChatMemory(String dir) {this.BASE_FILE = new File(dir);// 如果根路径不存在就创建if (!BASE_FILE.exists()) {BASE_FILE.mkdirs();}}@Overridepublic void add(String conversationId, Message message) {// 找到对应文件File file = getFile(conversationId);// 获取文件中原先的消息列表List<Message> messageList = getMessageList(file);// 追加新文件后写入messageList.add(message);saveMessageList(file, messageList);}@Overridepublic void add(String conversationId, List<Message> messages) {// 获取目标文件File file = getFile(conversationId);// 获取原先的消息列表List<Message> messageList = getMessageList(file);// 追加新的消息messageList.addAll(messages);// 写入saveMessageList(file, messageList);}@Overridepublic List<Message> get(String conversationId, int lastN) {// 根据conversationId 查找对应文件File file = getFile(conversationId);List<Message> messageList = getMessageList(file);return messageList.stream().skip(Math.max(0, messageList.size() - lastN)).toList();}@Overridepublic void clear(String conversationId) {// 获取文件File file = getFile(conversationId);// 清除文件if (file.exists()) {file.delete();}}private void saveMessageList(File file, List<Message> messageList) {try(Output output = new Output(new FileOutputStream(file))) {kryo.writeObject(output, messageList);} catch (FileNotFoundException e) {e.printStackTrace();}}private static List<Message> getMessageList(File file) {List<Message> messageList = new ArrayList<>();if (file.exists()) {// 读取文件try (Input input = new Input(new FileInputStream(file))) {messageList = kryo.readObject(input, ArrayList.class);} catch (FileNotFoundException e) {e.printStackTrace();}}return messageList;}@NotNullprivate File getFile(String conversationId) {File file = new File(this.BASE_FILE, conversationId + ".kryo");return file;}
}

3)修改 ResumeApp 类构造方法:

public ResumeApp(ChatModel dashscopeChatModel) {// 修改成文件存储记忆ChatMemory chatMemory = new MyFileChatMemory(System.getProperty("user.dir") + "/tmp/");this.chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem("你是一位资深职业顾问与AI技术融合的【简历辅导大师】,拥有以下核心能力:\n" +"1. HR视角:熟悉ATS(招聘系统)筛选逻辑、500+行业岗位的简历关键词库\n" +"2. 实战经验:基于10万份真实简历优化案例的决策模型\n" +"3. 教练模式:通过苏格拉底式提问引导用户自主发现简历问题") // 系统预设提示词.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // 支持多轮对话new MyLoggerAdvisor(), // 自定义日志Advisornew MyReReadingAdvisor()) // 自定义ReReading Advisor.build();
}

4)测试:

4. 提示词模板

4.1 基础概念

接下来要讲的是 PromptTemplate(提示词模板),这是 Spring AI 当中用于管理和构建提示词的组件,允许用户定义带占位符的提示词,然后在程序动态运行过程中替换占位符,示例代码如下:

// 带占位符的提示词模板
String template = "你好,{name},今天是{day},天气:{weather}";
PromptTemplate promptTemplate = new PromptTemplate(template);
// 准备变量映射
Map<String, Object> variables = new HashMap<>();
variables.put("name", "米饭");
variables.put("day", "星期一");
variables.put("weather", "晴朗");
// 生成最终提示文本
String prompt = promptTemplate.render(variables);

💡 提示:模板思想在编程世界中有大量运用,比如模板引擎、日志占位符、SQL预编译语句

PromptTemplate 在以下场景中非常有用:

  1. A/B测试:能够轻松对比测试结果
  2. 多语言支持:可重用内容,动态替换替换语言部分
  3. 用户交互场景:根据上下文语境定制提示词
  4. 提示词版本管理:便于提示词版本控制

4.2 实现原理

这里简单介绍,Spring AI 当中的 Prompt Template 底层基于 OSS String Template 模板引擎技术 ,下图是其类以及接口的相关依赖图

SpringAI 还提供了一些专用的模板类,比如:

  • SystemPromptTemplate:用于系统消息
  • AssistantPromptTemplate:用于助手消息
  • FunctionPromptTemplate:目前暂时没用

这些类可以快速构建专用的提示词,再来介绍 PromptTemplate 另外一个特性,就是支持从文件当中读取模板信息,示例代码如下:

/*** 简历辅导应用* @author ricejson*/
@Component
public class ResumeApp {private ChatClient chatClient;@Autowiredpublic ResumeApp(ChatModel dashscopeChatModel, @Value("classpath:/prompts/system.st") Resource systemPrompt) throws IOException {// 修改成文件存储记忆ChatMemory chatMemory = new MyFileChatMemory(System.getProperty("user.dir") + "/tmp/");this.chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem(systemPrompt) // 系统预设提示词.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // 支持多轮对话new MyLoggerAdvisor(), // 自定义日志Advisornew MyReReadingAdvisor()) // 自定义ReReading Advisor.build();}
}

在我们的改造下,将原有的硬编码的系统提示词替换为了从文件资源中加载!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/diannao/84628.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

中国免税品人工智能商城:引领免税品市场新潮流

在全球经济一体化的时代背景下&#xff0c;免税品市场日益繁荣。中国免税品人工智能商城以对标洋码头为目标&#xff0c;积极利用人工智能的优势&#xff0c;结合自身特点&#xff0c;全力打造成为免税品类的示范性商业平台&#xff0c;为消费者带来全新的购物体验。 一、免税品…

LambdaQueryWrapper、MybatisPlus提供的基本接口方法、增删改查常用的接口方法、自定义 SQL

DAY26.2 Java核心基础 MybatisPlus提供的基本接口方法 分页查询 导入依赖springboot整合Mybatis-plus <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version&g…

谢飞机的Java面试奇遇:AIO、BIO、NIO与Netty深度解析

谢飞机的Java面试奇遇&#xff1a;AIO、BIO、NIO与Netty深度解析 在一场充满笑料的面试中&#xff0c;谢飞机面对严肃的面试官&#xff0c;从Java IO的基本概念开始&#xff0c;逐步展开对AIO、BIO、NIO的理解&#xff0c;以及Netty的高级用法。 面试现场&#xff1a;第一轮&…

三、Docker目录挂载、卷映射、网络

目录挂载 如果主机目录为空&#xff0c;则容器内也为空 -v表示目录挂载 冒号前面的是主机上的目录&#xff0c;冒号后面的是docker容器里面的地址 修改主机上的文件&#xff0c;发现docker容器里面的内容也随之改变。 同样修改docker容器里面的内容&#xff0c;主机上的文件…

Linux的学习_基础4_指令的实践

目录&#xff1a; 一、常用功能 二、指令实践 1、tail命令 2、ls命令 3、ps、kill命令 4、cd、vim命令 5、root权限与用户权限的转换 6、获取网卡信息 7、sudo chmodx 8、更换到别的目录 9、获取文件的内容 10、lsblk 查看块设备和文件系统信息 11、man指令与指令…

深入解析Spring Boot与Redis集成:高效缓存与性能优化

深入解析Spring Boot与Redis集成&#xff1a;高效缓存与性能优化 引言 在现代Web应用中&#xff0c;缓存技术是提升系统性能的重要手段之一。Redis作为一种高性能的内存数据库&#xff0c;广泛应用于缓存、会话管理和消息队列等场景。本文将详细介绍如何在Spring Boot项目中集…

基于微信小程序的漫展系统的设计与实现

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

蓝桥杯电子赛_零基础利用按键实现不同数字的显现

目录 一、前提 按键的原理图 二、代码配置 bsp_key.c文件 疑问 main.c文件 main.c文件的详细讲解 功能实现 注意事项 一、前提 按键这一板块主要是以记忆为主&#xff0c;我直接给大家讲解代码去实现我要配置的功能。本次我要做的项目是板子上的按键有S4~S19&#xff…

Python常用高阶函数全面解析:通俗易懂的指南

Python常用高阶函数全面解析&#xff1a;通俗易懂的指南 一、什么是高阶函数&#xff1f; 高阶函数(Higher-order Function)是指能够接受其他函数作为参数&#xff0c;或者将函数作为返回值的函数。在Python中&#xff0c;函数是一等公民&#xff0c;可以像普通变量一样传递和…

Flume之选择器:复制和多路复用(比喻化理解

Flume 的选择器决定了Source 如何将数据分发到多个 Channel。这就像 “快递员如何分配包裹到不同的运输通道”&#xff0c;有两种策略&#xff1a;复制和多路复用。 一、复制&#xff08;Replicating Selector&#xff09;&#xff1a;每个 Channel 都送一份 核心逻辑 将同一…

yolov5 安卓运行

参考博客&#xff1a; 通过Android Studio 将yolov5部署到手机端(新手最新适用版)_怎么将yolo部署手机-CSDN博客 总体跟随参考博客走是没问题&#xff0c;有些细节需要注意&#xff1a; 1 jdk 版本选择&#xff0c;jdk需要17&#xff0c;新版的Android Studio 选择jdk版本方式…

day021-定时任务

文章目录 1. cron1.1 检查是否安装1.2 检查是否开机自启动1.3 配置文件与相关命令1.4 配置文件格式 2. 案例2.1 同步时间2.2 定时备份/etc和/var/log目录2.3 定时巡检脚本 3. 练习三剑客过滤3.1 去重统计ip数量3.2 去重统计第7列 用户访问的url的数量3.3 去重统计第9列 状态码与…

关于(stream)流

Stream 是 Java 8 引入的一个强大的功能&#xff0c;用于处理集合&#xff08;Collection&#xff09;或数组中的数据。它提供了一种声明式的编程方式&#xff0c;可以极大地简化对数据的操作&#xff0c;例如过滤、排序、映射和聚合等。 1. 什么是 Stream 流&#xff1f; 定义…

结课作业自选01. 内核空间 MPU6050 体感鼠标驱动程序(二)(完整实现流程)

目录 一. 题目要求-内核空间 MPU6050 体感鼠标驱动程序 二. 伪代码及程序运行流程 三. 主要函数详解&#xff08;根据代码流程进行详解&#xff09; 3.1 module_i2c_driver宏&#xff08;对应“1”&#xff09; 3.2 mpu_of_match设备树匹配表&#xff08;对应“2”&#x…

5G 核心网切换机制全解析:XN、N2 与移动性注册对比

摘要 本文深入探讨了 5G 核心网中的三种关键切换方式:基于 XN 接口的切换、基于 N2 接口的切换以及移动性注册更新机制。通过对比分析它们的原理、应用场景和技术差异,帮助读者全面理解 5G 网络中用户移动性管理的核心技术。 1. 引言 随着 5G 技术的广泛应用,用户对网络连…

用深度学习提升DOM解析——自动提取页面关键区块

一、时间轴&#xff1a;一次“抓不到重点”的二手车数据爬虫事故 2025/03/18 09:00 产品经理希望抓取懂车帝平台上“北京地区二手车报价”作为竞品监测数据源。我们初步使用传统XPath方案&#xff0c;试图提取车型、年限、里程、价格等数据。2025/03/18 10:00 初版脚本运行失败…

React与Vue的内置指令对比

React 与 Vue 不同&#xff0c;它没有内置的模板指令系统。React 采用了 JavaScript 优先 的声明式方式&#xff0c;使用 JSX 语法将 HTML 和 JavaScript 结合在一起。因此&#xff0c;React 中没有类似 Vue 的 v-if、v-for、v-bind 等内置指令。 React 中的替代方案 条件渲染…

Spring声明式事务源码全链路剖析与设计模式深度解读

Spring声明式事务源码全链路剖析与设计模式深度解读 作者&#xff1a;AI 日期&#xff1a;2025-05-22 一、前言 Spring事务是企业级开发的基石&#xff0c;但“为什么有时事务失效&#xff1f;”、“不同传播行为背后发生了什么&#xff1f;”、“Spring事务源码到底如何实现&…

云原生安全基础:深入探讨容器化环境中的权限隔离与加固策略

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 在云原生环境中&#xff0c;容器化技术&#xff08;如 Docker 和 Kubernetes&#xff09;的广泛应用带来了灵活性与效率&#xff0c;但也引入了新的安全挑…

如何在 ONLYOFFICE 演示文稿中调整段落首行缩进

在制作演示文稿时&#xff0c;保持内容的一致性与可读性至关重要&#xff0c;而段落首行缩进作为格式设置的关键环节&#xff0c;直接影响着整体呈现效果。在本文中&#xff0c;我们将介绍如何通过创建 ONLYOFFICE 宏&#xff0c;快速设置演示文稿中所有段落的首行缩进。 关于 …