news 2026/10/9 9:05:08

MyBatis Plus 字段自动填充:生产级原理分析与实现方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis Plus 字段自动填充:生产级原理分析与实现方案

先抛个结论:MyBatis Plus 的字段自动填充,不是帮你少写两行 setter 的小工具,而是把数据审计、创建时间、更新时间、操作人这些横切字段的赋值逻辑,统一收敛到一个可复用、可维护的地方。只要你的项目要落库,基本都绕不开这张“谁在什么时候改了什么”的表。标题里的“生产级实现方案与原理分析”八个字,拆开来看就是两件事:第一,搞清楚框架到底在哪个环节替你做了填充;第二,把这套能力用到真实业务里,别在批量插入、异步线程、字段覆盖这些场景下翻车。这篇文章,我按自己实际接入时的路径来写,先讲清楚原理,再给一份可以直接抄的写法,最后把我在生产环境踩过的坑和排查思路一并说掉。

1. 先别急着写代码:自动填充到底解决了什么问题

1.1 你项目里的“审计字段”重复劳动

绝大多数业务表都会带这么几个字段:create_time、update_time,讲究一点还会加上create_by、update_by,甚至逻辑删除的deleted。以前没接自动填充的时候,代码里到处都是这种片段:

User user = new User(); user.setName("张三"); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); user.setCreateBy(UserContext.getUserId()); user.setUpdateBy(UserContext.getUserId()); userMapper.insert(user);

一个两个表还能忍,几十张表、上百个插入更新点之后,你一定会遇到三个问题。第一,漏填,总有业务代码急着写核心字段,忘了给审计字段赋值,结果线上数据时间戳是 null,排查数据链路的时候根本说不清这条记录是什么时候产生的。第二,乱填,不同开发同学写出来的取值方式不一致,有的用LocalDateTime.now(),有的用new Date(),有的甚至直接拼字符串,时间字段的类型和精度乱成一锅粥。第三,没法统一改,一旦审计需求变化(比如要记录操作来源端是 PC 还是 App),你得把所有插入更新点捞出来改一遍,纯体力活还容易漏。

字段自动填充,本质就是把“审计字段到底怎么取值”这个规则给收口了。你只需要在实体字段上标一个注解,再写一个全局的处理器,MyBatis Plus 就会在 SQL 真正执行之前,把该赋的值帮你填进去。后续业务代码只管业务字段,审计字段的赋值逻辑集中在一个类里维护。

1.2 自动填充 VS 数据库默认值

有人可能会说:这玩意用数据库的 DEFAULT 不也能实现吗?我在create_time上设置DEFAULT CURRENT_TIMESTAMP,插入的时候不写这个字段,数据库也会自动给我填。这话对一半,但生产环境里你很快会发现数据库默认值有几个硬伤。

首先是update_time的自动更新。MySQL 确实可以用ON UPDATE CURRENT_TIMESTAMP,但如果你用的是 MyBatis Plus 来执行更新 SQL,框架默认只更新非 null 字段,这条记录只要没有其他的列被修改,update_time根本不会变。更麻烦的是,团队里如果混用数据库和框架两种方案,数据库迁移的时候一不小心就把这段给丢了,测试环境跑得好好的,上生产才发现时间戳不对。

其次是审计字段通常不只是时间,还有操作人。数据库默认值写不了“当前登录用户是谁”这种应用层信息,除非你在数据库连接里做文章,但那明显是没事找事。

所以我的判断很简单:时间类型的默认值,可以让数据库兜底;但凡是涉及业务语义的字段(操作人、租户、逻辑删除标记),都应该走应用层自动填充。这两者不冲突,甚至可以双保险,后面讲方案的时候我会再展开。

2. 核心原理:字段填充是怎么在 SQL 层面生效的

2.1 三个关键角色:注解、枚举、MetaObjectHandler

说到原理,先得认识三个角色。第一个是@TableField注解,你把它标在实体的某个字段上,同时指定fill属性:

@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime;

第二个是FieldFill枚举,它决定了这个字段在哪种操作下要被处理。枚举值就那么几个:

枚举值含义典型使用场景
DEFAULT默认策略,不参与自动填充普通业务字段
INSERT只在插入时填充创建时间、创建人
UPDATE只在更新时填充更新时间、更新人,单独用的场景较少
INSERT_UPDATE插入和更新时都填充更新时间,或者你希望创建时间更新时也被覆盖的情况

