mybatis之Sql绑定
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. 核心属性:存储接口与代理工厂的映射
public class MapperRegistry {
private final Configuration config; // MyBatis 全局配置
// 核心映射:key=Mapper接口(如UserMapper.class),value=代理工厂(MapperProxyFactory)
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
}
2. 注册流程:addMapper () 方法
MyBatis 初始化时(如解析 mybatis-config.xml 的 <mappers> 标签),会调用 addMapper() 方法,完成以下操作:
- 校验接口合法性(仅接口可注册,排除类 / 抽象类);
- 创建该接口对应的
MapperProxyFactory,存入knownMappers; - 解析接口的注解 SQL(如
@Select)或关联的 XML 文件,完成 SQL 与方法的绑定。
源码解析
public <T> void addMapper(Class<T> type) {
// 1. 仅注册接口(非接口直接返回)
if (type.isInterface()) {
// 2. 避免重复注册
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 3. 核心:将接口与代理工厂绑定,存入 knownMappers
knownMappers.put(type, new MapperProxyFactory<>(type));
// 4. 解析 Mapper 接口(注解 SQL 或 XML SQL)
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse(); // 解析 @Select/@Insert 等注解,或关联 XML 中的 SQL
loadCompleted = true;
} finally {
// 5. 解析失败时移除注册,避免脏数据
if (!loadCompleted) {
knownMappers.remove(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 中获取对应代理工厂,生成代理对象:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 1. 从注册中心获取代理工厂
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 2. 代理工厂生成代理对象(传入 SqlSession,用于后续执行 SQL)
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
阶段 2:生成代理对象 ——MapperProxyFactory 与 MapperProxy
MapperProxyFactory 是 Mapper 接口代理对象的工厂类,通过 JDK 动态代理生成 MapperProxy(实现 InvocationHandler),为接口方法调用提供拦截能力。
1. MapperProxyFactory:代理工厂核心逻辑
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface; // 目标 Mapper 接口
// 方法缓存:key=接口方法(Method),value=方法执行器(MapperMethodInvoker)
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
// 生成代理对象(核心方法)
public T newInstance(SqlSession sqlSession) {
// 1. 创建 InvocationHandler 实现类:MapperProxy
MapperProxy<T> mapperProxy = new MapperProxy<>(
sqlSession,
mapperInterface,
methodCache
);
// 2. 通过 JDK 动态代理生成接口实例
return newInstance(mapperProxy);
}
// JDK 动态代理生成实例
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(), // 类加载器
new Class[]{mapperInterface}, // 目标接口(仅支持单接口)
mapperProxy // 拦截器(InvocationHandler)
);
}
}
关键技术:JDK 动态代理
JDK 动态代理要求目标对象必须是接口(这也是 MyBatis Mapper 必须是接口的原因),通过 Proxy.newProxyInstance 生成代理对象,所有接口方法调用都会被 MapperProxy.invoke() 拦截。
2. MapperProxy:代理对象的核心拦截逻辑
MapperProxy 实现 InvocationHandler 接口,是接口方法调用的实际拦截者。其核心逻辑是:区分 “Object 类方法” 和 “Mapper 接口方法”,仅拦截接口方法并委托执行器处理。
源码解析:invoke () 方法
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession; // 用于执行 SQL
private final Class<T> mapperInterface; // 目标接口
private final Map<Method, MapperMethodInvoker> methodCache; // 方法缓存
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 1. 拦截 Object 类的方法(如 toString()、hashCode()),直接执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 2. 拦截 Mapper 接口方法:获取方法执行器并执行
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
// 获取方法执行器(从缓存或新建)
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
// 1. 先查缓存,避免重复创建
MapperMethodInvoker invoker = methodCache.get(method);
if (invoker != null) {
return invoker;
}
// 2. 缓存未命中,创建执行器
return methodCache.computeIfAbsent(method, m -> {
// 2.1 处理 Java 8+ 接口默认方法(如 default void test() {})
if (m.isDefault()) {
// 省略默认方法处理逻辑...
return new DefaultMethodInvoker(...);
} else {
// 2.2 处理普通接口方法:创建 PlainMethodInvoker,包装 MapperMethod
return new PlainMethodInvoker(
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())
);
}
});
}
}
关键区分:
- 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 构造与核心属性
public class MapperMethod {
// SQL 命令:包含 SQL 的 id(namespace+methodName)和类型
private final SqlCommand command;
// 方法签名:包含方法返回值、参数等信息
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 1. 解析 SQL 命令(从 Configuration 中获取 MappedStatement)
this.command = new SqlCommand(config, mapperInterface, method);
// 2. 解析方法签名
this.method = new MethodSignature(config, mapperInterface, method);
}
// SQL 命令内部类
public static class SqlCommand {
private final String name; // SQL 的 id(如 com.example.UserMapper.getUser)
private final SqlCommandType type; // SQL 类型(INSERT/SELECT 等)
public SqlCommand(Configuration config, Class<?> mapperInterface, Method method) {
// 1. 生成 SQL 的 id:接口全限定名 + 方法名
String methodName = method.getName();
String namespace = mapperInterface.getName();
String sqlId = namespace + "." + methodName;
// 2. 从 Configuration 中获取 MappedStatement(XML/注解解析后的 SQL 元信息)
MappedStatement ms = config.getMappedStatement(sqlId);
if (ms == null) {
throw new BindingException("Invalid bound statement (not found): " + sqlId);
}
// 3. 存储 SQL 名称和类型
this.name = sqlId;
this.type = ms.getSqlCommandType();
}
}
// 方法签名内部类(省略,主要解析返回值、参数等)
public static class MethodSignature { /* ... */ }
}
关键解析:
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)。
源码解析
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
// 根据 SQL 类型,调用 SqlSession 对应的方法
switch (command.getType()) {
case INSERT:
// 1. 处理参数(将 args 转换为 SQL 所需的参数格式,如 Map/JavaBean)
param = method.convertArgsToSqlCommandParam(args);
// 2. 调用 SqlSession.insert(),并处理返回值(如受影响行数)
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
case UPDATE:
param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
case DELETE:
param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
case SELECT:
// 处理 SELECT 多种返回场景
if (method.returnsVoid() && method.hasResultHandler()) {
// 场景 1:返回 void,结果由 ResultHandler 处理
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 场景 2:返回集合(List)
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 场景 3:返回 Map
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 场景 4:返回 Cursor(流式读取)
result = executeForCursor(sqlSession, args);
} else {
// 场景 5:返回单个对象(如 User)
param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
// 处理 Optional 返回值(Java 8+)
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());
}
// 处理基本类型返回值为 null 的异常(如返回 int 却返回 null)
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a primitive return type.");
}
return result;
}
关键步骤:
- 参数处理:
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注解指定参数名:
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,返回结果