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

Spring AI ChatMemory 对话记忆配置JDBC方式到Mysql数据库实战示例与原理讲解

场景Spring AI ChatMemory 对话记忆配置指南概念、实战与常见问题https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/161020514上述示例对话记忆使用内存方式如何使用JDBC方式将对话记忆到Mysql中。之前我们使用的 InMemoryChatMemoryRepository 将所有对话历史存储在 JVM 堆内存中服务重启后所有会话数据都会丢失且无法在多个服务实例间共享。将存储层切换为 JDBC如 MySQL、PostgreSQL后这些痛点可以得到有效解决。Spring AI 对存储介质做过一次重要的架构演进。早期版本直接提供了 JdbcChatMemory 类。但在 1.1.0 版本之后框架采用了更清晰的职责分离设计将原先的 JdbcChatMemory 标记为 Deprecated并引入了 MessageWindowChatMemory JdbcChatMemoryRepository 的组合模式。这一改动的核心思想是存储逻辑Repository与内存窗口管理ChatMemory彻底解耦使架构更具扩展性也可以更灵活地混合搭配不同的记忆策略与存储后端。核心知识点JdbcChatMemoryRepositoryJdbcChatMemoryRepository 是 ChatMemoryRepository 接口的 JDBC 实现它通过 JdbcTemplate 直接与关系型数据库交互将消息存储在一张名为 SPRING_AI_CHAT_MEMORY 的约定表中。它开箱即用地支持 MySQL、PostgreSQL、SQL Server、HSQLDB、H2 等多种数据库。每个会话由 conversation_id 唯一标识该组件负责根据此 ID 进行存取操作框架会自动完成消息的序列化与反序列化。MessageWindowChatMemory这是 Spring AI 当前推荐的核心记忆实现负责维护一个固定大小的消息窗口可通过 maxMessages 配置。当消息数量超过上限时自动移除旧消息但会保留系统消息SystemMessage。在 JDBC 持久化场景下ChatMemory Bean 注入 JdbcChatMemoryRepository 即可实现底层存储切换上层的滑动窗口策略完全不变。MessageChatMemoryAdvisor作为拦截器层Advisor 负责在每次请求中自动注入对话历史从 ChatMemory 中通过 conversation_id 加载并在收到模型回复后将新的消息写回存储。建表脚本与自动初始化框架约定的表名为 SPRING_AI_CHAT_MEMORY需要包含以下列列名类型说明idBIGINT主键自增conversation_idVARCHAR(36)会话唯一标识contentTEXT消息内容JSON 序列化typeVARCHAR(10)消息类型USER / ASSISTANT / SYSTEM / TOOLtimestampTIMESTAMP消息时间戳框架支持通过配置自动执行建表脚本。spring.ai.chat.memory.repository.jdbc.initialize-schema 的可选值包括 always每次启动都建表、embedded仅内嵌数据库自动建表、never不自动建表。开发环境建议使用 always生产环境建议使用 never由 DBA 手动管理表结构。Spring AI 内置了针对各种数据库的建表 SQL存放在 org/springframework/ai/chat/memory/jdbc/sql/ 目录下框架会根据检测到的数据源类型自动选择对应的脚本。也可以通过 schema 属性指定自定义脚本路径。架构对比维度InMemoryJDBC 持久化数据生命周期随 JVM 重启丢失持久化到数据库重启不丢跨实例共享不支持支持多实例共享同一数据库内存压力长期运行占用堆内存数据库承担存储压力审计与分析不支持可直接查询数据库配置复杂度零配置需配置数据源引入 JDBC 依赖注博客https://blog.csdn.net/badao_liumang_qizhi实现MySQL 环境准备-- 创建数据库 CREATE DATABASE IF NOT EXISTS spring_ai DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;pom.xml在之前依赖的基础上移除 InMemoryChatMemoryRepository 的手动配置依赖新增 JDBC Starter 和 MySQL 驱动properties java.version17/java.version spring-ai.version1.1.2/spring-ai.version /properties !-- 新增使用 Spring AI BOM 统一管理所有模块版本 -- dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring AI Ollama 核心 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-ollama/artifactId /dependency !-- Spring AI JDBC 聊天记忆持久化 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-chat-memory-repository-jdbc/artifactId /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies依赖变化说明新增 spring-ai-starter-model-chat-memory-repository-jdbc提供 JdbcChatMemoryRepository 的自动配置包含 spring-ai-autoconfigure-model-chat-memory 和 JDBC 存储实现两个模块。新增 mysql-connector-jMySQL 的 JDBC 驱动推荐使用新 artifactId mysql-connector-j 而非旧的 mysql-connector-java。移除 InMemoryChatMemoryRepository 的显式 Bean 声明框架在检测到 JdbcChatMemoryRepository 时会自动将默认的 InMemoryChatMemoryRepository 替换为 JDBC 实现不再需要手动声明。建表脚本创建 src/main/resources/sql/schema-mysql.sqlCREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( id BIGINT NOT NULL AUTO_INCREMENT, conversation_id VARCHAR(36) NOT NULL, content TEXT NOT NULL, type VARCHAR(10) NOT NULL, timestamp TIMESTAMP NOT NULL, PRIMARY KEY (id), INDEX idx_conv_ts (conversation_id, timestamp), CONSTRAINT type_check CHECK (type IN (USER, ASSISTANT, SYSTEM, TOOL)) );application.yml​ server: port: 886 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url:jdbc:mysql://localhost:3306/spring_ai?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf-8 username: root password: 123456 # 替换为实际密码 ai: ollama: base-url: http://localhost:11434 chat: model: qwen2.5:7b-instruct options: temperature: 0.7 num-ctx: 4096 # Ollama 上下文窗口大小 chat: memory: repository: jdbc: initialize-schema: always # 开发环境每次启动都执行建表脚本 schema: classpath:sql/schema-mysql.sql # 自定义建表脚本路径 logging: level: org.springframework.ai.chat.client.advisor: DEBUG org.springframework.jdbc: DEBUG # 可选查看 SQL 执行日志 ​配置重点解析spring.datasource.*标准的 Spring Boot 数据源配置连接 MySQL 数据库。spring.ai.chat.memory.repository.jdbc.initialize-schema: always每次启动时执行建表脚本开发环境推荐。spring.ai.chat.memory.repository.jdbc.schema指定自定义 SQL 脚本的路径覆盖框架内置的建表脚本。注意如果未指定 schema 属性框架会自动从 org/springframework/ai/chat/memory/jdbc/sql/ 加载内置的建表脚本支持 schema-mysql.sql、schema-postgresql.sql 等。ChatConfig 配置类关键变化不再手动创建 InMemoryChatMemoryRepository改为注入由 Starter 自动提供的 JdbcChatMemoryRepository。package com.badao.ai.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository; import org.springframework.ai.chat.model.ChatModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class ChatConfig { Bean public ChatMemory chatMemory(JdbcChatMemoryRepository jdbcChatMemoryRepository) { // 只需将 Repository 替换为 JDBC 实现窗口策略完全不变 return MessageWindowChatMemory.builder() .chatMemoryRepository(jdbcChatMemoryRepository) .maxMessages(20) // 每次最多携带 20 条历史消息 .build(); } Bean public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) { return ChatClient.builder(chatModel) .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build() ) .build(); } }改造对比从 InMemory 到 JDBC只需要在 chatMemory Bean 中将构造参数从 new InMemoryChatMemoryRepository() 替换为注入 JdbcChatMemoryRepository其余代码和业务逻辑全部不变。MemoryChatController 控制器package com.badao.ai.controller; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.memory.ChatMemory; // 导入 ChatMemory 接口 import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api) public class MemoryChatController { private final ChatClient chatClient; public MemoryChatController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/chat/memory) public ChatResponse chatWithMemory(RequestBody MemoryChatRequest request) { String result chatClient.prompt() .user(request.message()) .advisors(advisor - advisor.param( ChatMemory.CONVERSATION_ID, request.conversationId() )) .call() .content(); return new ChatResponse(200, success, result); } public record MemoryChatRequest(String message, String conversationId) {} public record ChatResponse(int code, String msg, String data) {} }测试验证原理深入1、Spring Boot 自动配置如何发现 JdbcChatMemory 并替换 InMemory整个过程由 Spring Boot 的自动配置机制驱动依赖触发在 pom.xml 中引入 spring-ai-starter-model-chat-memory-repository-jdbc 后JdbcChatMemoryRepository 类被加入 Classpath。条件装配框架中的 ChatMemoryAutoConfiguration 会检测 Classpath 中是否存在 JdbcChatMemoryRepository。如果存在且已配置 DataSource则自动创建一个 JdbcChatMemoryRepository Bean。Bean 覆盖由于存在 JdbcChatMemoryRepository Bean默认的 InMemoryChatMemoryRepository 自动配置会失效被条件装配注解跳过ChatMemory Bean 直接注入 JdbcChatMemoryRepository从而实现存储介质的无缝切换。整个过程无需任何 ConditionalOnMissingBean 的手动干预框架已内置完整的条件装配逻辑。2、JdbcChatMemoryRepository 如何将消息写入 MySQLJdbcChatMemoryRepository 的核心实现基于 Spring 的 JdbcTemplate它直接操作 SPRING_AI_CHAT_MEMORY 表。一条消息的写入过程如下MessageChatMemoryAdvisor 在 AI 返回响应后调用 ChatMemory.add(conversationId, messages)。MessageWindowChatMemory 收到新消息后先合并窗口中已有的消息再将合并后的完整消息列表通过 JdbcChatMemoryRepository.saveAll(conversationId, messages) 写回数据库。JdbcChatMemoryRepository.saveAll() 内部采用先删后插策略先 DELETE 该会话的所有旧消息再 INSERT 全部新消息。这种“全量替换”策略的好处是避免并发场景下的增量同步困难同时配合自增主键保证原子性。查询消息时findByConversationId() 会按 timestamp 升序排列所有消息再由 MessageWindowChatMemory 截取最近 maxMessages 条注入到 Prompt 中。3、数据库表结构与索引设计框架约定的表结构如下CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( id BIGINT NOT NULL AUTO_INCREMENT, conversation_id VARCHAR(36) NOT NULL, content TEXT NOT NULL, type VARCHAR(10) NOT NULL, timestamp TIMESTAMP NOT NULL, PRIMARY KEY (id), INDEX idx_conv_ts (conversation_id, timestamp), CONSTRAINT type_check CHECK (type IN (USER, ASSISTANT, SYSTEM, TOOL)) );索引设计分析联合索引 idx_conv_ts (conversation_id, timestamp) 覆盖了 findByConversationId() 的查询模式WHERE conversation_id ? ORDER BY timestamp形成覆盖索引查询性能极高无需回表。时间类型选择说明TIMESTAMP 类型能自动记录消息写入时的精确时间相比 DATETIME 更紧凑4 字节 vs 8 字节且受数据库时区设置影响适合追踪消息时序。序列化格式说明content 列存储的是消息的 JSON 序列化字符串。Spring AI 使用 Jackson 将 Message 对象序列化为 JSON包含消息文本、元数据如 Token 使用量、模型名称等完整信息直接以 TEXT 类型存储。4、滑动窗口如何与数据库配合MessageWindowChatMemory 在 get() 时只从数据库中取最近 maxMessages 条消息而非全部加载。SQL 查询层面按 timestamp 升序排列再在 Java 层截断。这保证了即使某次对话积累了成千上万条消息每次调用 AI 时仍然只携带可控数量的上下文。数据库中的旧消息不会被删除可随时通过直接查询表来获取完整的对话历史。5、ChatMemory 配置最佳实践配置项说明生产环境推荐值maxMessages记忆窗口大小10-20 条视模型上下文窗口而定initialize-schema建表策略never由 DBA 通过 Flyway/Liquibase 管理schema自定义脚本路径classpath:sql/schema-mysql.sql版本化管理