第三个是MetaObjectHandler接口,这才是真正干活的类。你实现这个接口,重写insertFill和updateFill两个方法,框架会在合适的时机调过来。

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

这里插一句,很多新手会把这三个角色拆开记,实际上它们是一条链路的三段。注解是“声明需求”,枚举是“指定时机”,Handler 是“执行动作”。理解了这个顺序,后面对着源码看会非常顺。

2.2 insert 和 update 的执行链路

那 MyBatis Plus 到底是在哪个节点调用 Handler 的呢?我以你自己的 Mapper 接口UserMapper.insert(user)为例,一步步拆。

你调用的insert是 MyBatis Plus 在BaseMapper里预定义好的方法,它内部会走SqlHelper拿到实体对应的TableInfo。这个TableInfo里存了实体的所有字段信息,包括哪个字段是主键、哪个字段有fill属性、字段名和数据库列名的映射关系。然后框架开始构建 INSERT SQL,它会遍历实体字段列表,把非 null 字段、主键字段、以及带fill属性的字段都放进 SQL 的列清单里。

但这里有个关键点:SQL 的列清单是在执行前动态生成的,而字段值是在 SQL 生成之后、参数绑定之前被赋值进去的。具体说,当你调用insert时,MyBatis Plus 会先对你传入的实体做一次“加工”:走到MetaObjectHandler.insertFill方法,把createTime、updateTime这些字段的值 set 回实体对象里。实体对象里有了值,后续 SQL 参数绑定自然就能拿到。

所以你在 Handler 里执行strictInsertFill之后,当前实体实例上对应字段就已经有了新值。这一点特别重要,因为很多同学以为自动填充只是“SQL 里多了一个值”,其实它是直接把你的入参对象给改了。这意味着如果调用方在插入完之后,直接拿同一个对象去干别的事,它会发现这个对象上的时间字段已经被填上了,这是正常现象,不用害怕。

update 链路的思路类似。updateById(user)内部会构建 UPDATE SQL,先把组装好的字段列表和 SQL 片段确定下来,然后调用updateFill给实体的updateTime赋新值,最后真正发出 UPDATE。逻辑上是“先算好 set 哪些列,再给实体补上这些列的值”。

2.3 为什么 Handler 能拿到实体字段值

MetaObjectHandler的方法签名里有一个MetaObject,很多人第一次看到这个参数会比较懵。它不是实体对象本身,而是 MyBatis 对对象的一层元数据封装,有点像一个“反射增强包装器”。通过它,你可以在不知道具体类型的情况下读取、修改对象的属性值。

框架之所以不用实体对象做参数,是因为MetaObject可以处理各种复杂的对象结构,包括嵌套对象、集合元素,甚至 Map。这样MetaObjectHandler的适用范围就更广。你在实现里写的getFieldValByName("createTime", metaObject),本质上是利用MetaObject去反射获取字段当前值;setFieldValByName("createTime", value, metaObject)则是反射写入字段值。

明白了这一层,你就不会好奇“为什么我的实体类没有继承任何基类也能被填充”了。只要字段名对得上,类型匹配得上,框架就能通过MetaObject把值塞进去。这也是为什么后面我要重点强调字段命名一致性的原因:字段名对不上,Handler 里写一百遍也没有用,因为反射找不到这个属性。

顺带补充一个 3.3.0 版本之后的变化。老版本的insertFill写法是:

this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);

这行代码的逻辑是“无脑覆盖”,不管实体里这个字段是不是已经有值了,都强制覆盖。而 3.3.0 之后推荐的strictInsertFill会先检查字段当前是否有值,只有为 null 时才填充。这一改,业务代码里手工赋值的优先级就保住了,不再被自动填充反杀。后面我会细说这个区别。

3. 生产级实现方案(可直接抄作业)

3.1 字段清单设计

真正动手之前,先把“哪些字段需要自动填充”理清楚。我根据自己的项目经验,列一张通用的字段设计表,你拿到就能用:

字段名Java 类型数据库列类型fill 策略说明
createTimeLocalDateTimeDATETIMEINSERT创建时间,一旦生成不再变动
updateTimeLocalDateTimeDATETIMEINSERT_UPDATE更新时间,插入和修改都刷新
createByLongBIGINTINSERT创建人 ID,取自登录态
updateByLongBIGINTINSERT_UPDATE最后修改人 ID
deletedIntegerTINYINTINSERT逻辑删除标记,默认填充 0
tenantIdLongBIGINTINSERT多租户场景下填充租户 ID

