0
# Plugin System
1
2
Interceptor-based plugin system for AOP-style cross-cutting concerns like logging, performance monitoring, and custom behavior injection. MyBatis plugins provide powerful hooks into the core execution pipeline.
3
4
## Capabilities
5
6
### Interceptor Interface
7
8
Core plugin interface that allows interception of method calls on key MyBatis objects.
9
10
```java { .api }
11
/**
12
* Plugin interceptor interface for AOP-style method interception
13
*/
14
interface Interceptor {
15
/** Intercept method call and potentially modify behavior */
16
Object intercept(Invocation invocation) throws Throwable;
17
18
/** Wrap target object with proxy (default implementation available) */
19
default Object plugin(Object target) {
20
return Plugin.wrap(target, this);
21
}
22
23
/** Set configuration properties for the interceptor */
24
default void setProperties(Properties properties) {
25
// Default empty implementation
26
}
27
}
28
```
29
30
### Plugin Configuration Annotations
31
32
#### @Intercepts
33
34
Specifies which methods should be intercepted by the plugin.
35
36
```java { .api }
37
/**
38
* Specifies intercepted methods for plugin
39
*/
40
@interface Intercepts {
41
/** Array of method signatures to intercept */
42
Signature[] value();
43
}
44
```
45
46
#### @Signature
47
48
Defines specific method signature to intercept.
49
50
```java { .api }
51
/**
52
* Method signature for interception
53
*/
54
@interface Signature {
55
/** Target class containing the method */
56
Class<?> type();
57
58
/** Method name to intercept */
59
String method();
60
61
/** Method parameter types */
62
Class<?>[] args();
63
}
64
```
65
66
### Invocation Object
67
68
Wrapper for intercepted method calls providing access to target, method, and arguments.
69
70
```java { .api }
71
/**
72
* Method invocation wrapper for intercepted calls
73
*/
74
class Invocation {
75
/** Create invocation wrapper */
76
public Invocation(Object target, Method method, Object[] args);
77
78
/** Get target object being invoked */
79
public Object getTarget();
80
81
/** Get method being invoked */
82
public Method getMethod();
83
84
/** Get method arguments */
85
public Object[] getArgs();
86
87
/** Proceed with original method execution */
88
public Object proceed() throws InvocationTargetException, IllegalAccessException;
89
}
90
```
91
92
### Plugin Utility Class
93
94
Utility for creating proxy objects that intercept method calls.
95
96
```java { .api }
97
/**
98
* Plugin wrapper utility for creating intercepting proxies
99
*/
100
class Plugin implements InvocationHandler {
101
/** Wrap target object with interceptor */
102
public static Object wrap(Object target, Interceptor interceptor);
103
104
/** Unwrap proxy to get original target */
105
public static Object unwrap(Object proxy);
106
107
/** Handle intercepted method invocations */
108
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
109
}
110
```
111
112
### Interceptable Objects
113
114
MyBatis provides several key interception points in the execution pipeline:
115
116
#### Executor Interception
117
118
Intercept SQL execution at the executor level.
119
120
```java { .api }
121
// Available Executor interception points:
122
// - update(MappedStatement ms, Object parameter)
123
// - query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
124
// - query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql)
125
// - queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds)
126
// - flushStatements()
127
// - commit(boolean required)
128
// - rollback(boolean required)
129
// - getTransaction()
130
// - close(boolean forceRollback)
131
// - isClosed()
132
```
133
134
**Usage Examples:**
135
136
```java
137
@Intercepts({
138
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
139
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
140
})
141
public class ExecutorInterceptor implements Interceptor {
142
@Override
143
public Object intercept(Invocation invocation) throws Throwable {
144
Object target = invocation.getTarget();
145
Method method = invocation.getMethod();
146
Object[] args = invocation.getArgs();
147
148
// Pre-processing
149
long startTime = System.currentTimeMillis();
150
MappedStatement ms = (MappedStatement) args[0];
151
System.out.println("Executing: " + ms.getId());
152
153
try {
154
// Execute original method
155
Object result = invocation.proceed();
156
157
// Post-processing
158
long endTime = System.currentTimeMillis();
159
System.out.println("Execution time: " + (endTime - startTime) + "ms");
160
161
return result;
162
} catch (Exception e) {
163
System.out.println("Execution failed: " + e.getMessage());
164
throw e;
165
}
166
}
167
}
168
```
169
170
#### ParameterHandler Interception
171
172
Intercept parameter setting before SQL execution.
173
174
```java { .api }
175
// Available ParameterHandler interception points:
176
// - getParameterObject()
177
// - setParameters(PreparedStatement ps)
178
```
179
180
**Usage Examples:**
181
182
```java
183
@Intercepts({
184
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class})
185
})
186
public class ParameterInterceptor implements Interceptor {
187
@Override
188
public Object intercept(Invocation invocation) throws Throwable {
189
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
190
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
191
192
// Access parameter object
193
Object parameterObject = parameterHandler.getParameterObject();
194
195
// Custom parameter processing (e.g., encryption, validation)
196
if (parameterObject instanceof User) {
197
User user = (User) parameterObject;
198
if (user.getPassword() != null) {
199
user.setPassword(encryptPassword(user.getPassword()));
200
}
201
}
202
203
// Proceed with original parameter setting
204
return invocation.proceed();
205
}
206
207
private String encryptPassword(String password) {
208
// Custom encryption logic
209
return "encrypted:" + password;
210
}
211
}
212
```
213
214
#### ResultSetHandler Interception
215
216
Intercept result set processing after SQL execution.
217
218
```java { .api }
219
// Available ResultSetHandler interception points:
220
// - handleResultSets(Statement stmt)
221
// - handleCursorResultSets(Statement stmt)
222
// - handleOutputParameters(CallableStatement cs)
223
```
224
225
**Usage Examples:**
226
227
```java
228
@Intercepts({
229
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
230
})
231
public class ResultSetInterceptor implements Interceptor {
232
@Override
233
public Object intercept(Invocation invocation) throws Throwable {
234
// Execute original result set handling
235
Object result = invocation.proceed();
236
237
// Post-process results
238
if (result instanceof List) {
239
List<?> list = (List<?>) result;
240
System.out.println("Retrieved " + list.size() + " records");
241
242
// Custom result processing (e.g., data masking, audit logging)
243
for (Object item : list) {
244
if (item instanceof User) {
245
User user = (User) item;
246
// Mask sensitive data
247
user.setPassword("***MASKED***");
248
}
249
}
250
}
251
252
return result;
253
}
254
}
255
```
256
257
#### StatementHandler Interception
258
259
Intercept statement preparation and execution.
260
261
```java { .api }
262
// Available StatementHandler interception points:
263
// - prepare(Connection connection, Integer transactionTimeout)
264
// - parameterize(Statement statement)
265
// - batch(Statement statement)
266
// - update(Statement statement)
267
// - query(Statement statement, ResultHandler resultHandler)
268
// - queryCursor(Statement statement)
269
// - getBoundSql()
270
// - getParameterHandler()
271
```
272
273
**Usage Examples:**
274
275
```java
276
@Intercepts({
277
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
278
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})
279
})
280
public class StatementInterceptor implements Interceptor {
281
@Override
282
public Object intercept(Invocation invocation) throws Throwable {
283
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
284
String methodName = invocation.getMethod().getName();
285
286
if ("prepare".equals(methodName)) {
287
// Intercept SQL preparation
288
BoundSql boundSql = statementHandler.getBoundSql();
289
String originalSql = boundSql.getSql();
290
291
// Modify SQL if needed (e.g., add tenant filters)
292
String modifiedSql = addTenantFilter(originalSql);
293
294
// Use reflection to modify the SQL
295
Field sqlField = boundSql.getClass().getDeclaredField("sql");
296
sqlField.setAccessible(true);
297
sqlField.set(boundSql, modifiedSql);
298
}
299
300
return invocation.proceed();
301
}
302
303
private String addTenantFilter(String sql) {
304
// Add tenant filtering logic
305
if (sql.toLowerCase().contains("select") && !sql.toLowerCase().contains("tenant_id")) {
306
// Simple example - add tenant filter to WHERE clause
307
return sql + " AND tenant_id = " + getCurrentTenantId();
308
}
309
return sql;
310
}
311
312
private Long getCurrentTenantId() {
313
// Get current tenant from security context
314
return SecurityContextHolder.getTenantId();
315
}
316
}
317
```
318
319
### Common Plugin Use Cases
320
321
#### Logging Plugin
322
323
Comprehensive logging of SQL execution with timing and parameter information.
324
325
```java
326
@Intercepts({
327
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
328
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
329
})
330
public class SqlLoggingPlugin implements Interceptor {
331
private static final Logger logger = LoggerFactory.getLogger(SqlLoggingPlugin.class);
332
333
@Override
334
public Object intercept(Invocation invocation) throws Throwable {
335
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
336
Object parameter = invocation.getArgs()[1];
337
338
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
339
String sql = boundSql.getSql();
340
341
long startTime = System.currentTimeMillis();
342
343
try {
344
Object result = invocation.proceed();
345
346
long endTime = System.currentTimeMillis();
347
long executionTime = endTime - startTime;
348
349
logger.info("SQL Executed: {} | Parameters: {} | Execution Time: {}ms",
350
sql.replaceAll("\\s+", " "), parameter, executionTime);
351
352
return result;
353
} catch (Exception e) {
354
logger.error("SQL Execution Failed: {} | Parameters: {} | Error: {}",
355
sql.replaceAll("\\s+", " "), parameter, e.getMessage());
356
throw e;
357
}
358
}
359
360
@Override
361
public void setProperties(Properties properties) {
362
// Configure logging levels, formats, etc.
363
}
364
}
365
```
366
367
#### Performance Monitoring Plugin
368
369
Monitor and alert on slow queries and performance issues.
370
371
```java
372
@Intercepts({
373
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
374
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
375
})
376
public class PerformanceMonitoringPlugin implements Interceptor {
377
private long slowQueryThreshold = 1000; // 1 second
378
private final MetricRegistry metrics = new MetricRegistry();
379
380
@Override
381
public Object intercept(Invocation invocation) throws Throwable {
382
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
383
String statementId = ms.getId();
384
385
Timer.Context context = metrics.timer(statementId).time();
386
387
try {
388
Object result = invocation.proceed();
389
390
long executionTime = context.stop() / 1_000_000; // Convert to milliseconds
391
392
if (executionTime > slowQueryThreshold) {
393
// Alert on slow query
394
alertSlowQuery(statementId, executionTime);
395
}
396
397
return result;
398
} catch (Exception e) {
399
metrics.meter(statementId + ".errors").mark();
400
throw e;
401
}
402
}
403
404
private void alertSlowQuery(String statementId, long executionTime) {
405
System.err.println("SLOW QUERY ALERT: " + statementId + " took " + executionTime + "ms");
406
// Send to monitoring system
407
}
408
409
@Override
410
public void setProperties(Properties properties) {
411
this.slowQueryThreshold = Long.parseLong(properties.getProperty("slowQueryThreshold", "1000"));
412
}
413
}
414
```
415
416
#### Security Plugin
417
418
Implement row-level security and data masking.
419
420
```java
421
@Intercepts({
422
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
423
})
424
public class SecurityPlugin implements Interceptor {
425
private final Set<String> sensitiveFields = Set.of("password", "ssn", "creditCard");
426
427
@Override
428
public Object intercept(Invocation invocation) throws Throwable {
429
Object result = invocation.proceed();
430
431
// Apply data masking based on user role
432
String userRole = getCurrentUserRole();
433
434
if (!"ADMIN".equals(userRole)) {
435
maskSensitiveData(result);
436
}
437
438
return result;
439
}
440
441
@SuppressWarnings("unchecked")
442
private void maskSensitiveData(Object result) {
443
if (result instanceof List) {
444
((List<Object>) result).forEach(this::maskObjectFields);
445
} else if (result != null) {
446
maskObjectFields(result);
447
}
448
}
449
450
private void maskObjectFields(Object obj) {
451
if (obj == null) return;
452
453
Class<?> clazz = obj.getClass();
454
for (Field field : clazz.getDeclaredFields()) {
455
if (sensitiveFields.contains(field.getName().toLowerCase())) {
456
field.setAccessible(true);
457
try {
458
if (field.getType() == String.class) {
459
field.set(obj, "***MASKED***");
460
}
461
} catch (IllegalAccessException e) {
462
// Log error
463
}
464
}
465
}
466
}
467
468
private String getCurrentUserRole() {
469
// Get from security context
470
return "USER";
471
}
472
}
473
```
474
475
### Plugin Configuration
476
477
#### XML Configuration
478
479
```xml
480
<!-- Configure plugins in mybatis-config.xml -->
481
<configuration>
482
<plugins>
483
<plugin interceptor="com.example.SqlLoggingPlugin">
484
<property name="logLevel" value="INFO"/>
485
<property name="includeParameters" value="true"/>
486
</plugin>
487
488
<plugin interceptor="com.example.PerformanceMonitoringPlugin">
489
<property name="slowQueryThreshold" value="2000"/>
490
<property name="enableMetrics" value="true"/>
491
</plugin>
492
493
<plugin interceptor="com.example.SecurityPlugin">
494
<property name="maskingEnabled" value="true"/>
495
<property name="adminRoles" value="ADMIN,SUPER_ADMIN"/>
496
</plugin>
497
</plugins>
498
</configuration>
499
```
500
501
#### Programmatic Configuration
502
503
```java
504
// Add plugins programmatically
505
Configuration configuration = new Configuration();
506
507
// Add logging plugin
508
SqlLoggingPlugin loggingPlugin = new SqlLoggingPlugin();
509
Properties loggingProps = new Properties();
510
loggingProps.setProperty("logLevel", "DEBUG");
511
loggingPlugin.setProperties(loggingProps);
512
configuration.addInterceptor(loggingPlugin);
513
514
// Add performance monitoring plugin
515
PerformanceMonitoringPlugin perfPlugin = new PerformanceMonitoringPlugin();
516
Properties perfProps = new Properties();
517
perfProps.setProperty("slowQueryThreshold", "1500");
518
perfPlugin.setProperties(perfProps);
519
configuration.addInterceptor(perfPlugin);
520
521
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(configuration);
522
```
523
524
## Types
525
526
```java { .api }
527
/**
528
* Plugin chain for managing multiple interceptors
529
*/
530
class InterceptorChain {
531
/** Add interceptor to chain */
532
public void addInterceptor(Interceptor interceptor);
533
534
/** Apply all interceptors to target object */
535
public Object pluginAll(Object target);
536
537
/** Get all interceptors */
538
public List<Interceptor> getInterceptors();
539
}
540
541
/**
542
* Plugin-related exceptions
543
*/
544
class PluginException extends PersistenceException {
545
public PluginException(String message);
546
public PluginException(String message, Throwable cause);
547
}
548
549
/**
550
* Metadata about intercepted methods
551
*/
552
class InterceptorMetadata {
553
/** Get target class */
554
public Class<?> getTargetClass();
555
556
/** Get intercepted methods */
557
public Set<Method> getInterceptedMethods();
558
559
/** Check if method is intercepted */
560
public boolean isMethodIntercepted(Method method);
561
}
562
```