MyBatis operates as a semi-automated ORM framework, decoupling SQL from Java code via XML or annotations. Its SQL execution follows a delegation chain: SqlSession.getMapper() produces a proxy, MapperProxy intercepts calls, MapperMethod translates them, and Executor handles database interaction, caching, and plugins.
This article dissects the call chain SqlSession → MapperProxy → MapperMethod → Executor, explains first- and second-level cache behavior and invalidation rules, and demonstrates how interceptors like PageHelper modify SQL at runtime. The content targets developers troubleshooting cache inefficacy, plugin failures, or exploring MyBatis internals.
Core Components Overview
- SqlSession: Main entry interface providing
getMapper(),selectOne(), etc. It bridges the application and the framework. - MapperProxy: A dynamic proxy (implementing
InvocationHandler) that intercepts mapper interface method calls and delegates toMapperMethod. - MapperMethod: Encapsulates SQL statements, parameter types, and return types for a given mapper method. It acts as the link between Java methods and SQL.
- Executor: The execution engine responsible for running SQL, managing caches, and handling transactions.
Execution Flow: Step-by-Step
1. SqlSession.getMapper() — Generate Proxy
The user obtains a proxy for the mapper interface through SqlSession.getMapper(). Internally, it calls Configuration.getMapper(), wich delegates to MapperRegistry.
// Configuration.java
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
// MapperRegistry.java
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
This returns a MapperProxy dynamic proxy, not a concrete implementation. All subsequent method calls on the interface go through the proxy.
2. MapperProxy — Intercept and Forward
MapperProxy.invoke() catches all mapper interface calls.
// MapperProxy.java
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
}
The cache (methodCache) avoids recreating MapperMethod instances for repeated calls. Each interface method eventually reaches MapperMethod.execute().
3. MapperMethod — Translate and Delegate to Executer
MapperMethod bridges the interface method to its SQL configuration. It extracts the SQL command, wraps parameters, and hands control to the Executor.
// MapperMethod.java
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature methodSignature;
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: result = insert(sqlSession, args); break;
case UPDATE: result = update(sqlSession, args); break;
case DELETE: result = delete(sqlSession, args); break;
case SELECT: result = select(sqlSession, args); break;
default: throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
private <E> Object select(SqlSession sqlSession, Object[] args) {
Executor executor = sqlSession.getExecutor();
Object param = methodSignature.convertArgsToSqlCommandParam(args);
return executor.query(command.getTarget(), param, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
}
}
4. Executor — SQL Execution Engine
Executor is the central hub that runs queries, manages caches, and coordinates transactions. Implementations include SimpleExecutor (default, creates a new Statement per query), ReuseExecutor (reuses Statements), BatchExecutor (batch operations), and CachingExecutor (wraps others with cache logic).
// CachingExecutor.java
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) {
CacheKey key = createCacheKey(ms, parameter, rowBounds);
List<E> cachedResult = (List<E>) tcm.getObject(ms, key);
if (cachedResult != null) {
return cachedResult;
}
List<E> result = delegate.query(ms, parameter, rowBounds, resultHandler);
tcm.putObject(ms, key, result);
return result;
}
}
The CachingExecutor checks the cache before delegating to the underlying executor. Cache keys are derived from SQL metadata such as statement ID and parameters to ensure uniqueness.
Cache Mechanism: Level 1 and Level 2
Both cache levels use PerpetualCache (backed by HashMap) with decorators for eviction policies (e.g., LRU).
First-Level Cache (SqlSession Scope)
- Implementation:
PerpetualCache - Scope: Single
SqlSession - Content: Query results
- Invalidation: Cleared after any insert, update, or delete; manual
SqlSession.clearCache()call;SqlSessioncloses.
Second-Level Cache (SessionFactory Scope)
- Default Implementation:
LruCachedecorator - Scope: Shared across
SqlSessioninstances from the same factory - Content: Query results
- Invalidation: Cleared after any DML operasion; after a configured
flushInterval; manualConfiguration.clearCache(); namespace isolation (separate caches per mapper).
Example: Query and Update Behavior
// UserMapper.java
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(Long id);
@Update("UPDATE user SET name = #{name} WHERE id = #{id}")
void updateUser(User user);
}
Consider these steps inside a single SqlSession:
getUserById(1)— Cache miss, queries database, stores result in L1 cache.getUserById(1)— Cache hit, returns cached result.updateUser(...)— Clears L1 cache.getUserById(1)— Cache miss, re-queries database, re-caches.
After closing and reopening the SqlSession, the second-level cache (if enabled) serves the result.
Interceptor Mechanism: Modifying SQL with Plugins
MyBatis interceptors (plugins) can hook into Executor, StatementHandler, ParameterHandler, or ResultSetHandler methods. The @Intercepts annotation specifies target methods.
Example: PageHelper Pagination Interceptor
// PageInterceptor.java (simplified)
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
Page<?> page = PageHelper.getLocalPage();
if (page == null) {
return invocation.proceed();
}
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
String paginatedSql = sql + " LIMIT " + page.getPageSize() + " OFFSET " + page.getOffset();
// Replace original SQL in MappedStatement (via reflection or API)
return invocation.proceed();
}
}
Registration in the configuration file:
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>
At startup, MyBatis builds an InterceptorChain. When Executor.query() is called, the chain wraps the executor in a Plugin proxy, and each interceptor’s intercept() method fires in registration order.
PageHelper uses ThreadLocal to pass pagination parameters, enabling the interceptor to modify the SQL before execution.
Practical Considerations
- First-level cache: After DML in the same
SqlSession, the L1 cache is cleared. If you query again without re-querying the database, you avoid stale data—but only if you don't rely on the old cache. - Second-level cache: Best for read-heavy scenarios (e.g., reference tables). Frequent updates cause thrashing.
- Interceptor ordering: Use the
orderattribute (if supported) to control execution sequence. - PageHelper usage: Call
PageHelper.startPage()before the query; otherwise, no pagination is applied.