☰
面试官:MyBatis你只写了接口为啥就能执行SQL啊?
2026/10/7 21:30:09 网站建设 项目流程

一、面试现场:一个让无数人卡壳的问题

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 的"接口自动实现"有什么区别

本文结构:

  1. 一句话结论

  2. 从使用场景到实现原理

  3. JDK 动态代理基础

  4. MyBatis 初始化流程:Mapper 是怎么被注册的

  5. MapperProxyFactory:代理工厂

  6. MapperProxy:方法调用的拦截器

  7. MapperMethod:方法到 SQL 的映射

  8. SqlSession 与 Executor:SQL 真正执行的地方

  9. 为什么 Mapper 接口不支持方法重载

  10. 实战问题排查

  11. 高频追问与回答模板

  12. 总结


二、一句话结论

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>为例,它会:

  1. 扫描包下所有.class文件

  2. 找出所有接口(不是类、不是枚举、不是注解)

  3. 为每个接口调用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="...">,然后:

  1. 尝试通过Resources.classForName(namespace)加载这个类

  2. 如果成功,调用configuration.addMapper(namespaceClass)

  3. 如果失败(比如 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执行 SQL

  • cachedMapperMethod:用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:普通 Statement

  • PreparedStatementHandler:预编译 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

排查:

  1. XML 的namespace是否等于 Mapper 接口的全限定名

  2. XML 的<select id="...">是否等于方法名

  3. XML 是否在 MyBatis 的扫描路径下(mybatis.mapper-locations)

  4. Mapper 接口是否被@Mapper或@MapperScan扫描到

  5. 如果使用 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 高频追问

  1. MyBatis 的 Mapper 接口为什么能执行 SQL?

  2. 动态代理是 JDK 的还是 CGLIB 的?

  3. 代理对象什么时候创建的?

  4. MapperProxyFactory和MapperProxy有什么区别?

  5. MapperMethod是怎么找到 SQL 的?

  6. 为什么 Mapper 不支持重载?

  7. Mapper 接口和 XML 的 namespace 必须一致吗?

  8. 一级缓存和二级缓存在哪个环节生效?

  9. 多个 SqlSession 使用同一个 Mapper 接口,代理对象是同一个吗?

  10. MyBatis 和 Spring Data JPA 的接口自动实现在原理上有什么区别?

12.2 回答模板

回答"Mapper 接口为什么能执行 SQL"时,可以按这个结构组织:

  1. 一句话结论:MyBatis 用 JDK 动态代理为 Mapper 接口生成代理对象,调用走MapperProxy.invoke()。

  2. 讲启动阶段:MyBatis 启动时解析 XML 和注解,通过MapperRegistry把接口和MapperProxyFactory关联起来。

  3. 讲调用阶段:sqlSession.getMapper()生成代理对象,调用方法时进入MapperProxy.invoke(),通过MapperMethod找到MappedStatement。

  4. 讲执行阶段:MapperMethod.execute()根据 SQL 类型调用SqlSession对应方法,最终由Executor和 JDBC 执行。

  5. 讲约束:statementId 是"接口全限定名 + 方法名",因此不支持方法重载,namespace 必须匹配接口全限定名。

12.3 对比 Spring Data JPA

维度MyBatisSpring Data JPA
接口实现方式JDK 动态代理 + MapperProxyJDK 动态代理 + 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 关键要点

  1. 动态代理类型:JDK 动态代理(因为 Mapper 是接口)

  2. 代理对象生成时机:sqlSession.getMapper()时

  3. 方法映射规则:statementId = 接口全限定名 + "." + 方法名

  4. 不支持重载:因为 statementId 不含参数信息

  5. namespace 必须匹配:XML 的 namespace 必须等于接口全限定名

  6. methodCache 缓存:方法映射只解析一次,应用级别共享

13.3 一句话总结

MyBatis 的 Mapper 接口本身不会执行 SQL,能执行 SQL 的是 MyBatis 通过 JDK 动态代理生成的代理对象。代理对象根据"接口全限定名 + 方法名"找到 SQL,再交给 SqlSession 和 Executor 执行。

理解了这条链路,你不仅能回答面试题,还能在遇到"Invalid bound statement"、"方法重载冲突"、"代理对象类型转换异常"等问题时快速定位根因。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询