How MyBatis Interceptors Can Safeguard Your Java Service from Memory Overruns
This article explains how oversized database query results can cause JVM heap spikes, frequent Full GC, or OOM crashes in Java services, and demonstrates a non‑intrusive MyBatis interceptor solution that monitors, grades, and blocks risky queries while exposing Prometheus metrics for proactive alerting and capacity planning.
1. Memory Protection Background: Risks of Large Database Queries
In Java services, returning overly large result sets can cause JVM heap spikes, frequent Full GC or OOM, and thread blocking due to massive row counts.
Result set size too big (e.g., >20 MB) → heap surge, Full GC, OOM.
Result set row count too high (e.g., >100 k rows) → CPU‑intensive object mapping, interface timeout.
Therefore a precise monitoring and interception mechanism at the DAO layer is required.
2. MyBatis Interceptor: Core Principles and Custom Implementation
2.1 Core Principle
MyBatis interceptors use the dynamic‑proxy pattern to insert custom logic at key execution points without altering existing code.
Four Core Objects
Executor – manages the whole SQL execution lifecycle.
StatementHandler – can modify or enhance SQL before execution.
ParameterHandler – can alter or validate parameters before they are set.
ResultSetHandler – can modify or analyze results before returning.
2.2 Custom Interceptor Implementation Steps
Define phase – use @Intercepts and @Signature to declare the target.
Register phase – add the interceptor in the MyBatis configuration.
Execute phase – the interceptor chain runs in order when the target method is invoked.
1. @Intercepts annotation
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})2. Implement Interceptor interface
public class GuardInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// pre‑process
preProcess(invocation);
Object result = invocation.proceed();
// post‑process
postProcess(invocation, result);
return result;
}
}3. Register interceptor
<plugins>
<plugin interceptor="com.example.GuardInterceptor">
<property name="maxBytes" value="20971520"/>
</plugin>
</plugins>2.3 Interceptor Execution Sequence
2.4 Development Considerations
Performance
Avoid heavy computation inside the interceptor.
Consider asynchronous processing for result‑set analysis.
Consistency
Do not start new transactions inside the interceptor.
Thoroughly test write‑operation interception.
3. Memory Protection Scheme Based on MyBatis Interceptor
3.1 Overall Architecture
3.2 Prometheus Metric Design
A Histogram metric named sql_query_stats records query duration together with three labels: mapper method, row‑level, and byte‑level.
Row‑level grading (L0‑L5)
Focus on data volume.
Prevent full‑table scans.
L3 is the performance tipping point; L4+ requires blocking.
Byte‑level grading (L0‑L6)
Focus on per‑row size.
Identify large‑object issues.
L3 (≈1 MB) is the memory warning line.
3.3 Full Interceptor Workflow
3.4 Core Interceptor Code (simplified)
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class EnhancedMemoryGuardInterceptor implements Interceptor {
private int rowWarnThreshold = 3000;
private int rowBlockThreshold = 10000;
private long byteWarnThreshold = 5L * 1024 * 1024; // 5 MB
private long byteBlockThreshold = 10L * 1024 * 1024; // 10 MB
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long duration = System.currentTimeMillis() - start;
int rowCount = (result instanceof Collection) ? ((Collection<?>) result).size() : (result == null ? 0 : 1);
long byteSize = MemoryMeasurer.measureBytes(result);
int rowLevel = mapToLevel(rowCount, SqlExecutionMetrics.ROW_LEVELS);
int byteLevel = mapToLevel((int)(byteSize / 1024), SqlExecutionMetrics.BYTE_LEVELS);
recordMetrics(getSqlId(invocation), rowLevel, byteLevel, duration);
checkRowThresholds(getSqlId(invocation), rowCount, duration);
checkByteThresholds(getSqlId(invocation), byteSize, duration);
return result;
}
// mapping, threshold checks, metric recording omitted for brevity
}3.5 Result‑Size Statistics
Three estimation strategies are compared: lightweight type‑based estimation, ByteArrayOutputStream serialization, and JSON serialization. The lightweight approach offers nanosecond‑level overhead with acceptable accuracy for monitoring.
3.6 Extended Features
Asynchronous monitoring
Off‑load size estimation and grading to a thread‑pool to avoid blocking the main request thread.
Configurable thresholds
Support external configuration (e.g., Spring properties, config center) for warning and blocking limits, sampling rates, and whitelist/blacklist rules.
Dynamic threshold adjustment
Periodically adjust row‑level and byte‑level thresholds based on recent Prometheus percentiles.
High‑risk query identification
Combine row‑level and byte‑level labels to trigger multi‑dimensional alerts and optional query circuit‑breaking.
4. Value and Benefits of the Memory Protection Scheme
4.1 Core Value
Multi‑dimensional monitoring at mapper‑method granularity.
Proactive alerts before thresholds are breached.
Fast root‑cause location via Prometheus label combinations.
Capacity planning based on historical level distributions.
4.2 Effectiveness
Improved system stability by preventing OOM and Full GC spikes.
Optimized resource utilization, reducing unnecessary CPU/memory consumption.
Accelerated problem diagnosis with precise row/byte metrics.
Business continuity through circuit‑break and alert mechanisms.
Reinforced development standards encouraging size‑aware SQL design.
5. Conclusion
MyBatis interceptors provide a low‑cost, non‑intrusive gate that detects and blocks dangerous queries, delivers actionable metrics, and dramatically lowers the probability of memory‑related failures in Java services.
Sanyou's Java Diary
Passionate about technology, though not great at solving problems; eager to share, never tire of learning!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