看到这里你应该会发现,字段清单设计不是乱拍脑袋,它是和数据权限、审计需求强相关的。创建时间创建人、修改时间修改人,这四项是刚需;逻辑删除标记和租户 ID,属于“要看你的业务有没有”的可选项。注意deleted字段的 fill 策略要选 INSERT,因为逻辑删除的值变更由@TableLogic的 SQL 去处理,不需要自动填充来干预,updateFill里压根不用管它。

另一个建议是:如果你用的是逻辑删除,尝试把deleted的默认值也交给自动填充来做。这样做的好处是,当你直接往数据库里灌测试数据、手工 SQL 插入时可能漏了这个字段,但走应用层插入时绝对不会漏,避免出现“查询死活查不出来,单看数据库又有记录”的诡异问题。

3.2 实体字段注解写法

字段清单定了,实体类里的写法就比较固定了。我给一个组合示例,直接把字段声明、fill 注解、逻辑删除注解放一起:

public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableField(fill = FieldFill.INSERT) private Long createBy; @TableField(fill = FieldFill.INSERT_UPDATE) private Long updateBy; @TableLogic @TableField(fill = FieldFill.INSERT) private Integer deleted; @TableField(fill = FieldFill.INSERT) private Long tenantId; }

这里有两个细节值得展开。

第一个是@TableField(fill = FieldFill.INSERT_UPDATE)的语义:它既在 insert 时触发,也在 update 时触发。创建时间字段千万别这么标,否则你在执行更新操作时,框架会把createTime也刷新成当前时间,审计语义直接崩掉。创建时间只标INSERT。

第二个是createBy和updateBy的类型。这里我用Long举例,如果你的用户主键是 String 或者别的类型,只要 Handler 里strictInsertFill传入的类型匹配就行。但我要提醒一句:这个类型设计要和整个项目的用户上下文类型对齐,不要实体里是Long,你 Handler 里却用String去填充,反射校验类型不一致会直接报错。

3.3 MetaObjectHandler 完整实现

实体类有了,下面给一份完整的处理器实现,我平时项目里就是这套,稍微改下字段名就能用:

@Component public class AuditMetaObjectHandler implements MetaObjectHandler { /** * 插入时的填充策略:所有审计字段都先判断是否已有值,已有值不覆盖。 */ @Override public void insertFill(MetaObject metaObject) { LocalDateTime now = LocalDateTime.now(); Long currentUserId = UserContext.getUserIdIfPresent(); this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now); this.strictInsertFill(metaObject, "createBy", Long.class, currentUserId); this.strictInsertFill(metaObject, "updateBy", Long.class, currentUserId); this.strictInsertFill(metaObject, "deleted", Integer.class, 0); this.strictInsertFill(metaObject, "tenantId", Long.class, TenantContext.getTenantId()); } /** * 更新时的填充策略:只填更新时间、更新人。 */ @Override public void updateFill(MetaObject metaObject) { LocalDateTime now = LocalDateTime.now(); Long currentUserId = UserContext.getUserIdIfPresent(); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, now); this.strictUpdateFill(metaObject, "updateBy", Long.class, currentUserId); } }

关于UserContext和TenantContext,我不展开具体实现,但你要理解它们的用法。它们通常是基于 ThreadLocal 封装的一个上下文对象,在请求进来的时候由拦截器或者过滤器把当前登录用户、当前租户塞进去,业务代码没有感知。到了 Handler 这里,直接从上下文里取就行。

这里有个生产环境必须要注意的点:异步线程里拿不到主线程的 ThreadLocal。比如你用@Async异步发送消息,或者用线程池处理批量任务,线程切过去之后UserContext.getUserId()很可能返回 null。所以在写 Handler 时,我习惯把“获取用户 ID”封装成一个不会抛异常的方法,拿不到就用一个默认的“system”用户 ID,或者直接留 null,具体看你的审计需求。

再补充一个和数据源相关的点。strictInsertFill里传入的LocalDateTime.class是类型标记,告诉反射工具你要填充的字段是什么类型。如果你的实体字段用了java.util.Date,这里就要改传Date.class,同时now也要换成new Date()。我建议新项目统一用LocalDateTime,原因下一个小节会讲。

3.4 版本兼容与时间类型选择

