接口绑定原理
MyBatis 的核心特性之一是只需定义 Mapper 接口而无需编写实现类,框架通过 JDK 动态代理自动将接口方法绑定到对应的 SQL 语句。理解这一机制是掌握 MyBatis 的关键。
动态代理创建原理
MyBatis 使用 JDK 动态代理为 Mapper 接口生成代理对象,核心流程如下:
Java
// 用户代码
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// MyBatis 内部实现(简化)
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
// MapperRegistry 中
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> factory = knownMappers.get(type);
return factory.newInstance(sqlSession);
}
MapperProxyFactory 通过 Proxy.newProxyInstance() 创建代理对象:
Java
// MapperProxyFactory 核心逻辑
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy
);
}
代理对象的拦截器是 MapperProxy,它实现了 InvocationHandler 接口。
方法到 SQL 语句的映射
当调用代理对象的方法时,MapperProxy.invoke() 被触发,执行以下流程:
Java
// MapperProxy.invoke() 简化流程
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 如果是 Object 类的方法(如 toString、equals),直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. 获取或缓存 MapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 3. 执行 SQL
return mapperMethod.execute(sqlSession, args);
}
方法到 SQL 的绑定关系如下表所示:
| 步骤 | 说明 | 示例 |
|---|---|---|
| 1. 获取 namespace | 从接口全限定名获取 | com.example.mapper.UserMapper |
| 2. 获取 statementId | 从方法名获取 | selectById |
| 3. 组合完整 ID | namespace + "." + methodName | com.example.mapper.UserMapper.selectById |
| 4. 查找 MappedStatement | 从 Configuration 中查找 | configuration.getMappedStatement("...") |
| 5. 执行 SQL | 根据 SQL 类型调用对应方法 | sqlSession.selectOne() |
Java
// MapperMethod 执行逻辑
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 SELECT:
// 根据返回值类型选择 selectOne 或 selectList
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
} else if (method.returnsMany()) {
result = sqlSession.selectList(command.getName(), param);
} else {
result = sqlSession.selectOne(command.getName(), param);
}
break;
// ... UPDATE, DELETE
}
return result;
}
BindingException 常见原因
当接口绑定失败时,MyBatis 会抛出 BindingException,常见原因如下:
1. namespace 与接口不匹配
XML
<!-- 错误:namespace 与实际接口不一致 -->
<mapper namespace="com.example.mapper.OldUserMapper">
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
Java
// 实际使用的接口
public interface UserMapper {
User selectById(Long id);
}
// 抛出:BindingException: Invalid bound statement (not found): com.example.mapper.UserMapper.selectById
解决:namespace 必须与接口全限定名一致。
2. XML 文件未被扫描
Java
// mybatis-config.xml 中未配置 mapper 位置
<mappers>
<!-- 缺少 UserMapper.xml 的注册 -->
<mapper resource="mapper/OrderMapper.xml"/>
</mappers>
解决:确保 XML 文件在 <mappers> 中注册,或使用 @MapperScan 扫描包路径。
3. 方法名与 statement id 不匹配
Java
public interface UserMapper {
User findById(Long id); // 方法名为 findById
}
XML
<!-- XML 中 id 为 selectById,与接口方法名不一致 -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 抛出:BindingException: Invalid bound statement (not found): com.example.mapper.UserMapper.findById -->
解决:XML 中的 id 必须与接口方法名完全一致。
4. 接口未被 @Mapper 注解或未被扫描
Java
// 忘记添加 @Mapper 注解
public interface UserMapper {
User selectById(Long id);
}
解决:添加 @Mapper 注解或在启动类配置 @MapperScan("com.example.mapper")。
绑定流程总结
text
用户调用 mapper.selectById(1)
↓
JDK 动态代理拦截
↓
MapperProxy.invoke()
↓
组合 statementId = "namespace.methodName"
↓
从 Configuration 查找 MappedStatement
↓
根据 SQL 类型执行 sqlSession.selectOne/selectList/insert/update/delete
↓
返回结果
要点总结
- MyBatis 通过 JDK 动态代理为 Mapper 接口生成代理对象,无需编写实现类
- 代理对象的方法调用通过
namespace.方法名定位到对应的 SQL 语句 - MapperProxy 是核心拦截器,负责方法到 SQL 的转换与执行
- BindingException 通常由 namespace 不匹配、XML 未注册、方法名与 id 不一致导致
- 接口必须被 @Mapper 注解或通过 @MapperScan 扫描,否则无法生成代理
📝 发现内容有误?点击此处直接编辑