相关文章:

Spring AI ChatMemory 对话记忆配置JDBC方式到Mysql数据库实战示例与原理讲解

场景 Spring AI ChatMemory 对话记忆配置指南:概念、实战与常见问题: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/161020514 上述示例对话记忆使用内存方式,如何使用JDBC方式将对话记忆到Mysql中。 之前我们使用的 InMe…...

混沌工程实战:使用Roast平台提升分布式系统韧性

1. 项目概述:从“烤”代码到“烤”出高质量最近在跟几个做后端开发的朋友聊天,大家普遍有个痛点:项目迭代快了,代码质量就容易滑坡。单元测试覆盖率看着还行,但一上线,各种稀奇古怪的线上问题就冒出来了&am…...

天线阻抗匹配原理与工程实践指南

1. 天线阻抗匹配基础概念解析阻抗匹配是射频工程师日常工作中最常遇到的技术挑战之一。简单来说,它就像是在为天线系统"调音",确保射频能量能够顺畅地从发射电路传递到天线,而不会在连接处产生"回声"(反射波&…...

2026快消日化CRM选型指南,这几点一定注意

针对洗护日化行业SKU繁杂、全渠道(KA/CS/母婴)管理难的技术痛点,企业在CRM选型时必须关注SFA执行、DMS协同及ERP深度集成的能力。我们在日化赛道,通过勤策SFAAI Agent方案,帮客户把陈列识别准确率提升至98%&#xff0c…...

非傍轴效应在量子比特操控中的影响与优化策略

1. 非傍轴效应与量子比特操控:从理论到实验的全景解析在量子计算与模拟领域,光学镊子技术正经历着革命性的发展。这项技术通过高度聚焦的激光束,实现了对单个原子或离子的精确操控,为构建大规模量子处理器提供了可能路径。然而&am…...

PRIME OS:基于React与Supabase的浏览器操作系统架构解析

1. 项目概述如果你和我一样,对“浏览器里的操作系统”这个概念着迷,同时又对市面上那些要么过于玩具化、要么复杂到无从下手的项目感到失望,那么今天聊的这个项目——PRIME OS,绝对值得你花时间深入研究。它不是一个简单的桌面模拟…...

Swagger Skills:让OpenAPI文档活起来,实现自动化契约测试与场景编排

1. 项目概述:一个为Swagger API文档注入“技能”的利器如果你是一名后端开发者,或者经常需要与API打交道,那么Swagger(现在更常被称为OpenAPI)对你来说一定不陌生。它通过一个标准的YAML或JSON文件,清晰地描…...

16Gb容量+1866Mbps速率:NT6CL512T32AM-H1的LPDDR3移动存储参数解析

NT6CL512T32AM-H1:16Gb LPDDR3移动DRAM的技术解析在移动计算、工业嵌入式以及车载信息娱乐等对功耗和性能双重敏感的应用领域,内存子系统的选择直接影响产品的续航能力和数据处理效率。NT6CL512T32AM-H1是南亚科技推出的一款16Gb LPDDR3 SDRAM&#xff0…...

3分钟掌握Windows文件占用检测:PowerToys File Locksmith终极解决方案

3分钟掌握Windows文件占用检测:PowerToys File Locksmith终极解决方案 【免费下载链接】PowerToys Microsoft PowerToys is a collection of utilities that supercharge productivity and customization on Windows 项目地址: https://gitcode.com/GitHub_Trendi…...

轻量级负载均衡器Codex-lb:云原生场景下的部署与调优实践

1. 项目概述:一个轻量级的负载均衡解决方案 最近在折腾一些个人项目和小型服务部署时,我遇到了一个挺实际的问题:如何在不引入复杂架构和运维负担的前提下,为多个后端服务实例提供一个统一的、可靠的入口。你可能也遇到过类似场景…...

企业级知识管理新门槛:NotebookLM单用户年成本超$298?我们用5类典型场景算清ROI临界点

更多请点击: https://intelliparadigm.com 第一章:企业级知识管理新门槛:NotebookLM单用户年成本超$298?我们用5类典型场景算清ROI临界点 当企业评估AI增强型知识管理工具时,隐性成本常被低估——NotebookLM虽未公开…...

云代理商:Hermes Agent如何通过技能沉淀降低长期算力消耗

在 AI 智能体规模化落地的今天,算力成本高、重复推理多、长期运行效率衰减,已成为企业和开发者的核心痛点。传统 AI 智能体每处理一次相似任务,都要从零开始推理、反复调用工具,大量算力浪费在重复劳动中,长期使用成本…...

GitHub MDC文件渲染优化:基于UserScript的Markdown预览增强方案

1. 项目概述:让GitHub读懂Cursor的“规则文件”如果你和我一样,是Cursor的深度用户,那你肯定没少和.mdc文件打交道。这些文件是Cursor AI的“规则集”(Cursor Rules),本质上就是一份用Markdown语法写的项目…...

闲置烽火HG680L变身全能播放器:S905L-3B芯片刷机后安装EmuELEC游戏系统+CoreELEC影音库

闲置烽火HG680L改造指南:打造全能家庭娱乐终端 家里角落积灰的烽火HG680L机顶盒,其实是一块被低估的硬件宝藏。搭载Amlogic S905L-3B芯片的它,性能远超普通电视盒子。通过巧妙改造,不仅能流畅运行复古游戏系统,还能变身…...

DSP28335新手避坑指南:手把手教你用CCS6.2生成10KHz SPWM(附完整工程)

DSP28335实战:从零构建10KHz SPWM的完整工程指南 第一次接触DSP28335开发板时,面对复杂的寄存器配置和编译环境问题,很多工程师都会感到无从下手。本文将带你一步步完成从CCS工程创建到SPWM波形输出的全过程,特别针对新手容易遇到…...

手把手教你配置i.MX RT1052的BOOT引脚:从HyperFlash到QSPI的启动选择实战

手把手教你配置i.MX RT1052的BOOT引脚:从HyperFlash到QSPI的启动选择实战 在嵌入式系统开发中,启动配置是硬件工程师和开发者面临的第一个关键挑战。i.MX RT1052作为一款高性能跨界处理器,其灵活的启动选项既带来了强大的适应性,也…...

告别时钟线!用三根线搞定高速传输:MIPI C-PHY硬件连接与编码原理详解

告别时钟线!用三根线搞定高速传输:MIPI C-PHY硬件连接与编码原理详解 在高速数据传输领域,传统并行总线的时钟同步机制已成为提升速率的瓶颈。MIPI联盟推出的C-PHY标准,以革命性的"三线无时钟"架构打破了这一僵局。本文…...

保姆级避坑指南:在Ubuntu18.04上用ROS Melodic搞定UR5+Realsense D435i手眼标定(附旧版easy_handeye包)

深度避坑实战:Ubuntu18.04ROS Melodic手眼标定全流程精解 当机械臂的末端执行器需要与视觉系统协同工作时,手眼标定成为连接两者的关键桥梁。本文将以UR5机械臂搭配Realsense D435i相机为例,深入剖析在Ubuntu18.04和ROS Melodic环境下实现高精…...

怀旧服WLK:10人NAXX教官拉苏维奥斯保姆级攻略,暗牧控制与学员轮换时间轴详解

怀旧服WLK:10人NAXX教官拉苏维奥斯保姆级攻略,暗牧控制与学员轮换时间轴详解 在《魔兽世界》怀旧服巫妖王之怒版本中,纳克萨玛斯军事区的教官拉苏维奥斯堪称团队配合的"试金石"。这个看似机制简单的BOSS,却因学员控制与…...

2026年,想找A研发公司?这些关键选择要点你不可不知!

在科技飞速发展的2026年,AI技术已经广泛应用于各个领域,众多企业都希望借助AI研发公司的力量来提升自身竞争力。然而,面对市场上众多的AI研发公司,如何做出正确的选择成为了一大难题。下面就为大家介绍一些选择AI研发公司的关键要…...

修复肝衰竭的“免疫刹车”:ANXA1是控制炎症失控、促进消退的关键内源信号

慢加急性肝衰竭(ACLF)的发病进程主要由全身性炎症反应及免疫功能紊乱共同驱动,其病理机制复杂且临床预后较差。2026年4月,浙江大学与斯坦福大学,在Hepatology期刊在线发表了题为“Dissecting the liver inflammation e…...

AMD锐龙SMU调试工具:从新手到专家的完整调优指南

AMD锐龙SMU调试工具:从新手到专家的完整调优指南 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: https://gitcode…...

Windows XP图标主题:如何在现代Linux桌面重现经典视觉体验

Windows XP图标主题:如何在现代Linux桌面重现经典视觉体验 【免费下载链接】Windows-XP Remake of classic YlmfOS theme with some mods for icons to scale right 项目地址: https://gitcode.com/gh_mirrors/win/Windows-XP 还在为现代桌面环境的单调图标感…...

别再手动画图了!用Python ASE + Matplotlib一键生成高质量材料结构图(附完整代码)

科研绘图革命:用Python ASEMatplotlib实现材料结构可视化自动化 深夜的实验室里,屏幕荧光映照着一张疲惫的脸——这可能是许多材料科学研究者共同的记忆。当你在论文截稿日前夕,还在反复调整VESTA中的原子位置、尝试各种角度截图时&#xff0…...

别再死记硬背了!用PyTorch和TensorFlow动手实现池化层,5分钟搞懂Max Pooling和Average Pooling的区别

用PyTorch和TensorFlow实战池化层:5分钟可视化Max与Average Pooling差异 刚接触深度学习的开发者常被各种理论概念困扰,尤其是池化层这类看似简单却暗藏玄机的操作。与其死记硬背定义,不如打开Jupyter Notebook,用PyTorch和Tensor…...

别再死记PCA步骤了!用Python手推一遍协方差矩阵与特征值,真正搞懂降维本质

从协方差矩阵到特征值分解:用Python彻底理解PCA的数学本质 主成分分析(PCA)作为数据降维的经典算法,在实际应用中常被简化为"标准化→协方差矩阵→特征分解→降维"的固定流程。但真正理解其数学本质的开发者却寥寥无几—…...

Python操控Photoshop的终极指南:如何用代码实现高效图像处理自动化

Python操控Photoshop的终极指南:如何用代码实现高效图像处理自动化 【免费下载链接】photoshop-python-api Python API for Photoshop. 项目地址: https://gitcode.com/gh_mirrors/ph/photoshop-python-api 如果你还在手动重复Photoshop操作,那么…...

5分钟快速掌握:Sonar CNES Report代码质量报告生成终极指南

5分钟快速掌握:Sonar CNES Report代码质量报告生成终极指南 【免费下载链接】sonar-cnes-report Generates analysis reports from SonarQube web API. 项目地址: https://gitcode.com/gh_mirrors/so/sonar-cnes-report 你是否曾为向团队展示代码质量数据而烦…...

Ubuntu 下 Rider 无法识别 Unreal Engine 的解决方法

Ubuntu 下 Rider 无法识别 Unreal Engine 的解决方法适用环境:JetBrains Rider Ubuntu Unreal Engine(含预发布/自定义安装版本)问题描述在 Ubuntu 上使用 Rider 打开 UE 项目时,IDE 提示找不到引擎,或 .uproject 文…...

JavaScript 遍历 JSON 所有 Key 的方法

1️⃣ for…in 循环(最常用) const json {name: "张三",age: 25,city: "北京" };for (let key in json) {console.log(key); // name, age, cityconsole.log(json[key]); // 张三, 25, 北京 }2️⃣ Object.keys()&am…...