MyBatis Mapper 接口与 SQL 绑定机制全解析:从动态代理到 SQL 执行
MyBatis 的核心特性之一是 “接口无实现类却能执行 SQL”—— 开发者只需定义 Mapper 接口(如 UserMapper),并在 XML / 注解中编写对应 SQL,调用接口方法即可触发 SQL 执行。这一 “魔法” 的本质是 JDK 动态代理 + 注册中心 + 方法与 SQL 绑定 的协同机制。从 “注册→代理→绑定→执行” 四个阶段,彻底拆解 Mapper 接口与 SQL 的绑定原理。
核心问题:为什么接口能执行 SQL?
Mapper 接口本身没有实现类,MyBatis 通过以下机制实现 “接口调用→SQL 执行” 的闭环:
- 注册中心(MapperRegistry):初始化时扫描 Mapper 接口,将 “接口” 与 “代理工厂(MapperProxyFactory)” 绑定并注册;
- 动态代理(MapperProxy):调用接口方法时,通过代理工厂生成接口的动态代理对象,拦截方法调用;
- 方法与 SQL 绑定(MapperMethod):代理对象将接口方法解析为 “SQL 标识(namespace+methodName)” 和 “参数信息”,找到对应的 SQL;
- 委托执行(SqlSession):通过 SqlSession 调用底层执行器(Executor),最终执行 SQL 并返回结果。
阶段 1:初始化注册 ——MapperRegistry 管理绑定关系
MapperRegistry 是 MyBatis 的 Mapper 接口注册中心,负责在 MyBatis 初始化时,将 Mapper 接口与对应的 “代理工厂(MapperProxyFactory)” 关联并存储,为后续生成代理对象做准备。
1. 核心属性:存储接口与代理工厂的映射
1 | public class MapperRegistry { |
2. 注册流程:addMapper () 方法
MyBatis 初始化时(如解析 mybatis-config.xml 的 <mappers> 标签),会调用 addMapper() 方法,完成以下操作:
- 校验接口合法性(仅接口可注册,排除类 / 抽象类);
- 创建该接口对应的
MapperProxyFactory,存入knownMappers; - 解析接口的注解 SQL(如
@Select)或关联的 XML 文件,完成 SQL 与方法的绑定。
源码解析
1 | public <T> void addMapper(Class<T> type) { |
关键细节:
MapperProxyFactory作用:为后续生成 Mapper 接口的动态代理对象提供工厂,每个接口对应一个工厂;- 解析逻辑:
MapperAnnotationBuilder会解析接口上的注解 SQL,或根据接口全限定名(如com.example.UserMapper)找到对应的 XML 文件(如UserMapper.xml),将 SQL 语句与接口方法绑定(存储在MappedStatement中,id为 “接口全限定名 + 方法名”,如com.example.UserMapper.getUser)。
3. 获取代理工厂:getMapper () 方法
当开发者通过 SqlSession.getMapper(UserMapper.class) 获取 Mapper 接口实例时,MyBatis 会调用 MapperRegistry.getMapper(),从 knownMappers 中获取对应代理工厂,生成代理对象:
1 | public <T> T getMapper(Class<T> type, SqlSession sqlSession) { |
阶段 2:生成代理对象 ——MapperProxyFactory 与 MapperProxy
MapperProxyFactory 是 Mapper 接口代理对象的工厂类,通过 JDK 动态代理生成 MapperProxy(实现 InvocationHandler),为接口方法调用提供拦截能力。
1. MapperProxyFactory:代理工厂核心逻辑
1 | public class MapperProxyFactory<T> { |
关键技术:JDK 动态代理
JDK 动态代理要求目标对象必须是接口(这也是 MyBatis Mapper 必须是接口的原因),通过 Proxy.newProxyInstance 生成代理对象,所有接口方法调用都会被 MapperProxy.invoke() 拦截。
2. MapperProxy:代理对象的核心拦截逻辑
MapperProxy 实现 InvocationHandler 接口,是接口方法调用的实际拦截者。其核心逻辑是:区分 “Object 类方法” 和 “Mapper 接口方法”,仅拦截接口方法并委托执行器处理。
源码解析:invoke () 方法
1 | public class MapperProxy<T> implements InvocationHandler, Serializable { |
关键区分:
- Object 类方法:如
toString()、equals(),直接通过method.invoke(this, args)执行,不关联 SQL; - 普通接口方法:如
User getUser(int id),创建PlainMethodInvoker,包装MapperMethod(方法与 SQL 的绑定核心); - 默认方法:Java 8+ 接口支持的
default方法,通过DefaultMethodInvoker执行,与 SQL 无关。
阶段 3:方法与 SQL 绑定 ——MapperMethod 的核心作用
MapperMethod 是 Mapper 接口方法与 SQL 语句的 “绑定器”,封装了两个关键信息:
SqlCommand:SQL 的标识(id= 接口全限定名 + 方法名)和类型(INSERT/SELECT/UPDATE/DELETE);MethodSignature:接口方法的签名(返回值类型、参数类型、是否支持结果处理器等)。
通过 MapperMethod.execute() 方法,将接口方法调用转换为 SqlSession 的对应操作(如 sqlSession.selectOne()、sqlSession.insert()),完成 “方法→SQL” 的绑定。
1. MapperMethod 构造与核心属性
1 | public class MapperMethod { |
关键解析:
sqlId的生成:如UserMapper接口的getUser方法,sqlId为com.example.UserMapper.getUser,与 XML 中<select id="getUser">的id完全匹配;MappedStatement:MyBatis 解析 XML / 注解后生成的 SQL 元信息,包含 SQL 语句、参数映射、结果映射等,config.getMappedStatement(sqlId)是 “方法→SQL” 绑定的关键一步。
2. 执行 SQL:execute () 方法
execute() 方法根据 SqlCommandType 调用 SqlSession 的对应方法,完成 SQL 执行,并根据 MethodSignature 处理返回值(如将 int 转为 void、将 List 转为 Optional)。
源码解析
1 | public Object execute(SqlSession sqlSession, Object[] args) { |
关键步骤:
- 参数处理:
convertArgsToSqlCommandParam(args)将接口方法的参数(如getUser(int id)中的id=1)转换为 SQL 所需的格式(如单个值、Map、JavaBean); - SQL 执行:根据 SQL 类型调用
sqlSession.insert/update/selectOne等方法,这些方法最终委托 Executor 执行 SQL; - 返回值处理:
rowCountResult()将受影响行数(int)转换为方法返回值(如void则忽略,boolean则判断是否 >0),并处理Optional等特殊返回类型。
完整流程总结:从接口调用到 SQL 执行
以 UserMapper.getUser(1) 为例,梳理整个绑定与执行流程:
sequenceDiagram
participant 开发者
participant MapperProxy 代理对象
participant MapperMethod 方法 SQL绑定
participant SqlSession
participant Executor 执行器
participant 数据库
开发者->>MapperProxy:调用userMapper.getUser(1)
MapperProxy->>MapperProxy:invoke() 拦截方法调用
MapperProxy->>MapperMethod:cachedInvoker() 获取执行器
MapperMethod->>MapperMethod:解析 SqlCommand id=com.example.UserMapper.getUser,类型=SELECT
MapperMethod->>SqlSession:调用 sqlSession.selectOne(sqlId, 1)
SqlSession->>Executor:委托 executor.query()
Executor->>数据库:执行 SQL SELECT * FROM user WHERE id=1
数据库-->>Executor:返回 ResultSet
Executor->>SqlSession:返回映射后的 User 对象
SqlSession-->>MapperMethod:返回 User 对象
MapperMethod-->>MapperProxy:返回 User 对象
MapperProxy-->>开发者:返回 User 对象
关键问题与解决方案
1. 报错 “Invalid bound statement (not found)”
- 原因:
sqlId不存在,即 Mapper 接口方法与 XML / 注解中的 SQL 未绑定; - 解决方案:
- 检查 XML 中
<select>等标签的id与接口方法名一致; - 检查 XML 的
namespace与接口全限定名一致; - 检查 MyBatis 配置是否扫描了该 Mapper(如
<mappers>标签配置正确)。
- 检查 XML 中
2. 多参数方法如何绑定?
问题:接口方法有多个参数(如
User getUser(String name, int age)),如何与 SQL 中的#{name}、#{age}绑定?解决方案:
使用@Param注解指定参数名:
1
User getUser( String name, int age);
MyBatis 会将参数封装为 Map,
key为@Param注解值,value为参数值,XML 中可直接使用#{name}、#{age}。
3. 为什么 Mapper 接口不能有实现类?
- 原因:MyBatis 使用 JDK 动态代理生成接口实例,而 JDK 动态代理要求目标对象必须是接口(无法为类生成代理);
- 例外:若需自定义实现,可通过
@MapperScan扫描实现类,但需手动关联 SQL,失去 MyBatis 自动绑定的优势。
总结
MyBatis 中 Mapper 接口与 SQL 的绑定,本质是 “注册中心管理映射关系 + 动态代理拦截方法调用 + MapperMethod 绑定方法与 SQL + SqlSession 委托执行” 的协同过程:
- 注册:初始化时通过
MapperRegistry注册 “接口→代理工厂”; - 代理:调用接口方法时,通过
MapperProxyFactory生成动态代理对象; - 绑定:
MapperProxy拦截调用,MapperMethod解析方法为 SQL 标识和参数; - 执行:委托
SqlSession和Executor执行 SQL,返回结果