先说版本。如果你用的是 MyBatis Plus 3.3.0 之前的版本,strictInsertFill和strictUpdateFill这两个方法是不存在的,只能退回去用:

if (getFieldValByName("createTime", metaObject) == null) { setFieldValByName("createTime", LocalDateTime.now(), metaObject); }

这样写也能跑,但代码会长一点。我建议条件允许就升到 3.3.0 以上,一方面是自动填充 API 更规范,另一方面是框架修了不少老版本在字段填充上配合逻辑删除、乐观锁时的边界问题。

再说时间类型。用LocalDateTime还是java.util.Date,看起来只是 API 偏好,实际上牵涉到三个问题。第一是精度,Date虽然底层有毫秒,但如果你配合MyBatis的javaType映射,某些版本下到 MySQL 的DATETIME会丢掉毫秒;LocalDateTime配合DATETIME(3)能更好保留精度。第二是格式化,Date在不同时区下 toString 会不一样,排错的时候容易看着时间点犯迷糊。第三是 Java 8 之后的编程习惯,LocalDateTime有一大堆日期运算方法,团队协作时心智负担更小。我目前接触过的生产项目里,凡是用LocalDateTime的,在自动填充这条链路里几乎没有因为类型不匹配翻过车。

当然,如果数据库列设置的是TIMESTAMP,还要留意数据库时区配置。MySQL 的TIMESTAMP会做时区转换,如果应用层的LocalDateTime和数据库时区不一致,会造成“写入后查出来差了 8 个小时”。这不是自动填充的问题,是连接参数serverTimezone的问题。排查这类时间偏移问题,先看 JDBC 连接串,再看数据库时区,千万别一上来就怀疑 Handler。

4. 常见问题与排查实录

4.1 批量插入时填充失效

这是我被问过最多的问题:为什么单条 insert 自动填充好用,换成分批插入就没值了?

先说结论:如果你用的是 MyBatis Plus 自带的分批插入(比如insertBatchSomeColumn这类自定义 SQL 注入方法,或者循环里逐条insert),自动填充是能生效的。真正失效的场景是:你在 XML 里自己写了一个foreach批量 INSERT 语句,入参是List<User>,然后期望 MyBatis Plus 自动帮你填充集合里每个元素的审计字段。

很遗憾,框架做不到。原因前面已经说过了,自动填充发生在BaseMapper.insert(entity)这个入口,它知道你当前要处理的实体对象是哪一个,可以调用 Handler 去填充。但你自己写的foreach批量 SQL,MyBatis 只会把整个List当作一个参数传给你,框架没有机会去逐个调用insertFill。

解决方案有三个,按推荐程度排序。第一个,用 MyBatis Plus 的SqlInjector扩展一个批量插入方法,让它在内部逐条填充后再批量执行 SQL,这是最优雅的。第二个,在 XML 的foreach之前,自己在 Service 层遍历 List,逐个调用一个填充辅助方法,把审计字段手动 set 进去,然后用insertBatch批量执行。第三个,改造成单条循环 insert,性能差点,但填充逻辑能正常工作。

我自己的习惯是:超过几百条的数据,直接在业务层显式填充审计字段后再走自定义批量 SQL;几十条以内的,用逐条insert反而更清晰。没必要为了一个批量插入把 Handler 的封装绕晕。

4.2 update 不触发/值不更新

另一个高频问题是:我明明执行了updateById(user),为什么updateTime没有被刷新?

先说第一种情况:实体里updateTime是 null,数据库列是DATETIME DEFAULT NULL,执行更新后数据库还是 null。这种现象的根源多半不是自动填充没触发,而是框架的 update 策略。MyBatis Plus 默认的字段更新策略是NOT_NULL,即实体中为 null 的字段不参与 SET 子句生成。自动填充是在 SQL 构建之后、执行之前,往实体里塞值,注意这里有个顺序细节:updateFill的调用时机是在 SQL 的 SET 片段生成之前还是之后,不同版本存在差异。在我的使用经验里,更新填充通常能正常写入 SET 片段,但如果你用的是非常老的版本,存在“SET 列表已经定死,填充的值没进去”的情况。升级到 3.4 以后的版本基本都能解决。

第二种情况更隐蔽:你在实体上只标了@TableField(fill = FieldFill.INSERT),也就是创建时间那种,期望它更新时也能刷一下。这是不可能的,FieldFill枚举已经限定死了,更新时框架只会处理UPDATE和INSERT_UPDATE。所以更新字段要刷新,必须用INSERT_UPDATE或者UPDATE。

