0
# Plugin System
1
2
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.
3
4
## Core Components
5
6
### MybatisPlusInterceptor
7
8
The main interceptor that coordinates all inner interceptors.
9
10
```java { .api }
11
public class MybatisPlusInterceptor implements Interceptor {
12
private List<InnerInterceptor> interceptors = new ArrayList<>();
13
14
public void addInnerInterceptor(InnerInterceptor innerInterceptor);
15
public List<InnerInterceptor> getInterceptors();
16
public void setInterceptors(List<InnerInterceptor> interceptors);
17
18
@Override
19
public Object intercept(Invocation invocation) throws Throwable;
20
21
@Override
22
public Object plugin(Object target);
23
24
@Override
25
public void setProperties(Properties properties);
26
}
27
```
28
29
### InnerInterceptor Interface
30
31
Base interface for all inner interceptors providing lifecycle hooks.
32
33
```java { .api }
34
public interface InnerInterceptor {
35
36
// Query lifecycle hooks
37
default void willDoQuery(Executor executor, MappedStatement ms, Object parameter,
38
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
39
throws SQLException {}
40
41
default void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
42
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
43
throws SQLException {}
44
45
// Update lifecycle hooks
46
default void willDoUpdate(Executor executor, MappedStatement ms, Object parameter)
47
throws SQLException {}
48
49
default void beforeUpdate(Executor executor, MappedStatement ms, Object parameter)
50
throws SQLException {}
51
52
// Statement lifecycle hooks
53
default void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {}
54
55
default void beforeGetBoundSql(StatementHandler sh) {}
56
57
// Configuration
58
default void setProperties(Properties properties) {}
59
}
60
```
61
62
## Built-in Interceptors
63
64
### PaginationInnerInterceptor
65
66
Provides automatic pagination support with count queries and limit/offset handling.
67
68
```java { .api }
69
public class PaginationInnerInterceptor implements InnerInterceptor {
70
private DbType dbType;
71
private IDialect dialect;
72
private boolean optimizeJoin = true;
73
private Long maxLimit;
74
private boolean overflow = false;
75
76
public PaginationInnerInterceptor();
77
public PaginationInnerInterceptor(DbType dbType);
78
79
// Configuration methods
80
public void setDbType(DbType dbType);
81
public void setDialect(IDialect dialect);
82
public void setOptimizeJoin(boolean optimizeJoin);
83
public void setMaxLimit(Long maxLimit);
84
public void setOverflow(boolean overflow);
85
}
86
```
87
88
### TenantLineInnerInterceptor
89
90
Implements multi-tenant line-level security by automatically adding tenant conditions to SQL.
91
92
```java { .api }
93
public class TenantLineInnerInterceptor implements InnerInterceptor {
94
private TenantLineHandler tenantLineHandler;
95
96
public TenantLineInnerInterceptor();
97
public TenantLineInnerInterceptor(TenantLineHandler tenantLineHandler);
98
99
public void setTenantLineHandler(TenantLineHandler tenantLineHandler);
100
public TenantLineHandler getTenantLineHandler();
101
}
102
103
public interface TenantLineHandler {
104
Expression getTenantId();
105
String getTenantIdColumn();
106
boolean ignoreTable(String tableName);
107
boolean ignoreInsert(List<Column> columns, String tenantIdColumn);
108
}
109
```
110
111
### DynamicTableNameInnerInterceptor
112
113
Enables dynamic table name replacement at runtime.
114
115
```java { .api }
116
public class DynamicTableNameInnerInterceptor implements InnerInterceptor {
117
private Map<String, TableNameHandler> tableNameHandlerMap = new HashMap<>();
118
119
public void setTableNameHandler(String tableName, TableNameHandler handler);
120
public void setTableNameHandlerMap(Map<String, TableNameHandler> tableNameHandlerMap);
121
}
122
123
public interface TableNameHandler {
124
String dynamicTableName(String sql, String tableName);
125
}
126
```
127
128
### OptimisticLockerInnerInterceptor
129
130
Implements optimistic locking by automatically handling version fields.
131
132
```java { .api }
133
public class OptimisticLockerInnerInterceptor implements InnerInterceptor {
134
135
// No public configuration methods - works with @Version annotation
136
}
137
```
138
139
### BlockAttackInnerInterceptor
140
141
Blocks potentially dangerous SQL operations like full-table updates or deletes.
142
143
```java { .api }
144
public class BlockAttackInnerInterceptor implements InnerInterceptor {
145
146
// Automatically blocks dangerous operations
147
}
148
```
149
150
### IllegalSQLInnerInterceptor
151
152
Detects and blocks illegal SQL patterns.
153
154
```java { .api }
155
public class IllegalSQLInnerInterceptor implements InnerInterceptor {
156
157
// Configurable illegal SQL detection
158
}
159
```
160
161
### DataPermissionInterceptor
162
163
Implements data-level permissions by modifying SQL queries.
164
165
```java { .api }
166
public class DataPermissionInterceptor implements InnerInterceptor {
167
private DataPermissionHandler dataPermissionHandler;
168
169
public DataPermissionInterceptor();
170
public DataPermissionInterceptor(DataPermissionHandler dataPermissionHandler);
171
172
public void setDataPermissionHandler(DataPermissionHandler dataPermissionHandler);
173
}
174
175
public interface DataPermissionHandler {
176
Expression getSqlSegment(Table table, Expression where, String mappedStatementId);
177
}
178
```
179
180
### DataChangeRecorderInnerInterceptor
181
182
Records data changes for audit trails.
183
184
```java { .api }
185
public class DataChangeRecorderInnerInterceptor implements InnerInterceptor {
186
187
// Automatically records data changes
188
}
189
```
190
191
### ReplacePlaceholderInnerInterceptor
192
193
Replaces custom placeholders in SQL statements.
194
195
```java { .api }
196
public class ReplacePlaceholderInnerInterceptor implements InnerInterceptor {
197
198
// Placeholder replacement functionality
199
}
200
```
201
202
## Configuration Examples
203
204
### Basic Configuration
205
206
```java
207
@Configuration
208
public class MybatisPlusConfig {
209
210
@Bean
211
public MybatisPlusInterceptor mybatisPlusInterceptor() {
212
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
213
214
// Add pagination support
215
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
216
217
// Add optimistic locking
218
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
219
220
// Block dangerous operations
221
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
222
223
return interceptor;
224
}
225
}
226
```
227
228
### Multi-Tenant Configuration
229
230
```java
231
@Component
232
public class CustomTenantHandler implements TenantLineHandler {
233
234
@Override
235
public Expression getTenantId() {
236
// Get current tenant ID from security context
237
String tenantId = SecurityContextHolder.getCurrentTenantId();
238
return new StringValue(tenantId);
239
}
240
241
@Override
242
public String getTenantIdColumn() {
243
return "tenant_id";
244
}
245
246
@Override
247
public boolean ignoreTable(String tableName) {
248
// Ignore system tables
249
return "system_config".equals(tableName) || "global_settings".equals(tableName);
250
}
251
252
@Override
253
public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
254
// Check if tenant_id is already provided
255
return columns.stream().anyMatch(column ->
256
tenantIdColumn.equals(column.getColumnName()));
257
}
258
}
259
260
@Configuration
261
public class TenantConfig {
262
263
@Autowired
264
private CustomTenantHandler tenantHandler;
265
266
@Bean
267
public MybatisPlusInterceptor mybatisPlusInterceptor() {
268
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
269
270
// Add tenant line interceptor
271
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantHandler));
272
273
return interceptor;
274
}
275
}
276
```
277
278
### Dynamic Table Name Configuration
279
280
```java
281
@Configuration
282
public class DynamicTableConfig {
283
284
@Bean
285
public MybatisPlusInterceptor mybatisPlusInterceptor() {
286
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
287
288
DynamicTableNameInnerInterceptor dynamicTableNameInterceptor =
289
new DynamicTableNameInnerInterceptor();
290
291
// Configure table name handlers
292
dynamicTableNameInterceptor.setTableNameHandler("user", (sql, tableName) -> {
293
// Route to different tables based on date
294
String month = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
295
return tableName + "_" + month;
296
});
297
298
dynamicTableNameInterceptor.setTableNameHandler("order", (sql, tableName) -> {
299
// Route based on tenant
300
String tenantId = getCurrentTenantId();
301
return "tenant_" + tenantId + "_" + tableName;
302
});
303
304
interceptor.addInnerInterceptor(dynamicTableNameInterceptor);
305
306
return interceptor;
307
}
308
309
private String getCurrentTenantId() {
310
// Implementation to get current tenant
311
return "default";
312
}
313
}
314
```
315
316
### Data Permission Configuration
317
318
```java
319
@Component
320
public class CustomDataPermissionHandler implements DataPermissionHandler {
321
322
@Override
323
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
324
// Apply data permissions based on current user role
325
String currentRole = getCurrentUserRole();
326
String tableName = table.getName();
327
328
if ("user".equals(tableName) && !"ADMIN".equals(currentRole)) {
329
// Non-admin users can only see their own data
330
String currentUserId = getCurrentUserId();
331
Expression userCondition = new EqualsTo(
332
new Column("created_by"),
333
new StringValue(currentUserId)
334
);
335
336
if (where != null) {
337
return new AndExpression(where, userCondition);
338
} else {
339
return userCondition;
340
}
341
}
342
343
return where;
344
}
345
346
private String getCurrentUserRole() {
347
// Get from security context
348
return "USER";
349
}
350
351
private String getCurrentUserId() {
352
// Get from security context
353
return "user123";
354
}
355
}
356
357
@Configuration
358
public class DataPermissionConfig {
359
360
@Autowired
361
private CustomDataPermissionHandler dataPermissionHandler;
362
363
@Bean
364
public MybatisPlusInterceptor mybatisPlusInterceptor() {
365
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
366
367
interceptor.addInnerInterceptor(
368
new DataPermissionInterceptor(dataPermissionHandler));
369
370
return interceptor;
371
}
372
}
373
```
374
375
### Advanced Pagination Configuration
376
377
```java
378
@Configuration
379
public class PaginationConfig {
380
381
@Bean
382
public MybatisPlusInterceptor mybatisPlusInterceptor() {
383
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
384
385
PaginationInnerInterceptor paginationInterceptor =
386
new PaginationInnerInterceptor(DbType.MYSQL);
387
388
// Configure pagination options
389
paginationInterceptor.setMaxLimit(1000L); // Max page size
390
paginationInterceptor.setOverflow(false); // Don't allow overflow
391
paginationInterceptor.setOptimizeJoin(true); // Optimize count queries
392
393
interceptor.addInnerInterceptor(paginationInterceptor);
394
395
return interceptor;
396
}
397
}
398
```
399
400
## Usage Examples
401
402
### Using with Optimistic Locking
403
404
```java
405
@TableName("user")
406
public class User {
407
@TableId
408
private Long id;
409
410
private String name;
411
412
@Version
413
private Integer version; // Optimistic lock field
414
415
// getters and setters
416
}
417
418
// Usage
419
User user = userService.getById(1L);
420
user.setName("Updated Name");
421
boolean updated = userService.updateById(user); // Version automatically handled
422
```
423
424
### Pagination Usage
425
426
```java
427
// Pagination is automatically applied when using Page parameter
428
Page<User> page = new Page<>(1, 10);
429
Page<User> result = userService.page(page,
430
new QueryWrapper<User>().eq("active", true));
431
432
// Get results
433
List<User> users = result.getRecords();
434
long total = result.getTotal(); // Total count automatically calculated
435
long pages = result.getPages(); // Total pages calculated
436
```
437
438
### Multi-Tenant Usage
439
440
```java
441
// With tenant interceptor configured, all queries automatically include tenant condition
442
List<User> users = userService.list(); // Automatically filtered by tenant_id
443
444
// Insert automatically adds tenant_id
445
User newUser = new User("John", "john@example.com");
446
userService.save(newUser); // tenant_id automatically added
447
```
448
449
### Dynamic Table Names
450
451
```java
452
// With dynamic table name handler configured
453
// Calls to user table are automatically routed to user_202401, user_202402, etc.
454
List<User> users = userService.list(); // Uses dynamic table name
455
```
456
457
## Custom Interceptor Development
458
459
### Creating Custom Interceptor
460
461
```java
462
public class CustomAuditInterceptor implements InnerInterceptor {
463
464
@Override
465
public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {
466
// Add audit fields automatically
467
if (parameter instanceof Map) {
468
Map<String, Object> params = (Map<String, Object>) parameter;
469
params.put("updatedBy", getCurrentUserId());
470
params.put("updatedTime", LocalDateTime.now());
471
}
472
}
473
474
@Override
475
public void willDoQuery(Executor executor, MappedStatement ms, Object parameter,
476
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
477
// Log all queries for debugging
478
System.out.println("Executing query: " + boundSql.getSql());
479
}
480
481
private String getCurrentUserId() {
482
// Get current user from security context
483
return "current_user";
484
}
485
}
486
```
487
488
### Registering Custom Interceptor
489
490
```java
491
@Configuration
492
public class CustomInterceptorConfig {
493
494
@Bean
495
public MybatisPlusInterceptor mybatisPlusInterceptor() {
496
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
497
498
// Add custom interceptor
499
interceptor.addInnerInterceptor(new CustomAuditInterceptor());
500
501
// Add other interceptors
502
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
503
504
return interceptor;
505
}
506
}
507
```
508
509
## Interceptor Order
510
511
The order in which interceptors are added matters. Common ordering recommendations:
512
513
1. **TenantLineInnerInterceptor** - Should be first to ensure tenant filtering
514
2. **DynamicTableNameInnerInterceptor** - Early to handle table name resolution
515
3. **PaginationInnerInterceptor** - Middle for pagination logic
516
4. **OptimisticLockerInnerInterceptor** - Late for version handling
517
5. **BlockAttackInnerInterceptor** - Last for security validation
518
519
```java
520
@Bean
521
public MybatisPlusInterceptor mybatisPlusInterceptor() {
522
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
523
524
// Order matters!
525
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantHandler));
526
interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor());
527
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
528
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
529
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
530
531
return interceptor;
532
}
533
```