How Automatic Audit Field Population Works in ContiNew Admin: MyBatis-Plus MetaObjectHandler Explained

ContiNew Admin leverages MyBatis-Plus MetaObjectHandler to automatically populate createUser and createTime fields during database inserts by retrieving the current authenticated user from Sa-Token sessions and applying timestamps without requiring manual service-layer code.

ContiNew Admin eliminates boilerplate audit logging through a sophisticated automatic audit field population mechanism built on MyBatis-Plus infrastructure. This open-source Spring Boot admin system automatically injects creation metadata into every persistent entity without developers writing explicit assignment code in service layers. Understanding this architecture requires examining four integrated components that handle entity definition, handler registration, value resolution, and user context retrieval.

The Four-Component Architecture of Automatic Audit Field Population

Entity Foundation with BaseDO

All database entities extend BaseDO, which defines the standard audit columns and instructs MyBatis-Plus when to fill them. Located at continew-common/src/main/java/top/continew/admin/common/base/model/entity/BaseDO.java, this base class declares createUser, createTime, updateUser, and updateTime fields annotated with @TableField(fill = FieldFill.INSERT) for creation fields and FieldFill.INSERT_UPDATE for modification tracking. These annotations signal MyBatis-Plus to invoke the registered MetaObjectHandler whenever an insert or update operation occurs, ensuring the framework knows which fields require external population.

Handler Registration via MybatisPlusConfiguration

The system activates automatic filling through Spring bean registration in continew-common/src/main/java/top/continew/admin/common/config/mybatis/MybatisPlusConfiguration.java. This configuration class exposes a MetaObjectHandler bean that returns an instance of MyBatisPlusMetaObjectHandler, which MyBatis-Plus automatically detects and applies to all SQL operations. By registering this handler as a Spring-managed component, ContiNew Admin ensures every MyBatis-Plus insert or update triggers the audit population logic without explicit mapper configuration or XML modifications.

Insert-Fill Logic in MyBatisPlusMetaObjectHandler

The core implementation resides in continew-common/src/main/java/top/continew/admin/common/config/mybatis/MyBatisPlusMetaObjectHandler.java, specifically within the insertFill method. When MyBatis-Plus executes an insert, this handler obtains the current user ID via UserContextHolder.getUserId() and captures the current moment using LocalDateTime.now(). For entities extending BaseDO, the handler invokes concrete setters setCreateUser and setCreateTime directly. For entities that do not inherit from BaseDO, the generic fillFieldValue method employs reflection to locate and populate fields named exactly createUser and createTime, providing flexibility for legacy or specialized entities while maintaining the automatic audit field population contract.

User Context Resolution via UserContextHolder

Authentication context flows through continew-common/src/main/java/top/continew/admin/common/context/UserContextHolder.java, which maintains the current user in a TransmittableThreadLocal to support thread-safe access across asynchronous boundaries. The static getUserId() method extracts the user identifier from the active Sa-Token session via StpUtil.getSession().getModel, returning the primary key of the authenticated principal. This architecture decouples the audit mechanism from HTTP request handling, allowing the automatic audit field population to function consistently whether triggered by web controllers, scheduled jobs, or message consumers.

The Complete Insert Flow: From Service Call to Persisted Audit Data

When a service layer invokes userMapper.insert(entity), the automatic audit field population executes through this sequence:

  1. Entity Preparation: The application constructs the entity object, typically via MapStruct conversion from a DTO, leaving audit fields null.
  2. MyBatis-Plus Interception: The framework detects the @TableField(fill = FieldFill.INSERT) annotations and delegates to the registered MyBatisPlusMetaObjectHandler.
  3. Value Resolution: The handler calls insertFill, which retrieves LocalDateTime.now() for temporal data and invokes UserContextHolder.getUserId() to resolve the creator's identity from the Sa-Token session stored in thread-local storage.
  4. Field Population: For BaseDO subclasses, the handler uses type-safe setters; for other entities, it uses reflection via fillFieldValue to inject values into createUser and createTime fields.
  5. SQL Execution: MyBatis-Plus generates and executes the INSERT statement with the now-populated audit columns, persisting the record with complete provenance metadata.

This seamless integration ensures that createUser contains the current user's ID and createTime stores the precise LocalDateTime of insertion without explicit assignment in business logic.

Practical Implementation Example

The following example demonstrates automatic audit field population in action when creating a new user entity:

// Entity extends BaseDO, inheriting createUser and createTime fields
UserDO user = new UserDO();
user.setUsername("alice");
user.setPassword(passwordEncoder.encode("secret"));
user.setEmail("alice@example.com");

// Insert operation triggers automatic audit field population
userMapper.insert(user);

// After insertion, audit fields are automatically populated:
// user.getCreateUser() returns current authenticated user ID
// user.getCreateTime() returns LocalDateTime of insertion
System.out.println("Created by: " + user.getCreateUser());
System.out.println("Created at: " + user.getCreateTime());

No manual handling of createUser or createTime appears in the service layer. The userMapper.insert() call transparently triggers MyBatisPlusMetaObjectHandler.insertFill, which pulls the authenticated principal from UserContextHolder and applies the timestamp, demonstrating the zero-boilerplate advantage of this architecture.

Summary

  • BaseDO establishes the contract: All entities inherit audit fields annotated with @TableField(fill = FieldFill.INSERT), signaling MyBatis-Plus to invoke automatic population.
  • Spring bean registration activates the handler: MybatisPlusConfiguration exposes MyBatisPlusMetaObjectHandler as a MetaObjectHandler bean, enabling framework-level interception.
  • Dual population strategy: The handler uses direct setters for BaseDO subclasses and reflection-based fillFieldValue for other entities, ensuring flexibility.
  • Sa-Token integration: UserContextHolder provides thread-safe user ID retrieval from StpUtil sessions, making audit fields user-aware without coupling to web layers.
  • Zero service-layer code: Developers call standard MyBatis-Plus insert() methods while the framework handles createUser and createTime assignment automatically.

Frequently Asked Questions

Does automatic audit field population work for batch insert operations?

Yes. MyBatis-Plus invokes insertFill for each entity in a batch operation individually. The MyBatisPlusMetaObjectHandler processes every object in the collection, calling UserContextHolder.getUserId() and LocalDateTime.now() for each record, ensuring consistent audit metadata across bulk inserts without requiring explicit loop handling in service code.

What happens if no user is authenticated when an entity is inserted?

If UserContextHolder.getUserId() cannot resolve a user from the current Sa-Token session (returning null), the handler still populates createTime with the current timestamp but sets createUser to null. For system operations or scheduled tasks running without authentication, developers should explicitly set the createUser field before insertion or extend the handler logic to apply a system user default.

Can I customize the field names for audit tracking?

Yes. While BaseDO uses standard createUser and createTime names, the fillFieldValue method in MyBatisPlusMetaObjectHandler supports arbitrary entities through reflection. To use custom field names, either extend BaseDO and override the setter methods, or implement a custom MetaObjectHandler that looks for your specific column names instead of the defaults.

How does the audit mechanism handle update operations?

The same handler manages updates through the updateFill method. Fields marked with @TableField(fill = FieldFill.INSERT_UPDATE) or FieldFill.UPDATE trigger population of updateUser and updateTime using the same UserContextHolder.getUserId() and LocalDateTime.now() pattern, maintaining modification history alongside creation metadata.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →