MyBatis-Plus Extension module providing advanced features including service layers, Kotlin extensions, SQL parsers, caching mechanisms, and various interceptors for enhanced ORM functionality
—
MyBatis-Plus Extension provides a powerful interceptor-based plugin system that enables cross-cutting concerns like pagination, multi-tenancy, data permissions, optimistic locking, and SQL security features. The plugin system is built around the MybatisPlusInterceptor main interceptor and various InnerInterceptor implementations.
The main interceptor that coordinates all inner interceptors.
public class MybatisPlusInterceptor implements Interceptor {
private List<InnerInterceptor> interceptors = new ArrayList<>();
public void addInnerInterceptor(InnerInterceptor innerInterceptor);
public List<InnerInterceptor> getInterceptors();
public void setInterceptors(List<InnerInterceptor> interceptors);
@Override
public Object intercept(Invocation invocation) throws Throwable;
@Override
public Object plugin(Object target);
@Override
public void setProperties(Properties properties);
}Base interface for all inner interceptors providing lifecycle hooks.
public interface InnerInterceptor {
// Query lifecycle hooks
default void willDoQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException {}
default void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException {}
// Update lifecycle hooks
default void willDoUpdate(Executor executor, MappedStatement ms, Object parameter)
throws SQLException {}
default void beforeUpdate(Executor executor, MappedStatement ms, Object parameter)
throws SQLException {}
// Statement lifecycle hooks
default void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {}
default void beforeGetBoundSql(StatementHandler sh) {}
// Configuration
default void setProperties(Properties properties) {}
}Provides automatic pagination support with count queries and limit/offset handling.
public class PaginationInnerInterceptor implements InnerInterceptor {
private DbType dbType;
private IDialect dialect;
private boolean optimizeJoin = true;
private Long maxLimit;
private boolean overflow = false;
public PaginationInnerInterceptor();
public PaginationInnerInterceptor(DbType dbType);
// Configuration methods
public void setDbType(DbType dbType);
public void setDialect(IDialect dialect);
public void setOptimizeJoin(boolean optimizeJoin);
public void setMaxLimit(Long maxLimit);
public void setOverflow(boolean overflow);
}Implements multi-tenant line-level security by automatically adding tenant conditions to SQL.
public class TenantLineInnerInterceptor implements InnerInterceptor {
private TenantLineHandler tenantLineHandler;
public TenantLineInnerInterceptor();
public TenantLineInnerInterceptor(TenantLineHandler tenantLineHandler);
public void setTenantLineHandler(TenantLineHandler tenantLineHandler);
public TenantLineHandler getTenantLineHandler();
}
public interface TenantLineHandler {
Expression getTenantId();
String getTenantIdColumn();
boolean ignoreTable(String tableName);
boolean ignoreInsert(List<Column> columns, String tenantIdColumn);
}Enables dynamic table name replacement at runtime.
public class DynamicTableNameInnerInterceptor implements InnerInterceptor {
private Map<String, TableNameHandler> tableNameHandlerMap = new HashMap<>();
public void setTableNameHandler(String tableName, TableNameHandler handler);
public void setTableNameHandlerMap(Map<String, TableNameHandler> tableNameHandlerMap);
}
public interface TableNameHandler {
String dynamicTableName(String sql, String tableName);
}Implements optimistic locking by automatically handling version fields.
public class OptimisticLockerInnerInterceptor implements InnerInterceptor {
// No public configuration methods - works with @Version annotation
}Blocks potentially dangerous SQL operations like full-table updates or deletes.
public class BlockAttackInnerInterceptor implements InnerInterceptor {
// Automatically blocks dangerous operations
}Detects and blocks illegal SQL patterns.
public class IllegalSQLInnerInterceptor implements InnerInterceptor {
// Configurable illegal SQL detection
}Implements data-level permissions by modifying SQL queries.
public class DataPermissionInterceptor implements InnerInterceptor {
private DataPermissionHandler dataPermissionHandler;
public DataPermissionInterceptor();
public DataPermissionInterceptor(DataPermissionHandler dataPermissionHandler);
public void setDataPermissionHandler(DataPermissionHandler dataPermissionHandler);
}
public interface DataPermissionHandler {
Expression getSqlSegment(Table table, Expression where, String mappedStatementId);
}Records data changes for audit trails.
public class DataChangeRecorderInnerInterceptor implements InnerInterceptor {
// Automatically records data changes
}Replaces custom placeholders in SQL statements.
public class ReplacePlaceholderInnerInterceptor implements InnerInterceptor {
// Placeholder replacement functionality
}@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// Add pagination support
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// Add optimistic locking
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// Block dangerous operations
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
}@Component
public class CustomTenantHandler implements TenantLineHandler {
@Override
public Expression getTenantId() {
// Get current tenant ID from security context
String tenantId = SecurityContextHolder.getCurrentTenantId();
return new StringValue(tenantId);
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
// Ignore system tables
return "system_config".equals(tableName) || "global_settings".equals(tableName);
}
@Override
public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
// Check if tenant_id is already provided
return columns.stream().anyMatch(column ->
tenantIdColumn.equals(column.getColumnName()));
}
}
@Configuration
public class TenantConfig {
@Autowired
private CustomTenantHandler tenantHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// Add tenant line interceptor
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantHandler));
return interceptor;
}
}@Configuration
public class DynamicTableConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
DynamicTableNameInnerInterceptor dynamicTableNameInterceptor =
new DynamicTableNameInnerInterceptor();
// Configure table name handlers
dynamicTableNameInterceptor.setTableNameHandler("user", (sql, tableName) -> {
// Route to different tables based on date
String month = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
return tableName + "_" + month;
});
dynamicTableNameInterceptor.setTableNameHandler("order", (sql, tableName) -> {
// Route based on tenant
String tenantId = getCurrentTenantId();
return "tenant_" + tenantId + "_" + tableName;
});
interceptor.addInnerInterceptor(dynamicTableNameInterceptor);
return interceptor;
}
private String getCurrentTenantId() {
// Implementation to get current tenant
return "default";
}
}@Component
public class CustomDataPermissionHandler implements DataPermissionHandler {
@Override
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
// Apply data permissions based on current user role
String currentRole = getCurrentUserRole();
String tableName = table.getName();
if ("user".equals(tableName) && !"ADMIN".equals(currentRole)) {
// Non-admin users can only see their own data
String currentUserId = getCurrentUserId();
Expression userCondition = new EqualsTo(
new Column("created_by"),
new StringValue(currentUserId)
);
if (where != null) {
return new AndExpression(where, userCondition);
} else {
return userCondition;
}
}
return where;
}
private String getCurrentUserRole() {
// Get from security context
return "USER";
}
private String getCurrentUserId() {
// Get from security context
return "user123";
}
}
@Configuration
public class DataPermissionConfig {
@Autowired
private CustomDataPermissionHandler dataPermissionHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(
new DataPermissionInterceptor(dataPermissionHandler));
return interceptor;
}
}@Configuration
public class PaginationConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInterceptor =
new PaginationInnerInterceptor(DbType.MYSQL);
// Configure pagination options
paginationInterceptor.setMaxLimit(1000L); // Max page size
paginationInterceptor.setOverflow(false); // Don't allow overflow
paginationInterceptor.setOptimizeJoin(true); // Optimize count queries
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}@TableName("user")
public class User {
@TableId
private Long id;
private String name;
@Version
private Integer version; // Optimistic lock field
// getters and setters
}
// Usage
User user = userService.getById(1L);
user.setName("Updated Name");
boolean updated = userService.updateById(user); // Version automatically handled// Pagination is automatically applied when using Page parameter
Page<User> page = new Page<>(1, 10);
Page<User> result = userService.page(page,
new QueryWrapper<User>().eq("active", true));
// Get results
List<User> users = result.getRecords();
long total = result.getTotal(); // Total count automatically calculated
long pages = result.getPages(); // Total pages calculated// With tenant interceptor configured, all queries automatically include tenant condition
List<User> users = userService.list(); // Automatically filtered by tenant_id
// Insert automatically adds tenant_id
User newUser = new User("John", "john@example.com");
userService.save(newUser); // tenant_id automatically added// With dynamic table name handler configured
// Calls to user table are automatically routed to user_202401, user_202402, etc.
List<User> users = userService.list(); // Uses dynamic table namepublic class CustomAuditInterceptor implements InnerInterceptor {
@Override
public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {
// Add audit fields automatically
if (parameter instanceof Map) {
Map<String, Object> params = (Map<String, Object>) parameter;
params.put("updatedBy", getCurrentUserId());
params.put("updatedTime", LocalDateTime.now());
}
}
@Override
public void willDoQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// Log all queries for debugging
System.out.println("Executing query: " + boundSql.getSql());
}
private String getCurrentUserId() {
// Get current user from security context
return "current_user";
}
}@Configuration
public class CustomInterceptorConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// Add custom interceptor
interceptor.addInnerInterceptor(new CustomAuditInterceptor());
// Add other interceptors
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}The order in which interceptors are added matters. Common ordering recommendations:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// Order matters!
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantHandler));
interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor());
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}Install with Tessl CLI
npx tessl i tessl/maven-com-baomidou--mybatis-plus-extension