第三种情况:你手工把user.setUpdateTime(null)之后执行更新,希望 Handler 能兜底补上当前时间。注意strictUpdateFill的语义是“只有字段为 null 才填充”,所以这个场景下 Handler 反而会补上,但如果你用的是老式setFieldValByName,无论有没有值都会无脑覆盖。这里最容易出问题的点是:框架本身采用NOT_NULL策略,生成 SQL 时都把整列排除了,那填充还有什么意义?所以我才反复强调,执行更新操作前,确保实体里相应字段的状态是可控的,或者干脆用独立UpdateWrapper手动 set 更新时间。

4.3 手工赋值被覆盖或没生效

再讲一个和 Handler 覆盖策略强相关的问题:业务代码里手工给createTime赋了一个特殊值,执行 insert 之后,发现数据库里存的时间变成了系统的当前时间。

这个现象通常意味着你用的是老版本写法:

this.setFieldValByName("createTime", now, metaObject);

这行代码不带任何判断,无脑覆盖。所以你的手工赋值在框架看来就是“不存在的”,反正最后还是会被覆盖掉。解决方式是换成strictInsertFill,它内部会先调用getFieldValByName判断字段当前值是否为 null,只有 null 才写入新值。

反过来还有一个方向的问题:手工赋值了,但数据库里还是 null。这种情况十有八九是字段名写错了。你实体里定义的是createTime,Handler 里写的是create_time,MetaObject的反射取名是按属性名来的,不是按数据库列名,框架在填充时找的是属性名映射。所以同名不同名这种低级错误,排查方式是开 SQL 日志,看生成的 INSERT 列里到底有没有 createTime,没有就是没填充成功;有但值不对,再找覆盖逻辑的问题。

4.4 异步线程拿不到操作人

生产环境做异步化改造之后,最常见的翻车现场是:异步线程里执行updateById,自动填充确实触发了,但updateBy变成了 null,或者变成了你 Debug 时看到的默认值。

原因就是前面提过的 ThreadLocal 在线程切换时丢失。UserContext里的用户 ID 是请求线程独有的,线程池里的工作线程并不持有这份数据。

我的做法是在 Handler 里加一层“拿不到就兜底”的逻辑:

private Long getCurrentUserId() { try { Long userId = UserContext.getUserId(); return userId != null ? userId : SYSTEM_USER_ID; } catch (Exception e) { return SYSTEM_USER_ID; } }

SYSTEM_USER_ID通常用一个常量表示,比如 0L 或者 1L,这样至少审计数据里能区分“系统操作”和“用户操作”。如果你不想把“系统”也记成一个伪造的用户 ID,那就允许它为 null,但数据库列要保持可空,并且你们得约定好审计查询时 null 的含义。我更推荐落一个明确的兜底值,因为后续做数据对账、权限复核,完全是未知用户的 null 会很麻烦。

更高的方案是,在把任务提交给线程池之前,在任务体里显式绑定一份用户上下文的快照,线程执行时再去解析快照。这就不只是自动填充的范畴了,属于异步链路上下文传递的设计问题,但最终效果是让 Handler 在任何线程里都能拿到正确的操作人。这层改造我建议在项目一开始就做,等线上出现“不知道谁改了数据”的工单再补,代价就大了。

4.5 常见问题速查表

顺手整理一个排查速查表,以后遇到问题照着看:

现象可能原因排查思路与处理办法
insert 后审计字段为 nullHandler 没注册为 Bean;注解没有写 fill 属性;字段名不一致看 Spring 启动日志有没有注入 Handler;打印完整 SQL,看 INSERT 列里是否包含该字段
update 后 updateTime 没变fill 策略没配INSERT_UPDATE;实体对象 updateTime 为 null 且 SQL 生成时机早于填充把 fill 策略改成INSERT_UPDATE;升级到新版 MyBatis Plus
手工赋值被框架覆盖使用了老版setFieldValByName无脑覆盖改用strictInsertFill/strictUpdateFill
批量 foreach 插入没有填充自己写的 XML 批量 SQL 不经过 BaseMapper 入口在 Service 层手动填充;用 SqlInjector 扩展批量插入
异步线程 updateBy 为空ThreadLocal 主线程上下文未传递Handler 里做默认值兜底;改造异步链路,传上下文快照
时间差 8 小时JDBC 时区参数与数据库时区不一致检查serverTimezone参数,确认数据库会话时区
实体对象插入后时间字段被改了Handler 直接修改了入参对象这是正常行为,如需对外返回原始入参,用副本

