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.
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.
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.
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
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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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!
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.
