Basic Concepts
MyBatis uses Mapper Proxy dynamic proxy to encapsulate database operations. Users only need to define an interface (Mapper interface) and specify SQL statement execution logic through annotations or XML files on interface methods, without manually implementing interface methods.
- Mapper Interface: Interface defining database operation methods, such as insertUser, getUserById, etc.
- Dynamic Proxy: Generates instances of interface implementation classes at runtime, mapping method calls to corresponding SQL operations.
MyBatis’s Mapper Proxy is a dynamic proxy mechanism that implements data access layer interfaces, used to map interface methods to specific SQL statement execution logic at runtime.
Main Components
- SqlSession: MyBatis’s core interface for executing SQL statements and managing transactions. Mapper Proxy uses SqlSession to perform database operations.
- Mapper Proxy: Dynamic proxy implementation class provided by MyBatis for proxying Mapper interfaces.
- Mapper Proxy Factory: MapperProxyFactory is a factory class for creating MapperProxy objects. Each Mapper interface corresponds to one MapperProxyFactory instance.
Advantages and Limitations
Advantages
- Decoupling: Developers only need to define interfaces and SQL without concerning themselves with specific implementations.
- Simplicity: Reduced code volume, especially suitable for simple CRUD operations.
- Dynamic: Based on dynamic proxy, no actual implementation classes need to be generated.
Limitations
- Performance Overhead: Dynamic proxy generates objects at runtime, performance is slightly lower than hand-written implementation classes.
- Readability: When there are many Mapper interfaces or complex methods, debugging and maintenance may become difficult.
- Complex Logic Limitations: Complex queries and multi-table join operations need to rely on XML or other methods.
Additional Extensions
- Use XML configuration or annotation configuration to define SQL.
- Can extend MapperProxy behavior through plugins (Interceptors), such as recording SQL execution time, log output, etc.
Runtime Mechanism
Getting Mapper Instance
When obtaining an instance of a Mapper interface through SqlSession.getMapper(Class type), MyBatis executes the following steps:
- Proxy factory creates proxy object: MyBatis uses MapperProxyFactory to generate a dynamic proxy object of MapperProxy.
- MapperProxyFactory internally calls JDK’s dynamic proxy utility to generate proxy class.
- The generated proxy class implements the user-defined Mapper interface.
- Returns proxy object: This proxy object appears to be an implementation class of the user-defined interface, but its method calls are intercepted and processed.
Method Call Interception
When calling a method in the Mapper interface, it actually triggers the invoke method of MapperProxy:
- Determine method ownership: MapperProxy first checks whether the method is a method in the Object class (such as toString, hashCode, etc.). These methods directly call Object implementations.
- Get SQL mapping: If the method is a Mapper interface method, MapperProxy looks up or creates a MapperMethod through the cachedMapperMethod method. MapperMethod is an internal utility class representing the mapping relationship between Mapper interface methods and SQL statements.
- Execute SQL operation: Call the corresponding SQL through MapperMethod.execute() method.
SQL Execution
When MapperMethod executes, it calls database operation methods provided by SqlSession, such as:
- selectOne: Execute single record query
- selectList: Execute multiple record query
- insert: Execute insert operation
- update: Execute update operation
- delete: Execute delete operation
Mapper Proxy
Example code:
public class WzkicuPage01 {
public static void main(String[] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
PageHelper.startPage(2, 1);
List<WzkUser> dataList = userMapper.selectList();
for (WzkUser wzk : dataList) {
System.out.println(wzk);
}
sqlSession.close();
}
}
During MyBatis initialization, the interface processing, MapperRegistry is an attribute in Configuration that internally maintains a HashMap for storing mapper interface factory classes, with each interface corresponding to one factory class.
<mappers>
<mapper resource="mapper.xml"/>
<mapper resource="OrderMapper.xml"/>
<mapper resource="UserMapper.xml"/>
<mapper resource="RoleMapper.xml"/>
<mapper resource="UserCacheMapper.xml"/>
</mappers>
When parsing the mappers tag, the CRUD in the corresponding configuration files are encapsulated as MappedStatement objects and stored in MappedStatements.
getMapper Source Code Analysis
// getMapper in DefaultSqlSession
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
// getMapper in Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
// getMapper in MapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// Get MapperProxyFactory from HashMap in MapperRegistry
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// Generate instance through dynamic proxy factory
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
// newInstance method in MapperProxyFactory class
public T newInstance(SqlSession sqlSession) {
// Create JDK dynamic proxy Handler class
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// Call overloaded method
return newInstance(mapperProxy);
}
// MapperProxy class, implements InvocationHandler interface
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
// Omit other implementation parts
}
invoke
After dynamic proxy, during execution in the invoke method in MapperProxy:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 1. If it's a method defined in Object class, call directly
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. If it's a default method, call the default method
else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
// 3. Catch and unwrap exception
throw ExceptionUtil.unwrapThrowable(t);
}
// 4. Get MapperMethod object
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 5. Execute the method corresponding to MapperMethod
return mapperMethod.execute(sqlSession, args);
}
execute Method
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// Determine the type of operation based on the command's type
switch (command.getType()) {
case INSERT:
result = executeInsert(sqlSession, args);
break;
case UPDATE:
result = executeUpdate(sqlSession, args);
break;
case DELETE:
result = executeDelete(sqlSession, args);
break;
case SELECT:
result = executeSelect(sqlSession, args);
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
// Check for null result and primitive return type, throw exception if necessary
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName() +
"' attempted to return null from a method with a primitive return type(" + method.getReturnType() + ").");
}
return result;
}
private Object executeInsert(SqlSession sqlSession, Object[] args) {
Object param = method.convertArgsToSqlCommandParam(args);
int rowCount = sqlSession.insert(command.getName(), param);
return rowCountResult(rowCount);
}
private Object executeUpdate(SqlSession sqlSession, Object[] args) {
Object param = method.convertArgsToSqlCommandParam(args);
int rowCount = sqlSession.update(command.getName(), param);
return rowCountResult(rowCount);
}
private Object executeDelete(SqlSession sqlSession, Object[] args) {
Object param = method.convertArgsToSqlCommandParam(args);
int rowCount = sqlSession.delete(command.getName(), param);
return rowCountResult(rowCount);
}
private Object executeSelect(SqlSession sqlSession, Object[] args) {
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
return null;
} else if (method.returnsMany()) {
return executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
return executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
return executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
Object result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
return Optional.ofNullable(result);
}
return result;
}
}