Skip to content

自动填充

框架通过 KsMybatisPlusMetaObjectHandler 实现了 MyBatis-Plus 的自动填充功能,无需手动设置 createTimeupdateTimecreateUserupdateUser 等字段。

核心特性

  • 自动填充时间:插入/更新时自动填充当前时间
  • 自动填充用户:从 RequestInfo 上下文获取用户信息
  • 线程安全:基于 TransmittableThreadLocal 实现
  • 异步支持:支持异步场景下的用户信息传递
  • 零侵入:无需手动编码,继承 BaseDO 即可

自动填充字段

插入时(INSERT)

字段数据来源填充值说明
createTimeLocalDateTime.now()当前时间创建时间
updateTimeLocalDateTime.now()当前时间修改时间
createUserRequestInfo.getUserId()用户 ID创建人(从请求上下文获取)
updateUserRequestInfo.getUserName()用户名修改人(从请求上下文获取)

更新时(UPDATE)

字段数据来源填充值说明
updateTimeLocalDateTime.now()当前时间修改时间
updateUserRequestInfo.getUserId()用户 ID修改人(从请求上下文获取)

注意

用户信息(createUserupdateUser)的填充依赖 RequestInfo 上下文,需要在请求头中传递用户信息。

工作原理

架构图

mermaid
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

核心代码

java
@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

java
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 后,实体类自动拥有以下字段:

java
private Long id;                   // 主键(雪花ID)
private LocalDateTime createTime;  // 创建时间(自动填充)
private String createUser;         // 创建人(自动填充)
private LocalDateTime updateTime;  // 修改时间(自动填充)
private String updateUser;         // 修改人(自动填充)

2. 创建数据库表

sql
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:请求头传递(推荐)

http
POST /api/user HTTP/1.1
Host: localhost:8080
Content-Type: application/json
userId: 1001
userName: admin

{
  "username": "zhangsan",
  "email": "zhangsan@example.com"
}

方式2:在拦截器中设置

java
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 层使用

java
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 存储,支持跨线程传递。

java
@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 层获取请求信息

java
@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);
    }
}

生命周期

mermaid
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 响应

关键点

  1. 创建:在 Filter 或 Interceptor 中创建 RequestInfo 并存入 ThreadLocal
  2. 使用:在整个请求处理过程中都可以通过 REQUEST_CONTEXT.get() 获取
  3. 清理:请求结束后必须调用 REQUEST_CONTEXT.remove() 清理,防止内存泄漏

使用场景

场景1:用户创建数据

java
@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

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:批量插入数据

java
@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:更新数据

java
@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 为空,此时只填充时间字段:

java
@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

sql
-- 定时任务中更新(没有 updateUser)
UPDATE sys_user 
SET status = 0, 
    update_time = '2024-12-01 02:00:00', 
    update_user = NULL
WHERE id = 1234567890123456789;

如何在定时任务中填充用户信息?

可以手动设置 RequestInfo 上下文:

java
@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:自定义填充逻辑

java
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:添加自定义填充字段

java
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;
}
java
@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);
        }
    }
}

最佳实践

✅ 推荐做法

  1. 实体类继承 BaseDO

    java
    public class User extends BaseAssignDO { }  // ✅ 自动获得标准字段
  2. 请求头传递用户信息

    http
    userId: 1001
    userName: admin
  3. 在拦截器中设置用户上下文

    java
    RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();  // ✅
    requestInfo.setUserId(user.getId());
    requestInfo.setUserName(user.getName());
  4. 定时任务手动设置上下文

    java
    RequestInfo requestInfo = new RequestInfo();  // ✅
    requestInfo.setUserId("SYSTEM");
    RequestInfo.REQUEST_CONTEXT.set(requestInfo);
  5. 使用完清理上下文

    java
    try {
        // 业务逻辑
    } finally {
        RequestInfo.REQUEST_CONTEXT.remove();  // ✅ 防止内存泄漏
    }

❌ 不推荐做法

  1. ❌ 手动设置自动填充字段

    java
    user.setCreateTime(LocalDateTime.now());  // ❌ 多余,会被覆盖
    user.setCreateUser("admin");              // ❌ 应该从上下文获取
  2. ❌ 不清理 ThreadLocal

    java
    RequestInfo.REQUEST_CONTEXT.set(requestInfo);
    // 业务逻辑
    // ❌ 忘记 remove(),导致内存泄漏
  3. ❌ 直接使用 Thread.currentThread()

    java
    // ❌ 线程池复用会导致数据混乱
    String userId = Thread.currentThread().getName();
  4. ❌ 在 Entity 中添加业务逻辑

    java
    @Data
    public class User extends BaseAssignDO {
        public void setCreateTime(LocalDateTime time) {  // ❌
            this.createTime = time.plusHours(8);
        }
    }

常见问题

Q: 自动填充不生效怎么办?

A: 排查步骤

  1. 检查实体类是否继承 BaseDO

    java
    public class User extends BaseAssignDO { }  // ✅
  2. 检查字段是否配置了自动填充注解

    java
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;
  3. 检查数据库表是否有对应字段

    sql
    DESC sys_user;  -- 查看表结构
  4. 检查是否使用了正确的保存方法

    java
    this.save(user);        // ✅ 会触发自动填充
    this.saveBatch(users);  // ✅ 会触发自动填充
    mapper.insert(user);    // ✅ 会触发自动填充

Q: 用户信息填充为 null 怎么办?

A: 原因RequestInfo 上下文为空或未设置用户信息。

解决方案

java
// 方案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: admin

Q: 能否禁用某个字段的自动填充?

A: 可以,使用 @TableField 覆盖:

java
@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 库可以在异步场景下传递。

java
@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

java
@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);
        }
    }
}

下一步

扩展阅读

Released under the Apache-2.0 License