一、面试现场:一个让无数人卡壳的问题
1.1 面试对话还原
面试官翻了翻简历,看到"熟练掌握 MyBatis",抬起头问了一句:
"你说你熟悉 MyBatis,那我问你,你的UserMapper只是一个接口,没有实现类,为什么调用userMapper.selectById(1L)就能执行 SQL 呢?"
候选人开始回忆:"因为……MyBatis 用了动态代理?"
面试官追问:"什么动态代理?JDK 还是 CGLIB?代理对象是什么时候生成的?selectById又是怎么映射到 XML 里的 SQL 的?"
候选人:"这个……应该是启动的时候扫描 XML 吧……"
面试官:"那你说说,如果 Mapper 接口有两个同名方法,MyBatis 能区分吗?如果 XML 的 namespace 写错了会怎样?"
到这里,很多候选人已经答不上来了。
这个问题看似简单,实则是一道"分水岭题"——它能一次性考察你对JDK 动态代理、MyBatis 初始化流程、MapperRegistry 注册机制、MapperProxy 调用链、MapperMethod 方法映射、SqlSession 执行流程、Executor 执行器体系的理解深度。
1.2 为什么面试官爱问这道题
这道题的价值在于:
看你是不是只会用:很多人用 MyBatis 几年,但从来没想过"接口为什么能执行 SQL"
看你能不能讲清原理:从接口到代理,从代理到 SQL,这条链路涉及多个核心组件
看你能不能延伸到实战:Mapper 重载为什么不支持、namespace 写错会怎样、二级缓存与代理的关系
看你能不能对比技术:和 Spring Data JPA 的"接口自动实现"有什么区别
本文结构:
一句话结论
从使用场景到实现原理
JDK 动态代理基础
MyBatis 初始化流程:Mapper 是怎么被注册的
MapperProxyFactory:代理工厂
MapperProxy:方法调用的拦截器
MapperMethod:方法到 SQL 的映射
SqlSession 与 Executor:SQL 真正执行的地方
为什么 Mapper 接口不支持方法重载
实战问题排查
高频追问与回答模板
总结
二、一句话结论
MyBatis 在启动时,通过 JDK 动态代理为每个 Mapper 接口生成了一个"代理对象"。你调用的userMapper.selectById(1L),实际上调用的是这个代理对象的invoke()方法。代理对象内部根据"接口全限定名 + 方法名"找到对应的 SQL 语句(MappedStatement),然后交给 SqlSession 执行。
一句话总结:接口不能执行 SQL,能执行 SQL 的是 MyBatis 生成的代理对象。
三、从使用场景到实现原理
3.1 一个最简单的 MyBatis 使用例子
java
public interface UserMapper { User selectById(Long id); }xml
<mapper namespace="com.example.mapper.UserMapper"> <select id="selectById" resultType="com.example.entity.User"> SELECT * FROM user WHERE id = #{id} </select> </mapper>java
SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.selectById(1L);
疑惑:UserMapper只是一个接口,没有实现类,userMapper.selectById(1L)却可以执行 SQL。这是为什么?
3.2 核心答案:动态代理
sqlSession.getMapper(UserMapper.class)返回的并不是UserMapper的某个实现类,而是MyBatis 通过 JDK 动态代理生成的一个代理对象。
这个代理对象实现了UserMapper接口,因此可以赋值给UserMapper类型的变量。但它所有的方法调用都会被拦截到MapperProxy.invoke()方法中,在invoke()里完成"找到 SQL、执行 SQL、返回结果"的全流程。
四、JDK 动态代理基础
4.1 什么是 JDK 动态代理
JDK 动态代理是 Java 原生提供的代理机制,位于java.lang.reflect包下。它要求:
目标对象必须实现接口(这是 JDK 动态代理的硬性要求)
代理对象与目标对象实现相同的接口
方法调用会被
InvocationHandler.invoke()拦截
4.2 JDK 动态代理的核心 API
java
public interface InvocationHandler { Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } public static Object newProxyInstance( ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)4.3 一个简单的 JDK 动态代理示例
java
public interface UserService { void sayHello(String name); } public class UserServiceImpl implements UserService { @Override public void sayHello(String name) { System.out.println("Hello, " + name); } } public class MyInvocationHandler implements InvocationHandler { private Object target; public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before method: " + method.getName()); Object result = method.invoke(target, args); System.out.println("after method: " + method.getName()); return result; } } public class Main { public static void main(String[] args) { UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), new Class<?>[]{UserService.class}, new MyInvocationHandler(target) ); proxy.sayHello("张三"); } }输出:
text
before method: sayHello Hello, 张三 after method: sayHello
关键点:代理对象实现了UserService接口,但它的所有方法调用都会先进入InvocationHandler.invoke()。
4.4 MyBatis 是怎么用这套机制的
MyBatis 生成的 Mapper 代理和上面的例子类似,但有两点不同:
目标对象不存在:MyBatis 没有为接口写实现类,所以代理对象不需要"委托给 target"
拦截逻辑不同:
MapperProxy.invoke()不是简单地"前后加日志",而是"根据方法名找到 SQL 并执行"
换句话说,MyBatis 的代理对象就是一个"空壳",它唯一的作用就是把接口方法调用转换成 SQL 执行。
五、MyBatis 初始化流程:Mapper 是怎么被注册的
5.1 从 SqlSessionFactory 的构建说起
MyBatis 启动时,会解析mybatis-config.xml和所有 Mapper XML,构建一个Configuration对象,并封装成SqlSessionFactory。
核心流程:
text
SqlSessionFactoryBuilder.build() → XMLConfigBuilder.parse() → Configuration 初始化 → mapperElement() 解析 <mappers> 标签 → MapperRegistry.addMapper()
5.2 解析 <mappers> 标签
mybatis-config.xml中配置:
xml
<mappers> <mapper resource="mapper/UserMapper.xml"/> <package name="com.example.mapper"/> </mappers>
XMLConfigBuilder.mapperElement()会遍历这些配置,逐一处理。以<package>为例,它会:
扫描包下所有
.class文件找出所有接口(不是类、不是枚举、不是注解)
为每个接口调用
configuration.addMapper(interface)
5.3 MapperRegistry:接口注册中心
Configuration内部维护了一个MapperRegistry:
java
public class MapperRegistry { private final Configuration config; private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // 核心:为接口创建 MapperProxyFactory knownMappers.put(type, new MapperProxyFactory<>(type)); // 解析 XML(如果存在同名的 .xml 文件) MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } } }关键:
knownMappers的 key 是接口的Class对象,value 是MapperProxyFactory只有接口才会被注册(
type.isInterface())注册后会立即解析注解和 XML,把
MappedStatement放进Configuration
5.4 XML 的 namespace 必须等于接口全限定名
这一点非常关键:MyBatis 在解析 XML 时,会读取<mapper namespace="...">,然后:
尝试通过
Resources.classForName(namespace)加载这个类如果成功,调用
configuration.addMapper(namespaceClass)如果失败(比如 namespace 拼写错误),不会抛出异常,但Mapper 接口和 XML 就没有关联
这就是为什么很多初学者遇到"Invalid bound statement (not found)"报错——十有八九是 namespace 写错了。
六、MapperProxyFactory:代理工厂
6.1 作用
MapperProxyFactory<T>是一个泛型工厂类,负责为指定的 Mapper 接口生成代理对象。它被注册在MapperRegistry.knownMappers中,等待getMapper调用。
6.2 核心源码
java
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethodInvoker> getMethodCache() { return methodCache; } // 核心方法:生成代理对象 protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }关键点:
newInstance(SqlSession)中创建了一个MapperProxy,然后把SqlSession、接口 Class、方法缓存传进去Proxy.newProxyInstance传入的接口数组只有mapperInterface,因此代理对象只实现这一个接口代理对象不委托任何 target:因为 MyBatis 没有实现类,代理对象自己就是"实现"
6.3 methodCache:为什么需要缓存
methodCache是一个ConcurrentHashMap<Method, MapperMethodInvoker>,它缓存了"方法 -> MapperMethod"的映射。
为什么需要缓存?
每次调用
mapper.selectById(1L)时,MapperProxy.invoke()需要根据Method找到对应的MapperMethod如果不缓存,每次都要重新解析注解、拼接 statementId,性能会受影响
Method对象在同一个类中是唯一的,作为 key 很合适用
ConcurrentHashMap保证线程安全
6.4 代理对象的生命周期
代理对象是SqlSession级别的还是应用级别的?
答:代理对象由MapperProxyFactory.newInstance(sqlSession)生成,每次调用sqlSession.getMapper()都会生成一个新的代理对象。但MapperProxyFactory和methodCache是应用级别的(由Configuration持有),所以:
代理对象:每次
getMapper都新建,但 JVM 会对相同接口的代理类进行复用(代理 Class 只会生成一次)methodCache:应用级别共享,方法映射只解析一次
七、MapperProxy:方法调用的拦截器
7.1 作用
MapperProxy<T>实现了InvocationHandler,是所有 Mapper 方法调用的统一入口。
7.2 核心源码
java
public class MapperProxy<T> implements InvocationHandler, Serializable { private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethodInvoker> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 1. 如果调用的是 Object 类的方法(toString、hashCode、equals),直接执行 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } // 2. 如果调用的是接口默认方法(Java 8+),走默认实现 if (method.isDefault()) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 3. 核心:获取或创建 MapperMethod,然后调用 final MapperMethodInvoker mapperMethod = cachedMapperMethod(method); return mapperMethod.invoke(proxy, method, args, sqlSession); } private MapperMethodInvoker cachedMapperMethod(Method method) { return methodCache.computeIfAbsent(method, k -> new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()))); } }关键点:
Object 方法:
toString、hashCode、equals直接被代理对象自己处理默认方法:Java 8 的接口默认方法走
MethodHandles调用,不进入 SQL 执行流程普通方法:走
MapperMethod执行 SQLcachedMapperMethod:用
computeIfAbsent保证MapperMethod只被创建一次
7.3 PlainMethodInvoker 与 DefaultMethodInvoker
MyBatis 3.5.x 引入了MapperMethodInvoker接口,有两个实现:
PlainMethodInvoker:普通方法,走 SQL 执行DefaultMethodInvoker:默认方法,走 Java 默认实现
这种设计让"默认方法"和"接口方法"的执行路径分离,代码更清晰。
7.4 一个完整调用链
text
userMapper.selectById(1L) → 代理对象.selectById(1L) → MapperProxy.invoke(proxy, selectById, [1L]) → cachedMapperMethod(selectById) // 从 methodCache 获取或创建 → PlainMethodInvoker.invoke() → MapperMethod.execute(sqlSession, args) → SqlSession.selectOne(statementId, args) → Executor.query(...) → JDBC 执行 SQL
八、MapperMethod:方法到 SQL 的映射
8.1 MapperMethod 的组成
MapperMethod是方法到 SQL 的桥梁,它由两部分组成:
java
public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); } }SqlCommand:负责"找到对应的 SQL 语句"
MethodSignature:负责"解析方法的返回值、参数等信息"
8.2 SqlCommand:从方法到 statementId
java
public static class SqlCommand { private final String name; // statementId private final SqlCommandType type; // SELECT / INSERT / UPDATE / DELETE public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { // 1. 拼接 statementId = 接口全限定名 + "." + 方法名 final String methodName = method.getName(); final Class<?> declaringClass = method.getDeclaringClass(); MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); if (ms == null) { throw new BindingException("Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName); } this.name = ms.getId(); this.type = ms.getSqlCommandType(); } }关键:
statementId = 接口全限定名 + "." + 方法名,例如com.example.mapper.UserMapper.selectById用这个 statementId 去
Configuration.mappedStatements中查找MappedStatement找不到就抛出
BindingException,这就是"Invalid bound statement"的由来
8.3 MethodSignature:方法签名解析
MethodSignature负责解析:
返回值类型:
void、Object、List、Map、Cursor等参数位置:
@Param注解、RowBounds、ResultHandler是否是
@MapKey方法:返回Map时用哪个字段作为 key
java
public static class MethodSignature { private final boolean returnsMany; // 是否返回集合 private final boolean returnsMap; // 是否返回 Map private final boolean returnsVoid; // 是否返回 void private final boolean returnsCursor; // 是否返回 Cursor private final Class<?> returnType; // 返回类型 private final String mapKey; // @MapKey 的值 private final Integer resultHandlerIndex; private final Integer rowBoundsIndex; private final ParamNameResolver paramNameResolver; }8.4 execute 方法:根据 SQL 类型分发
java
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } 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; }关键:
根据
SqlCommandType决定调用sqlSession.insert、update、delete还是selectOne@Select、@Insert、@Update、@Delete注解也是在这一步被识别的
九、SqlSession 与 Executor:SQL 真正执行的地方
9.1 SqlSession:用户接口
SqlSession是 MyBatis 对外的核心接口,封装了所有数据库操作:
java
public interface SqlSession extends Closeable { <T> T selectOne(String statement); <T> T selectOne(String statement, Object parameter); <E> List<E> selectList(String statement); <E> List<E> selectList(String statement, Object parameter); int insert(String statement); int update(String statement); int delete(String statement); // ... }它的默认实现是DefaultSqlSession,内部持有一个Executor。
9.2 Executor:执行器体系
Executor是 MyBatis 的核心执行器,采用装饰器模式组织:
text
Executor(接口) ├── BaseExecutor(抽象类,实现模板方法) │ ├── SimpleExecutor(简单执行器,每次新建 Statement) │ ├── ReuseExecutor(复用执行器,缓存 Statement) │ └── BatchExecutor(批量执行器,批量提交) ├── CachingExecutor(二级缓存装饰器) └── ...
当SqlSession.selectOne()被调用时,执行流程是:
text
DefaultSqlSession.selectOne() → DefaultSqlSession.selectList() → CachingExecutor.query() // 如果开启二级缓存 → BaseExecutor.query() → 一级缓存查询(PerpetualCache) → 缓存未命中 → queryFromDatabase() → SimpleExecutor.doQuery() → StatementHandler.prepare() → StatementHandler.parameterize() → StatementHandler.query() → ResultSetHandler.handleResultSets() → 返回结果
9.3 StatementHandler:SQL 语句处理器
StatementHandler是真正和 JDBC 打交道的地方,有四种实现:
SimpleStatementHandler:普通 StatementPreparedStatementHandler:预编译 Statement(默认)CallableStatementHandler:存储过程RoutingStatementHandler:根据 MappedStatement 的 statementType 路由
9.4 ResultSetHandler:结果集处理器
ResultSetHandler负责把 JDBC 的ResultSet转换成 Java 对象,主要工作:
自动映射:根据列名和属性名自动匹配
手动映射:根据
<resultMap>配置映射类型转换:调用
TypeHandler把 JDBC 类型转换为 Java 类型
十、为什么 Mapper 接口不支持方法重载
10.1 MyBatis 的映射规则
MyBatis 的statementId是接口全限定名 + "." + 方法名,例如:
text
com.example.mapper.UserMapper.selectById
注意:这个方法名里不包含参数信息。
10.2 重载会导致什么问题
如果 Mapper 接口定义了两个同名方法:
java
public interface UserMapper { User selectById(Long id); User selectById(Long id, boolean includeDeleted); }两个方法对应的 statementId 都是:
text
com.example.mapper.UserMapper.selectById
在Configuration.mappedStatements中,这个 statementId 只能对应一个MappedStatement。后注册的会覆盖先注册的,导致其中一个方法"找不到 SQL"或"走错 SQL"。
10.3 官方立场
MyBatis 官方文档明确说明:Mapper 接口不支持方法重载。这也是为什么 MyBatis 的 Mapper 方法命名通常很具体,例如selectById、selectByUserIdAndStatus。
10.4 面试加分点
如果面试官问"为什么 Mapper 不支持重载",你可以这样回答:
"MyBatis 的映射规则是 statementId = 接口全限定名 + '.' + 方法名,不包含参数信息。所以两个同名方法会映射到同一个 statementId,导致覆盖或冲突。如果要支持重载,MyBatis 需要把参数信息也纳入 statementId,但这会破坏现有的 XML namespace + id 的唯一性约束,因此官方明确不支持。"
十一、实战问题排查
11.1 常见错误一:Invalid bound statement (not found)
现象:
text
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.mapper.UserMapper.selectById
排查:
XML 的
namespace是否等于 Mapper 接口的全限定名XML 的
<select id="...">是否等于方法名XML 是否在 MyBatis 的扫描路径下(
mybatis.mapper-locations)Mapper 接口是否被
@Mapper或@MapperScan扫描到如果使用 Spring Boot,检查
mybatis-plus或mybatis-spring-boot-starter的自动配置
11.2 常见错误二:方法重载导致的冲突
现象:两个同名方法,只有一个能正常工作。
排查:检查 Mapper 接口是否有重载方法。如果确实需要重载,改为不同方法名。
11.3 常见错误三:代理对象类型转换异常
现象:
text
java.lang.ClassCastException: com.sun.proxy.$Proxy123 cannot be cast to com.example.mapper.UserMapperImpl
排查:MyBatis 生成的代理对象只实现了 Mapper 接口,不能转换为任何实现类。检查是否错误地把代理对象强转为具体实现类。
11.4 常见错误四:默认方法调用不走 SQL
现象:Mapper 接口中定义了默认方法,调用时不执行 SQL。
排查:Java 8+ 的默认方法走DefaultMethodInvoker,不进入 SQL 执行流程。如果默认方法内部调用了接口方法,那些接口方法依然会走代理执行 SQL。
11.5 常见错误五:Object 方法被误用
现象:在代码中调用userMapper.toString()、userMapper.hashCode(),返回的是代理对象的实现,而不是 SQL 执行结果。
排查:这是正常行为,MapperProxy.invoke()对Object类方法做了特殊处理。不要在 Mapper 中定义toString等方法的业务语义。
十二、高频追问与回答模板
12.1 高频追问
MyBatis 的 Mapper 接口为什么能执行 SQL?
动态代理是 JDK 的还是 CGLIB 的?
代理对象什么时候创建的?
MapperProxyFactory和MapperProxy有什么区别?MapperMethod是怎么找到 SQL 的?为什么 Mapper 不支持重载?
Mapper 接口和 XML 的 namespace 必须一致吗?
一级缓存和二级缓存在哪个环节生效?
多个 SqlSession 使用同一个 Mapper 接口,代理对象是同一个吗?
MyBatis 和 Spring Data JPA 的接口自动实现在原理上有什么区别?
12.2 回答模板
回答"Mapper 接口为什么能执行 SQL"时,可以按这个结构组织:
一句话结论:MyBatis 用 JDK 动态代理为 Mapper 接口生成代理对象,调用走
MapperProxy.invoke()。讲启动阶段:MyBatis 启动时解析 XML 和注解,通过
MapperRegistry把接口和MapperProxyFactory关联起来。讲调用阶段:
sqlSession.getMapper()生成代理对象,调用方法时进入MapperProxy.invoke(),通过MapperMethod找到MappedStatement。讲执行阶段:
MapperMethod.execute()根据 SQL 类型调用SqlSession对应方法,最终由Executor和 JDBC 执行。讲约束:statementId 是"接口全限定名 + 方法名",因此不支持方法重载,namespace 必须匹配接口全限定名。
12.3 对比 Spring Data JPA
| 维度 | MyBatis | Spring Data JPA |
|---|---|---|
| 接口实现方式 | JDK 动态代理 + MapperProxy | JDK 动态代理 + RepositoryFactorySupport |
| 方法映射 | statementId = 接口名 + 方法名 | 方法名解析 + @Query 注解 |
| SQL 来源 | XML / 注解 | JPQL / 原生 SQL / 方法名派生 |
| 灵活性 | 高(XML 可写复杂 SQL) | 中(复杂查询需要 @Query) |
| 学习曲线 | 中 | 高 |
十三、总结
13.1 核心结论
MyBatis 的 Mapper 接口能执行 SQL,是因为 MyBatis 用 JDK 动态代理为接口生成了代理对象。
完整链路:
text
启动阶段: 解析 XML / 注解 → MapperRegistry.addMapper() → 创建 MapperProxyFactory → 放入 knownMappers 调用阶段: sqlSession.getMapper(UserMapper.class) → MapperProxyFactory.newInstance(sqlSession) → 创建 MapperProxy → Proxy.newProxyInstance() 执行阶段: userMapper.selectById(1L) → MapperProxy.invoke() → MapperMethod.execute() → SqlSession.selectOne() → Executor.query() → JDBC 执行 SQL
13.2 关键要点
动态代理类型:JDK 动态代理(因为 Mapper 是接口)
代理对象生成时机:
sqlSession.getMapper()时方法映射规则:statementId = 接口全限定名 + "." + 方法名
不支持重载:因为 statementId 不含参数信息
namespace 必须匹配:XML 的 namespace 必须等于接口全限定名
methodCache 缓存:方法映射只解析一次,应用级别共享
13.3 一句话总结
MyBatis 的 Mapper 接口本身不会执行 SQL,能执行 SQL 的是 MyBatis 通过 JDK 动态代理生成的代理对象。代理对象根据"接口全限定名 + 方法名"找到 SQL,再交给 SqlSession 和 Executor 执行。
理解了这条链路,你不仅能回答面试题,还能在遇到"Invalid bound statement"、"方法重载冲突"、"代理对象类型转换异常"等问题时快速定位根因。