How MyBatis Interceptors Can Safeguard Your Java Service from Out‑of‑Memory Crashes

This article explains how oversized database query results can cause JVM memory spikes and OOM errors, and shows how to use MyBatis interceptors to monitor, limit, and protect memory consumption with non‑intrusive code, Prometheus metrics, and configurable thresholds, ultimately improving system stability and performance.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
How MyBatis Interceptors Can Safeguard Your Java Service from Out‑of‑Memory Crashes

1. Memory Protection Background: Risks of Large Database Queries

In Java services, returning an overly large result set can cause two main risks: the result set size exceeding 20 MB, which leads to JVM heap growth, frequent Full GC, or OOM crashes; and the result set containing too many rows (e.g., 100 k rows), which consumes excessive CPU for object mapping and can block threads, causing timeouts.

To avoid these risks, a precise monitoring and interception mechanism should be built at the DAO layer to control the size of query results.

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 SQL execution points without altering existing code.

Four core objects + interceptor chain

Four Core Objects

Executor – manages the whole SQL execution lifecycle.

StatementHandler – can modify or enhance SQL before execution.

ParameterHandler – can modify or validate parameters before they are set on the statement.

ResultSetHandler – can modify or analyze results before they are returned to the application.

Four core objects diagram
Four core objects diagram

Interceptor Chain Working Mechanism

The interceptor intercepts specific methods of the four core objects, forming a chain that is triggered sequentially when the SQL reaches the corresponding node, similar to adding a quality‑check step in a production line.

Interceptor chain diagram
Interceptor chain diagram

2.2 Custom Interceptor Implementation Steps

Definition: use @Intercepts and @Signature annotations to declare the interception target.

Registration: configure the interceptor in the MyBatis configuration file.

Execution: when the target method is called, the interceptor chain executes in order.

@Intercepts({
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
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;
    }
}

2.3 Interceptor Execution Sequence

The interceptor mainly intercepts Executor.query to embed monitoring and control logic before and after SQL execution.

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 startTime = System.currentTimeMillis();
        Object result = invocation.proceed();
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;
        // ... calculate rowCount, byteSize, map to levels, record Prometheus metrics, check thresholds ...
        return result;
    }
    // helper methods omitted for brevity
}

3. Memory Protection Scheme: Design and Practice Based on MyBatis Interceptor

3.1 Overall Architecture

Architecture diagram
Architecture diagram

3.2 Prometheus Metric Design

A Histogram metric named sql_query_stats records query duration with three labels: mapper method, row‑level, and byte‑level.

static final Histogram SQL_QUERY_STATS = Histogram.build()
    .name("sql_query_stats")
    .help("SQL execution statistics")
    .labelNames("dao_method", "row_level", "byte_level")
    .buckets(10, 50, 100, 500, 1000, 5000)
    .register();

3.3 Row‑Level and Byte‑Level Definitions

Row levels (L0‑L5) divide result‑set row counts into five grades; byte levels (L0‑L6) divide result‑set size into six grades. L3 is the performance tipping point, and L4+ triggers forced limits.

Row level diagram
Row level diagram
Byte level diagram
Byte level diagram

3.4 Result Size Statistics

Three estimation strategies are provided: lightweight type‑based estimation, serialization via ByteArrayOutputStream, and JSON serialization (e.g., Jackson). In performance‑sensitive interceptor scenarios, the lightweight approach is preferred.

Feature

Lightweight Estimate

ByteArrayOutputStream

JSON Serialization

Implementation principle

Type mapping calculation

Object serialization to byte stream

Object to JSON string

Performance

Very high (nanoseconds)

Low (microseconds)

Medium (microseconds)

Accuracy

Medium (estimate)

High (exact size)

High (text size)

3.5 Core Size‑Measurer Code (lightweight)

public abstract class MemoryMeasurer {
    @FunctionalInterface
    public interface SizeCalculator { long calculate(Object obj); }
    private static final Map<Class<?>, SizeCalculator> SIZE_CALCULATORS = new ConcurrentHashMap<>();
    static {
        SIZE_CALCULATORS.put(Byte.class, o -> 1);
        SIZE_CALCULATORS.put(Short.class, o -> 2);
        SIZE_CALCULATORS.put(Integer.class, o -> 4);
        // ... other primitive wrappers and common types ...
        SIZE_CALCULATORS.put(String.class, o -> ((String) o).getBytes(StandardCharsets.UTF_8).length);
    }
    public static long measureBytes(Object result) {
        if (result == null) return 0;
        if (result instanceof List) {
            long total = 0;
            for (Object row : (List<?>) result) total += estimateRowSize(row);
            return total;
        }
        return estimateRowSize(result);
    }
    private static long estimateRowSize(Object row) {
        if (row == null) return 0;
        long size = 0;
        if (row instanceof Map) {
            for (Object v : ((Map<?, ?>) row).values()) size += estimateValueSize(v);
        } else {
            for (Field f : getCachedFields(row.getClass())) {
                try { f.setAccessible(true); size += estimateValueSize(f.get(row)); }
                catch (IllegalAccessException ignored) {}
            }
        }
        return size + 16; // object header overhead
    }
    private static long estimateValueSize(Object value) {
        if (value == null) return 0;
        SizeCalculator calc = SIZE_CALCULATORS.get(value.getClass());
        if (calc != null) return calc.calculate(value);
        for (Map.Entry<Class<?>, SizeCalculator> e : SIZE_CALCULATORS.entrySet())
            if (e.getKey().isAssignableFrom(value.getClass())) return e.getValue().calculate(value);
        return value.toString().getBytes(StandardCharsets.UTF_8).length;
    }
    private static final Map<Class<?>, List<Field>> FIELD_CACHE = new ConcurrentHashMap<>();
    private static List<Field> getCachedFields(Class<?> clazz) {
        return FIELD_CACHE.computeIfAbsent(clazz, k -> {
            List<Field> fields = new ArrayList<>();
            Class<?> cur = clazz;
            while (cur != Object.class) { Collections.addAll(fields, cur.getDeclaredFields()); cur = cur.getSuperclass(); }
            return fields;
        });
    }
}

3.6 Extended Features

Asynchronous monitoring to avoid blocking the main thread.

Configurable thresholds via configuration center, annotations, or Spring properties (warning thresholds, block thresholds, logging switches, sampling rates, whitelist/blacklist).

Deep field‑level statistics for identifying large fields.

Dynamic threshold adjustment based on historical data.

Combined row‑level and byte‑level high‑risk query detection with Prometheus alerts.

4. Value and Benefits of the Memory Protection Scheme

4.1 Core Value

Multi‑dimensional monitoring (method granularity, row count, byte size).

Proactive safety alerts and automatic circuit‑break when thresholds are exceeded.

Root‑cause定位 via Prometheus label combinations.

Capacity planning based on historical level distribution.

4.2 Effectiveness

Improved system stability by preventing heap spikes, Full GC storms, and OOM crashes.

Optimized resource utilization, avoiding single large queries from starving other requests.

Faster problem diagnosis through precise row/byte metrics.

Business continuity ensured by alert‑and‑break mechanisms.

Strengthened development standards, encouraging “query‑by‑need” and pagination practices.

5. Conclusion

MyBatis interceptors provide a low‑cost, non‑intrusive guard that can detect dangerous operations, intercept risky queries, and convey critical information, dramatically enhancing system stability and reducing the probability of failures caused by uncontrolled data retrieval.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaperformancePrometheusMyBatisInterceptormemory protection
Java Tech Enthusiast
Written by

Java Tech Enthusiast

Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!

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.