Databases 18 min read

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.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
How MyBatis Interceptors Can Safeguard Your Java Service from Memory Overruns

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.

Four core objects
Four core objects

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

Interceptor execution sequence
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

Overall architecture
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.

Prometheus metric
Prometheus metric

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

Full interceptor flow
Full interceptor flow

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.

JavaPrometheusMyBatisInterceptormemory protection
Sanyou's Java Diary
Written by

Sanyou's Java Diary

Passionate about technology, though not great at solving problems; eager to share, never tire of learning!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.