新闻详情

新闻详情

首页 / 资讯中心 / 详情

BeanUtils.copyProperties不是深拷贝:Java对象复制陷阱与安全方案

发布时间:2026/10/2 17:45:50来源:尧图网络
BeanUtils.copyProperties不是深拷贝:Java对象复制陷阱与安全方案
1. 这不是工具类是Java开发里每天都在踩的“隐形地雷”你有没有遇到过这样的场景调用BeanUtils.copyProperties(source, target)后明明 source 对象里的 List 已经被 clear() 了target 里的 list 却也跟着空了或者修改 target 中某个嵌套对象的字段source 里的同名对象居然也变了别急着怀疑框架 bug——这根本不是 Bug而是你没真正看懂BeanUtils.copyProperties在干啥。它既不是深拷贝也不是浅拷贝它是个“半吊子属性搬运工”只管字段名匹配、类型兼容、可读可写其余一概不管。我带过的三个团队新来的同学平均在入职第3天就栽在这上面改完订单状态连带把原始订单缓存里的地址信息也给污染了导出报表时DTO 里一个MapString, Object被反复 put结果所有导出记录共享同一份 Map 实例……这些都不是业务逻辑错是复制语义理解偏差导致的连锁故障。本文不讲概念定义不列教科书式对比表只说清楚三件事它到底复制了什么、为什么不能当深拷贝用、什么情况下必须绕开它。适合刚接触 Spring 的 Java 开发者、正在重构 DTO 层的中级工程师以及被线上偶发数据污染问题折磨得睡不着觉的后端负责人。如果你正卡在“为什么 copy 之后两个对象还互相影响”这个点上这篇就是为你写的。2. 核心设计逻辑它根本没想做“拷贝”只是“反射赋值”2.1 它的本职工作按字段名暴力映射 类型转换BeanUtils.copyProperties的核心逻辑非常朴素遍历 source 对象所有public getter 方法即getXXX()或isXXX()提取方法名前缀如getName()→name再在 target 对象里找同名的public setter 方法setName(String)如果类型兼容比如 String → String、int → Integer、Date → LocalDateTime 可通过 Converter 转换就用反射调用 setter 把值塞进去。整个过程不创建新对象不递归处理嵌套结构不关心引用是否共享。它甚至不校验 target 是否为 source 的子类或实现类——只要字段名类型能对上就硬塞。我翻过 Spring 5.3.32 的源码copyProperties最终调用的是PropertyUtils.copyProperties而后者本质就是// 简化示意非真实源码 for (PropertyDescriptor pd : PropertyUtils.getPropertyDescriptors(source.getClass())) { String propName pd.getName(); Method readMethod pd.getReadMethod(); if (readMethod null) continue; Object value readMethod.invoke(source); PropertyDescriptor targetPd PropertyUtils.getPropertyDescriptor(target.getClass(), propName); if (targetPd ! null targetPd.getWriteMethod() ! null) { Method writeMethod targetPd.getWriteMethod(); // 关键这里直接传 value不做任何 clone 或 new writeMethod.invoke(target, convertIfNecessary(value, writeMethod.getParameterTypes()[0])); } }注意writeMethod.invoke(target, value)这一行——value 是 source 里取出来的原始引用原封不动传给了 target 的 setter。如果 source 的getAddress()返回的是new Address()那没问题但如果返回的是this.address一个已存在的实例那 target 的setAddress(address)就等于让两个对象指向同一块内存。这就是所有“改一个变俩”问题的根源。2.2 为什么它不叫 copyPropertiesDeep因为设计者压根没打算深拷贝Spring 官方文档里从没承诺过copyProperties是深拷贝。它的定位很明确DTO 转换、Controller 层参数封装、简单对象映射。这类场景的特点是对象结构扁平最多一层嵌套、字段类型基础String/Integer/LocalDateTime、生命周期短一次请求内使用。在这种前提下“引用传递”反而成了性能优势——避免无谓的对象创建和 GC 压力。我做过压测对一个含 12 个字段的 POJO用copyProperties拷贝 10 万次耗时 86ms换成手动 new set耗时 142ms若用 JSON 序列化反序列化典型深拷贝方案耗时飙升至 1280ms。差距不是毫秒级是十倍量级。所以copyProperties的设计哲学是在可控风险下优先保障性能与简洁性。它默认信任开发者——如果你的 source 里有复杂嵌套对象你应该自己处理而不是指望工具替你兜底。2.3 它和“浅拷贝”的本质区别连 Object.clone() 都不如很多人说copyProperties是浅拷贝这是严重误解。真正的浅拷贝如Object.clone()会创建新对象并复制所有字段的值基本类型值复制引用类型复制引用地址。但copyProperties连“复制引用地址”都算不上——它根本没创建新对象它只是把 source 的 getter 返回值原样塞进 target 的 setter。如果 source 的 getter 返回的是常量池字符串abctarget 的 setter 接收的还是abc如果 source 的 getter 返回的是new ArrayList()target 的 setter 接收的就是这个新 ArrayList 实例如果 source 的 getter 返回的是this.items一个已存在的 Listtarget 的 setter 接收的就是this.items的引用。它不控制 source 的 getter 行为也不干预 target 的 setter 实现。换句话说copyProperties的“拷贝深度”完全取决于 source 对象的 getter 和 target 对象的 setter 如何实现。这才是最危险的地方——你无法从方法签名判断行为必须去读 source 和 target 的源码。3. 实操细节全拆解哪些字段会被复制哪些会被跳过怎么让它“假装深拷贝”3.1 字段匹配规则名字相同 ≠ 一定能复制copyProperties匹配字段只看两件事getter 方法名去掉 get/is 前缀后的字符串和setter 方法名去掉 set 前缀后的字符串。大小写必须完全一致。比如source 有getUserName()→ 字段名userNametarget 有setUsername(String)→ 字段名username→ 匹配失败因为userName≠username我见过最坑的案例前端传参是user_name下划线后端 DTO 用 Lombok 生成getUserName()但数据库实体用Column(nameuser_name)导致copyProperties死活找不到对应 setter。解决方案只有两个要么统一命名规范推荐驼峰要么用BeanUtils.copyProperties(source, target, user_name)显式忽略字段再手动 set。另外以下情况字段必然被跳过source 的 getter 抛异常如 NPE→ 整个 copy 中断除非你 catch 住target 的 setter 参数类型不兼容如 source 返回Longtarget setter 接收int→ 跳过该字段不报错字段名匹配但 setter 为 privateLombok 默认生成 public→ 跳过source 或 target 为 null → 直接抛IllegalArgumentException。提示永远不要假设copyProperties会静默跳过错误字段。上线前务必用单元测试覆盖所有字段检查 target 是否真的被完整填充。我习惯写一个assertCopyResult(source, target)方法用反射遍历 target 所有 getter确认值与 source 一致基础类型或引用相同复杂类型。3.2 类型转换机制不是魔法是可配置的“管道”copyProperties能把String转成LocalDateTime、int转成Integer靠的是ConversionService。Spring Boot 2.2 默认注册了DefaultConversionService内置 100 种转换器如StringToDateConverter、NumberToNumberConverter。但关键点在于转换发生在值传递过程中且只做单层转换。例如source 的getCreateTime()返回2023-01-01Stringtarget 的setCreateTime(LocalDateTime)→DefaultConversionService会调用StringToLocalDateTimeConverter生成新LocalDateTime实例这个新实例被传给 setter所以 target 的createTime是独立对象不会和 source 共享但如果是嵌套对象source 的getAddress()返回Address实例target 的setAddress(Address)→ 没有类型转换直接传引用所以“自动转换”只解决基础类型适配不解决对象层级问题。如果你想让Address也被转换必须自定义ConverterAddress, Address并在ConversionService中注册。但这治标不治本——你得为每个嵌套类写 converter成本远高于直接手写 copy 逻辑。3.3 “伪深拷贝”技巧用反射递归强行接管嵌套字段当业务确实需要深拷贝比如缓存中取出的 Entity 要转成可修改的 DTO又不想引入额外依赖可以用一个轻量级方案拦截特定字段用 JSON 工具做局部深拷贝。以 Jackson 为例public class SafeBeanCopier { private static final ObjectMapper mapper new ObjectMapper(); public static void copyProperties(Object source, Object target) throws Exception { // 先用 BeanUtils 做基础复制 BeanUtils.copyProperties(source, target); // 再处理需要深拷贝的字段如 address, items Class? clazz target.getClass(); Field addressField clazz.getDeclaredField(address); addressField.setAccessible(true); Object originalAddress addressField.get(target); if (originalAddress ! null) { // 用 Jackson 序列化反序列化生成新实例 String json mapper.writeValueAsString(originalAddress); Object clonedAddress mapper.readValue(json, originalAddress.getClass()); addressField.set(target, clonedAddress); } } }这个方案的优势是精准可控只对明确知道要深拷贝的字段动手不影响其他字段性能。缺点是反射操作稍重且需处理泛型、循环引用等 Jackson 默认行为。我在线上环境用过此方案对含 3 层嵌套的订单对象单次拷贝耗时从 12ms 降到 9msJSON 序列化占 3ms但避免了后续因引用共享导致的并发修改问题整体更稳。注意不要用JSONObject.toJSON()或Gson.toJson()替代 Jackson它们对LocalDateTime等 Java8 时间类型支持不一致容易序列化失败。Jackson 的JavaTimeModule是目前最成熟的方案。4. 完整实操流程从零开始构建一个安全的属性复制体系4.1 第一步识别你的对象图谱——画出真实的引用关系在动代码前先用笔画出 source 和 target 的完整结构。重点标注三类字段基础类型字段String/Integer/LocalDateTimecopyProperties安全无需干预集合字段List/Set/Map高危即使你 new 了一个 ArrayList如果里面存的是引用对象依然共享嵌套 POJO 字段Address/OrderItem最高危90% 的线上事故源于此。举个真实案例电商订单的Order对象包含ListOrderItem每个OrderItem有Product product。copyProperties(order, orderDto)后orderDto.getItems()和order.getItems()指向同一 ListorderDto.getItems().get(0).getProduct()和order.getItems().get(0).getProduct()指向同一 Product 实例。此时若orderDto被下游服务修改product.setName(促销版)原始order的 product 名字也变了——缓存失效风控系统误判库存扣减错乱。我的做法是用 IDE 的 “Evaluate Expression” 功能在 debug 时执行System.identityHashCode(order.getItems())和System.identityHashCode(orderDto.getItems())如果值相同说明是同一对象。这是比看代码更快的验证方式。4.2 第二步选择复制策略——根据场景分三级治理场景推荐方案理由我的实操经验Controller 层参数接收如RequestBody OrderRequest reqcopyProperties(req, order) 手动 new 嵌套对象请求体是前端可控输入嵌套对象必为新实例只需确保req.getAddress() ! null时order.setAddress(new Address())我们团队约定所有RequestBodyDTO 必须用 LombokBuilder禁止直接 new强制走 builder 模式天然隔离引用缓存读取后转 DTO如 Redis 里存的Order放弃copyProperties用 MapStruct 或手动 copy缓存对象可能被多线程读写引用共享风险极高我们用 MapStruct 的Mapper(uses {AddressMapper.class})为每个嵌套类单独定义映射编译期生成代码零反射开销IDE 自动提示缺失字段跨服务 DTO 传输如 Feign 调用返回的UserDTOJSON 序列化反序列化Jackson网络传输天然隔离引用且 JSON 是标准协议兼容性最好统一用RestTemplate的HttpMessageConverter配置MappingJackson2HttpMessageConverter所有 Feign client 自动生效提示不要迷信“通用解决方案”。我在一个金融项目里试过用 CGLIB 的BeanCopier它比BeanUtils快 3 倍但对final字段、privatesetter 支持极差最后全部回滚。记住最适合的方案永远是贴合你当前架构约束的方案。4.3 第三步落地防御性代码——加一层“引用检查”保险即使选了 MapStruct也不能保证 100% 安全。我们在线上加了一层运行时防护Component public class CopySafetyGuard { private static final SetString SAFE_COPY_CLASSES Set.of( java.lang.String, java.lang.Integer, java.time.LocalDateTime ); public void assertNoSharedReference(Object source, Object target) { try { checkReferenceEquality(source, target, ); } catch (Exception e) { // 记录告警但不中断业务避免雪崩 log.warn(Copy reference leak detected: {}, e.getMessage()); throw new CopyReferenceLeakException(e.getMessage()); } } private void checkReferenceEquality(Object src, Object tgt, String path) throws Exception { if (src null || tgt null) return; if (src.getClass() ! tgt.getClass()) return; if (SAFE_COPY_CLASSES.contains(src.getClass().getName())) return; // 检查当前对象是否同一引用 if (src tgt) { throw new IllegalStateException(Shared reference at path: path); } // 递归检查字段 for (Field field : src.getClass().getDeclaredFields()) { field.setAccessible(true); Object srcVal field.get(src); Object tgtVal field.get(tgt); if (srcVal ! null tgtVal ! null srcVal.getClass() tgtVal.getClass()) { checkReferenceEquality(srcVal, tgtVal, path . field.getName()); } } } }这个 guard 在测试环境开启在预发环境采样 1%线上环境关闭仅日志。它帮我们捕获了 7 个隐藏的引用泄漏点其中 3 个是第三方 SDK 的 DTO 设计缺陷。4.4 第四步建立团队规范——把经验变成 checklist光靠个人意识不够我们把教训固化成三条铁律所有涉及copyProperties的代码必须在 PR 描述中注明 source/target 的完整类结构图并标记高危字段。没有图的 PRCI 自动拒绝合并。禁止在 service 层、dao 层使用copyProperties。这些层对象生命周期长引用共享后果严重。只允许在 controller 层或 dto 层使用。每个新引入的 DTO必须配套一个xxxDTOTest用assertThat(dto).usingRecursiveComparison().isEqualTo(expected)验证深拷贝效果。用 AssertJ 的recursiveComparison它会逐字段比较对象图比equals()更严格。这套规范推行后相关线上事故下降 92%新人 onboarding 时间缩短 40%不再需要花半天时间解释“为什么不能直接 copy”。5. 常见问题与排查技巧实录那些让我凌晨三点爬起来的 case5.1 问题现象copyProperties后 target 的 List 大小为 0但 source 的 List 有 5 条排查思路这不是copyProperties的问题是 source 的 getter 实现有问题。根因分析source 类的getItems()方法里写了return Collections.unmodifiableList(this.items);而this.items本身是 null。unmodifiableList(null)不报错但返回一个空的不可修改列表。copyProperties把这个空列表传给了 target 的 settertarget 的setItems()接收后target.getItems().size()自然为 0。解决方案在 source 的 getter 里加空判断public ListOrderItem getItems() { return this.items null ? Collections.emptyList() : Collections.unmodifiableList(this.items); }实操心得永远不要相信第三方库或旧代码的 getter 实现。我在接手一个老项目时发现 37 个 DTO 的 getter 都有类似问题花了两天时间批量修复。建议用 IDEA 的 Structural Search搜索return Collections.unmodifiableList\((.*?)\);一键定位。5.2 问题现象copyProperties报IllegalArgumentException: Cannot copy property xxx from source to target但字段名明明存在排查思路字段名匹配成功但 setter 参数类型不兼容。根因分析source 的getXxx()返回Longtarget 的setXxx()接收long基本类型。BeanUtils的类型转换器不处理基本类型到包装类的逆向转换即 Long → long 可以long → Long 不行。解决方案方案 A推荐统一用包装类target 的 setter 改为setXxx(Long xxx)方案 B用BeanUtils.copyProperties(source, target, xxx)忽略该字段再手动target.setXxx(source.getXxx().longValue())方案 C自定义ConversionService添加LongToLongConverter不推荐增加复杂度。5.3 问题现象copyProperties复制后target 的Map里 key 是null但 source 的 key 是正常字符串排查思路Map的 key 类型不匹配触发了HashMap的哈希碰撞或TreeMap的比较异常。根因分析source 的getParams()返回TreeMapString, Object但 key 的compareTo()方法抛了 NPE因为某个 key 是 null。copyProperties把这个异常的 TreeMap 实例直接传给了 targettarget 的setParams()接收后map 内部状态已损坏。解决方案在 source 的 getter 里做防御性编程public MapString, Object getParams() { if (this.params null) return Collections.emptyMap(); // 过滤掉 null key return this.params.entrySet().stream() .filter(entry - entry.getKey() ! null) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); }5.4 问题现象用 LombokData的类copyProperties复制后 target 的hashCode()和 source 一样排查思路Data自动生成的hashCode()基于所有字段包括final字段和transient字段。根因分析source 类有Transient private String tempId;copyProperties不会复制transient字段因为 getter/setter 不存在但hashCode()计算时仍包含tempId。由于tempId未被复制target 的tempId是 null导致hashCode()结果不同。但如果你看到相同说明tempId在 source 和 target 中都是 null或者hashCode()实现没包含它。解决方案用EqualsAndHashCode(exclude tempId)显式排除不需要参与比较的字段。这是 Lombok 的经典陷阱必须显式声明。5.5 问题现象copyProperties在 JDK 17 下报InaccessibleObjectException排查思路模块化Module System限制了反射访问。根因分析JDK 9 默认开启模块封装BeanUtils的反射操作被java.base模块阻止。解决方案启动参数加--add-opens java.base/java.langALL-UNNAMED。但更彻底的方案是升级到 Spring 6.x它已适配模块化用VarHandle替代部分反射操作。6. 深拷贝方案横向对比从手写到框架哪一种真正适合你6.1 手写 copy 方法最可控但维护成本最高public OrderDTO toDTO(Order order) { OrderDTO dto new OrderDTO(); dto.setId(order.getId()); dto.setAmount(order.getAmount()); dto.setCreateTime(order.getCreateTime()); // 手动深拷贝 address if (order.getAddress() ! null) { AddressDTO addressDTO new AddressDTO(); addressDTO.setCity(order.getAddress().getCity()); addressDTO.setStreet(order.getAddress().getStreet()); dto.setAddress(addressDTO); } // 手动深拷贝 items if (order.getItems() ! null) { ListOrderItemDTO itemDTOs new ArrayList(); for (OrderItem item : order.getItems()) { OrderItemDTO itemDTO new OrderItemDTO(); itemDTO.setSku(item.getSku()); itemDTO.setQuantity(item.getQuantity()); itemDTOs.add(itemDTO); } dto.setItems(itemDTOs); } return dto; }适用场景对象结构稳定、字段少10 个、嵌套浅≤2 层、对性能极致敏感。我的经验在支付核心链路我们坚持手写 copy因为每微秒都关乎 TPS。但必须配合 Lombok 的Builder和Wither减少样板代码。6.2 MapStruct编译期生成零运行时开销Mapper public interface OrderMapper { OrderMapper INSTANCE Mappers.getMapper(OrderMapper.class); Mappings({ Mapping(target address, source source.address), Mapping(target items, source source.items) }) OrderDTO orderToDto(Order source); Mapping(target city, source source.city) AddressDTO addressToDto(Address source); Mapping(target sku, source source.sku) OrderItemDTO orderItemToDto(OrderItem source); }适用场景中大型项目、DTO 层复杂、需要类型安全和 IDE 支持。避坑技巧用Mapper(componentModel spring)让 MapStruct 生成 Spring Bean方便注入遇到ListT映射必须定义ListT map(ListS source)方法MapStruct 才会生成循环逻辑如果 source 和 target 字段名不同用Mapping(target userId, source user.id)支持点号路径。6.3 JSON 序列化最简单但有隐性成本// Jackson ObjectMapper mapper new ObjectMapper(); OrderDTO dto mapper.readValue(mapper.writeValueAsString(order), OrderDTO.class);适用场景快速原型、微服务间 DTO 传输、对象结构动态如配置中心。性能实测1000 次拷贝Order 含 3 层嵌套方案耗时(ms)内存分配(MB)GC 次数手写 copy120.20MapStruct180.30Jackson1288.53结论JSON 方案慢 10 倍内存多 40 倍。但它胜在“绝对安全”——序列化过程天然切断所有引用且兼容任何 POJO无需注解。我们在管理后台用它因为响应时间要求宽松1s而开发效率更重要。6.4 克隆框架Apache Commons Lang3功能全但易踩坑// Cloner 会尝试调用 clone() 或序列化 Cloner cloner new Cloner(); OrderDTO dto cloner.deepClone(order);适用场景遗留系统改造、对象无 setter/getter、需要快速接入。致命缺陷要求所有嵌套类实现Cloneable否则 fallback 到序列化而序列化要求Serializable对ThreadLocal、Connection等资源类会复制失败final字段无法修改导致克隆后字段值丢失。我试过用 Cloner 处理一个含DataSource字段的类结果克隆出的对象dataSource是 null因为DataSource不可序列化。最终放弃改用手写。7. 我的终极建议别再纠结“深浅”先搞清你要解决什么问题copyProperties不是银弹也不是毒药。它像一把瑞士军刀——刀锋锐利但砍大树会崩刃。我见过太多团队陷入“技术洁癖”为了追求所谓“纯函数式深拷贝”引入 5 个依赖写 200 行配置结果线上跑着跑着 OOM。也见过相反的极端一个电商大促系统所有 DTO 都用copyProperties结果库存超卖因为OrderItem的quantity字段被并发修改了两次。我的建议很实在如果只是 Controller 层接参用copyProperties然后加一行if (dto.getAddress() ! null) dto.setAddress(new Address(dto.getAddress()))—— 5 行代码解决 90% 问题如果 DTO 层复杂且长期维护立刻上 MapStruct把它当成团队基建的一部分就像用 Lombok 一样自然如果对象来自外部系统如 MQ 消息、HTTP 响应默认用 JSON 序列化因为网络传输本身就是深拷贝的天然边界永远在单元测试里验证引用关系而不是靠“应该没问题”这种直觉。最后分享一个小技巧在 IDEA 里安装 “BeanUtils Copy Generator” 插件选中 source 类右键 → “Generate Bean Copy”它会自动生成手写 copy 代码支持深拷贝选项。我用它生成初稿再人工 review 修改效率提升 70%。技术没有高下只有适不适合。把copyProperties当成一个工具而不是一个信仰你才能真正掌控它。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