这里面每一条都是我真实遇到或者陪同事排查过的。说实话,自动填充的报错机制不如普通业务代码那么直接,因为问题往往不出在语法上,而是出在“SQL 生成了,但列没进去”或者“值进去了,但被覆盖了”。所以我的排查口诀永远是:开 SQL 日志,先看 SQL 里到底有没有这个字段,再看执行参数里有没有值。两步定位,九成问题都能找到答案。

最后再说点实际操作中的体会

这套方案我用下来最大的感受是,它真正解决的不是“少写代码”,而是“让审计字段这件事变成默认值”。你不需要在每条插入更新逻辑里思考“要不要维护 createTime、updateTime”,框架已经把它变成了像主键一样自然的存在。但也不要把它当成银弹,批量插入要绕开、异步线程要兜底、版本升级要测,这些生产细节都是要自己扛一遍的。最后分享一个低成本小技巧:Handler 里不要写死字段值,所有取值都走方法封装,这样后续加字段、调整租户策略、换用户上下文实现,你只需要改动一个方法,而不是在一堆 Handler 逻辑里翻找。自动填充这东西,越往后维护,你就会越庆幸当初把规则收口了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 9:03:12

高性能计算框架实现:从GPU利用率到训练效率的全面优化

1. 先别急着写代码&#xff1a;算力账单逼出来的框架需求 今年初我们团队遇到一个所有做深度学习的人都懂的尴尬&#xff1a;GPU服务器的账单比上季度翻了一倍&#xff0c;但模型迭代速度反而更慢了。查了一圈监控才发现&#xff0c;集群的整体GPU利用率只有40%出头&#xff0c…

作者头像 李华
网站建设 2026/10/9 9:02:52

多平台短视频无水印解析工具 v3.0 技术拆解与源码实践

短视频这个赛道做了三年多&#xff0c;手头最常用的工具不是剪辑软件&#xff0c;而是一个自己写的解析程序。每次从各平台保存素材&#xff0c;默认下载总带个水印&#xff0c;剪辑时还得手动裁剪或者打码遮挡&#xff0c;费时又难看。这段时间我把自己的解析工具重构成了第三…

作者头像 李华
网站建设 2026/10/9 9:02:51

JVM面试全攻略:从内存模型到垃圾回收与调优实战

1. JVM基础概念题&#xff1a;别让“送分题”变成送命题1.1 面试官问“什么是JVM”&#xff0c;到底在考什么JVM面试题有一个很有意思的现象&#xff1a;越是看起来基础的问题&#xff0c;越容易把候选人筛选掉。我面过不少简历上写着“熟练掌握JVM”的人&#xff0c;一问“JVM…

作者头像 李华
网站建设 2026/10/9 9:02:44

AI辅助文献综述写作:从结构生成到引用规范的全流程指南

写文献综述这事儿&#xff0c;经历过的人都懂&#xff1a;文献检索一大堆、读了就忘、动笔时脑子空空&#xff0c;好不容易憋出一段&#xff0c;又被导师批“只是文献罗列&#xff0c;没有综述的样子”。我这些年帮学生改过不少综述&#xff0c;也自己写过几篇&#xff0c;算是…

作者头像 李华
网站建设 2026/10/9 9:02:43

Python+Vue前后端分离搭建婴幼儿用品销售网站实战

做婴幼儿用品销售网站&#xff0c;是目前很多学Python的朋友喜欢拿来练手的一个项目&#xff0c;刚好能把Python、Vue、Pycharm、Django/Flask这一整套技术串起来。我之前带毕业设计的时候&#xff0c;遇到过不少同学问&#xff1a;后端到底用Django还是Flask&#xff1f;前端为…

作者头像 李华
网站建设 2026/10/9 9:02:22

从学习随笔到知识管理:三步笔记法与复盘实践

最近整理手头的学习资料时&#xff0c;翻到了3月16日那天写下的随笔。当时只是为了把当天的思考和操作流程留住&#xff0c;没想到回头再看&#xff0c;反而比当时学的内容本身更有价值。很多当时没想明白的问题&#xff0c;在这篇随笔里都能看到思考的轨迹&#xff1b;一些当时…

作者头像 李华