自动填充
框架通过 KsMybatisPlusMetaObjectHandler 实现了 MyBatis-Plus 的自动填充功能,无需手动设置 createTime、updateTime、createUser、updateUser 等字段。
核心特性
- ✅ 自动填充时间:插入/更新时自动填充当前时间
- ✅ 自动填充用户:从
RequestInfo上下文获取用户信息 - ✅ 线程安全:基于
TransmittableThreadLocal实现 - ✅ 异步支持:支持异步场景下的用户信息传递
- ✅ 零侵入:无需手动编码,继承
BaseDO即可
自动填充字段
插入时(INSERT)
| 字段 | 数据来源 | 填充值 | 说明 |
|---|---|---|---|
createTime | LocalDateTime.now() | 当前时间 | 创建时间 |
updateTime | LocalDateTime.now() | 当前时间 | 修改时间 |
createUser | RequestInfo.getUserId() | 用户 ID | 创建人(从请求上下文获取) |
updateUser | RequestInfo.getUserName() | 用户名 | 修改人(从请求上下文获取) |
更新时(UPDATE)
| 字段 | 数据来源 | 填充值 | 说明 |
|---|---|---|---|
updateTime | LocalDateTime.now() | 当前时间 | 修改时间 |
updateUser | RequestInfo.getUserId() | 用户 ID | 修改人(从请求上下文获取) |
注意
用户信息(createUser、updateUser)的填充依赖 RequestInfo 上下文,需要在请求头中传递用户信息。
工作原理
架构图
sequenceDiagram
participant C as Controller
participant I as Interceptor
participant S as Service
participant H as MetaObjectHandler
participant DB as Database
C->>I: HTTP 请求
Note over I: 解析请求头<br/>提取 userId/userName
I->>I: RequestInfo.REQUEST_CONTEXT.set(requestInfo)
I->>S: 调用 Service 方法
S->>H: 执行 INSERT/UPDATE
Note over H: insertFill() 或 updateFill()
H->>H: 获取 RequestInfo
H->>H: 填充时间和用户字段
H->>DB: 执行 SQL
DB-->>S: 返回结果
S-->>C: 返回响应
Note over I: 清理 RequestInfo核心代码
@AutoConfiguration
public class KsMybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 从 ThreadLocal 获取请求上下文
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
// 填充创建时间和修改时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
// 填充创建人和修改人(如果上下文存在)
if (Objects.nonNull(requestInfo)) {
this.strictInsertFill(metaObject, "createUser", requestInfo::getUserId, String.class);
this.strictInsertFill(metaObject, "updateUser", requestInfo::getUserName, String.class);
}
}
@Override
public void updateFill(MetaObject metaObject) {
// 从 ThreadLocal 获取请求上下文
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
// 填充修改时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
// 填充修改人(如果上下文存在)
if (Objects.nonNull(requestInfo)) {
this.strictUpdateFill(metaObject, "updateUser", requestInfo::getUserId, String.class);
}
}
}快速开始
1. 实体类继承 BaseDO
import host.springboot.framework.mybatisplus.domain.BaseAssignDO;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
public class User extends BaseAssignDO {
private String username;
private String email;
private String phone;
private Integer status;
}继承 BaseAssignDO 后,实体类自动拥有以下字段:
private Long id; // 主键(雪花ID)
private LocalDateTime createTime; // 创建时间(自动填充)
private String createUser; // 创建人(自动填充)
private LocalDateTime updateTime; // 修改时间(自动填充)
private String updateUser; // 修改人(自动填充)2. 创建数据库表
CREATE TABLE `sys_user` (
`id` BIGINT(20) NOT NULL COMMENT '主键ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`status` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
-- 自动填充字段
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`create_user` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
`update_time` DATETIME NOT NULL COMMENT '修改时间',
`update_user` VARCHAR(50) DEFAULT NULL COMMENT '修改人',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_create_time` (`create_time`),
KEY `idx_update_time` (`update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';3. 发送请求时传递用户信息
方式1:请求头传递(推荐)
POST /api/user HTTP/1.1
Host: localhost:8080
Content-Type: application/json
userId: 1001
userName: admin
{
"username": "zhangsan",
"email": "zhangsan@example.com"
}方式2:在拦截器中设置
import host.springboot.framework3.core.model.RequestInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class UserContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 从 Token 中解析用户信息
String token = request.getHeader("Authorization");
if (token != null) {
UserInfo userInfo = parseToken(token);
// 设置到 RequestInfo 上下文
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
if (requestInfo != null) {
requestInfo.setUserId(String.valueOf(userInfo.getId()));
requestInfo.setUserName(userInfo.getUsername());
}
}
return true;
}
private UserInfo parseToken(String token) {
// 解析 JWT Token 获取用户信息
return jwtService.parseToken(token);
}
}4. Service 层使用
import host.springboot.framework.mybatisplus.service.BaseServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User>
implements UserService {
@Transactional(rollbackFor = Exception.class)
public void createUser(CreateUserRequest request) {
// 创建用户对象
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setStatus(1);
// 保存时自动填充 createTime、updateTime、createUser、updateUser
this.saveValidate(user);
// 打印填充后的字段
System.out.println("ID: " + user.getId());
System.out.println("创建时间: " + user.getCreateTime());
System.out.println("创建人: " + user.getCreateUser());
System.out.println("修改时间: " + user.getUpdateTime());
System.out.println("修改人: " + user.getUpdateUser());
}
@Transactional(rollbackFor = Exception.class)
public void updateUser(UpdateUserRequest request) {
// 查询用户
User user = this.getByIdValidate(request.getId());
// 更新字段
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
// 更新时自动填充 updateTime、updateUser
this.updateByIdValidate(user);
// 打印填充后的字段
System.out.println("修改时间: " + user.getUpdateTime());
System.out.println("修改人: " + user.getUpdateUser());
}
}执行结果:
-- 插入时生成的 SQL
INSERT INTO sys_user (id, username, email, phone, status, create_time, create_user, update_time, update_user)
VALUES (1234567890123456789, 'zhangsan', 'zhangsan@example.com', '13800138000', 1,
'2024-12-01 10:30:00', '1001', '2024-12-01 10:30:00', 'admin');
-- 更新时生成的 SQL
UPDATE sys_user
SET email = 'new_email@example.com',
phone = '13900139000',
update_time = '2024-12-01 11:00:00',
update_user = '1001'
WHERE id = 1234567890123456789;RequestInfo 上下文
数据模型
RequestInfo 是框架的请求上下文模型,使用 TransmittableThreadLocal 存储,支持跨线程传递。
@Data
@Accessors(chain = true)
public class RequestInfo implements Serializable {
// 上下文存储(ThreadLocal)
public static final TransmittableThreadLocal<RequestInfo> REQUEST_CONTEXT =
new TransmittableThreadLocal<>();
// 线程信息
private String threadId; // 线程 ID
private String threadName; // 线程名称
// 请求信息
private String uri; // 请求 URI
private String clientIp; // 客户端 IP
private String userAgent; // User-Agent
private String os; // 操作系统
private String browser; // 浏览器
private String methodType; // 请求方法类型(GET/POST)
// 方法信息
private String fullMethodName; // 完整方法名
private Object[] requestMethodArgs; // 方法参数
// 用户信息
private String userId; // 用户 ID
private String userName; // 用户名称
}使用示例
在 Service 层获取请求信息:
@Service
public class OrderServiceImpl extends BaseServiceImpl<OrderMapper, Order>
implements OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderRequest request) {
// 获取请求上下文
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
if (requestInfo != null) {
String clientIp = requestInfo.getClientIp();
String userId = requestInfo.getUserId();
String userName = requestInfo.getUserName();
// 记录操作日志
log.info("用户[{}-{}]在IP[{}]创建订单", userId, userName, clientIp);
}
// 创建订单(自动填充 createUser 和 updateUser)
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
this.saveValidate(order);
}
}生命周期
sequenceDiagram
participant C as Client
participant F as Filter/Interceptor
participant S as Service
participant DB as Database
C->>F: HTTP 请求
Note over F: 创建 RequestInfo<br/>存入 ThreadLocal
F->>S: 调用业务方法
S->>DB: 执行 SQL<br/>自动填充字段
DB-->>S: 返回结果
S-->>F: 返回响应
Note over F: 清理 ThreadLocal<br/>防止内存泄漏
F-->>C: 返回 HTTP 响应关键点:
- 创建:在 Filter 或 Interceptor 中创建
RequestInfo并存入ThreadLocal - 使用:在整个请求处理过程中都可以通过
REQUEST_CONTEXT.get()获取 - 清理:请求结束后必须调用
REQUEST_CONTEXT.remove()清理,防止内存泄漏
使用场景
场景1:用户创建数据
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
/**
* 创建用户
* 自动填充:createTime、updateTime、createUser、updateUser
*/
@PostMapping
public BaseVO create(@Valid @RequestBody CreateUserRequest request) {
userService.createUser(request);
return R.ok("创建成功");
}
}生成的 SQL:
INSERT INTO sys_user
(id, username, email, create_time, create_user, update_time, update_user)
VALUES
(1234567890123456789, 'zhangsan', 'zhangsan@example.com',
'2024-12-01 10:00:00', '1001', '2024-12-01 10:00:00', 'admin');场景2:批量插入数据
@Service
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User>
implements UserService {
@Transactional(rollbackFor = Exception.class)
public void batchCreate(List<CreateUserRequest> requests) {
// 构建用户列表
List<User> users = requests.stream()
.map(req -> {
User user = new User();
user.setUsername(req.getUsername());
user.setEmail(req.getEmail());
return user;
})
.toList();
// 批量保存(每条记录都会自动填充)
this.saveBatch(users, 100);
}
}场景3:更新数据
@Service
public class ProductServiceImpl extends BaseServiceImpl<ProductMapper, Product>
implements ProductService {
@Transactional(rollbackFor = Exception.class)
public void updatePrice(Long productId, BigDecimal newPrice) {
// 查询商品
Product product = this.getByIdValidate(productId);
// 更新价格
product.setPrice(newPrice);
// 保存(自动填充 updateTime 和 updateUser)
this.updateByIdValidate(product);
// 记录日志
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
log.info("用户[{}]更新商品[{}]价格为[{}]",
requestInfo.getUserName(), productId, newPrice);
}
}场景4:无请求上下文场景
在定时任务、消息队列等场景中,没有 HTTP 请求,RequestInfo 为空,此时只填充时间字段:
@Component
public class ScheduledTask {
private final UserService userService;
/**
* 定时任务:清理过期用户
* 只填充 updateTime(createUser 和 updateUser 为 null)
*/
@Scheduled(cron = "0 0 2 * * ?")
public void cleanExpiredUsers() {
// RequestInfo 为 null
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
System.out.println("RequestInfo: " + requestInfo); // null
// 更新过期用户状态
List<User> expiredUsers = userService.list(Wrappers.<User>query()
.lt("expire_time", LocalDateTime.now()));
expiredUsers.forEach(user -> {
user.setStatus(0);
// updateTime 会自动填充,updateUser 为 null
userService.updateByIdValidate(user);
});
}
}生成的 SQL:
-- 定时任务中更新(没有 updateUser)
UPDATE sys_user
SET status = 0,
update_time = '2024-12-01 02:00:00',
update_user = NULL
WHERE id = 1234567890123456789;如何在定时任务中填充用户信息?
可以手动设置 RequestInfo 上下文:
@Scheduled(cron = "0 0 2 * * ?")
public void cleanExpiredUsers() {
// 手动创建 RequestInfo
RequestInfo requestInfo = new RequestInfo();
requestInfo.setUserId("SYSTEM");
requestInfo.setUserName("定时任务");
RequestInfo.REQUEST_CONTEXT.set(requestInfo);
try {
// 执行业务逻辑
// updateUser 会被填充为 "SYSTEM"
} finally {
// 清理上下文
RequestInfo.REQUEST_CONTEXT.remove();
}
}自定义自动填充
场景1:自定义填充逻辑
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
@Component
public class CustomMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 自定义创建时间(东八区时间)
this.strictInsertFill(metaObject, "createTime",
() -> LocalDateTime.now().plusHours(8), LocalDateTime.class);
// 自定义创建人(从 Spring Security 获取)
String currentUser = SecurityContextHolder.getContext()
.getAuthentication()
.getName();
this.strictInsertFill(metaObject, "createUser",
() -> currentUser, String.class);
}
@Override
public void updateFill(MetaObject metaObject) {
// 自定义更新时间
this.strictUpdateFill(metaObject, "updateTime",
LocalDateTime::now, LocalDateTime.class);
}
}场景2:添加自定义填充字段
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
public class User extends BaseAssignDO {
private String username;
private String email;
/**
* 创建来源(自定义填充字段)
*/
@TableField(value = "create_source", fill = FieldFill.INSERT)
private String createSource;
}@Component
public class CustomMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 继续使用框架的默认填充
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
// 自定义填充 createSource
if (requestInfo != null) {
String source = requestInfo.getUri().startsWith("/api") ? "API" : "WEB";
this.strictInsertFill(metaObject, "createSource", () -> source, String.class);
this.strictInsertFill(metaObject, "createUser", requestInfo::getUserId, String.class);
this.strictInsertFill(metaObject, "updateUser", requestInfo::getUserName, String.class);
}
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时填充
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
if (requestInfo != null) {
this.strictUpdateFill(metaObject, "updateUser", requestInfo::getUserId, String.class);
}
}
}最佳实践
✅ 推荐做法
实体类继承 BaseDO
javapublic class User extends BaseAssignDO { } // ✅ 自动获得标准字段请求头传递用户信息
httpuserId: 1001 userName: admin在拦截器中设置用户上下文
javaRequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get(); // ✅ requestInfo.setUserId(user.getId()); requestInfo.setUserName(user.getName());定时任务手动设置上下文
javaRequestInfo requestInfo = new RequestInfo(); // ✅ requestInfo.setUserId("SYSTEM"); RequestInfo.REQUEST_CONTEXT.set(requestInfo);使用完清理上下文
javatry { // 业务逻辑 } finally { RequestInfo.REQUEST_CONTEXT.remove(); // ✅ 防止内存泄漏 }
❌ 不推荐做法
❌ 手动设置自动填充字段
javauser.setCreateTime(LocalDateTime.now()); // ❌ 多余,会被覆盖 user.setCreateUser("admin"); // ❌ 应该从上下文获取❌ 不清理 ThreadLocal
javaRequestInfo.REQUEST_CONTEXT.set(requestInfo); // 业务逻辑 // ❌ 忘记 remove(),导致内存泄漏❌ 直接使用 Thread.currentThread()
java// ❌ 线程池复用会导致数据混乱 String userId = Thread.currentThread().getName();❌ 在 Entity 中添加业务逻辑
java@Data public class User extends BaseAssignDO { public void setCreateTime(LocalDateTime time) { // ❌ this.createTime = time.plusHours(8); } }
常见问题
Q: 自动填充不生效怎么办?
A: 排查步骤:
检查实体类是否继承 BaseDO
javapublic class User extends BaseAssignDO { } // ✅检查字段是否配置了自动填充注解
java@TableField(value = "create_time", fill = FieldFill.INSERT) private LocalDateTime createTime;检查数据库表是否有对应字段
sqlDESC sys_user; -- 查看表结构检查是否使用了正确的保存方法
javathis.save(user); // ✅ 会触发自动填充 this.saveBatch(users); // ✅ 会触发自动填充 mapper.insert(user); // ✅ 会触发自动填充
Q: 用户信息填充为 null 怎么办?
A: 原因:RequestInfo 上下文为空或未设置用户信息。
解决方案:
// 方案1:在拦截器中设置用户信息
@Component
public class UserContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, ...) {
String token = request.getHeader("Authorization");
UserInfo user = parseToken(token);
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
if (requestInfo != null) {
requestInfo.setUserId(String.valueOf(user.getId()));
requestInfo.setUserName(user.getUsername());
}
return true;
}
}
// 方案2:在请求头中传递
// userId: 1001
// userName: adminQ: 能否禁用某个字段的自动填充?
A: 可以,使用 @TableField 覆盖:
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends BaseAssignDO {
/**
* 禁用自动填充,手动设置创建人
*/
@TableField(value = "create_user", fill = FieldFill.DEFAULT)
private String createUser;
}Q: 如何在异步任务中传递用户信息?
A: 使用 TransmittableThreadLocal:
RequestInfo 已经使用了 TransmittableThreadLocal,配合阿里的 transmittable-thread-local 库可以在异步场景下传递。
@Service
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User>
implements UserService {
@Async
public void asyncCreateUser(CreateUserRequest request) {
// TransmittableThreadLocal 自动传递
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
System.out.println("异步任务中的用户: " + requestInfo.getUserName());
// 创建用户(自动填充 createUser)
User user = new User();
user.setUsername(request.getUsername());
this.saveValidate(user);
}
}Q: createUser 和 updateUser 填充的值为什么不一样?
A: 设计如此:
createUser填充RequestInfo.getUserId()(用户 ID)updateUser填充RequestInfo.getUserName()(用户名)
如果需要统一,可以自定义 MetaObjectHandler:
@Component
public class CustomMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
if (requestInfo != null) {
// 统一填充用户 ID
this.strictInsertFill(metaObject, "createUser", requestInfo::getUserId, String.class);
this.strictInsertFill(metaObject, "updateUser", requestInfo::getUserId, String.class);
}
}
@Override
public void updateFill(MetaObject metaObject) {
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
if (requestInfo != null) {
// 统一填充用户 ID
this.strictUpdateFill(metaObject, "updateUser", requestInfo::getUserId, String.class);
}
}
}下一步
- 基础实体类 - 了解
BaseDO和BaseAssignDO的设计 - 增强 Service 层 - 了解
BaseService的 CRUD 方法 - 快速开始 - 回顾完整的使用流程
