0
# Plugins and Interceptors
1
2
MyBatis-Plus provides an extensible plugin system for adding cross-cutting functionality like pagination, optimistic locking, tenant isolation, SQL security, and performance monitoring through interceptors.
3
4
## Capabilities
5
6
### MybatisPlusInterceptor
7
8
Main interceptor that manages inner interceptors for different functionalities.
9
10
```java { .api }
11
/**
12
* MyBatis-Plus main interceptor
13
*/
14
public class MybatisPlusInterceptor implements Interceptor {
15
16
/**
17
* Add inner interceptor
18
* @param innerInterceptor Inner interceptor to add
19
*/
20
public void addInnerInterceptor(InnerInterceptor innerInterceptor);
21
22
/**
23
* Get all inner interceptors
24
* @return List of inner interceptors
25
*/
26
public List<InnerInterceptor> getInterceptors();
27
28
/**
29
* Set properties for configuration
30
* @param properties Configuration properties
31
*/
32
public void setProperties(Properties properties);
33
}
34
```
35
36
### InnerInterceptor Interface
37
38
Base interface for all inner interceptors.
39
40
```java { .api }
41
/**
42
* Inner interceptor interface
43
*/
44
public interface InnerInterceptor {
45
46
/**
47
* Check if query should be intercepted
48
* @param executor MyBatis executor
49
* @param ms Mapped statement
50
* @param parameter Query parameters
51
* @param rowBounds Row bounds
52
* @param resultHandler Result handler
53
* @param boundSql Bound SQL
54
* @return true to intercept
55
*/
56
default boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter,
57
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
58
return true;
59
}
60
61
/**
62
* Before query execution
63
* @param executor MyBatis executor
64
* @param ms Mapped statement
65
* @param parameter Query parameters
66
* @param rowBounds Row bounds
67
* @param resultHandler Result handler
68
* @param boundSql Bound SQL
69
*/
70
default void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
71
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
72
// Implementation specific
73
}
74
75
/**
76
* Check if update should be intercepted
77
* @param executor MyBatis executor
78
* @param ms Mapped statement
79
* @param parameter Update parameters
80
* @return true to intercept
81
*/
82
default boolean willDoUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
83
return true;
84
}
85
86
/**
87
* Before update execution
88
* @param executor MyBatis executor
89
* @param ms Mapped statement
90
* @param parameter Update parameters
91
*/
92
default void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
93
// Implementation specific
94
}
95
}
96
```
97
98
### Core Interceptors
99
100
#### PaginationInnerInterceptor
101
102
Automatic pagination support with count query optimization.
103
104
```java { .api }
105
/**
106
* Pagination interceptor
107
*/
108
public class PaginationInnerInterceptor implements InnerInterceptor {
109
110
/**
111
* Default constructor
112
*/
113
public PaginationInnerInterceptor();
114
115
/**
116
* Constructor with database type
117
* @param dbType Database type
118
*/
119
public PaginationInnerInterceptor(DbType dbType);
120
121
/**
122
* Set maximum limit for single query
123
* @param maxLimit Maximum limit
124
*/
125
public void setMaxLimit(Long maxLimit);
126
127
/**
128
* Set overflow handling
129
* @param overflow Whether to handle page overflow
130
*/
131
public void setOverflow(boolean overflow);
132
133
/**
134
* Set whether to optimize count query
135
* @param optimizeCountSql Optimization flag
136
*/
137
public void setOptimizeCountSql(boolean optimizeCountSql);
138
}
139
```
140
141
#### OptimisticLockerInnerInterceptor
142
143
Optimistic locking support using version fields.
144
145
```java { .api }
146
/**
147
* Optimistic locking interceptor
148
*/
149
public class OptimisticLockerInnerInterceptor implements InnerInterceptor {
150
151
/**
152
* Default constructor
153
*/
154
public OptimisticLockerInnerInterceptor();
155
}
156
```
157
158
#### TenantLineInnerInterceptor
159
160
Multi-tenant support with automatic tenant line filtering.
161
162
```java { .api }
163
/**
164
* Tenant line interceptor for multi-tenant applications
165
*/
166
public class TenantLineInnerInterceptor implements InnerInterceptor {
167
168
/**
169
* Constructor with tenant line handler
170
* @param tenantLineHandler Tenant line handler
171
*/
172
public TenantLineInnerInterceptor(TenantLineHandler tenantLineHandler);
173
174
/**
175
* Set tenant line handler
176
* @param tenantLineHandler Tenant line handler
177
*/
178
public void setTenantLineHandler(TenantLineHandler tenantLineHandler);
179
}
180
181
/**
182
* Tenant line handler interface
183
*/
184
public interface TenantLineHandler {
185
186
/**
187
* Get tenant ID
188
* @return Current tenant ID
189
*/
190
Expression getTenantId();
191
192
/**
193
* Get tenant ID column name
194
* @return Tenant ID column name
195
*/
196
String getTenantIdColumn();
197
198
/**
199
* Check if table should ignore tenant filtering
200
* @param tableName Table name
201
* @return true to ignore tenant filtering
202
*/
203
default boolean ignoreTable(String tableName) {
204
return false;
205
}
206
207
/**
208
* Check if INSERT should ignore tenant column
209
* @param columns Insert columns
210
* @param tenantIdColumn Tenant ID column
211
* @return true to ignore
212
*/
213
default boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
214
return false;
215
}
216
}
217
```
218
219
#### DynamicTableNameInnerInterceptor
220
221
Dynamic table name support for table sharding scenarios.
222
223
```java { .api }
224
/**
225
* Dynamic table name interceptor
226
*/
227
public class DynamicTableNameInnerInterceptor implements InnerInterceptor {
228
229
/**
230
* Constructor with table name handler
231
* @param tableNameHandler Table name handler
232
*/
233
public DynamicTableNameInnerInterceptor(TableNameHandler tableNameHandler);
234
235
/**
236
* Set table name handler
237
* @param tableNameHandler Table name handler
238
*/
239
public void setTableNameHandler(TableNameHandler tableNameHandler);
240
}
241
242
/**
243
* Table name handler interface
244
*/
245
public interface TableNameHandler {
246
247
/**
248
* Dynamic table name replacement
249
* @param sql Original SQL
250
* @param tableName Original table name
251
* @return New table name
252
*/
253
String dynamicTableName(String sql, String tableName);
254
}
255
```
256
257
#### BlockAttackInnerInterceptor
258
259
Protection against dangerous SQL operations.
260
261
```java { .api }
262
/**
263
* Block attack interceptor to prevent dangerous operations
264
*/
265
public class BlockAttackInnerInterceptor implements InnerInterceptor {
266
267
/**
268
* Default constructor
269
*/
270
public BlockAttackInnerInterceptor();
271
}
272
```
273
274
#### IllegalSQLInnerInterceptor
275
276
Detection and blocking of illegal SQL patterns.
277
278
```java { .api }
279
/**
280
* Illegal SQL interceptor
281
*/
282
public class IllegalSQLInnerInterceptor implements InnerInterceptor {
283
284
/**
285
* Default constructor
286
*/
287
public IllegalSQLInnerInterceptor();
288
}
289
```
290
291
#### DataChangeRecorderInnerInterceptor (MyBatis-Plus 3.5.7+)
292
293
Audit interceptor for tracking data changes with before/after values, user information, and timestamps.
294
295
```java { .api }
296
/**
297
* Data change recording interceptor for audit trails
298
*/
299
public class DataChangeRecorderInnerInterceptor implements InnerInterceptor {
300
301
/**
302
* Default constructor
303
*/
304
public DataChangeRecorderInnerInterceptor();
305
306
/**
307
* Constructor with custom data change handler
308
* @param dataChangeHandler Custom handler for processing data changes
309
*/
310
public DataChangeRecorderInnerInterceptor(DataChangeHandler dataChangeHandler);
311
312
/**
313
* Set custom data change handler
314
* @param dataChangeHandler Handler for processing data changes
315
*/
316
public void setDataChangeHandler(DataChangeHandler dataChangeHandler);
317
}
318
319
/**
320
* Handler interface for processing data changes
321
*/
322
public interface DataChangeHandler {
323
324
/**
325
* Handle data change event
326
* @param changeRecord Data change record containing details
327
*/
328
void handleDataChange(DataChangeRecord changeRecord);
329
}
330
331
/**
332
* Data change record containing audit information
333
*/
334
public class DataChangeRecord {
335
private String tableName;
336
private String operation; // INSERT, UPDATE, DELETE
337
private Object primaryKey;
338
private Map<String, Object> beforeData;
339
private Map<String, Object> afterData;
340
private String userId;
341
private LocalDateTime changeTime;
342
private String changeReason;
343
344
// getters and setters...
345
}
346
```
347
348
## Usage Examples
349
350
**Basic Interceptor Configuration:**
351
352
```java
353
@Configuration
354
public class MybatisPlusConfig {
355
356
@Bean
357
public MybatisPlusInterceptor mybatisPlusInterceptor() {
358
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
359
360
// Add pagination interceptor
361
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
362
363
// Add optimistic locking interceptor
364
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
365
366
// Add block attack interceptor
367
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
368
369
return interceptor;
370
}
371
}
372
```
373
374
**Pagination Configuration:**
375
376
```java
377
@Bean
378
public MybatisPlusInterceptor mybatisPlusInterceptor() {
379
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
380
381
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
382
383
// Set maximum limit to prevent large queries
384
paginationInterceptor.setMaxLimit(1000L);
385
386
// Enable overflow handling (redirect to first page if page exceeds total)
387
paginationInterceptor.setOverflow(true);
388
389
// Optimize count queries
390
paginationInterceptor.setOptimizeCountSql(true);
391
392
interceptor.addInnerInterceptor(paginationInterceptor);
393
394
return interceptor;
395
}
396
```
397
398
**Multi-Tenant Configuration:**
399
400
```java
401
@Component
402
public class CustomTenantLineHandler implements TenantLineHandler {
403
404
@Override
405
public Expression getTenantId() {
406
// Get current tenant ID from security context, session, etc.
407
String tenantId = TenantContextHolder.getCurrentTenantId();
408
return new LongValue(Long.parseLong(tenantId));
409
}
410
411
@Override
412
public String getTenantIdColumn() {
413
return "tenant_id";
414
}
415
416
@Override
417
public boolean ignoreTable(String tableName) {
418
// Ignore tenant filtering for system tables
419
return Arrays.asList("sys_config", "sys_dict", "sys_log").contains(tableName);
420
}
421
422
@Override
423
public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
424
// Check if tenant column already exists in INSERT
425
return columns.stream().anyMatch(column ->
426
column.getColumnName().equalsIgnoreCase(tenantIdColumn));
427
}
428
}
429
430
@Configuration
431
public class MybatisPlusConfig {
432
433
@Autowired
434
private CustomTenantLineHandler tenantLineHandler;
435
436
@Bean
437
public MybatisPlusInterceptor mybatisPlusInterceptor() {
438
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
439
440
// Add tenant line interceptor
441
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler));
442
443
return interceptor;
444
}
445
}
446
```
447
448
**Dynamic Table Name Configuration:**
449
450
```java
451
@Component
452
public class CustomTableNameHandler implements TableNameHandler {
453
454
@Override
455
public String dynamicTableName(String sql, String tableName) {
456
// Table sharding by date
457
if (\"user_log\".equals(tableName)) {\n String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern(\"yyyyMM\"));\n return tableName + \"_\" + datePrefix; // user_log_202312\n }\n \n // Table sharding by tenant\n if (\"order\".equals(tableName)) {\n String tenantId = TenantContextHolder.getCurrentTenantId();\n return tableName + \"_\" + tenantId; // order_tenant_001\n }\n \n return tableName;\n }\n}\n\n@Configuration\npublic class MybatisPlusConfig {\n \n @Autowired\n private CustomTableNameHandler tableNameHandler;\n \n @Bean\n public MybatisPlusInterceptor mybatisPlusInterceptor() {\n MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();\n \n // Add dynamic table name interceptor\n interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor(tableNameHandler));\n \n return interceptor;\n }\n}"
458
},
459
{
460
"old_string": "",
461
"new_string": "**Optimistic Locking Usage:**\n\n```java\n// Entity with version field\n@TableName(\"product\")\npublic class Product {\n @TableId(type = IdType.AUTO)\n private Long id;\n \n private String name;\n private BigDecimal price;\n \n @Version // Optimistic locking version field\n private Integer version;\n \n // getters and setters...\n}\n\n// Service method with optimistic locking\n@Service\npublic class ProductService {\n \n @Autowired\n private ProductMapper productMapper;\n \n public boolean updateProductPrice(Long productId, BigDecimal newPrice) {\n // Get current product with version\n Product product = productMapper.selectById(productId);\n if (product == null) {\n return false;\n }\n \n // Update price\n product.setPrice(newPrice);\n \n // Update will automatically check version and increment it\n int result = productMapper.updateById(product);\n \n // result = 0 means version conflict (concurrent update)\n return result > 0;\n }\n}\n```\n\n**Custom Interceptor Development:**\n\n```java\n@Component\npublic class SqlLoggingInterceptor implements InnerInterceptor {\n \n private static final Logger logger = LoggerFactory.getLogger(SqlLoggingInterceptor.class);\n \n @Override\n public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,\n RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {\n logSqlExecution(\"QUERY\", ms.getId(), boundSql.getSql(), parameter);\n }\n \n @Override\n public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {\n BoundSql boundSql = ms.getBoundSql(parameter);\n logSqlExecution(\"UPDATE\", ms.getId(), boundSql.getSql(), parameter);\n }\n \n private void logSqlExecution(String type, String mapperId, String sql, Object parameter) {\n if (logger.isDebugEnabled()) {\n logger.debug(\"[{}] Mapper: {}\", type, mapperId);\n logger.debug(\"[{}] SQL: {}\", type, sql.replaceAll(\"\\\\s+\", \" \").trim());\n logger.debug(\"[{}] Parameters: {}\", type, parameter);\n }\n }\n}\n\n// Register custom interceptor\n@Configuration\npublic class MybatisPlusConfig {\n \n @Autowired\n private SqlLoggingInterceptor sqlLoggingInterceptor;\n \n @Bean\n public MybatisPlusInterceptor mybatisPlusInterceptor() {\n MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();\n \n // Add custom logging interceptor\n interceptor.addInnerInterceptor(sqlLoggingInterceptor);\n \n // Add other interceptors...\n \n return interceptor;\n }\n}\n```\n\n**Performance Monitoring Interceptor:**\n\n```java\n@Component\npublic class PerformanceMonitorInterceptor implements InnerInterceptor {\n \n private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitorInterceptor.class);\n private final ThreadLocal<Long> startTime = new ThreadLocal<>();\n \n @Override\n public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,\n RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {\n startTime.set(System.currentTimeMillis());\n }\n \n @Override\n public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {\n startTime.set(System.currentTimeMillis());\n }\n \n // Note: For complete implementation, you'd need to implement Interceptor interface directly\n // This is a simplified example showing the concept\n \n public void afterExecution(String mapperId) {\n Long start = startTime.get();\n if (start != null) {\n long duration = System.currentTimeMillis() - start;\n if (duration > 1000) { // Log slow queries (> 1 second)\n logger.warn(\"Slow query detected: {} took {}ms\", mapperId, duration);\n }\n startTime.remove();\n }\n }\n}\n```\n\n**Data Permission Interceptor:**\n\n```java\n@Component\npublic class DataPermissionInterceptor implements InnerInterceptor {\n \n @Override\n public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,\n RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {\n \n // Check if current user has permission to access data\n String userId = SecurityContextHolder.getCurrentUserId();\n String userRole = SecurityContextHolder.getCurrentUserRole();\n \n // Apply data filtering based on user permissions\n if (!\"ADMIN\".equals(userRole)) {\n // For non-admin users, add WHERE condition to limit data access\n String originalSql = boundSql.getSql();\n String permissionSql = addDataPermissionCondition(originalSql, userId, ms.getId());\n \n // Modify the bound SQL (implementation would require reflection)\n // This is a conceptual example\n }\n }\n \n private String addDataPermissionCondition(String originalSql, String userId, String mapperId) {\n // Add conditions based on business rules\n if (mapperId.contains(\"selectUser\")) {\n return originalSql + \" AND (created_by = '\" + userId + \"' OR assigned_to = '\" + userId + \"')\";\n }\n return originalSql;\n }\n}\n```\n\n**Interceptor Order and Priority:**\n\n```java\n@Configuration\npublic class MybatisPlusConfig {\n \n @Bean\n public MybatisPlusInterceptor mybatisPlusInterceptor() {\n MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();\n \n // Order matters! Interceptors are executed in the order they are added\n \n // 1. Multi-tenant filtering (should be first for security)\n interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler));\n \n // 2. Data permission filtering\n interceptor.addInnerInterceptor(new DataPermissionInterceptor());\n \n // 3. Dynamic table name (for sharding)\n interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor(tableNameHandler));\n \n // 4. Pagination (should be after filtering interceptors)\n interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));\n \n // 5. Optimistic locking (for update operations)\n interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());\n \n // 6. Security interceptors (should be after business logic)\n interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());\n interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());\n \n // 7. Monitoring and logging (should be last)\n interceptor.addInnerInterceptor(new PerformanceMonitorInterceptor());\n \n return interceptor;\n }\n}\n```\n\n**Conditional Interceptor Configuration:**\n\n```java\n@Configuration\npublic class MybatisPlusConfig {\n \n @Value(\"${mybatis-plus.interceptor.pagination.enabled:true}\")\n private boolean paginationEnabled;\n \n @Value(\"${mybatis-plus.interceptor.tenant.enabled:false}\")\n private boolean tenantEnabled;\n \n @Bean\n public MybatisPlusInterceptor mybatisPlusInterceptor() {\n MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();\n \n // Conditionally add interceptors based on configuration\n if (tenantEnabled) {\n interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler));\n }\n \n if (paginationEnabled) {\n interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));\n }\n \n // Always add security interceptors in production\n if (isProductionEnvironment()) {\n interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());\n interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());\n }\n \n return interceptor;\n }\n \n private boolean isProductionEnvironment() {\n return \"production\".equals(System.getProperty(\"spring.profiles.active\"));\n }\n}\n```\n\nThis plugin system provides powerful extensibility for adding cross-cutting concerns to MyBatis-Plus operations while maintaining clean separation of business logic and infrastructure concerns."
462
}]
463
}
464
]