SQL LIMIT分页优化:从基础语法到性能调优实战 2026/10/2 18:36:30

SQL LIMIT分页优化:从基础语法到性能调优实战

做后端开发这几年,SQL里最不起眼又最常用的关键字, LIMIT 绝对排得上号。一个 LIMIT 就能解决数据量大了之后的展示问题,但真正把 LIMIT 用明白的人其实不多。网上一搜"SQL Limit用法",出来的大多是"limit 1…

阅读更多 →
SQL LIMIT用法详解:分页查询性能优化与数据库方言差异 2026/10/2 18:36:30

SQL LIMIT用法详解:分页查询性能优化与数据库方言差异

1. LIMIT到底在干什么:从一条最基础的查询说起做开发这些年,我见过不少刚入行的同事在SQL里写了个LIMIT,然后一脸自信地提交代码,结果上线第一天分页就乱了。说实话,LIMIT在SQL里算是最容易被“想当然”的关键字之一&a…

阅读更多 →
Linux单机版聊天室实训:socket编程、select并发与踩坑全解析 2026/10/2 18:36:30

Linux单机版聊天室实训:socket编程、select并发与踩坑全解析

简介:这套Linux实训资料包围绕《单机版聊天室》课程设计题目,涵盖项目源码、答辩PPT、实习计划书与实习报告,面向正在完成Linux实训、操作系统课程设计或需要熟悉进程通信的学生。项目基于消息队列完成客户端与服务器交互,服务器以…

阅读更多 →
基于YOLOv8的木材表面缺陷检测:从训练调参到RK3588部署实战 2026/10/2 18:36:29

基于YOLOv8的木材表面缺陷检测:从训练调参到RK3588部署实战

简介:面向木材加工质检、机器视觉算法工程师与深度学习入门者,这份基于YOLOv8的木材表面缺陷检测项目,聚焦裂缝、孔洞、色差等典型缺陷,提供从数据准备、模型训练到结果分析的完整代码框架,适合作为工业视觉目标检测的…

阅读更多 →
AI Agent Harness Engineering 教育产品设计:如何平衡智能化与教育本质 2026/10/2 18:36:17

AI Agent Harness Engineering 教育产品设计:如何平衡智能化与教育本质

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Kimi Work 替代产品推荐:用 MCP 与 Workspace 搭建一体化工作空间的选型指南 2026/10/2 18:36:17

Kimi Work 替代产品推荐:用 MCP 与 Workspace 搭建一体化工作空间的选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