SQL mapping framework that eliminates JDBC boilerplate and couples objects with stored procedures or SQL statements using XML descriptors or annotations.
—
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.
Core plugin interface that allows interception of method calls on key MyBatis objects.
/**
* Plugin interceptor interface for AOP-style method interception
*/
interface Interceptor {
/** Intercept method call and potentially modify behavior */
Object intercept(Invocation invocation) throws Throwable;
/** Wrap target object with proxy (default implementation available) */
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/** Set configuration properties for the interceptor */
default void setProperties(Properties properties) {
// Default empty implementation
}
}Specifies which methods should be intercepted by the plugin.
/**
* Specifies intercepted methods for plugin
*/
@interface Intercepts {
/** Array of method signatures to intercept */
Signature[] value();
}Defines specific method signature to intercept.
/**
* Method signature for interception
*/
@interface Signature {
/** Target class containing the method */
Class<?> type();
/** Method name to intercept */
String method();
/** Method parameter types */
Class<?>[] args();
}Wrapper for intercepted method calls providing access to target, method, and arguments.
/**
* Method invocation wrapper for intercepted calls
*/
class Invocation {
/** Create invocation wrapper */
public Invocation(Object target, Method method, Object[] args);
/** Get target object being invoked */
public Object getTarget();
/** Get method being invoked */
public Method getMethod();
/** Get method arguments */
public Object[] getArgs();
/** Proceed with original method execution */
public Object proceed() throws InvocationTargetException, IllegalAccessException;
}Utility for creating proxy objects that intercept method calls.
/**
* Plugin wrapper utility for creating intercepting proxies
*/
class Plugin implements InvocationHandler {
/** Wrap target object with interceptor */
public static Object wrap(Object target, Interceptor interceptor);
/** Unwrap proxy to get original target */
public static Object unwrap(Object proxy);
/** Handle intercepted method invocations */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}MyBatis provides several key interception points in the execution pipeline:
Intercept SQL execution at the executor level.
// Available Executor interception points:
// - update(MappedStatement ms, Object parameter)
// - query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
// - query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql)
// - queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds)
// - flushStatements()
// - commit(boolean required)
// - rollback(boolean required)
// - getTransaction()
// - close(boolean forceRollback)
// - isClosed()Usage Examples:
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExecutorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
Method method = invocation.getMethod();
Object[] args = invocation.getArgs();
// Pre-processing
long startTime = System.currentTimeMillis();
MappedStatement ms = (MappedStatement) args[0];
System.out.println("Executing: " + ms.getId());
try {
// Execute original method
Object result = invocation.proceed();
// Post-processing
long endTime = System.currentTimeMillis();
System.out.println("Execution time: " + (endTime - startTime) + "ms");
return result;
} catch (Exception e) {
System.out.println("Execution failed: " + e.getMessage());
throw e;
}
}
}Intercept parameter setting before SQL execution.
// Available ParameterHandler interception points:
// - getParameterObject()
// - setParameters(PreparedStatement ps)Usage Examples:
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class})
})
public class ParameterInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
// Access parameter object
Object parameterObject = parameterHandler.getParameterObject();
// Custom parameter processing (e.g., encryption, validation)
if (parameterObject instanceof User) {
User user = (User) parameterObject;
if (user.getPassword() != null) {
user.setPassword(encryptPassword(user.getPassword()));
}
}
// Proceed with original parameter setting
return invocation.proceed();
}
private String encryptPassword(String password) {
// Custom encryption logic
return "encrypted:" + password;
}
}Intercept result set processing after SQL execution.
// Available ResultSetHandler interception points:
// - handleResultSets(Statement stmt)
// - handleCursorResultSets(Statement stmt)
// - handleOutputParameters(CallableStatement cs)Usage Examples:
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultSetInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// Execute original result set handling
Object result = invocation.proceed();
// Post-process results
if (result instanceof List) {
List<?> list = (List<?>) result;
System.out.println("Retrieved " + list.size() + " records");
// Custom result processing (e.g., data masking, audit logging)
for (Object item : list) {
if (item instanceof User) {
User user = (User) item;
// Mask sensitive data
user.setPassword("***MASKED***");
}
}
}
return result;
}
}Intercept statement preparation and execution.
// Available StatementHandler interception points:
// - prepare(Connection connection, Integer transactionTimeout)
// - parameterize(Statement statement)
// - batch(Statement statement)
// - update(Statement statement)
// - query(Statement statement, ResultHandler resultHandler)
// - queryCursor(Statement statement)
// - getBoundSql()
// - getParameterHandler()Usage Examples:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})
})
public class StatementInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
String methodName = invocation.getMethod().getName();
if ("prepare".equals(methodName)) {
// Intercept SQL preparation
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
// Modify SQL if needed (e.g., add tenant filters)
String modifiedSql = addTenantFilter(originalSql);
// Use reflection to modify the SQL
Field sqlField = boundSql.getClass().getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, modifiedSql);
}
return invocation.proceed();
}
private String addTenantFilter(String sql) {
// Add tenant filtering logic
if (sql.toLowerCase().contains("select") && !sql.toLowerCase().contains("tenant_id")) {
// Simple example - add tenant filter to WHERE clause
return sql + " AND tenant_id = " + getCurrentTenantId();
}
return sql;
}
private Long getCurrentTenantId() {
// Get current tenant from security context
return SecurityContextHolder.getTenantId();
}
}Comprehensive logging of SQL execution with timing and parameter information.
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SqlLoggingPlugin implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlLoggingPlugin.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String sql = boundSql.getSql();
long startTime = System.currentTimeMillis();
try {
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
logger.info("SQL Executed: {} | Parameters: {} | Execution Time: {}ms",
sql.replaceAll("\\s+", " "), parameter, executionTime);
return result;
} catch (Exception e) {
logger.error("SQL Execution Failed: {} | Parameters: {} | Error: {}",
sql.replaceAll("\\s+", " "), parameter, e.getMessage());
throw e;
}
}
@Override
public void setProperties(Properties properties) {
// Configure logging levels, formats, etc.
}
}Monitor and alert on slow queries and performance issues.
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class PerformanceMonitoringPlugin implements Interceptor {
private long slowQueryThreshold = 1000; // 1 second
private final MetricRegistry metrics = new MetricRegistry();
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
String statementId = ms.getId();
Timer.Context context = metrics.timer(statementId).time();
try {
Object result = invocation.proceed();
long executionTime = context.stop() / 1_000_000; // Convert to milliseconds
if (executionTime > slowQueryThreshold) {
// Alert on slow query
alertSlowQuery(statementId, executionTime);
}
return result;
} catch (Exception e) {
metrics.meter(statementId + ".errors").mark();
throw e;
}
}
private void alertSlowQuery(String statementId, long executionTime) {
System.err.println("SLOW QUERY ALERT: " + statementId + " took " + executionTime + "ms");
// Send to monitoring system
}
@Override
public void setProperties(Properties properties) {
this.slowQueryThreshold = Long.parseLong(properties.getProperty("slowQueryThreshold", "1000"));
}
}Implement row-level security and data masking.
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class SecurityPlugin implements Interceptor {
private final Set<String> sensitiveFields = Set.of("password", "ssn", "creditCard");
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
// Apply data masking based on user role
String userRole = getCurrentUserRole();
if (!"ADMIN".equals(userRole)) {
maskSensitiveData(result);
}
return result;
}
@SuppressWarnings("unchecked")
private void maskSensitiveData(Object result) {
if (result instanceof List) {
((List<Object>) result).forEach(this::maskObjectFields);
} else if (result != null) {
maskObjectFields(result);
}
}
private void maskObjectFields(Object obj) {
if (obj == null) return;
Class<?> clazz = obj.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (sensitiveFields.contains(field.getName().toLowerCase())) {
field.setAccessible(true);
try {
if (field.getType() == String.class) {
field.set(obj, "***MASKED***");
}
} catch (IllegalAccessException e) {
// Log error
}
}
}
}
private String getCurrentUserRole() {
// Get from security context
return "USER";
}
}<!-- Configure plugins in mybatis-config.xml -->
<configuration>
<plugins>
<plugin interceptor="com.example.SqlLoggingPlugin">
<property name="logLevel" value="INFO"/>
<property name="includeParameters" value="true"/>
</plugin>
<plugin interceptor="com.example.PerformanceMonitoringPlugin">
<property name="slowQueryThreshold" value="2000"/>
<property name="enableMetrics" value="true"/>
</plugin>
<plugin interceptor="com.example.SecurityPlugin">
<property name="maskingEnabled" value="true"/>
<property name="adminRoles" value="ADMIN,SUPER_ADMIN"/>
</plugin>
</plugins>
</configuration>// Add plugins programmatically
Configuration configuration = new Configuration();
// Add logging plugin
SqlLoggingPlugin loggingPlugin = new SqlLoggingPlugin();
Properties loggingProps = new Properties();
loggingProps.setProperty("logLevel", "DEBUG");
loggingPlugin.setProperties(loggingProps);
configuration.addInterceptor(loggingPlugin);
// Add performance monitoring plugin
PerformanceMonitoringPlugin perfPlugin = new PerformanceMonitoringPlugin();
Properties perfProps = new Properties();
perfProps.setProperty("slowQueryThreshold", "1500");
perfPlugin.setProperties(perfProps);
configuration.addInterceptor(perfPlugin);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(configuration);/**
* Plugin chain for managing multiple interceptors
*/
class InterceptorChain {
/** Add interceptor to chain */
public void addInterceptor(Interceptor interceptor);
/** Apply all interceptors to target object */
public Object pluginAll(Object target);
/** Get all interceptors */
public List<Interceptor> getInterceptors();
}
/**
* Plugin-related exceptions
*/
class PluginException extends PersistenceException {
public PluginException(String message);
public PluginException(String message, Throwable cause);
}
/**
* Metadata about intercepted methods
*/
class InterceptorMetadata {
/** Get target class */
public Class<?> getTargetClass();
/** Get intercepted methods */
public Set<Method> getInterceptedMethods();
/** Check if method is intercepted */
public boolean isMethodIntercepted(Method method);
}Install with Tessl CLI
npx tessl i tessl/maven-org-mybatis--mybatis